fix: restore image paste for Claude (desktop + mobile) in fullscreen (v0.4.3)#154
Merged
Conversation
…reen
Cmd+V / Ctrl+V image paste broke once Claude Code's fullscreen
("no-flicker") renderer became the default. The old flow sent an empty
bracketed-paste signal (\e[200~\e[201~) and relied on Claude reading the
macOS system clipboard itself — behavior its alt-screen renderer no
longer performs, so nothing got attached.
Both renderers DO keep bracketed paste enabled, and Claude attaches an
image when it receives the image's file PATH inside a bracketed paste
(not raw-typed, not an empty bracket). So deliver an explicit path:
- Cmd+V: upload the clipboard image blob (already in the paste event) to
/api/paste-image, then send the returned path wrapped in bracketed
paste. Works cross-machine; no dependency on the system clipboard.
- Cmd+V Finder image / metadata-only image: resolve a path via
/api/clipboard-file-path and send it bracketed.
- Ctrl+V (no paste event): resolve a pasteboard path via
/api/clipboard-file-path and send it bracketed.
- Codex keeps its literal Ctrl+V signal.
- Falls back to the empty-bracket signal only when no path resolves.
Verified end-to-end (browser -> tmux -> Claude fullscreen) that both
Cmd+V and Ctrl+V now produce a native [Image #N] attachment.
The mobile paste button uploaded the clipboard image then sent the /tmp path raw via onSendKey. Claude inserts a raw-typed path as literal text (no attachment), so the button never produced an [Image #N] for Claude sessions. Deliver the uploaded path through a shared agent-aware helper: - Claude (and unknown agents): wrap the path in bracketed-paste markers so Claude attaches it natively ([Image #N]). - Codex: send the raw path unchanged — Codex recognizes a typed image path and attaches it on its own. - Extract bracketedPaste() to src/client/utils/paste.ts (shared with the desktop Cmd+V/Ctrl+V path in useTerminal) and add imagePathInput(). - Thread the attached session's agentType into TerminalControls. Verified in dev-browser (mobile viewport): the paste button now yields [Image #1] for both Claude (bracketed) and Codex (raw).
- Guard async paste sends: only forward terminal-input if the session captured when the paste began is still attached, so a mid-paste session switch can't deliver to the wrong/no-longer-viewed session (the new image upload widens the async window; Ctrl+V also became async). Server already drops stale input, but this makes client intent explicit and consistent. - Mobile paste modal: add agentType to the paste useEffect deps so a Claude->Codex switch while the modal is open uses the right delivery. - Sanitize image paths (strip C0 controls + DEL) before wrapping in bracketed-paste markers, so a crafted filename can't embed the paste end marker / escape sequences and inject terminal control codes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pasting an image into a Claude Code session stopped attaching once Claude's fullscreen ("no-flicker") renderer became the default. The old flow sent an empty bracketed-paste signal (
\e[200~\e[201~) and relied on Claude reading the macOS system clipboard itself — behavior its alt-screen renderer no longer performs. Mobile separately never produced a real attachment (it inserted the path as literal text).Verified empirically (captured Claude's startup escape sequences + drove a live session): Claude keeps bracketed paste enabled in both renderers, and it attaches an image only when it receives the image's file path inside a bracketed paste — a raw path is just literal text. Codex instead attaches a raw typed path natively.
Fix
Deliver the image's file path wrapped in bracketed-paste markers so Claude attaches it (
[Image #N]) in both renderers; keep Codex on its native path.useTerminal.ts): newresolveImagePasteData(agentType, {blob?, path?}). Cmd+V uploads the clipboard blob to/api/paste-imagethen sends the path bracketed; Ctrl+V resolves a path via/api/clipboard-file-paththen sends it bracketed; Codex returns the literal\x16; falls back to the empty-bracket signal only when no path resolves.TerminalControls.tsx): the paste button/modal deliver the uploaded path via sharedimagePathInput(path, agentType)— bracketed for Claude/unknown, raw for Codex (which attaches raw paths natively).src/client/utils/paste.ts(bracketedPaste,imagePathInput,sanitizeImagePath).Hardening (from a Codex review pass)
terminal-inputif the session captured when the paste began is still attached (the image upload widens the async window; Ctrl+V also became async).agentTypeadded to the effect deps so a Claude↔Codex switch while it's open uses the right delivery.Verification
[Image #N]:\x16(no regression).bun install --frozen-lockfileclean.