fix(coding-agent): guarded pasted shell prompts from python
- Detected copied shell-prompt transcripts before the Python shortcut router. - Forwarded OMP terminal chrome pastes through normal prompt submission. - Added regression coverage for the #4678 transcript shape. Fixes #4678
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user