fix(coding-agent): attach clipboard image on empty bracketed paste
macOS terminals (iTerm2, Terminal.app, Warp, Ghostty without OSC 5522, …) intercept Cmd+V and read NSPasteboardTypeString first. For an image-only clipboard (Cmd+Shift+5 screenshot saved to clipboard, Chrome image copy, …) that read returns empty, so the terminal forwards a complete-but-empty bracketed paste — '\x1b[200~\x1b[201~' — to the app. CustomEditor.handleInput inserted that empty payload and the keystroke disappeared, forcing users back to Ctrl+V (which is never intercepted and already routes through handleImagePaste). Add isEmptyBracketedPaste and route a complete, empty-or-whitespace-only bracketed paste through the same onPasteImage smart reader the app.clipboard.pasteImage keybind uses, so Cmd+V attaches the clipboard image (or falls back to the #1628 smart text paste / 'clipboard is empty' diagnostic) instead of silent nothing. Bracketed pastes carrying any text (including the explicit image-file path branch from #3506) keep their existing routing. Fixes #3601
This commit is contained in:
@@ -234,6 +234,22 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
||||
return paths?.length === 1 ? paths[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` when `data` is exactly one complete bracketed paste whose payload is
|
||||
* empty (or whitespace-only). macOS terminals (iTerm2, Warp, Ghostty, …) ask
|
||||
* the OS pasteboard for `NSPasteboardTypeString` when the user hits `Cmd+V`;
|
||||
* an image-only clipboard (e.g. `Cmd+Shift+5` screenshot saved to clipboard,
|
||||
* Chrome image copy) returns `""`, so the terminal still emits the start/end
|
||||
* markers around an empty payload. Without a fallback the keystroke would
|
||||
* dead-end silently and force users back to `Ctrl+V`.
|
||||
*/
|
||||
export function isEmptyBracketedPaste(data: string): boolean {
|
||||
if (!data.startsWith(BRACKETED_PASTE_START)) return false;
|
||||
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
||||
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return false;
|
||||
return data.slice(BRACKETED_PASTE_START.length, endIndex).trim().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single image file path when `text` is exactly one explicit path
|
||||
* pointing at a supported image extension (`.png`, `.jpg`/`.jpeg`, `.gif`,
|
||||
@@ -591,6 +607,18 @@ export class CustomEditor extends Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
// #3601: a `Cmd+V`/`Ctrl+V` on an image-only clipboard makes terminals
|
||||
// that strip the pasteboard to text first (iTerm2, Warp, Ghostty,
|
||||
// Terminal.app, Windows Terminal, …) forward an empty bracketed paste
|
||||
// — the marker pair with no payload. Route it through the same smart
|
||||
// clipboard reader the `app.clipboard.pasteImage` keybind uses so the
|
||||
// keystroke attaches the image (or falls back to a text paste / empty
|
||||
// notice) instead of disappearing.
|
||||
if (isEmptyBracketedPaste(data) && this.onPasteImage) {
|
||||
void this.onPasteImage();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedKey = parseKey(data);
|
||||
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user