Skip to content

fix: restore image paste for Claude (desktop + mobile) in fullscreen (v0.4.3)#154

Merged
gbasin merged 4 commits into
masterfrom
fix/claude-fullscreen-image-paste
Jun 30, 2026
Merged

fix: restore image paste for Claude (desktop + mobile) in fullscreen (v0.4.3)#154
gbasin merged 4 commits into
masterfrom
fix/claude-fullscreen-image-paste

Conversation

@gbasin

@gbasin gbasin commented Jun 30, 2026

Copy link
Copy Markdown
Owner

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.

  • Desktop (useTerminal.ts): new resolveImagePasteData(agentType, {blob?, path?}). Cmd+V uploads the clipboard blob to /api/paste-image then sends the path bracketed; Ctrl+V resolves a path via /api/clipboard-file-path then sends it bracketed; Codex returns the literal \x16; falls back to the empty-bracket signal only when no path resolves.
  • Mobile (TerminalControls.tsx): the paste button/modal deliver the uploaded path via shared imagePathInput(path, agentType) — bracketed for Claude/unknown, raw for Codex (which attaches raw paths natively).
  • Shared src/client/utils/paste.ts (bracketedPaste, imagePathInput, sanitizeImagePath).

Hardening (from a Codex review pass)

  • Guard async paste sends: only forward terminal-input if the session captured when the paste began is still attached (the image upload widens the async window; Ctrl+V also became async).
  • Mobile paste modal: agentType added to the effect deps so a Claude↔Codex switch while it's open uses the right delivery.
  • Sanitize image paths (strip C0 controls + DEL) before bracketing, so a crafted filename can't break out of the paste sequence.

Verification

  • lint 0 · typecheck clean · 924 unit tests pass (added paste-util + Codex/Claude branching + sanitization tests).
  • End-to-end in a headless browser against live tmux+agentboard (isolated instance), Cmd+V, Ctrl+V, and the mobile paste button all produce [Image #N]:
    • Claude fullscreen — Cmd+V ✅ · Ctrl+V ✅
    • Claude classic renderer — Cmd+V ✅ · Ctrl+V ✅
    • Mobile paste button — Claude (bracketed) ✅ · Codex (raw) ✅
    • Codex — desktop Cmd+V/Ctrl+V emit the unchanged literal \x16 (no regression).
  • Patch version bump 0.4.2 → 0.4.3 (package.json + bun.lock pins); bun install --frozen-lockfile clean.

gbasin added 4 commits June 29, 2026 23:02
…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.
@gbasin gbasin merged commit e033ec4 into master Jun 30, 2026
5 checks passed
@gbasin gbasin deleted the fix/claude-fullscreen-image-paste branch June 30, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant