diff --git a/src/__tests__/claude-terminal-tmux-backend.test.ts b/src/__tests__/claude-terminal-tmux-backend.test.ts index cde852c7..d0f03203 100644 --- a/src/__tests__/claude-terminal-tmux-backend.test.ts +++ b/src/__tests__/claude-terminal-tmux-backend.test.ts @@ -198,6 +198,86 @@ describe('TmuxTerminalBackend', () => { ]); }); + it('Given Claude trust dialog with numbered menu, When pasteText is called, Then trust dialog is not treated as ready and prompt is only pasted after transition to real input', async () => { + const backend = new TmuxTerminalBackend(); + const stdinWrites: string[] = []; + const trustDialogPane = [ + '────────────────────────────────────────', + '❯ 1. Yes, I trust this folder', + ' 2. No, exit', + ].join('\n'); + const realReadyPane = [ + '────────────────────────────────────────', + '❯ Try "refactor routing.ts"', + '────────────────────────────────────────', + ].join('\n'); + const capturedPanes = [ + trustDialogPane, + trustDialogPane, + realReadyPane, + realReadyPane, + 'pasted prompt', + ]; + mockSpawn.mockImplementation(() => createSpawnChild(stdinWrites, 0, '')); + mockExecFile.mockImplementation((_file, args, _options, callback) => { + if (args[0] === 'capture-pane') { + callback(null, { stdout: capturedPanes.shift() ?? 'pasted prompt', stderr: '' }); + return; + } + callback(null, { stdout: '', stderr: '' }); + }); + + await backend.pasteText({ id: 'tmux-session', name: 'takt-session' }, 'implement task'); + + expect(stdinWrites).toEqual(['implement task']); + expect(mockExecFile.mock.calls.map((call) => call[1][0])).toEqual([ + 'capture-pane', + 'capture-pane', + 'capture-pane', + 'capture-pane', + 'paste-buffer', + 'capture-pane', + 'send-keys', + 'delete-buffer', + ]); + }); + + it('Given Claude v2.1 pane with placeholder hint after prompt char, When pasteText is called, Then prompt is detected as ready', async () => { + const backend = new TmuxTerminalBackend(); + const stdinWrites: string[] = []; + const v21ReadyPane = [ + '────────────────────────────────────────', + '❯ Try "refactor routing.ts"', + '────────────────────────────────────────', + ' ⏵⏵ auto mode on · ◉ xhigh · /effort', + ].join('\n'); + const capturedPanes = [ + v21ReadyPane, + v21ReadyPane, + 'implement task', + ]; + mockSpawn.mockImplementation(() => createSpawnChild(stdinWrites, 0, '')); + mockExecFile.mockImplementation((_file, args, _options, callback) => { + if (args[0] === 'capture-pane') { + callback(null, { stdout: capturedPanes.shift() ?? 'implement task', stderr: '' }); + return; + } + callback(null, { stdout: '', stderr: '' }); + }); + + await backend.pasteText({ id: 'tmux-session', name: 'takt-session' }, 'implement task'); + + expect(stdinWrites).toEqual(['implement task']); + expect(mockExecFile.mock.calls.map((call) => call[1][0])).toEqual([ + 'capture-pane', + 'capture-pane', + 'paste-buffer', + 'capture-pane', + 'send-keys', + 'delete-buffer', + ]); + }); + it('Given tmux paste-buffer fails after loading prompt, When pasteText rejects, Then tmux buffer is deleted', async () => { const backend = new TmuxTerminalBackend(); const stdinWrites: string[] = []; diff --git a/src/infra/claude-terminal/tmux-backend.ts b/src/infra/claude-terminal/tmux-backend.ts index 0b29119f..ed58453e 100644 --- a/src/infra/claude-terminal/tmux-backend.ts +++ b/src/infra/claude-terminal/tmux-backend.ts @@ -13,7 +13,7 @@ const PANE_CHANGE_TIMEOUT_MS = 5000; const PANE_POLL_INTERVAL_MS = 100; const CLAUDE_READY_TAIL_LINES = 3; const CLAUDE_BUSY_PATTERN = /(Running|thinking|Searching|Reading|Writing|Editing|Crunched)/i; -const CLAUDE_PROMPT_PATTERN = /^[❯❱>]$/; +const CLAUDE_PROMPT_PATTERN = /^[❯❱>](?:\s+(?!\d+\.\s)|$)/; function getProperty(error: object, property: string): unknown { return (error as Record)[property];