From 7a64eef491e059c8daec79e52015882c15a14b50 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:50:24 +0200 Subject: [PATCH] Show the command on Codex terminal rows (#5774) * fix(codex): label terminal writes with the command Codex started Codex app-server reports the PTY process id on the commandExecution item, and no longer sends the legacy exec_command_end event that carried it as text. Record the process-to-command label from the item so terminal writes show which command they went to. * ci: rerun after website e2e server start timeout * fix(codex): unwrap the shell from terminal labels like shell rows Terminal labels now use the same command normalization as the shell row, so a Linux wrapper such as /usr/bin/zsh -lc 'python3 -i' labels the terminal as python3 -i. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/codex-app-server-agent.test.ts | 45 +++++++++++++++++++ .../agent/providers/codex-app-server-agent.ts | 28 +++++++----- .../codex/test-utils/fake-app-server.ts | 21 +++++++++ .../agent/providers/codex/tool-call-mapper.ts | 2 +- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index bf3ab15c62..fb27b68e96 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1595,6 +1595,51 @@ describe("Codex app-server provider", () => { } }); + test.each(["python3 -i", "/usr/bin/zsh -lc 'python3 -i'"])( + "labels a terminal write started as %s with the command that started the terminal", + async (command) => { + const appServer = createFakeCodexAppServer(); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + + try { + await session.connect(); + appServer.startsTerminalCommand({ + threadId: "thread-1", + turnId: "turn-1", + itemId: "interactive-shell", + processId: "73", + command, + }); + + const terminalWrite = waitForTimelineToolCall(session, "terminal-session-73-1"); + appServer.typesIntoTerminal({ + threadId: "thread-1", + turnId: "turn-1", + itemId: "interactive-shell", + processId: "73", + text: "print(1)\n", + }); + + await expect(terminalWrite).resolves.toMatchObject({ + item: { + callId: "terminal-session-73-1", + name: "terminal", + detail: { type: "plain_text", label: "python3 -i", text: "print(1)\n" }, + metadata: { processId: "73" }, + }, + }); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }, + ); + test("keeps repeated writes to one terminal as separate timeline rows", async () => { const appServer = createFakeCodexAppServer(); const session = new CodexAppServerAgentSession( diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 81ea190454..f15c5a60ad 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -57,6 +57,7 @@ import { CodexAsyncQuestions, codexAsyncQuestionToTimeline } from "./codex/async import { mapCodexToolCallEnvelope, mapCodexToolCallFromThreadItem, + normalizeCommandExecutionCommand, splitCodexMcpToolResultImages, } from "./codex/tool-call-mapper.js"; import { @@ -6731,6 +6732,7 @@ export class CodexAppServerAgentSession implements AgentSession { ); const itemId = parsed.item.id; if (normalizedItemType === "commandExecution") { + this.rememberTerminalProcessForItem(parsed.item); const callId = timelineItem.callId || itemId; if (callId && this.emittedExecCommandStartedCallIds.has(callId)) { return; @@ -6850,19 +6852,21 @@ export class CodexAppServerAgentSession implements AgentSession { } private rememberTerminalProcessForCommand(command: unknown, output: string | null): void { - const normalizedCommand = normalizeCodexCommandValue(command); - if (!normalizedCommand) { - return; - } - const displayCommand = - typeof normalizedCommand === "string" - ? normalizedCommand - : normalizedCommand.join(" ").trim(); - if (!displayCommand) { - return; - } const processId = extractCodexTerminalSessionId(output ?? undefined); - if (!processId) { + if (processId) { + this.rememberTerminalProcess(processId, command); + } + } + + private rememberTerminalProcessForItem(item: { [key: string]: unknown }): void { + if (typeof item.processId === "string" && item.processId) { + this.rememberTerminalProcess(item.processId, item.command); + } + } + + private rememberTerminalProcess(processId: string, command: unknown): void { + const displayCommand = normalizeCommandExecutionCommand(command); + if (!displayCommand) { return; } this.terminalCommandByProcessId.set(processId, displayCommand); diff --git a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts index 713d744309..226eeb9738 100644 --- a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts +++ b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts @@ -25,6 +25,13 @@ interface FakeSilentCommand { command: string; cwd: string; } +interface FakeTerminalCommand { + threadId: string; + turnId: string; + itemId: string; + processId: string; + command: string; +} interface FakeTerminalInput { threadId: string; turnId: string; @@ -81,6 +88,7 @@ export interface FakeCodexAppServer { completesCommand(params: FakeLegacyCommand): void; completesSilentCommand(params: FakeSilentCommand): void; completesSilentLegacyCommand(params: FakeSilentCommand): void; + startsTerminalCommand(params: FakeTerminalCommand): void; typesIntoTerminal(params: FakeTerminalInput): void; says(params: { threadId: string; itemId?: string; text: string; chunks?: string[] }): void; requestCommandApproval(params: { @@ -487,6 +495,19 @@ export function createFakeCodexAppServer( success: true, }); }, + startsTerminalCommand(params) { + writeNotification("item/started", { + threadId: params.threadId, + turnId: params.turnId, + item: { + type: "commandExecution", + id: params.itemId, + status: "inProgress", + command: params.command, + processId: params.processId, + }, + }); + }, typesIntoTerminal(params) { writeNotification("item/commandExecution/terminalInteraction", { threadId: params.threadId, diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 2f274d2d3f..64629bc5d9 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -245,7 +245,7 @@ function isWindowsShellCommand(command: string): boolean { return /(?:^|\\)(?:pwsh|powershell|cmd)(?:\.exe)?$/i.test(normalized); } -function normalizeCommandExecutionCommand(value: unknown): string | undefined { +export function normalizeCommandExecutionCommand(value: unknown): string | undefined { if (typeof value === "string") { const normalized = unwrapShellCommand(value); return normalized.length > 0 ? normalized : undefined;