Skip to content

Commit cd79f2a

Browse files
brookscclaude
andauthored
fix(prompt): scale bracketed-paste delay by line count to prevent stuck initial prompts (#107)
* fix(prompt): scale bracketed-paste delay by line count to prevent stuck initial prompts When an agent enables bracketed paste mode (CSI ? 2004 h), synthetic prompt sends wrap the text in \x1b[200~...\x1b[201~ to avoid Codex's paste-burst guard. However, TUI agents like Claude Code process the paste asynchronously — if \r arrives while the paste is still being consumed, it is absorbed into the input buffer as a newline rather than submitting the prompt. The previous fixed 50ms delay was sufficient for short prompts but too short for large ones (e.g. a 31-line initial task prompt). The symptom is the agent sitting in INSERT mode with "[Pasted text #1 +N lines]" in its input, never sending. Fix: - Track bracketed paste mode (CSI ? 2004 h/l) in AgentTrackingState via updateBracketedPasteMode(), called on each PTY data chunk in markAgentOutput() - Export isAgentBracketedPasteEnabled() so sendPrompt can conditionally wrap - Scale the pre-Enter delay: max(50ms, lines * 15ms), capped at 500ms — a 31-line prompt now waits ~465ms instead of 50ms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(prompt): add pasteDelayMs regression tests; export fn; fix duplicate constants Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0c93d63 commit cd79f2a

2 files changed

Lines changed: 31 additions & 5 deletions

File tree

src/store/tasks.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ vi.mock('../lib/log', () => ({
7474
warn: vi.fn(),
7575
}));
7676

77-
import { sendPrompt } from './tasks';
77+
import { pasteDelayMs, sendPrompt } from './tasks';
7878

7979
function writePayloads(): string[] {
8080
return mockInvoke.mock.calls
@@ -180,3 +180,19 @@ describe('sendPrompt', () => {
180180
expect(writePayloads()[1]).toContain('IMPORTANT: Maintain .claude/steps.json');
181181
});
182182
});
183+
184+
describe('pasteDelayMs', () => {
185+
it('returns 50ms for a short single-line prompt', () => {
186+
expect(pasteDelayMs('hello')).toBe(50);
187+
});
188+
189+
it('scales by line count for a ~31-line prompt', () => {
190+
const text = Array.from({ length: 31 }, (_, i) => `line ${i + 1}`).join('\n');
191+
expect(pasteDelayMs(text)).toBe(Math.min(500, Math.max(50, 31 * 15)));
192+
});
193+
194+
it('caps at 500ms for a very large prompt', () => {
195+
const text = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join('\n');
196+
expect(pasteDelayMs(text)).toBe(500);
197+
});
198+
});

src/store/tasks.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ function sleep(ms: number): Promise<void> {
6060
return new Promise((resolve) => setTimeout(resolve, ms));
6161
}
6262

63+
// Delay between writing pasted text and the Enter key. Claude Code (and other
64+
// TUI agents) process bracketed paste asynchronously — if \r arrives before
65+
// the paste is fully consumed, it gets absorbed into the input buffer instead
66+
// of submitting it. We scale the delay by line count so large prompts (e.g.
67+
// initial task prompts of 30+ lines) reliably submit. Cap at 500ms to avoid
68+
// noticeable lag on normal sends.
69+
export function pasteDelayMs(text: string): number {
70+
const lines = text.split('\n').length;
71+
return Math.min(500, Math.max(50, lines * 15));
72+
}
73+
6374
function isAgentNotFoundError(err: unknown): boolean {
6475
return String(err).toLowerCase().includes('agent not found');
6576
}
@@ -496,13 +507,12 @@ export async function sendPrompt(taskId: string, agentId: string, text: string):
496507
// bracketed paste, wrap only the prompt text; this avoids Codex's paste-burst
497508
// guard treating rapid synthetic keystrokes plus Enter as a paste.
498509
setTaskLastInputAt(taskId);
510+
const useBracketed = isAgentBracketedPasteEnabled(agentId);
499511
await writeToAgentWhenReady(
500512
agentId,
501-
isAgentBracketedPasteEnabled(agentId)
502-
? `${BRACKETED_PASTE_START}${effectiveText}${BRACKETED_PASTE_END}`
503-
: effectiveText,
513+
useBracketed ? `${BRACKETED_PASTE_START}${effectiveText}${BRACKETED_PASTE_END}` : effectiveText,
504514
);
505-
await new Promise((r) => setTimeout(r, 50));
515+
await new Promise((r) => setTimeout(r, pasteDelayMs(effectiveText)));
506516
await writeToAgentWhenReady(agentId, '\r');
507517
setStore('tasks', taskId, 'lastPrompt', text);
508518
if (task && !hasPromptedAgent) {

0 commit comments

Comments
 (0)