diff --git a/.agents/cli-ui-tester.ts b/.agents/cli-ui-tester.ts new file mode 100644 index 000000000..1e15ac1d2 --- /dev/null +++ b/.agents/cli-ui-tester.ts @@ -0,0 +1,319 @@ +import type { AgentDefinition } from './types/agent-definition' + +const definition: AgentDefinition = { + id: 'cli-ui-tester', + displayName: 'CLI UI Tester', + model: 'anthropic/claude-opus-4.5', + + spawnerPrompt: `Expert at testing Codebuff CLI functionality using tmux. + +**Use this agent after modifying:** +- \`cli/src/components/\` - UI components, layouts, rendering +- \`cli/src/hooks/\` - hooks that affect what users see +- Any CLI visual elements: borders, colors, spacing, text formatting + +**When to use:** After implementing CLI UI changes, use this to verify the visual output actually renders correctly. Unit tests and typechecks cannot catch layout bugs, rendering issues, or visual regressions. This agent captures real terminal output including colors and layout. + +**What it does:** Spawns tmux sessions, sends input to the CLI, captures terminal output, and validates behavior. + +**Paper trail:** Session logs are saved to \`debug/tmux-sessions/{session}/\`. Use \`read_files\` to view captures. + +**Your responsibilities as the parent agent:** +1. If \`scriptIssues\` is not empty, fix the scripts in \`scripts/tmux/\` based on the suggested fixes +2. Use \`read_files\` on the capture paths to see what the CLI displayed +3. Re-run the test after fixing any script issues`, + + inputSchema: { + prompt: { + type: 'string', + description: + 'Description of what CLI functionality to test (e.g., "test that the help command displays correctly", "verify authentication flow works")', + }, + }, + + outputMode: 'structured_output', + outputSchema: { + type: 'object', + properties: { + overallStatus: { + type: 'string', + enum: ['success', 'failure', 'partial'], + description: 'Overall test outcome', + }, + summary: { + type: 'string', + description: 'Brief summary of what was tested and the outcome', + }, + testResults: { + type: 'array', + items: { + type: 'object', + properties: { + testName: { + type: 'string', + description: 'Name/description of the test', + }, + passed: { type: 'boolean', description: 'Whether the test passed' }, + details: { + type: 'string', + description: 'Details about what happened', + }, + capturedOutput: { + type: 'string', + description: 'Relevant output captured from the CLI', + }, + }, + required: ['testName', 'passed'], + }, + description: 'Array of individual test results', + }, + scriptIssues: { + type: 'array', + items: { + type: 'object', + properties: { + script: { + type: 'string', + description: + 'Which script had the issue (e.g., "tmux-start.sh", "tmux-send.sh")', + }, + issue: { + type: 'string', + description: 'What went wrong when using the script', + }, + errorOutput: { + type: 'string', + description: 'The actual error message or unexpected output', + }, + suggestedFix: { + type: 'string', + description: + 'Suggested fix or improvement for the parent agent to implement', + }, + }, + required: ['script', 'issue', 'suggestedFix'], + }, + description: + 'Issues encountered with the helper scripts that the parent agent should fix', + }, + captures: { + type: 'array', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Path to the capture file (relative to project root)', + }, + label: { + type: 'string', + description: + 'What this capture shows (e.g., "initial-cli-state", "after-help-command")', + }, + timestamp: { + type: 'string', + description: 'When the capture was taken', + }, + }, + required: ['path', 'label'], + }, + description: + 'Paths to saved terminal captures for debugging - check debug/tmux-sessions/{session}/', + }, + }, + required: [ + 'overallStatus', + 'summary', + 'testResults', + 'scriptIssues', + 'captures', + ], + }, + includeMessageHistory: false, + + toolNames: [ + 'run_terminal_command', + 'read_files', + 'code_search', + 'set_output', + ], + + systemPrompt: `You are an expert at testing the Codebuff CLI using tmux. You have access to helper scripts that handle the complexities of tmux communication with the CLI. + +## Helper Scripts + +Use these scripts in \`scripts/tmux/\` for reliable CLI testing: + +### Unified Script (Recommended) + +\`\`\`bash +# Start a test session (returns session name) +SESSION=$(./scripts/tmux/tmux-cli.sh start) + +# Send input to the CLI +./scripts/tmux/tmux-cli.sh send "$SESSION" "/help" + +# Capture output (optionally wait first) +./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 3 + +# Stop the session when done +./scripts/tmux/tmux-cli.sh stop "$SESSION" + +# Stop all test sessions +./scripts/tmux/tmux-cli.sh stop --all +\`\`\` + +### Individual Scripts (More Options) + +\`\`\`bash +# Start with custom settings +./scripts/tmux/tmux-start.sh --name my-test --width 160 --height 40 + +# Send text (auto-presses Enter) +./scripts/tmux/tmux-send.sh my-test "your prompt here" + +# Send without pressing Enter +./scripts/tmux/tmux-send.sh my-test "partial" --no-enter + +# Send special keys +./scripts/tmux/tmux-send.sh my-test --key Escape +./scripts/tmux/tmux-send.sh my-test --key C-c + +# Capture with colors +./scripts/tmux/tmux-capture.sh my-test --colors + +# Save capture to file +./scripts/tmux/tmux-capture.sh my-test -o output.txt +\`\`\` + +## Why These Scripts? + +The scripts handle **bracketed paste mode** automatically. Standard \`tmux send-keys\` drops characters with the Codebuff CLI due to how OpenTUI processes keyboard input. The helper scripts wrap input in escape sequences (\`\\e[200~...\\e[201~\`) so you don't have to. + +## Typical Test Workflow + +\`\`\`bash +# 1. Start a session +SESSION=$(./scripts/tmux/tmux-cli.sh start) +echo "Testing in session: $SESSION" + +# 2. Verify CLI started +./scripts/tmux/tmux-cli.sh capture "$SESSION" + +# 3. Run your test +./scripts/tmux/tmux-cli.sh send "$SESSION" "/help" +sleep 2 +./scripts/tmux/tmux-cli.sh capture "$SESSION" + +# 4. Clean up +./scripts/tmux/tmux-cli.sh stop "$SESSION" +\`\`\` + +## Session Logs (Paper Trail) + +All session data is stored in **YAML format** in \`debug/tmux-sessions/{session-name}/\`: + +- \`session-info.yaml\` - Session metadata (start time, dimensions, status) +- \`commands.yaml\` - YAML array of all commands sent with timestamps +- \`capture-{sequence}-{label}.txt\` - Captures with YAML front-matter + +\`\`\`bash +# Capture with a descriptive label (recommended) +./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "after-help-command" --wait 2 + +# Capture saved to: debug/tmux-sessions/{session}/capture-001-after-help-command.txt +\`\`\` + +Each capture file has YAML front-matter with metadata: +\`\`\`yaml +--- +sequence: 1 +label: after-help-command +timestamp: 2025-01-01T12:00:30Z +after_command: "/help" +dimensions: + width: 120 + height: 30 +--- +[terminal content] +\`\`\` + +The capture path is printed to stderr. Both you and the parent agent can read these files to see exactly what the CLI displayed. + +## Viewing Session Data + +Use the **tmux-viewer** to inspect session data interactively or as JSON: + +\`\`\`bash +# Interactive TUI (for humans) +bun .agents/tmux-viewer/index.tsx "$SESSION" + +# JSON output (for AIs) - includes all captures, commands, and timeline +bun .agents/tmux-viewer/index.tsx "$SESSION" --json + +# List available sessions +bun .agents/tmux-viewer/index.tsx --list +\`\`\` + +The viewer parses all YAML data (session-info.yaml, commands.yaml, capture front-matter) and presents it in a unified format. + +## Debugging Tips + +- **Attach interactively**: \`tmux attach -t SESSION_NAME\` +- **List sessions**: \`./scripts/tmux/tmux-cli.sh list\` +- **View session logs**: \`ls debug/tmux-sessions/{session-name}/\` +- **Get help**: \`./scripts/tmux/tmux-cli.sh help\` or \`./scripts/tmux/tmux-start.sh --help\``, + + instructionsPrompt: `Instructions: + +1. **Use the helper scripts** in \`scripts/tmux/\` - they handle bracketed paste mode automatically + +2. **Start a test session**: + \`\`\`bash + SESSION=$(./scripts/tmux/tmux-cli.sh start) + \`\`\` + +3. **Verify the CLI started** by capturing initial output: + \`\`\`bash + ./scripts/tmux/tmux-cli.sh capture "$SESSION" + \`\`\` + +4. **Send commands** and capture responses: + \`\`\`bash + ./scripts/tmux/tmux-cli.sh send "$SESSION" "your command here" + ./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 3 + \`\`\` + +5. **Always clean up** when done: + \`\`\`bash + ./scripts/tmux/tmux-cli.sh stop "$SESSION" + \`\`\` + +6. **Use labels when capturing** to create a clear paper trail: + \`\`\`bash + ./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "initial-state" + ./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "after-help-command" --wait 2 + \`\`\` + +7. **Report results using set_output** - You MUST call set_output with structured results: + - \`overallStatus\`: "success", "failure", or "partial" + - \`summary\`: Brief description of what was tested + - \`testResults\`: Array of test outcomes with testName, passed (boolean), details, capturedOutput + - \`scriptIssues\`: Array of any problems with the helper scripts (IMPORTANT for the parent agent!) + - \`captures\`: Array of capture paths with labels (e.g., {path: "debug/tmux-sessions/cli-test-123/capture-...", label: "after-help"}) + +8. **If a helper script doesn't work correctly**, report it in \`scriptIssues\` with: + - \`script\`: Which script failed (e.g., "tmux-send.sh") + - \`issue\`: What went wrong + - \`errorOutput\`: The actual error message + - \`suggestedFix\`: How the parent agent should fix the script + + The parent agent CAN edit the scripts - you cannot. Your job is to identify issues clearly. + +9. **Always include captures** in your output so the parent agent can see what you saw. + +For advanced options, run \`./scripts/tmux/tmux-cli.sh help\` or check individual scripts with \`--help\`.`, +} + +export default definition diff --git a/.agents/package.json b/.agents/package.json index e6dd6fc4e..b6b4bc4ac 100644 --- a/.agents/package.json +++ b/.agents/package.json @@ -6,6 +6,7 @@ "scripts": { "typecheck": "bun x tsc --noEmit -p tsconfig.json", "test": "bun test __tests__", - "test:e2e": "bun test e2e" + "test:e2e": "bun test e2e", + "view-session": "bun run tmux-viewer/index.tsx" } } diff --git a/.agents/tmux-viewer/README.md b/.agents/tmux-viewer/README.md new file mode 100644 index 000000000..18646ca62 --- /dev/null +++ b/.agents/tmux-viewer/README.md @@ -0,0 +1,245 @@ +# tmux-viewer + +Interactive TUI for viewing tmux session logs. Designed to work for **both humans and AIs**. + +## Usage + +```bash +# Interactive TUI (for humans) +bun .agents/tmux-viewer/index.tsx + +# Start in replay mode (auto-plays through captures like a video) +bun .agents/tmux-viewer/index.tsx --replay + +# JSON output (for AIs) +bun .agents/tmux-viewer/index.tsx --json + +# Export as animated GIF +bun .agents/tmux-viewer/index.tsx --export-gif output.gif + +# Export with custom frame delay (default: 1500ms) +bun .agents/tmux-viewer/index.tsx --export-gif output.gif --frame-delay 2000 + +# Export with custom font size (default: 14px) +bun .agents/tmux-viewer/index.tsx --export-gif output.gif --font-size 16 + +# List available sessions +bun .agents/tmux-viewer/index.tsx --list + +# View most recent session (if no session specified) +bun .agents/tmux-viewer/index.tsx +``` + +Or using the npm script: + +```bash +cd .agents && bun run view-session +``` + +## Layout + +The TUI uses a vertical layout designed for clarity: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Session: my-session 120x30 5 cmds 10 captures │ ← Header +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ │ +│ │ [terminal output │ │ ← Capture +│ │ centered in │ │ View +│ │ muted border] │ │ +│ └──────────────────┘ │ +│ │ +├─ ⏸ Paused ──────────────────────────────────────────────────────┤ +│ ┌─○ [1] 12:00:00─┐ ┌─▶ [2] 12:00:05─┐ ┌─○ [3] 12:00:10─┐ │ ← Timeline +│ │ initial-state │ │ after-command │ │ final-state │ │ Cards +│ │ $ codebuff... │ │ $ /help │ │ $ /quit │ │ +│ └────────────────┘ └────────────────┘ └────────────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ ▶ 2/10 @1.5s space: play/pause +/-: speed ←→: navigate │ ← Footer +└─────────────────────────────────────────────────────────────────┘ +``` + +- **Header**: Session name, dimensions, command/capture counts +- **Capture View**: Terminal output centered with a muted border showing exact capture dimensions +- **Timeline**: Horizontal card-style navigation at the bottom, selected card stays centered +- **Footer**: Playback status, position, speed, and keyboard shortcuts + +## Features + +### For Humans (Interactive TUI) +- **Capture view**: Terminal output centered with visible boundary +- **Timeline panel**: Card-style navigation at the bottom with label and triggering command +- **Auto-centering**: Selected timeline card stays centered in view +- **Metadata display**: Session info, dimensions, command count +- **Replay mode**: Auto-play through captures like a video player +- **Keyboard shortcuts**: + - `←` / `→` or `h` / `l`: Navigate between captures + - `Space`: Play/pause replay + - `+` / `-`: Adjust playback speed (faster/slower) + - `r`: Restart from beginning + - `q` or Ctrl+C: Quit + - Use the `--json` flag on the CLI entrypoint for JSON output + +### Replay Mode + +Replay mode auto-advances through captures chronologically, like a video player: + +```bash +# Start replay immediately +bun .agents/tmux-viewer/index.tsx my-session --replay + +# Or press Space in the TUI to start/stop replay +``` + +**Playback controls:** +- `Space` - Toggle play/pause +- `+` or `=` - Speed up (shorter interval between captures) +- `-` or `_` - Slow down (longer interval between captures) +- `r` - Restart from the first capture +- `←` / `→` - Navigate captures (automatically pauses replay) + +**Available speeds:** 0.5s, 1.0s, 1.5s (default), 2.0s, 3.0s, 5.0s per capture + +The timeline panel title shows `▶ Playing` or `⏸ Paused`, and the footer shows current position (e.g., `2/10`), playback speed (e.g., `@1.5s`), and controls. + +### For AIs (JSON Output) +Use the `--json` flag to get structured output: + +```json +{ + "session": { + "session": "cli-test-1234567890", + "started": "2025-01-01T12:00:00Z", + "dimensions": { "width": 120, "height": 30 }, + "status": "active" + }, + "commands": [ + { "timestamp": "...", "type": "text", "input": "/help", "auto_enter": true } + ], + "captures": [ + { + "sequence": 1, + "label": "initial-state", + "timestamp": "...", + "after_command": null, + "dimensions": { "width": 120, "height": 30 }, + "path": "debug/tmux-sessions/.../capture-001-initial-state.txt", + "content": "[terminal output]" + } + ], + "timeline": [ + { "timestamp": "...", "type": "command", "data": {...} }, + { "timestamp": "...", "type": "capture", "data": {...} } + ] +} +``` + +## Data Format + +The viewer reads YAML-formatted session data from `debug/tmux-sessions/{session}/`: + +- `session-info.yaml` - Session metadata +- `commands.yaml` - Array of commands sent +- `capture-*.txt` - Capture files with YAML front-matter + +### Session Info (session-info.yaml) +```yaml +session: cli-test-1234567890 +started: 2025-01-01T12:00:00Z +started_local: Wed Jan 1 12:00:00 PST 2025 +dimensions: + width: 120 + height: 30 +status: active +``` + +### Commands (commands.yaml) +```yaml +- timestamp: 2025-01-01T12:00:05Z + type: text + input: "/help" + auto_enter: true +``` + +### Capture Files (capture-001-label.txt) +```yaml +--- +sequence: 1 +label: initial-state +timestamp: 2025-01-01T12:00:30Z +after_command: null +dimensions: + width: 120 + height: 30 +--- +[terminal content here] +``` + +## Integration with cli-ui-tester + +The `@cli-ui-tester` agent can use this viewer to inspect session data: + +```typescript +// In cli-ui-tester output +{ + captures: [ + { path: "debug/tmux-sessions/cli-test-123/capture-001-initial.txt", label: "initial" } + ] +} + +// Parent agent can view the session +// bun .agents/tmux-viewer/index.tsx cli-test-123 --json +``` + +## GIF Export + +The `--export-gif` flag renders the session replay as an animated GIF, perfect for: +- Sharing CLI demonstrations +- Embedding in documentation +- Bug reports and issue tracking +- Creating tutorials + +### GIF Export Options + +| Option | Description | Default | +|--------|-------------|--------| +| `--export-gif [path]` | Output file path | `-.gif` | +| `--frame-delay ` | Delay between frames in milliseconds | `1500` | +| `--font-size ` | Font size for terminal text | `14` | + +### Examples + +```bash +# Basic export (auto-names the file) +bun .agents/tmux-viewer/index.tsx my-session --export-gif + +# Specify output path +bun .agents/tmux-viewer/index.tsx my-session --export-gif demo.gif + +# Fast playback (500ms per frame) +bun .agents/tmux-viewer/index.tsx my-session --export-gif fast.gif --frame-delay 500 + +# Larger text for readability +bun .agents/tmux-viewer/index.tsx my-session --export-gif large.gif --font-size 18 +``` + +### GIF Output + +The exported GIF includes: +- Terminal content rendered as monospace text +- Frame labels showing capture sequence number and label +- Timestamps for each frame +- Dark terminal-style background +- Automatic sizing based on terminal dimensions + +## Development + +```bash +# Typecheck +cd .agents && bun run typecheck + +# Run directly +bun .agents/tmux-viewer/index.tsx --list +``` diff --git a/.agents/tmux-viewer/components/session-viewer.tsx b/.agents/tmux-viewer/components/session-viewer.tsx new file mode 100644 index 000000000..6cb18ba18 --- /dev/null +++ b/.agents/tmux-viewer/components/session-viewer.tsx @@ -0,0 +1,551 @@ +/** + * SessionViewer - Interactive TUI for viewing tmux session data + * + * Designed to be simple and predictable for both humans and AIs: + * - Humans: navigate captures with arrow keys / vim keys, or use replay mode + * - AIs: typically use the --json flag on the CLI entrypoint instead of the TUI + */ + +import { TextAttributes } from '@opentui/core' +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import type { ScrollBoxRenderable } from '@opentui/core' + +import { getTheme } from './theme' + +import type { SessionData, Capture } from '../types' +import type { ViewerTheme } from './theme' + +interface SessionViewerProps { + data: SessionData + onExit: () => void + /** + * Reserved for future use if we ever want a TUI hotkey to print JSON. + * For now, AIs should call the CLI with --json instead. + */ + onJsonOutput?: () => void + /** + * Start in replay mode (auto-playing through captures) + */ + startInReplayMode?: boolean +} + +// Available playback speeds (seconds per capture) +const PLAYBACK_SPEEDS = [0.5, 1.0, 1.5, 2.0, 3.0, 5.0] +const DEFAULT_SPEED_INDEX = 2 // 1.5 seconds + +export const SessionViewer: React.FC = ({ + data, + onExit, + startInReplayMode = false, +}) => { + const theme = getTheme() + const captures = data.captures + + const [selectedIndex, setSelectedIndex] = useState(() => + captures.length > 0 ? 0 : -1, + ) + + // Replay state + const [isPlaying, setIsPlaying] = useState(startInReplayMode) + const [speedIndex, setSpeedIndex] = useState(DEFAULT_SPEED_INDEX) + const playbackSpeed = PLAYBACK_SPEEDS[speedIndex] + const timerRef = useRef | null>(null) + + // Auto-advance effect for replay mode + useEffect(() => { + if (!isPlaying || captures.length === 0) { + return + } + + timerRef.current = setTimeout(() => { + setSelectedIndex((prev) => { + const next = prev + 1 + if (next >= captures.length) { + // Reached the end, stop playing + setIsPlaying(false) + return prev + } + return next + }) + }, playbackSpeed * 1000) + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current) + timerRef.current = null + } + } + }, [isPlaying, selectedIndex, playbackSpeed, captures.length]) + + // Replay control functions + const togglePlay = useCallback(() => { + if (captures.length === 0) return + // If at end and pressing play, restart from beginning + if (!isPlaying && selectedIndex >= captures.length - 1) { + setSelectedIndex(0) + } + setIsPlaying((prev) => !prev) + }, [captures.length, isPlaying, selectedIndex]) + + const increaseSpeed = useCallback(() => { + setSpeedIndex((prev) => Math.max(0, prev - 1)) // Lower index = faster + }, []) + + const decreaseSpeed = useCallback(() => { + setSpeedIndex((prev) => Math.min(PLAYBACK_SPEEDS.length - 1, prev + 1)) + }, []) + + // Keyboard input handling (q/Ctrl+C to quit, arrows + vim keys to navigate, space for play/pause) + useEffect(() => { + const handleKey = (key: string) => { + // Quit: q or Ctrl+C + if (key === 'q' || key === '\x03') { + onExit() + return + } + + // Space: toggle play/pause + if (key === ' ') { + togglePlay() + return + } + + // +/= : increase speed (faster) + if (key === '+' || key === '=') { + increaseSpeed() + return + } + + // -/_ : decrease speed (slower) + if (key === '-' || key === '_') { + decreaseSpeed() + return + } + + // r: restart from beginning + if (key === 'r') { + setSelectedIndex(0) + return + } + + if (captures.length === 0) { + return + } + + // Stop playback on manual navigation + const stopAndNavigate = () => { + setIsPlaying(false) + } + + // Left: arrow left or h => previous capture + if (key === '\x1b[D' || key === 'h') { + stopAndNavigate() + setSelectedIndex((prev) => Math.max(0, prev - 1)) + return + } + + // Right: arrow right or l => next capture + if (key === '\x1b[C' || key === 'l') { + stopAndNavigate() + setSelectedIndex((prev) => + Math.min(captures.length - 1, Math.max(0, prev + 1)), + ) + } + } + + const stdin: NodeJS.ReadStream = process.stdin as any + const onData = (chunk: Buffer) => { + handleKey(chunk.toString()) + } + + stdin.setRawMode?.(true) + stdin.resume() + stdin.on('data', onData) + + return () => { + // Remove only this listener to avoid interfering with other handlers + if (typeof (stdin as any).off === 'function') { + ;(stdin as any).off('data', onData) + } else { + stdin.removeListener('data', onData as any) + } + } + }, [captures.length, onExit, togglePlay, increaseSpeed, decreaseSpeed]) + + const selectedCapture: Capture | undefined = + selectedIndex >= 0 && selectedIndex < captures.length + ? captures[selectedIndex] + : undefined + + return ( + + {/* Header */} + + + {/* Main content area */} + + + + + + + {/* Footer / help text with replay controls */} +