Skip to content

Commit 6964d29

Browse files
centdixclaude
andauthored
fix: delay codex terminal prompt submission (#226)
* fix: delay codex terminal prompt submission Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: preserve multiline terminal prompts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 89f876e commit 6964d29

6 files changed

Lines changed: 45 additions & 14 deletions

File tree

backend/src/__tests__/agent-chat-service.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "bun:test";
22
import { getAgentDefinition } from "../services/agent-registry";
3-
import { resolveAgentChatSupport } from "../services/agent-chat-service";
3+
import { resolveAgentChatSupport, resolveAgentTerminalSubmitDelayMs } from "../services/agent-chat-service";
44
import type { ProjectConfig } from "../domain/config";
55

66
const TEST_CONFIG: ProjectConfig = {
@@ -99,4 +99,21 @@ describe("resolveAgentChatSupport", () => {
9999
status: 404,
100100
});
101101
});
102+
103+
it("uses the same terminal submit delay for built-in Codex prompts", () => {
104+
expect(resolveAgentTerminalSubmitDelayMs({
105+
agentId: "codex",
106+
agent: getAgentDefinition(TEST_CONFIG, "codex"),
107+
})).toBe(200);
108+
109+
expect(resolveAgentTerminalSubmitDelayMs({
110+
agentId: "claude",
111+
agent: getAgentDefinition(TEST_CONFIG, "claude"),
112+
})).toBe(0);
113+
114+
expect(resolveAgentTerminalSubmitDelayMs({
115+
agentId: "gemini",
116+
agent: getAgentDefinition(TEST_CONFIG, "gemini"),
117+
})).toBe(0);
118+
});
102119
});

backend/src/__tests__/terminal-adapter.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,16 @@ describe("terminal adapter", () => {
189189
},
190190
"reply with EXACTLY OK",
191191
0,
192-
undefined,
192+
"Preamble:\n",
193193
200,
194194
);
195195

196196
expect(result).toEqual({ ok: true });
197197
expect(slept).toEqual([200]);
198198
expect(tmuxCalls).toEqual([
199+
["tmux", "send-keys", "-t", "owner:wm-feature/search.0", "-l", "--", "Preamble:\n"],
199200
["tmux", "load-buffer", "-b", expect.stringMatching(/^wm-prompt-/), "-",],
200-
["tmux", "paste-buffer", "-b", expect.stringMatching(/^wm-prompt-/), "-t", "owner:wm-feature/search.0", "-d"],
201+
["tmux", "paste-buffer", "-rp", "-b", expect.stringMatching(/^wm-prompt-/), "-t", "owner:wm-feature/search.0", "-d"],
201202
["tmux", "send-keys", "-t", "owner:wm-feature/search.0", "Enter"],
202203
]);
203204
});

backend/src/adapters/terminal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ export async function sendPrompt(
422422
return { ok: false, error: `load-buffer failed${load.stderr ? `: ${load.stderr}` : ""}` };
423423
}
424424

425-
const paste = await tmuxExec(["tmux", "paste-buffer", "-b", bufferName, "-t", paneTarget, "-d"]);
425+
const paste = await tmuxExec(["tmux", "paste-buffer", "-rp", "-b", bufferName, "-t", paneTarget, "-d"]);
426426
if (paste.exitCode !== 0) {
427427
return { ok: false, error: `paste-buffer failed${paste.stderr ? `: ${paste.stderr}` : ""}` };
428428
}

backend/src/server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { isRecord, isStringArray } from "./lib/type-guards";
4949
import { parseJsonBody, parseParams, parseQuery } from "./api-validation";
5050
import { hasRecentDashboardActivity, touchDashboardActivity } from "./services/dashboard-activity";
5151
import { buildArchivedWorktreePathSet, normalizeArchivePath } from "./services/archive-service";
52-
import { resolveAgentChatSupport } from "./services/agent-chat-service";
52+
import { resolveAgentChatSupport, resolveAgentTerminalSubmitDelayMs } from "./services/agent-chat-service";
5353
import { validateCustomAgentInput } from "./services/agent-validation-service";
5454
import { getAgentDefinition, isBuiltInAgentId, listAgentDetails, listAgentSummaries, normalizeCustomAgentId } from "./services/agent-registry";
5555
import {
@@ -319,6 +319,7 @@ async function withRemovingBranch<T>(branch: string, fn: () => Promise<T>): Prom
319319
async function resolveTerminalWorktree(branch: string): Promise<{
320320
worktreeId: string;
321321
attachTarget: TerminalAttachTarget;
322+
agentName: WorktreeSnapshot["agentName"];
322323
}> {
323324
ensureBranchNotBusy(branch);
324325
let state = projectRuntime.getWorktreeByBranch(branch);
@@ -339,6 +340,7 @@ async function resolveTerminalWorktree(branch: string): Promise<{
339340
ownerSessionName: state.session.sessionName,
340341
windowName: state.session.windowName,
341342
},
343+
agentName: state.agentName,
342344
};
343345
}
344346

