diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c5595a8932f..514e1491cbd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed pasted terminal transcripts beginning with a shell prompt (`$ ...`) being mistaken for local Python shortcuts instead of being submitted as normal prompts ([#4678](https://github.com/can1357/oh-my-pi/issues/4678)). + ## [16.3.9] - 2026-07-06 ### Added diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index ed16eb36cb5..9fd2387442f 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -86,6 +86,20 @@ function hasPasteText(value: unknown): value is PasteTarget { return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function"; } +const SHELL_PROMPT_COMMAND_RE = + /^(?:\.{0,2}\/|~\/|cd(?:\s|$)|sudo(?:\s|$)|git(?:\s|$)|bun(?:\s|$)|npm(?:\s|$)|pnpm(?:\s|$)|yarn(?:\s|$)|node(?:\s|$)|python\d*(?:\s|$)|cargo(?:\s|$)|go(?:\s|$)|make(?:\s|$)|docker(?:\s|$)|kubectl(?:\s|$))/; +const SHELL_PROMPT_OPERATOR_RE = /(?:^|\s)(?:&&|\|\||\||2>&1|[<>]{1,2})(?:\s|$)/; +const OMP_STATUS_LINE_RE = /^\s*in:\s+\d+\s+out:\s+\d+(?:\s+cache\s+\S+)?\s+t:\s+\S+\s+tok\/s:\s+\S+/m; + +function looksLikePastedShellPrompt(code: string): boolean { + const firstLine = code.split("\n", 1)[0]?.trimStart() ?? ""; + return ( + SHELL_PROMPT_COMMAND_RE.test(firstLine) || + SHELL_PROMPT_OPERATOR_RE.test(firstLine) || + OMP_STATUS_LINE_RE.test(code) + ); +} + function pythonCommandPrefixLength(trimmedText: string): 0 | 1 | 2 { if (trimmedText.charCodeAt(0) !== 36 /* $ */) return 0; if (trimmedText.charCodeAt(1) === 123 /* { */) return 0; @@ -100,8 +114,10 @@ function parsePythonCommandInput(text: string): { code: string; isExcluded: bool const trimmed = text.trimStart(); const prefixLength = pythonCommandPrefixLength(trimmed); if (prefixLength === 0) return undefined; + const code = trimmed.slice(prefixLength).trim(); + if (prefixLength === 1 && looksLikePastedShellPrompt(code)) return undefined; return { - code: trimmed.slice(prefixLength).trim(), + code, isExcluded: prefixLength === 2, }; } diff --git a/packages/coding-agent/test/input-controller-python-prefix.test.ts b/packages/coding-agent/test/input-controller-python-prefix.test.ts index 92cd133ca00..6e48d1af4da 100644 --- a/packages/coding-agent/test/input-controller-python-prefix.test.ts +++ b/packages/coding-agent/test/input-controller-python-prefix.test.ts @@ -109,6 +109,36 @@ describe("InputController Python prompt prefix", () => { ]); }); + it("submits pasted shell-prompt transcripts with OMP chrome as a normal prompt", async () => { + const transcript = + "$ cd ~/project && sudo ./build-and-push.sh o5.7 2>&1 | tail -4\n" + + " |\n" + + " in: 282 out: 152 cache 344K t: 3.3s tok/s: 351.9/s\n" + + " is this command stuck in limbo"; + const { ctx, editor, handlePythonCommand, onInputCallback, startPendingSubmission, submitted } = createContext(); + const controller = new InputController(ctx); + controller.setupEditorSubmitHandler(); + + await editor.onSubmit?.(transcript); + + expect(handlePythonCommand).not.toHaveBeenCalled(); + expect(startPendingSubmission).toHaveBeenCalledWith({ + text: transcript, + images: undefined, + imageLinks: undefined, + streamingBehavior: "steer", + }); + expect(onInputCallback).toHaveBeenCalledTimes(1); + expect(submitted).toEqual([ + { + text: transcript, + images: undefined, + imageLinks: undefined, + streamingBehavior: "steer", + }, + ]); + }); + it("keeps space-separated Python shortcuts available", async () => { const { ctx, editor, handlePythonCommand, onInputCallback } = createContext(); const controller = new InputController(ctx);