From fe9ffc7912e73a29a869ca7f5d82ca4db3b73609 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 22 Jun 2026 10:46:24 +0000 Subject: [PATCH] fix(tui): preserved bare png filename pastes Require bracketed image-path paste detection to see an explicit local path separator or file URI before routing .png-like text to image attachment handling.\n\nFixes #3253 --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../modes/components/custom-editor.test.ts | 22 +++++++++++++++++++ .../src/modes/components/custom-editor.ts | 11 +++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2e70c2bd9..37f462fb2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed bracketed paste treating a bare `.png` filename as an image attachment path instead of normal prompt text. ([#3253](https://github.com/can1357/oh-my-pi/issues/3253)) + ## [16.1.14] - 2026-06-22 ### Added diff --git a/packages/coding-agent/src/modes/components/custom-editor.test.ts b/packages/coding-agent/src/modes/components/custom-editor.test.ts index 6cb93a204..d779d53ea 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.test.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.test.ts @@ -3,6 +3,7 @@ import { $ } from "bun"; import { getEditorTheme, initTheme } from "../theme/theme"; import { CustomEditor, + extractBracketedImagePastePaths, SPACE_HOLD_MECHANICAL_RUN, SPACE_HOLD_RELEASE_MS, SPACE_REPEAT_MAX_GAP_MS, @@ -21,6 +22,12 @@ function makeEditor() { const REPEAT_GAP_MS = 30; /** A gap above the threshold — looks like a deliberate keypress. */ const TAP_GAP_MS = SPACE_REPEAT_MAX_GAP_MS + 80; +const BRACKETED_PASTE_START = "\x1b[200~"; +const BRACKETED_PASTE_END = "\x1b[201~"; + +function bracketedPaste(text: string): string { + return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`; +} /** Feed `count` spaces `gapMs` apart on the fake clock. The first space of a run has no prior * space, so its gap is effectively infinite and it always reads as a deliberate tap. */ @@ -66,6 +73,21 @@ describe("CustomEditor placeholder decoration", () => { }); }); +describe("CustomEditor bracketed image-path paste", () => { + it("leaves a pasted bare .png filename on the normal text path", () => { + expect(extractBracketedImagePastePaths(bracketedPaste("icon-photo-default.png"))).toBeUndefined(); + }); + + it("extracts explicit local image paths for attachment", () => { + expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/icon-photo-default.png"))).toEqual([ + "/tmp/icon-photo-default.png", + ]); + expect(extractBracketedImagePastePaths(bracketedPaste("C:\\Users\\me\\icon-photo-default.png"))).toEqual([ + "C:\\Users\\me\\icon-photo-default.png", + ]); + }); +}); + describe("CustomEditor space-hold push-to-talk", () => { beforeAll(async () => { await initTheme(); diff --git a/packages/coding-agent/src/modes/components/custom-editor.ts b/packages/coding-agent/src/modes/components/custom-editor.ts index ee567a477..88353b391 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.ts @@ -64,6 +64,9 @@ const BRACKETED_PASTE_END = "\x1b[201~"; const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i; const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi; const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g; +const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i; +const FILE_URI_REGEX = /^file:\/\//i; +const WINDOWS_DRIVE_PATH_REGEX = /^[a-z]:[\\/]/i; /** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a * deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */ @@ -118,6 +121,12 @@ function normalizePastedImagePath(path: string): string { return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1"); } +function isExplicitPastedImagePath(path: string): boolean { + if (WINDOWS_DRIVE_PATH_REGEX.test(path) || FILE_URI_REGEX.test(path)) return true; + if (URI_SCHEME_REGEX.test(path)) return false; + return path.includes("/") || path.includes("\\"); +} + export function extractBracketedImagePastePaths(data: string): string[] | undefined { if (!data.startsWith(BRACKETED_PASTE_START)) return undefined; const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length); @@ -139,7 +148,7 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi if (boundaryEnd === undefined) continue; const path = normalizePastedImagePath(pasted.slice(segmentStart, boundaryEnd)); - if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path)) return undefined; + if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path) || !isExplicitPastedImagePath(path)) return undefined; paths.push(path); segmentStart = boundaryEnd;