Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Explorbot provides a real-time TUI (Terminal User Interface) with three main are
│ ⯈ AddScenario(Verify company name field is pre-populated, HIGH) │
│ ⯈ AddScenario(Change company name and verify persistence, HIGH) │
│ ⯈ AddScenario(Navigate away and return to verify name persists, HIGH) │
│ Done. Press [ESC] to enable input │
│ Done. Press [ESC] to enable input. Press [CTRL + t] to toggle session timer
└─────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ > /plan user-management │
Expand All @@ -153,6 +153,8 @@ Explorbot provides a real-time TUI (Terminal User Interface) with three main are

**Tasks Pane**: Shows generated test scenarios with priorities and status

**Session Timer**: An optional timer displayed next to the activity line at the bottom of the TUI. It shows how much time has been spent in the current Explorbot session and can be toggled on and off with the `CTRL + t` key.

### Available Commands

**Application Commands:**
Expand All @@ -168,6 +170,12 @@ Explorbot provides a real-time TUI (Terminal User Interface) with three main are
- `I.fillField(locator, value)` - Fill form inputs
- All standard CodeceptJS commands are supported

### Keyboard Shortcuts
While using the TUI:
- `ESC` — enable the input line when Explorbot is waiting for commands
- `t` — toggle the session timer panel that shows elapsed time for the current session
- `Ctrl + C` — exit the current Explorbot session

## Command Line Usage

Explorbot provides several commands through the `maclay` CLI tool:
Expand Down
2 changes: 1 addition & 1 deletion src/components/ActivityPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const ActivityPane: React.FC = () => {
if (!activity) {
return (
<Box height={1} paddingX={1}>
<Text dimColor>Done. Press [ESC] to enable input</Text>
<Text dimColor>Done. Press [ESC] to enable input. Press [CTRL + t] to toggle session timer.</Text>
</Box>
);
}
Expand Down
25 changes: 22 additions & 3 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Text, useInput } from 'ink';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { CommandHandler } from '../command-handler.js';
import type { ExplorBot, ExplorBotOptions } from '../explorbot.ts';
import type { StateTransition, WebPageState } from '../state-manager.js';
Expand All @@ -9,6 +9,7 @@ import InputPane from './InputPane.js';
import LogPane from './LogPane.js';
import StateTransitionPane from './StateTransitionPane.js';
import TaskPane from './TaskPane.js';
import SessionTimer from './SessionTimer.js';

interface AppProps {
explorBot: ExplorBot;
Expand All @@ -17,6 +18,8 @@ interface AppProps {
}

export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = false }: AppProps) {
const sessionStartedAtRef = useRef<number>(Date.now());
const [showSessionTimer, setShowSessionTimer] = useState(false);
const [showInput, setShowInput] = useState(initialShowInput);
const [currentState, setCurrentState] = useState<WebPageState | null>(null);
const [lastTransition, setLastTransition] = useState<StateTransition | null>(null);
Expand Down Expand Up @@ -87,24 +90,40 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
return () => clearInterval(interval);
}, [explorBot]);

// Handle keyboard input - ESC to enable input, Ctrl-C to exit
useInput((input, key) => {
if (key.ctrl && input === 't') {
setShowSessionTimer((prev) => !prev);
return;
}

if (key.escape) {
setShowInput(true);
return;
}

if (key.ctrl && input === 'c') {
process.exit(0);
}
});


return (
<Box flexDirection="column">
<Box flexDirection="column" flexGrow={1}>
<LogPane verboseMode={explorBot.getOptions()?.verbose || false} />
</Box>

<Box height={3}>
<Box
height={3}
flexDirection="row"
justifyContent="space-between"
alignItems="center"
paddingX={1}
>
<ActivityPane />
{showSessionTimer && (
<SessionTimer startedAt={sessionStartedAtRef.current} />
)}
</Box>

{showInput && <Box height={1} />}
Expand Down
39 changes: 39 additions & 0 deletions src/components/SessionTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import { Box, Text } from 'ink';

type SessionTimerProps = {
startedAt: number;
};

const pad = (n: number) => String(n).padStart(2, '0');

const SessionTimer: React.FC<SessionTimerProps> = ({ startedAt }) => {
const [now, setNow] = useState(() => Date.now());

useEffect(() => {
const id = setInterval(() => {
setNow(Date.now());
}, 1000);

return () => clearInterval(id);
}, []);

const diffSec = Math.floor((now - startedAt) / 1000);
const seconds = diffSec % 60;
const minutes = Math.floor(diffSec / 60) % 60;
const hours = Math.floor(diffSec / 3600);

return (
<Box
borderStyle="round"
paddingX={1}
paddingY={0}
>
<Text>
Session time: {pad(hours)}:{pad(minutes)}:{pad(seconds)}
</Text>
</Box>
);
};

export default SessionTimer;
Loading