diff --git a/packages/app/src/utils/tool-call-parsers.test.ts b/packages/app/src/utils/tool-call-parsers.test.ts index 459ac430f..285b5605c 100644 --- a/packages/app/src/utils/tool-call-parsers.test.ts +++ b/packages/app/src/utils/tool-call-parsers.test.ts @@ -65,6 +65,21 @@ describe("parseToolCallDisplay", () => { } }); + test("strips shell + cd wrapper from command (Codex exec_command style)", () => { + const input = { + command: + '/bin/zsh -lc "cd /Users/moboudra/dev/paseo && nl -ba packages/app/src/utils/tool-call-parsers.test.ts | sed -n \'150,260p\'"', + }; + + const display: ToolCallDisplay = parseToolCallDisplay("shell", input, undefined); + expect(display.type).toBe("shell"); + if (display.type === "shell") { + expect(display.command).toBe( + "nl -ba packages/app/src/utils/tool-call-parsers.test.ts | sed -n '150,260p'" + ); + } + }); + test("handles command as array", () => { const input = { command: ["git", "status"] }; const result = { type: "command", output: "On branch main" }; diff --git a/packages/app/src/utils/tool-call-parsers.ts b/packages/app/src/utils/tool-call-parsers.ts index e8ff6b46f..1001685d2 100644 --- a/packages/app/src/utils/tool-call-parsers.ts +++ b/packages/app/src/utils/tool-call-parsers.ts @@ -1,5 +1,6 @@ import stripAnsi from "strip-ansi"; import { z } from "zod"; +import { stripShellWrapperPrefix } from "@paseo/server/utils/tool-call-parsers"; import { getNowMs, isPerfLoggingEnabled, perfLog } from "./perf"; const TOOL_CALL_DIFF_LOG_TAG = "[ToolCallDiff]"; @@ -964,9 +965,10 @@ const ShellToolCallSchema = z result: z.unknown(), }) .transform((data) => { - const command = Array.isArray(data.input.command) + const commandRaw = Array.isArray(data.input.command) ? data.input.command.join(" ") : data.input.command; + const command = stripShellWrapperPrefix(commandRaw); // Try parsing as success result first const resultParsed = ShellResultSchema.safeParse(data.result); diff --git a/packages/server/src/utils/tool-call-parsers.test.ts b/packages/server/src/utils/tool-call-parsers.test.ts index 45ad7141e..fd88a609a 100644 --- a/packages/server/src/utils/tool-call-parsers.test.ts +++ b/packages/server/src/utils/tool-call-parsers.test.ts @@ -11,6 +11,11 @@ describe("stripShellWrapperPrefix", () => { expect(stripShellWrapperPrefix(command)).toBe("npm run format"); }); + test("strips /bin/zsh -lc \"cd path &&\" wrapper", () => { + const command = '/bin/zsh -lc "cd /Users/moboudra/dev/blankpage/editor && npm run format"'; + expect(stripShellWrapperPrefix(command)).toBe("npm run format"); + }); + test("strips /bin/zsh -c cd path && prefix", () => { const command = "/bin/zsh -c cd /path/to/project && git status"; expect(stripShellWrapperPrefix(command)).toBe("git status"); @@ -36,12 +41,9 @@ describe("stripShellWrapperPrefix", () => { expect(stripShellWrapperPrefix(command)).toBe("/bin/zsh -lc npm run build"); }); - test("handles paths with spaces in quotes", () => { - // The regex matches \S+ for the path, so quoted paths won't fully match - // This is acceptable - quoted paths are edge cases + test("strips when cd path includes spaces in quotes", () => { const command = '/bin/zsh -lc cd "/path with spaces" && npm test'; - // Won't strip because the path pattern expects non-whitespace - expect(stripShellWrapperPrefix(command)).toBe('/bin/zsh -lc cd "/path with spaces" && npm test'); + expect(stripShellWrapperPrefix(command)).toBe("npm test"); }); }); diff --git a/packages/server/src/utils/tool-call-parsers.ts b/packages/server/src/utils/tool-call-parsers.ts index 14111d785..cc320b27b 100644 --- a/packages/server/src/utils/tool-call-parsers.ts +++ b/packages/server/src/utils/tool-call-parsers.ts @@ -56,12 +56,34 @@ export function stripCwdPrefix(filePath: string, cwd?: string): string { return filePath; } -// Strips shell wrapper prefixes like "/bin/zsh -lc cd /path && " from commands -// This is used for display purposes to show the actual command being run -const SHELL_WRAPPER_PATTERN = /^\/bin\/(?:zsh|bash|sh)\s+(?:-[a-zA-Z]+\s+)?cd\s+\S+\s+&&\s+/; +// Strips shell wrapper prefixes like: +// - `/bin/zsh -lc cd /path && ` +// - `/bin/zsh -lc "cd /path && "` +// This is used for display purposes to show the actual command being run. +const SHELL_WRAPPER_PREFIX_PATTERN = /^\/bin\/(?:zsh|bash|sh)\s+(?:-[a-zA-Z]+\s+)?/; +const CD_AND_PATTERN = /^cd\s+(?:"[^"]+"|'[^']+'|\S+)\s+&&\s+/; export function stripShellWrapperPrefix(command: string): string { - return command.replace(SHELL_WRAPPER_PATTERN, ""); + const prefixMatch = command.match(SHELL_WRAPPER_PREFIX_PATTERN); + if (!prefixMatch) { + return command; + } + + let rest = command.slice(prefixMatch[0].length).trim(); + if (rest.length >= 2) { + const first = rest[0]; + const last = rest[rest.length - 1]; + if ((first === `"` || first === `'`) && last === first) { + rest = rest.slice(1, -1); + } + } + + const stripped = rest.replace(CD_AND_PATTERN, ""); + if (stripped !== rest) { + return stripped; + } + + return command; } export function extractPrincipalParam(args: unknown, cwd?: string): string | undefined {