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/cli/tmux.knowledge.md b/cli/tmux.knowledge.md index 2300bcb30..5e3b42360 100644 --- a/cli/tmux.knowledge.md +++ b/cli/tmux.knowledge.md @@ -2,7 +2,77 @@ This document covers essential knowledge for using tmux to test and automate the Codebuff CLI. -## Critical: Sending Input to the CLI +## Recommended: Use the Helper Scripts + +**For most CLI testing, use the helper scripts in `scripts/tmux/`** instead of raw tmux commands. These scripts handle bracketed paste mode, session management, and logging automatically. + +### Quick Start + +```bash +# Start a test session +SESSION=$(./scripts/tmux/tmux-cli.sh start) + +# Send a command +./scripts/tmux/tmux-cli.sh send "$SESSION" "/help" + +# Capture output (auto-saves to debug/tmux-sessions/) +./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 2 --label "after-help" + +# Stop the session +./scripts/tmux/tmux-cli.sh stop "$SESSION" +``` + +### Available Scripts + +| Script | Purpose | +|--------|--------| +| `tmux-cli.sh` | Unified interface with subcommands (start, send, capture, stop, list) | +| `tmux-start.sh` | Start a CLI test session with custom name/dimensions | +| `tmux-send.sh` | Send input using bracketed paste mode (handles escaping) | +| `tmux-capture.sh` | Capture terminal output with YAML metadata | +| `tmux-stop.sh` | Stop individual or all test sessions | + +### Session Logs + +All session data is saved to `debug/tmux-sessions/{session}/` in YAML format: +- `session-info.yaml` - Session metadata +- `commands.yaml` - All commands sent with timestamps +- `capture-*.txt` - Terminal captures with YAML front-matter + +### Why Use Helper Scripts? + +1. Automatic **bracketed paste mode** so CLI input is reliable and characters are not dropped. +2. Automatic **session logging** in `debug/tmux-sessions/{session}/` so you always have a reproducible paper trail. +3. A shared **YAML format** consumed by both humans (via `tmux-viewer` TUI) and AIs (via `--json` output and the `@cli-ui-tester` agent). + +### Viewing Session Data + +Use the **tmux-viewer** to inspect sessions: + +```bash +# Interactive TUI (for humans) +bun .agents/tmux-viewer/index.tsx + +# JSON output (for AI consumption) +bun .agents/tmux-viewer/index.tsx --json + +# List available sessions +bun .agents/tmux-viewer/index.tsx --list +``` + +### CLI Tmux Tester Agent + +For automated testing, use the `@cli-ui-tester` agent which wraps all of this with structured output reporting. + +See `scripts/tmux/README.md` for comprehensive documentation. + +--- + +## Manual Approach (Understanding the Internals) + +The sections below explain how tmux communication with the CLI works at a low level. This is useful for understanding why the helper scripts exist and for debugging edge cases. + +### Critical: Sending Input to the CLI **Standard `tmux send-keys` does NOT work with the Codebuff CLI.** Characters are dropped or garbled due to how OpenTUI handles keyboard input. diff --git a/packages/agent-runtime/src/templates/__tests__/strings.test.ts b/packages/agent-runtime/src/templates/__tests__/strings.test.ts new file mode 100644 index 000000000..89d539625 --- /dev/null +++ b/packages/agent-runtime/src/templates/__tests__/strings.test.ts @@ -0,0 +1,298 @@ +import { TEST_AGENT_RUNTIME_IMPL } from '@codebuff/common/testing/impl/agent-runtime' +import { describe, test, expect, mock } from 'bun:test' +import { z } from 'zod/v4' + +import { getAgentPrompt } from '../strings' + +import type { AgentTemplate } from '../types' +import type { AgentState } from '@codebuff/common/types/session-state' +import type { ProjectFileContext } from '@codebuff/common/util/file' + +/** Create a mock logger using bun:test mock() for better test consistency */ +const createMockLogger = () => ({ + debug: mock(() => {}), + info: mock(() => {}), + warn: mock(() => {}), + error: mock(() => {}), +}) + +const createMockFileContext = (): ProjectFileContext => ({ + projectRoot: '/test', + cwd: '/test', + fileTree: [], + fileTokenScores: {}, + knowledgeFiles: {}, + gitChanges: { + status: '', + diff: '', + diffCached: '', + lastCommitMessages: '', + }, + changesSinceLastChat: {}, + shellConfigFiles: {}, + agentTemplates: {}, + customToolDefinitions: {}, + systemInfo: { + platform: 'test', + shell: 'test', + nodeVersion: 'test', + arch: 'test', + homedir: '/home/test', + cpus: 1, + }, +}) + +const createMockAgentState = (agentType: string): AgentState => ({ + agentId: 'test-agent-id', + agentType, + runId: 'test-run-id', + parentId: undefined, + messageHistory: [], + output: undefined, + stepsRemaining: 10, + creditsUsed: 0, + directCreditsUsed: 0, + childRunIds: [], + ancestorRunIds: [], + contextTokenCount: 0, + agentContext: {}, + subagents: [], + systemPrompt: '', + toolDefinitions: {}, +}) + +const createMockAgentTemplate = ( + overrides: Partial = {}, +): AgentTemplate => ({ + id: 'test-agent', + displayName: 'Test Agent', + model: 'gpt-4o-mini', + inputSchema: {}, + outputMode: 'last_message', + includeMessageHistory: false, + inheritParentSystemPrompt: false, + mcpServers: {}, + toolNames: [], + spawnableAgents: [], + systemPrompt: '', + instructionsPrompt: 'Test instructions', + stepPrompt: '', + ...overrides, +}) + +describe('getAgentPrompt', () => { + describe('spawnerPrompt inclusion in instructionsPrompt', () => { + test('includes spawnerPrompt for each spawnable agent with spawnerPrompt defined', async () => { + const filePickerTemplate = createMockAgentTemplate({ + id: 'file-picker', + displayName: 'File Picker', + spawnerPrompt: 'Spawn to find relevant files in a codebase', + }) + + const codeSearcherTemplate = createMockAgentTemplate({ + id: 'code-searcher', + displayName: 'Code Searcher', + spawnerPrompt: 'Mechanically runs multiple code search queries', + }) + + const mainAgentTemplate = createMockAgentTemplate({ + id: 'main-agent', + displayName: 'Main Agent', + spawnableAgents: ['file-picker', 'code-searcher'], + instructionsPrompt: 'Main agent instructions.', + }) + + const agentTemplates: Record = { + 'main-agent': mainAgentTemplate, + 'file-picker': filePickerTemplate, + 'code-searcher': codeSearcherTemplate, + } + + const result = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'instructionsPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(result).toBeDefined() + expect(result).toContain('You can spawn the following agents:') + expect(result).toContain('- file-picker: Spawn to find relevant files in a codebase') + expect(result).toContain('- code-searcher: Mechanically runs multiple code search queries') + }) + + test('includes only agent name when spawnerPrompt is not defined', async () => { + const agentWithoutSpawnerPrompt = createMockAgentTemplate({ + id: 'no-prompt-agent', + displayName: 'No Prompt Agent', + // spawnerPrompt is not defined + }) + + const mainAgentTemplate = createMockAgentTemplate({ + id: 'main-agent', + displayName: 'Main Agent', + spawnableAgents: ['no-prompt-agent'], + instructionsPrompt: 'Main agent instructions.', + }) + + const agentTemplates: Record = { + 'main-agent': mainAgentTemplate, + 'no-prompt-agent': agentWithoutSpawnerPrompt, + } + + const result = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'instructionsPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(result).toBeDefined() + expect(result).toContain('You can spawn the following agents:') + expect(result).toContain('- no-prompt-agent') + // Should not have a colon after the agent name when there's no spawnerPrompt + expect(result).not.toContain('- no-prompt-agent:') + }) + + test('handles mix of agents with and without spawnerPrompt', async () => { + const agentWithPrompt = createMockAgentTemplate({ + id: 'with-prompt', + displayName: 'Agent With Prompt', + spawnerPrompt: 'This agent has a description', + }) + + const agentWithoutPrompt = createMockAgentTemplate({ + id: 'without-prompt', + displayName: 'Agent Without Prompt', + // spawnerPrompt is not defined + }) + + const mainAgentTemplate = createMockAgentTemplate({ + id: 'main-agent', + displayName: 'Main Agent', + spawnableAgents: ['with-prompt', 'without-prompt'], + instructionsPrompt: 'Main agent instructions.', + }) + + const agentTemplates: Record = { + 'main-agent': mainAgentTemplate, + 'with-prompt': agentWithPrompt, + 'without-prompt': agentWithoutPrompt, + } + + const result = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'instructionsPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(result).toBeDefined() + expect(result).toContain('- with-prompt: This agent has a description') + expect(result).toContain('- without-prompt') + expect(result).not.toContain('- without-prompt:') + }) + + test('does not include spawnable agents section when no spawnable agents defined', async () => { + const mainAgentTemplate = createMockAgentTemplate({ + id: 'main-agent', + displayName: 'Main Agent', + spawnableAgents: [], + instructionsPrompt: 'Main agent instructions.', + }) + + const agentTemplates: Record = { + 'main-agent': mainAgentTemplate, + } + + const result = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'instructionsPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(result).toBeDefined() + expect(result).not.toContain('You can spawn the following agents:') + }) + + test('does not include spawnable agents for non-instructionsPrompt types', async () => { + const filePickerTemplate = createMockAgentTemplate({ + id: 'file-picker', + displayName: 'File Picker', + spawnerPrompt: 'Spawn to find relevant files in a codebase', + }) + + const mainAgentTemplate = createMockAgentTemplate({ + id: 'main-agent', + displayName: 'Main Agent', + spawnableAgents: ['file-picker'], + systemPrompt: 'System prompt content.', + stepPrompt: 'Step prompt content.', + }) + + const agentTemplates: Record = { + 'main-agent': mainAgentTemplate, + 'file-picker': filePickerTemplate, + } + + // Test systemPrompt - should not include spawnable agents + const systemResult = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'systemPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(systemResult).toBeDefined() + expect(systemResult).not.toContain('You can spawn the following agents:') + + // Test stepPrompt - should not include spawnable agents + const stepResult = await getAgentPrompt({ + agentTemplate: mainAgentTemplate, + promptType: { type: 'stepPrompt' }, + fileContext: createMockFileContext(), + agentState: createMockAgentState('main-agent'), + agentTemplates, + additionalToolDefinitions: async () => ({}), + logger: createMockLogger(), + apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey, + databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache, + fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase, + }) + + expect(stepResult).toBeDefined() + expect(stepResult).not.toContain('You can spawn the following agents:') + }) + }) +}) diff --git a/packages/agent-runtime/src/templates/strings.ts b/packages/agent-runtime/src/templates/strings.ts index dd5c5f322..c6e6ab935 100644 --- a/packages/agent-runtime/src/templates/strings.ts +++ b/packages/agent-runtime/src/templates/strings.ts @@ -202,8 +202,21 @@ export async function getAgentPrompt( } } else if (spawnableAgents.length > 0) { // For non-inherited tools, agents are already defined as tools with full schemas, - // so we just list the available agent IDs here - addendum += `\n\nYou can spawn the following agents: ${spawnableAgents.join(', ')}.` + // so we add the spawnerPrompt for each agent + const agentDescriptions = await Promise.all( + spawnableAgents.map(async (agentType) => { + const template = await getAgentTemplate({ + ...params, + agentId: agentType, + localAgentTemplates: agentTemplates, + }) + if (template?.spawnerPrompt) { + return `- ${agentType}: ${template.spawnerPrompt}` + } + return `- ${agentType}` + }), + ) + addendum += `\n\nYou can spawn the following agents:\n\n${agentDescriptions.join('\n')}` } // Add output schema information if defined diff --git a/scripts/tmux/README.md b/scripts/tmux/README.md new file mode 100644 index 000000000..c0be26b1e --- /dev/null +++ b/scripts/tmux/README.md @@ -0,0 +1,272 @@ +# tmux CLI Testing Scripts + +Helper scripts for testing the Codebuff CLI using tmux. These scripts abstract away the complexity of tmux communication, particularly the **bracketed paste mode** requirement. + +## Features + +- **Automatic bracketed paste mode** - Input is wrapped in escape sequences so characters don't get dropped +- **Automatic session logs** - Every capture is saved to `debug/tmux-sessions/{session}/` for debugging +- **Paper trail** - Both the subagent and parent agent can review what the CLI displayed + +## Why These Scripts? + +The Codebuff CLI uses OpenTUI for rendering, which processes keyboard input character-by-character. When tmux sends characters rapidly via `send-keys`, they get dropped or garbled. These scripts automatically wrap input in bracketed paste escape sequences (`\e[200~...\e[201~`), which tells the terminal to process the input atomically. + +**Without these scripts:** +```bash +# ❌ Characters get dropped! +tmux send-keys -t session "hello world" +# Result: Only "d" appears in the input! +``` + +**With these scripts:** +```bash +# ✅ Works correctly +./scripts/tmux/tmux-send.sh session "hello world" +# Result: "hello world" appears correctly +``` + +## Quick Start + +```bash +# Start a test session +SESSION=$(./scripts/tmux/tmux-cli.sh start) +echo "Started session: $SESSION" + +# Send a command +./scripts/tmux/tmux-cli.sh send "$SESSION" "/help" + +# Wait and capture output +./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 2 + +# Clean up +./scripts/tmux/tmux-cli.sh stop "$SESSION" +``` + +## Scripts + +### `tmux-cli.sh` (Unified Interface) + +The main entry point with subcommands: + +```bash +./scripts/tmux/tmux-cli.sh start # Start a session +./scripts/tmux/tmux-cli.sh send # Send input +./scripts/tmux/tmux-cli.sh capture # Capture output +./scripts/tmux/tmux-cli.sh stop # Stop a session +./scripts/tmux/tmux-cli.sh list # List sessions +./scripts/tmux/tmux-cli.sh help # Show help +``` + +### `tmux-start.sh` + +Start a new tmux session with the CLI. + +```bash +# Default settings +./scripts/tmux/tmux-start.sh +# Output: cli-test-1234567890 + +# Custom session name +./scripts/tmux/tmux-start.sh --name my-test + +# Custom dimensions +./scripts/tmux/tmux-start.sh -w 160 -h 40 + +# Custom wait time for CLI initialization +./scripts/tmux/tmux-start.sh --wait 6 +``` + +### `tmux-send.sh` + +Send input to a running session. + +```bash +# Send text (auto-presses Enter) +./scripts/tmux/tmux-send.sh SESSION "your prompt" + +# Send without pressing Enter +./scripts/tmux/tmux-send.sh SESSION "partial" --no-enter + +# Send special keys +./scripts/tmux/tmux-send.sh SESSION --key Escape +./scripts/tmux/tmux-send.sh SESSION --key C-c +./scripts/tmux/tmux-send.sh SESSION --key Enter +``` + +### `tmux-capture.sh` + +Capture output from a session. **Automatically saves captures** to `debug/tmux-sessions/{session}/`. + +```bash +# Basic capture (auto-saves to session logs) +./scripts/tmux/tmux-capture.sh SESSION + +# Capture with a descriptive label (recommended) +./scripts/tmux/tmux-capture.sh SESSION --label "after-help-command" +# Saves to: debug/tmux-sessions/SESSION/capture-{timestamp}-after-help-command.txt + +# Wait before capturing +./scripts/tmux/tmux-capture.sh SESSION --wait 3 + +# Capture without auto-saving to session logs +./scripts/tmux/tmux-capture.sh SESSION --no-save + +# Preserve colors +./scripts/tmux/tmux-capture.sh SESSION --colors + +# Save to specific file (disables auto-save) +./scripts/tmux/tmux-capture.sh SESSION -o output.txt +``` + +### `tmux-stop.sh` + +Stop/kill sessions. + +```bash +# Stop specific session +./scripts/tmux/tmux-stop.sh SESSION + +# Stop all test sessions +./scripts/tmux/tmux-stop.sh --all + +# List sessions first, then stop +./scripts/tmux/tmux-stop.sh --list SESSION +``` + +## Example: Full Test Workflow + +```bash +#!/bin/bash + +# Start session +SESSION=$(./scripts/tmux/tmux-cli.sh start --name auth-test) +echo "Testing authentication in: $SESSION" + +# Verify CLI started (capture auto-saved with label) +./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "initial-state" + +# Send /status command +./scripts/tmux/tmux-cli.sh send "$SESSION" "/status" +./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 2 --label "after-status" + +# Test a prompt +./scripts/tmux/tmux-cli.sh send "$SESSION" "hello world" +./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 5 --label "after-prompt" + +# Clean up +./scripts/tmux/tmux-cli.sh stop "$SESSION" +echo "Test complete!" + +# View all session logs +ls -la debug/tmux-sessions/$SESSION/ +``` + +## Session Logs Directory Structure + +``` +debug/tmux-sessions/ +└── cli-test-1234567890/ + ├── session-info.yaml # Session metadata (YAML format) + ├── commands.yaml # Log of all commands sent (YAML array) + ├── capture-001-initial-state.txt # Captures with YAML front-matter + ├── capture-002-after-status.txt + └── capture-003-after-prompt.txt +``` + +### 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 Log (commands.yaml) + +The `commands.yaml` file records every input sent to the CLI as a YAML array: + +```yaml +- timestamp: 2025-01-01T12:00:05Z + type: text + input: "/help" + auto_enter: true + +- timestamp: 2025-01-01T12:00:10Z + type: text + input: "hello world" + auto_enter: true + +- timestamp: 2025-01-01T12:00:15Z + type: key + input: "Escape" + +- timestamp: 2025-01-01T12:00:20Z + type: text + input: "partial input" + auto_enter: false +``` + +### Capture Files with YAML Front-Matter + +Each capture file includes YAML front-matter with metadata: + +```yaml +--- +sequence: 1 +label: initial-state +timestamp: 2025-01-01T12:00:30Z +after_command: null +dimensions: + width: 120 + height: 30 +--- +[actual terminal output below] +``` + +The front-matter provides: +- **sequence**: Order of captures (1, 2, 3, ...) +- **label**: Descriptive label if provided via `--label` +- **timestamp**: ISO 8601 timestamp +- **after_command**: The last command sent before this capture (or `null`) +- **dimensions**: Terminal dimensions at capture time + +This structured format enables both human reading and programmatic parsing (AI agents, viewers, etc.). + +## Debugging + +### Attach to a Session Interactively + +```bash +tmux attach -t SESSION_NAME +# Detach with Ctrl+B, D +``` + +### List All Sessions + +```bash +./scripts/tmux/tmux-cli.sh list +# or +tmux list-sessions +``` + +### Check If Session Exists + +```bash +tmux has-session -t SESSION_NAME && echo "exists" || echo "not found" +``` + +## Prerequisites + +- **tmux** must be installed: + - macOS: `brew install tmux` + - Ubuntu: `sudo apt-get install tmux` + - Arch: `sudo pacman -S tmux` + +## Used By + +These scripts are used by the `@cli-ui-tester` agent (`.agents/cli-ui-tester.ts`) to automate CLI testing. diff --git a/scripts/tmux/tmux-capture.sh b/scripts/tmux/tmux-capture.sh new file mode 100755 index 000000000..ff6c74c47 --- /dev/null +++ b/scripts/tmux/tmux-capture.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash + +####################################################################### +# tmux-capture.sh - Capture output from a tmux session +####################################################################### +# +# DESCRIPTION: +# Captures the current terminal output from a tmux session. +# Automatically saves captures to debug/tmux-sessions/{session}/ +# Useful for verifying CLI behavior and debugging. +# +# USAGE: +# ./scripts/tmux/tmux-capture.sh SESSION_NAME [OPTIONS] +# +# ARGUMENTS: +# SESSION_NAME Name of the tmux session +# +# OPTIONS: +# -c, --colors Preserve ANSI color codes in output +# -s, --start LINE Start capture from this line (default: -) +# -e, --end LINE End capture at this line (default: -) +# -o, --output FILE Write output to file instead of stdout +# --wait SECONDS Wait this many seconds before capturing (default: 0) +# --no-save Don't auto-save to session logs directory +# -l, --label LABEL Add a label to the capture filename +# --help Show this help message +# +# SESSION LOGS: +# By default, captures are automatically saved to: +# debug/tmux-sessions/{session-name}/capture-{timestamp}.txt +# +# The capture path is printed to stderr so you can capture it: +# CAPTURE_PATH=$(./scripts/tmux/tmux-capture.sh session 2>&1 >/dev/null) +# +# EXAMPLES: +# # Capture current output (auto-saves to session logs) +# ./scripts/tmux/tmux-capture.sh cli-test-123 +# +# # Capture with a label for the log file +# ./scripts/tmux/tmux-capture.sh cli-test-123 --label "after-help-command" +# +# # Capture with colors preserved +# ./scripts/tmux/tmux-capture.sh cli-test-123 --colors +# +# # Wait 2 seconds then capture (for async responses) +# ./scripts/tmux/tmux-capture.sh cli-test-123 --wait 2 +# +# # Save to specific file (disables auto-save to session logs) +# ./scripts/tmux/tmux-capture.sh cli-test-123 -o output.txt +# +# # Capture without auto-saving to session logs +# ./scripts/tmux/tmux-capture.sh cli-test-123 --no-save +# +# EXIT CODES: +# 0 - Success (output printed to stdout or file) +# 1 - Error (session not found) +# +####################################################################### + +set -e + +# Defaults +COLORS=false +START_LINE="-" +END_LINE="-" +OUTPUT_FILE="" +WAIT_SECONDS=0 +AUTO_SAVE=true +LABEL="" +SEQUENCE_FILE="" + +# Check minimum arguments +if [[ $# -lt 1 ]]; then + echo "Usage: $0 SESSION_NAME [OPTIONS]" >&2 + echo "Run with --help for more information" >&2 + exit 1 +fi + +# First argument is session name +SESSION_NAME="$1" +shift + +# Handle --help first +if [[ "$SESSION_NAME" == "--help" ]]; then + head -n 60 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 +fi + +# Parse remaining arguments +while [[ $# -gt 0 ]]; do + case $1 in + -c|--colors) + COLORS=true + shift + ;; + -s|--start) + START_LINE="$2" + shift 2 + ;; + -e|--end) + END_LINE="$2" + shift 2 + ;; + -o|--output) + OUTPUT_FILE="$2" + AUTO_SAVE=false # Manual output disables auto-save + shift 2 + ;; + --wait) + WAIT_SECONDS="$2" + shift 2 + ;; + --no-save) + AUTO_SAVE=false + shift + ;; + -l|--label) + LABEL="$2" + shift 2 + ;; + --help) + head -n 60 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +# Verify session exists +if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + echo "❌ Session '$SESSION_NAME' not found" >&2 + echo " Run: tmux list-sessions" >&2 + exit 1 +fi + +# Wait if requested +if [[ "$WAIT_SECONDS" -gt 0 ]]; then + sleep "$WAIT_SECONDS" +fi + +# Build capture command +CAPTURE_CMD="tmux capture-pane -t \"$SESSION_NAME\" -p" + +if [[ "$COLORS" == true ]]; then + CAPTURE_CMD="$CAPTURE_CMD -e" +fi + +if [[ "$START_LINE" != "-" ]]; then + CAPTURE_CMD="$CAPTURE_CMD -S $START_LINE" +fi + +if [[ "$END_LINE" != "-" ]]; then + CAPTURE_CMD="$CAPTURE_CMD -E $END_LINE" +fi + +# Get project root for session logs directory +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SESSION_DIR="$PROJECT_ROOT/debug/tmux-sessions/$SESSION_NAME" + +# Execute capture +if [[ -n "$OUTPUT_FILE" ]]; then + eval "$CAPTURE_CMD" > "$OUTPUT_FILE" +else + # Capture to variable first + CAPTURED_OUTPUT=$(eval "$CAPTURE_CMD") + + # Auto-save capture if enabled + if [[ "$AUTO_SAVE" == true ]]; then + mkdir -p "$SESSION_DIR" + + # Get sequence number from counter file + SEQUENCE_FILE="$SESSION_DIR/.capture-sequence" + if [[ -f "$SEQUENCE_FILE" ]]; then + SEQUENCE=$(cat "$SEQUENCE_FILE") + else + SEQUENCE=0 + fi + SEQUENCE=$((SEQUENCE + 1)) + echo "$SEQUENCE" > "$SEQUENCE_FILE" + + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + ISO_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + # Build filename with sequence prefix + SEQUENCE_PADDED=$(printf "%03d" "$SEQUENCE") + if [[ -n "$LABEL" ]]; then + CAPTURE_FILE="$SESSION_DIR/capture-${SEQUENCE_PADDED}-${LABEL}.txt" + else + CAPTURE_FILE="$SESSION_DIR/capture-${SEQUENCE_PADDED}-${TIMESTAMP}.txt" + fi + + # Get the last command from commands.yaml (if exists) + AFTER_COMMAND="null" + if [[ -f "$SESSION_DIR/commands.yaml" ]]; then + # Get the last input from the commands.yaml file + LAST_INPUT=$(grep '^ input:' "$SESSION_DIR/commands.yaml" | tail -1 | sed 's/^ input: //') + if [[ -n "$LAST_INPUT" ]]; then + AFTER_COMMAND="$LAST_INPUT" + fi + fi + + # Get terminal dimensions + TERM_WIDTH=$(tmux display-message -t "$SESSION_NAME" -p '#{window_width}' 2>/dev/null || echo "unknown") + TERM_HEIGHT=$(tmux display-message -t "$SESSION_NAME" -p '#{window_height}' 2>/dev/null || echo "unknown") + + # Write capture with YAML front-matter + cat > "$CAPTURE_FILE" << EOF +--- +sequence: $SEQUENCE +label: ${LABEL:-null} +timestamp: $ISO_TIMESTAMP +after_command: $AFTER_COMMAND +dimensions: + width: $TERM_WIDTH + height: $TERM_HEIGHT +--- +$CAPTURED_OUTPUT +EOF + # Print capture path to stderr so it can be captured separately + echo "$CAPTURE_FILE" >&2 + fi + + # Output to stdout + echo "$CAPTURED_OUTPUT" +fi diff --git a/scripts/tmux/tmux-cli.sh b/scripts/tmux/tmux-cli.sh new file mode 100755 index 000000000..9f201ca49 --- /dev/null +++ b/scripts/tmux/tmux-cli.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +####################################################################### +# tmux-cli.sh - Unified CLI testing helper using tmux +####################################################################### +# +# DESCRIPTION: +# A unified script for testing the Codebuff CLI using tmux. +# Provides subcommands for starting, sending input, capturing output, +# and stopping test sessions. +# +# USAGE: +# ./scripts/tmux/tmux-cli.sh [arguments] +# +# COMMANDS: +# start Start a new CLI test session +# send Send input to a session (uses bracketed paste) +# capture Capture output from a session +# stop Stop a session +# list List all active tmux sessions +# help Show this help message +# +# QUICK START: +# # Start a test session +# SESSION=$(./scripts/tmux/tmux-cli.sh start) +# echo "Started session: $SESSION" +# +# # Send a command +# ./scripts/tmux/tmux-cli.sh send "$SESSION" "/help" +# +# # Capture output with a label (auto-saves screenshot) +# ./scripts/tmux/tmux-cli.sh capture "$SESSION" --label "after-help" --wait 2 +# +# # Clean up +# ./scripts/tmux/tmux-cli.sh stop "$SESSION" +# +# SESSION LOGS: +# Captures are automatically saved to debug/tmux-sessions/{session}/ +# Use --label to add descriptive names to capture files. +# +# EXAMPLES: +# # Full test workflow +# SESSION=$(./scripts/tmux/tmux-cli.sh start --name my-test) +# ./scripts/tmux/tmux-cli.sh send "$SESSION" "hello world" +# ./scripts/tmux/tmux-cli.sh capture "$SESSION" --wait 3 +# ./scripts/tmux/tmux-cli.sh stop "$SESSION" +# +# # Stop all test sessions +# ./scripts/tmux/tmux-cli.sh stop --all +# +# # Get help for a specific command +# ./scripts/tmux/tmux-cli.sh start --help +# +# INDIVIDUAL SCRIPTS: +# For more options, use the individual scripts directly: +# - scripts/tmux/tmux-start.sh +# - scripts/tmux/tmux-send.sh +# - scripts/tmux/tmux-capture.sh +# - scripts/tmux/tmux-stop.sh +# +####################################################################### + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +show_help() { + head -n 58 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' +} + +show_short_help() { + echo "Usage: $0 [arguments]" + echo "" + echo "Commands:" + echo " start Start a new CLI test session" + echo " send Send input to a session" + echo " capture Capture output from a session" + echo " stop Stop a session" + echo " list List all active tmux sessions" + echo " help Show full help message" + echo "" + echo "Run '$0 --help' for command-specific help" +} + +# Check for command +if [[ $# -lt 1 ]]; then + show_short_help + exit 1 +fi + +COMMAND="$1" +shift + +case "$COMMAND" in + start) + exec "$SCRIPT_DIR/tmux-start.sh" "$@" + ;; + send) + exec "$SCRIPT_DIR/tmux-send.sh" "$@" + ;; + capture) + exec "$SCRIPT_DIR/tmux-capture.sh" "$@" + ;; + stop) + exec "$SCRIPT_DIR/tmux-stop.sh" "$@" + ;; + list) + echo "Active tmux sessions:" + tmux list-sessions 2>/dev/null || echo " (none)" + ;; + help|--help|-h) + show_help + ;; + *) + echo "Unknown command: $COMMAND" >&2 + echo "" + show_short_help + exit 1 + ;; +esac diff --git a/scripts/tmux/tmux-send.sh b/scripts/tmux/tmux-send.sh new file mode 100755 index 000000000..7a5d8482b --- /dev/null +++ b/scripts/tmux/tmux-send.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +####################################################################### +# tmux-send.sh - Send input to the Codebuff CLI in a tmux session +####################################################################### +# +# DESCRIPTION: +# Sends text input to a tmux session running the Codebuff CLI. +# Uses BRACKETED PASTE MODE which is REQUIRED for the CLI to receive +# input correctly. Standard tmux send-keys drops characters! +# +# IMPORTANT: +# This script handles the bracketed paste escape sequences automatically. +# You do NOT need to add escape sequences to your input. +# +# USAGE: +# ./scripts/tmux/tmux-send.sh SESSION_NAME "your text here" +# ./scripts/tmux/tmux-send.sh SESSION_NAME --key KEY +# +# ARGUMENTS: +# SESSION_NAME Name of the tmux session +# TEXT Text to send (will be wrapped in bracketed paste) +# +# OPTIONS: +# --key KEY Send a special key instead of text +# Supported: Enter, Escape, Up, Down, Left, Right, +# C-c, C-u, C-d, Tab +# --no-enter Don't automatically press Enter after text +# --help Show this help message +# +# EXAMPLES: +# # Send a command to the CLI +# ./scripts/tmux/tmux-send.sh cli-test-123 "/help" +# +# # Send text without pressing Enter +# ./scripts/tmux/tmux-send.sh cli-test-123 "partial text" --no-enter +# +# # Send a special key +# ./scripts/tmux/tmux-send.sh cli-test-123 --key Escape +# +# # Send Ctrl+C to interrupt +# ./scripts/tmux/tmux-send.sh cli-test-123 --key C-c +# +# WHY BRACKETED PASTE? +# The Codebuff CLI uses OpenTUI for rendering, which processes keyboard +# input character-by-character. When tmux sends characters rapidly, +# they get dropped or garbled. Bracketed paste mode (\e[200~...\e[201~) +# tells the terminal to treat the input as a paste operation, which is +# processed atomically. +# +# EXIT CODES: +# 0 - Success +# 1 - Error (missing arguments, session not found) +# +####################################################################### + +set -e + +# Get project root for logging +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Defaults +AUTO_ENTER=true +SPECIAL_KEY="" + +# Check minimum arguments +if [[ $# -lt 1 ]]; then + echo "Usage: $0 SESSION_NAME \"text\" [OPTIONS]" >&2 + echo " $0 SESSION_NAME --key KEY" >&2 + echo "Run with --help for more information" >&2 + exit 1 +fi + +# First argument is always session name +SESSION_NAME="$1" +shift + +# Handle --help first +if [[ "$SESSION_NAME" == "--help" ]]; then + head -n 55 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 +fi + +# Parse remaining arguments +TEXT="" +while [[ $# -gt 0 ]]; do + case $1 in + --key) + SPECIAL_KEY="$2" + shift 2 + ;; + --no-enter) + AUTO_ENTER=false + shift + ;; + --help) + head -n 55 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + *) + TEXT="$1" + shift + ;; + esac +done + +# Verify session exists +if ! tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + echo "❌ Session '$SESSION_NAME' not found" >&2 + echo " Run: tmux list-sessions" >&2 + exit 1 +fi + +# Send special key if specified +if [[ -n "$SPECIAL_KEY" ]]; then + tmux send-keys -t "$SESSION_NAME" "$SPECIAL_KEY" + + # Log the special key send as YAML + SESSION_DIR="$PROJECT_ROOT/debug/tmux-sessions/$SESSION_NAME" + if [[ -d "$SESSION_DIR" ]]; then + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + # Append YAML entry to commands.yaml + cat >> "$SESSION_DIR/commands.yaml" << EOF +- timestamp: $TIMESTAMP + type: key + input: "$SPECIAL_KEY" +EOF + fi + + exit 0 +fi + +# Check if text was provided +if [[ -z "$TEXT" ]]; then + echo "❌ No text or --key specified" >&2 + exit 1 +fi + +# Send text using bracketed paste mode +# \e[200~ = start bracketed paste +# \e[201~ = end bracketed paste +tmux send-keys -t "$SESSION_NAME" $'\e[200~'"$TEXT"$'\e[201~' + +# Optionally press Enter +if [[ "$AUTO_ENTER" == true ]]; then + tmux send-keys -t "$SESSION_NAME" Enter +fi + +# Log the text send as YAML +SESSION_DIR="$PROJECT_ROOT/debug/tmux-sessions/$SESSION_NAME" +if [[ -d "$SESSION_DIR" ]]; then + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + # Escape special characters in text for YAML (double quotes, backslashes) + ESCAPED_TEXT=$(echo "$TEXT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + # Append YAML entry to commands.yaml + cat >> "$SESSION_DIR/commands.yaml" << EOF +- timestamp: $TIMESTAMP + type: text + input: "$ESCAPED_TEXT" + auto_enter: $AUTO_ENTER +EOF +fi diff --git a/scripts/tmux/tmux-start.sh b/scripts/tmux/tmux-start.sh new file mode 100755 index 000000000..3bd103eed --- /dev/null +++ b/scripts/tmux/tmux-start.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +####################################################################### +# tmux-start.sh - Start a tmux session with the Codebuff CLI +####################################################################### +# +# DESCRIPTION: +# Creates a new detached tmux session running the Codebuff CLI. +# Returns the session name for use with other tmux helper scripts. +# Also creates a screenshots directory for capturing terminal output. +# +# USAGE: +# ./scripts/tmux/tmux-start.sh [OPTIONS] +# +# OPTIONS: +# -n, --name NAME Session name (default: cli-test-) +# -w, --width WIDTH Terminal width (default: 120) +# -h, --height HEIGHT Terminal height (default: 80) +# --wait SECONDS Seconds to wait for CLI to initialize (default: 4) +# --help Show this help message +# +# SESSION LOGS: +# Session logs are automatically saved to: +# debug/tmux-sessions/{session-name}/ +# +# Use tmux-capture.sh to save timestamped captures to this directory. +# +# EXAMPLES: +# # Start with default settings +# ./scripts/tmux/tmux-start.sh +# # Output: cli-test-1234567890 +# +# # Start with custom session name +# ./scripts/tmux/tmux-start.sh --name my-test-session +# +# # Start with custom dimensions +# ./scripts/tmux/tmux-start.sh -w 160 -h 40 +# +# EXIT CODES: +# 0 - Success (session name printed to stdout) +# 1 - Error (tmux not found or session creation failed) +# +####################################################################### + +set -e + +# Defaults +SESSION_NAME="" +WIDTH=120 +HEIGHT=80 # Tall enough to capture most output without scrolling +WAIT_SECONDS=4 + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -n|--name) + SESSION_NAME="$2" + shift 2 + ;; + -w|--width) + WIDTH="$2" + shift 2 + ;; + -h|--height) + HEIGHT="$2" + shift 2 + ;; + --wait) + WAIT_SECONDS="$2" + shift 2 + ;; + --help) + head -n 40 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +# Generate session name if not provided +if [[ -z "$SESSION_NAME" ]]; then + SESSION_NAME="cli-test-$(date +%s)" +fi + +# Check if tmux is available +if ! command -v tmux &> /dev/null; then + echo "❌ tmux not found" >&2 + echo "" >&2 + echo "📦 Installation:" >&2 + echo " macOS: brew install tmux" >&2 + echo " Ubuntu: sudo apt-get install tmux" >&2 + echo " Arch: sudo pacman -S tmux" >&2 + exit 1 +fi + +# Get project root (assuming script is in scripts/tmux/) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Create tmux session running CLI +if ! tmux new-session -d -s "$SESSION_NAME" \ + -x "$WIDTH" -y "$HEIGHT" \ + "cd '$PROJECT_ROOT' && bun --cwd=cli run dev 2>&1" 2>/dev/null; then + echo "❌ Failed to create tmux session" >&2 + exit 1 +fi + +# Create session logs directory +SESSION_DIR="$PROJECT_ROOT/debug/tmux-sessions/$SESSION_NAME" +mkdir -p "$SESSION_DIR" + +# Save session info as YAML +cat > "$SESSION_DIR/session-info.yaml" << EOF +session: $SESSION_NAME +started: $(date -u +%Y-%m-%dT%H:%M:%SZ) +started_local: $(date) +dimensions: + width: $WIDTH + height: $HEIGHT +status: active +EOF + +# Wait for CLI to initialize +if [[ "$WAIT_SECONDS" -gt 0 ]]; then + sleep "$WAIT_SECONDS" +fi + +# Output session name for use by other scripts +echo "$SESSION_NAME" diff --git a/scripts/tmux/tmux-stop.sh b/scripts/tmux/tmux-stop.sh new file mode 100755 index 000000000..c35e6896f --- /dev/null +++ b/scripts/tmux/tmux-stop.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +####################################################################### +# tmux-stop.sh - Stop a tmux session +####################################################################### +# +# DESCRIPTION: +# Kills a tmux session. Use this to clean up after testing. +# Silently succeeds if session doesn't exist (idempotent). +# +# USAGE: +# ./scripts/tmux/tmux-stop.sh SESSION_NAME [OPTIONS] +# +# ARGUMENTS: +# SESSION_NAME Name of the tmux session to stop +# +# OPTIONS: +# --all Stop ALL cli-test-* sessions +# --list List all active tmux sessions first +# --help Show this help message +# +# EXAMPLES: +# # Stop a specific session +# ./scripts/tmux/tmux-stop.sh cli-test-123 +# +# # Stop all test sessions +# ./scripts/tmux/tmux-stop.sh --all +# +# # List sessions then stop one +# ./scripts/tmux/tmux-stop.sh --list cli-test-123 +# +# EXIT CODES: +# 0 - Success (session stopped or already doesn't exist) +# 1 - Error (invalid arguments) +# +####################################################################### + +set -e + +# Defaults +STOP_ALL=false +LIST_FIRST=false + +# Check minimum arguments +if [[ $# -lt 1 ]]; then + echo "Usage: $0 SESSION_NAME [OPTIONS]" >&2 + echo " $0 --all" >&2 + echo "Run with --help for more information" >&2 + exit 1 +fi + +# First argument handling +SESSION_NAME="" + +# Handle --help and --all as first arg +case "$1" in + --help) + head -n 40 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + --all) + STOP_ALL=true + shift + ;; + --list) + LIST_FIRST=true + shift + if [[ $# -gt 0 && "$1" != "--"* ]]; then + SESSION_NAME="$1" + shift + fi + ;; + *) + SESSION_NAME="$1" + shift + ;; +esac + +# Parse remaining arguments +while [[ $# -gt 0 ]]; do + case $1 in + --all) + STOP_ALL=true + shift + ;; + --list) + LIST_FIRST=true + shift + ;; + --help) + head -n 40 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + *) + if [[ -z "$SESSION_NAME" ]]; then + SESSION_NAME="$1" + fi + shift + ;; + esac +done + +# List sessions if requested +if [[ "$LIST_FIRST" == true ]]; then + echo "Active tmux sessions:" + tmux list-sessions 2>/dev/null || echo " (none)" + echo "" +fi + +# Stop all test sessions +if [[ "$STOP_ALL" == true ]]; then + # Get all cli-test-* sessions + SESSIONS=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep '^cli-test-' || true) + + if [[ -z "$SESSIONS" ]]; then + echo "No cli-test-* sessions found" + exit 0 + fi + + COUNT=0 + while IFS= read -r session; do + tmux kill-session -t "$session" 2>/dev/null && ((COUNT++)) || true + done <<< "$SESSIONS" + + echo "Stopped $COUNT session(s)" + exit 0 +fi + +# Check if session name was provided +if [[ -z "$SESSION_NAME" ]]; then + echo "❌ No session name specified" >&2 + echo " Use --all to stop all test sessions" >&2 + exit 1 +fi + +# Stop the specific session (silently succeed if not found) +tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true