mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
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>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user