@@ -510,6 +512,13 @@ function resolveWorktreeAgentChatSupport(worktree: WorktreeSnapshot, action: "ch
510512
});
511513
}
512514

515+
function resolveWorktreeTerminalSubmitDelayMs(agentName: WorktreeSnapshot["agentName"]): number {
516+
return resolveAgentTerminalSubmitDelayMs({
517+
agentId: agentName,
518+
agent: agentName ? getAgentDefinition(config, agentName) : null,
519+
});
520+
}
521+
513522
async function apiAttachAgentsWorktree(branch: string): Promise<Response> {
514523
touchDashboardActivity();
515524
const resolved = await resolveAgentsWorktree(branch);
@@ -929,12 +938,14 @@ async function apiSendPrompt(name: string, req: Request): Promise<Response> {
929938
const preamble = body.preamble;
930939
log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
931940
const terminalWorktree = await resolveTerminalWorktree(name);
941+
const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
932942
const result = await sendTerminalPrompt(
933943
terminalWorktree.worktreeId,
934944
terminalWorktree.attachTarget,
935945
text,
936946
0,
937947
preamble,
948+
submitDelayMs,
938949
);
939950
if (!result.ok) return errorResponse(result.error, 503);
940951
return jsonResponse({ ok: true });

backend/src/services/agent-chat-service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ export interface AgentChatSupport {
55
submitDelayMs: number;
66
}
77

8+
const CODEX_SUBMIT_DELAY_MS = 200;
9+
10+
export function resolveAgentTerminalSubmitDelayMs(input: {
11+
agentId: string | null;
12+
agent: AgentDefinition | null;
13+
}): number {
14+
if (!input.agentId || !input.agent || input.agent.kind !== "builtin") return 0;
15+
return input.agent.implementation.agent === "codex" ? CODEX_SUBMIT_DELAY_MS : 0;
16+
}
17+
818
export function resolveAgentChatSupport(input: {
919
agentId: string | null;
1020
agentLabel: string | null;
@@ -51,7 +61,7 @@ export function resolveAgentChatSupport(input: {
5161
ok: true,
5262
data: {
5363
provider: input.agent.implementation.agent,
54-
submitDelayMs: input.agent.implementation.agent === "codex" ? 200 : 0,
64+
submitDelayMs: resolveAgentTerminalSubmitDelayMs(input),
5565
},
5666
};
5767
}

frontend/src/App.svelte

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -365,15 +365,9 @@
365365
let terminalRef:
366366
| {
367367
sendSelectPane: (pane: number) => void;
368-
sendInput: (data: string) => void;
369368
}
370369
| undefined = $state();
371370
372-
// Safety buffer after backend confirms paste-buffer completion.
373-
// paste-buffer exits once tmux has queued the data, but the PTY write
374-
// may not be fully flushed yet — this small delay lets it settle.
375-
const ENTER_DELAY_MS = 200;
376-
377371
let openingBranches = $state<Set<string>>(new Set());
378372
let archivingBranches = $state<Set<string>>(new Set());
379373
let trimmedWorktreeSearch = $derived(searchQuery.trim());
@@ -1278,7 +1272,6 @@
12781272
onclose={() => (ciDetailsPr = null)}
12791273
onfixsuccess={() => {
12801274
ciDetailsPr = null;
1281-
setTimeout(() => terminalRef?.sendInput("\r"), ENTER_DELAY_MS);
12821275
}}
12831276
/>
12841277
{/if}
@@ -1290,7 +1283,6 @@
12901283
onclose={() => (commentReviewPr = null)}
12911284
onsendsuccess={() => {
12921285
commentReviewPr = null;
1293-
setTimeout(() => terminalRef?.sendInput("\r"), ENTER_DELAY_MS);
12941286
}}
12951287
/>
12961288
{/if}

0 commit comments

Comments
 (0)