Skip to content

Commit de7e021

Browse files
hiranorrrclaude
andauthored
fix: detect claude-terminal prompt with v2.1 placeholder hint (#766)
* fix: detect claude-terminal prompt with v2.1 placeholder hint The tail-line regex required the prompt line to be exactly `❯` (or `❱` / `>`), but Claude Code v2.1 renders the input row as `❯ Try "..."` so the regex never matches and `waitForClaudeInputReady` times out after 60s. Relax the pattern to `^[❯❱>](?:\s|$)` so any prompt char followed by whitespace (or end-of-line) is accepted. The busy pattern still gates ready-detection, so the placeholder hint cannot cause a false positive. Refs #765 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: exclude numbered menu items from claude-terminal prompt detection The previous regex `^[❯❱>](?:\s|$)` accepted lines like `❯ 1. Yes, I trust this folder` as a ready prompt, which would cause `pasteText` to silently forward the user's prompt body into a trust/permission dialog instead of the real input box. The old `^[❯❱>]$` failed-loud with a 60s timeout in this scenario; the v2.1 fix re-introduced it as a silent regression. Tighten the pattern with a negative lookahead so any `<prompt> <digit>.` sequence (numbered menus) is rejected, and add a regression test that proves the trust dialog stays not-ready until claude transitions to a real input row. Refs #765 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 42ed7d6 commit de7e021

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/__tests__/claude-terminal-tmux-backend.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,86 @@ describe('TmuxTerminalBackend', () => {
198198
]);
199199
});
200200

201+
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 () => {
202+
const backend = new TmuxTerminalBackend();
203+
const stdinWrites: string[] = [];
204+
const trustDialogPane = [
205+
'────────────────────────────────────────',
206+
'❯ 1. Yes, I trust this folder',
207+
' 2. No, exit',
208+
].join('\n');
209+
const realReadyPane = [
210+
'────────────────────────────────────────',
211+
'❯ Try "refactor routing.ts"',
212+
'────────────────────────────────────────',
213+
].join('\n');
214+
const capturedPanes = [
215+
trustDialogPane,
216+
trustDialogPane,
217+
realReadyPane,
218+
realReadyPane,
219+
'pasted prompt',
220+
];
221+
mockSpawn.mockImplementation(() => createSpawnChild(stdinWrites, 0, ''));
222+
mockExecFile.mockImplementation((_file, args, _options, callback) => {
223+
if (args[0] === 'capture-pane') {
224+
callback(null, { stdout: capturedPanes.shift() ?? 'pasted prompt', stderr: '' });
225+
return;
226+
}
227+
callback(null, { stdout: '', stderr: '' });
228+
});
229+
230+
await backend.pasteText({ id: 'tmux-session', name: 'takt-session' }, 'implement task');
231+
232+
expect(stdinWrites).toEqual(['implement task']);
233+
expect(mockExecFile.mock.calls.map((call) => call[1][0])).toEqual([
234+
'capture-pane',
235+
'capture-pane',
236+
'capture-pane',
237+
'capture-pane',
238+
'paste-buffer',
239+
'capture-pane',
240+
'send-keys',
241+
'delete-buffer',
242+
]);
243+
});
244+
245+
it('Given Claude v2.1 pane with placeholder hint after prompt char, When pasteText is called, Then prompt is detected as ready', async () => {
246+
const backend = new TmuxTerminalBackend();
247+
const stdinWrites: string[] = [];
248+
const v21ReadyPane = [
249+
'────────────────────────────────────────',
250+
'❯ Try "refactor routing.ts"',
251+
'────────────────────────────────────────',
252+
' ⏵⏵ auto mode on · ◉ xhigh · /effort',
253+
].join('\n');
254+
const capturedPanes = [
255+
v21ReadyPane,
256+
v21ReadyPane,
257+
'implement task',
258+
];
259+
mockSpawn.mockImplementation(() => createSpawnChild(stdinWrites, 0, ''));
260+
mockExecFile.mockImplementation((_file, args, _options, callback) => {
261+
if (args[0] === 'capture-pane') {
262+
callback(null, { stdout: capturedPanes.shift() ?? 'implement task', stderr: '' });
263+
return;
264+
}
265+
callback(null, { stdout: '', stderr: '' });
266+
});
267+
268+
await backend.pasteText({ id: 'tmux-session', name: 'takt-session' }, 'implement task');
269+
270+
expect(stdinWrites).toEqual(['implement task']);
271+
expect(mockExecFile.mock.calls.map((call) => call[1][0])).toEqual([
272+
'capture-pane',
273+
'capture-pane',
274+
'paste-buffer',
275+
'capture-pane',
276+
'send-keys',
277+
'delete-buffer',
278+
]);
279+
});
280+
201281
it('Given tmux paste-buffer fails after loading prompt, When pasteText rejects, Then tmux buffer is deleted', async () => {
202282
const backend = new TmuxTerminalBackend();
203283
const stdinWrites: string[] = [];

src/infra/claude-terminal/tmux-backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const PANE_CHANGE_TIMEOUT_MS = 5000;
1313
const PANE_POLL_INTERVAL_MS = 100;
1414
const CLAUDE_READY_TAIL_LINES = 3;
1515
const CLAUDE_BUSY_PATTERN = /(Running|thinking|Searching|Reading|Writing|Editing|Crunched)/i;
16-
const CLAUDE_PROMPT_PATTERN = /^[>]$/;
16+
const CLAUDE_PROMPT_PATTERN = /^[>](?:\s+(?!\d+\.\s)|$)/;
1717

1818
function getProperty(error: object, property: string): unknown {
1919
return (error as Record<string, unknown>)[property];

0 commit comments

Comments
 (0)