fix(paste): inject image path as bracketed paste → real image artifact#262
Merged
Conversation
…ifact The host→island image-paste bridge staged the clipboard image correctly but injected its path as raw keystrokes. Claude Code (and other adapters) only run their file-path→image auto-attach on a PASTE event, so a streamed path rendered as a literal link instead of the `[Image #N]` attachment pill — the model never saw the image, just a path string. Wrap the injected path in DEC 2004 bracketed-paste markers (bpStart/bpEnd, the same constants the drop/paste interceptor already uses) at both inject sites: - the in-session Ctrl-V / drag-drop interceptor (main.go), and - the `dejima paste` command (paste.go), which also drops its `Read <path>` prefix and no longer auto-submits (the image pill lands in the prompt for the agent's next turn rather than sending a bare image alone). Verified live: injecting a bare image path into Claude Code v2.1.197 over tmux renders `/path/clip.png` as literal text; the same path wrapped in \e[200~…\e[201~ renders `[Image #1]`. Adds a bracketedPaste unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGTf7anGr5S1zjCD61hwC
This was referenced Jul 1, 2026
aoos
added a commit
that referenced
this pull request
Jul 2, 2026
) Getting a client-local file to an agent (e.g. a .p8 key) relied on terminal drag-drop, which is unreliable over tmux+SSH — the case operators actually live in. #177 adds a robust, terminal-independent path to hand an agent a file. Most of the pipeline already shipped with image-paste (#262): the WriteFile→docker-cp upload (which IS the remote/SSH path, since `dejima connect` dials the daemon), bracketed-paste injection, and the drag-drop/clipboard entry points. This adds the missing P1/MVP: a way to TYPE or PASTE a path. - In-session minibuffer: an attach chord (Ctrl-O, configurable via DEJIMA_ATTACH_KEY=ctrl-o|ctrl-]|off) opens a local-path prompt inside an attached island session. The typed/pasted path is uploaded via the shared stageLocalFile and its in-island path injected as a bracketed paste — non-image files land as a path the agent Reads (no artifact forcing), with a "📎 attached: <name> → <path>" affordance. - `dejima attach <island>[/<agent>] <path>` — non-interactive twin, mirroring `dejima paste` (any file; --agent, --no-inject). - stageLocalFile extracted from islandPaste.drop and reused by all three call sites (drag-drop, minibuffer, CLI). inject hoisted so the chord and the paste bridge share it. - Discoverability: session banner hint + a `dejima attach` row in the ? help overlay. Client-only; no daemon/endpoint change. P2a drag-drop + P2b clipboard already shipped (untouched); P3 (route all three through one affordance) is a fast-follow. Rides v0.8.5. Tests: splitOnAttach, the attachState minibuffer (type/backspace/Ctrl-U/ Esc/Ctrl-C/LF-CR/bracketed-paste-strip/control-ignore), expandClientPath, configuredAttachKey/label, and the attach command shape. go test + golangci-lint green. Claude-Session: https://claude.ai/code/session_01ENXLudmsVvUbqa2hh1NvwS Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aoos
added a commit
that referenced
this pull request
Jul 4, 2026
…s in a full-screen TUI (#262) (#299) Ctrl-V an image (or drag-drop a file) into a full-screen TUI agent (Claude Code on Windows) and the client printed "[dejima] pasted image → …" to os.Stderr in the middle of the agent's screen. That agent owns its input line + cursor, so the stray \r\n-wrapped write parked the cursor on the injected [Image #N] token — the operator had to End/Ctrl-L to resync. Add an alt-screen watcher (altscreen.go): it scans the AGENT'S OUTPUT stream for the alternate-screen escapes (\e[?1049h/l, and the 1047/47 variants) to track "the agent owns a full-screen TUI right now" — agent-agnostic and more correct than gating by agent type (a claude-code agent at a shell prompt is NOT in alt-screen; a bash agent running vim IS). A holdback tail catches a marker split across reads; the state is atomic (set in the reader goroutine, read elsewhere). Route the passive paste success notices ("uploaded →", "pasted image →") through a sessionNotice helper that stays silent while alt-screen is active — the injected token/path is feedback enough, and there's nowhere to print on a full-screen TUI without stomping its cells. Plain shells / headless agents still get the notices. This is the shared alt-screen primitive; the paste-of-path routing (#260) and the minibuffer stdout-pause (#31) build on the same signal. Claude-Session: https://claude.ai/code/session_01ENXLudmsVvUbqa2hh1NvwS Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aoos
added a commit
that referenced
this pull request
Jul 4, 2026
…hell, text in a TUI (#260) Pasting an existing file PATH as a text reference silently uploaded the file instead of passing the text through: pasteScanner → droppedLocalFile os.Stat's the bracketed-paste content, and any single existing regular-file path was swallowed + uploaded. Very common on Windows (pasting C:\...\java.exe into prose) — the text was lost and an unwanted drop-*.exe appeared in the intake. Uploading a paste is now the EXCEPTION, never the silent default. onDrop returns a "handled" bool; when false, the scanner forwards the paste verbatim as text. pasteDropPolicy routes on the alt-screen primitive (from #262): - Agent in a full-screen TUI → paste is TEXT. A confirm prompt can't be drawn over a TUI's screen anyway (that was the cursor-desync bug), and a TUI paste is almost always a reference. - Plain shell → confirm-before-ingest: '📎 <file> is a file on your computer — [u] upload · any other key = paste the path as text'. 'u'/'y' uploads+injects; ANY other key forwards the path as text then the operator's keystrokes — so a reflex Enter (the exact muscle-memory that caused this) sends the path as text, never an upload. - DEJIMA_PASTE_UPLOAD=off → always text (explicit upload stays via the attach chord / `dejima attach`). Clipboard-IMAGE paste (Ctrl-V) is unchanged — it's an explicit action, not a surprising path ingest. Tests: pasteUploadEnabled (env matrix); pasteDropPolicy (shell→confirm, TUI→text, disabled→text); process() forwards the paste verbatim when onDrop declines, swallows when it consumes. Builds on the alt-screen primitive from #299. Client-only, no daemon change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENXLudmsVvUbqa2hh1NvwS
aoos
added a commit
that referenced
this pull request
Jul 4, 2026
…hell, text in a TUI (#260) Pasting an existing file PATH as a text reference silently uploaded the file instead of passing the text through: pasteScanner → droppedLocalFile os.Stat's the bracketed-paste content, and any single existing regular-file path was swallowed + uploaded. Very common on Windows (pasting C:\...\java.exe into prose) — the text was lost and an unwanted drop-*.exe appeared in the intake. Uploading a paste is now the EXCEPTION, never the silent default. onDrop returns a "handled" bool; when false, the scanner forwards the paste verbatim as text. pasteDropPolicy routes on the alt-screen primitive (from #262): - Agent in a full-screen TUI → paste is TEXT. A confirm prompt can't be drawn over a TUI's screen anyway (that was the cursor-desync bug), and a TUI paste is almost always a reference. - Plain shell → confirm-before-ingest: '📎 <file> is a file on your computer — [u] upload · any other key = paste the path as text'. 'u'/'y' uploads+injects; ANY other key forwards the path as text then the operator's keystrokes — so a reflex Enter (the exact muscle-memory that caused this) sends the path as text, never an upload. - DEJIMA_PASTE_UPLOAD=off → always text (explicit upload stays via the attach chord / `dejima attach`). Clipboard-IMAGE paste (Ctrl-V) is unchanged — it's an explicit action, not a surprising path ingest. Tests: pasteUploadEnabled (env matrix); pasteDropPolicy (shell→confirm, TUI→text, disabled→text); process() forwards the paste verbatim when onDrop declines, swallows when it consumes. Builds on the alt-screen primitive from #299. Client-only, no daemon change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENXLudmsVvUbqa2hh1NvwS
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.
What
The image-paste bridge staged the clipboard image into the island correctly, but injected its path into the agent's prompt as raw keystrokes. Claude Code (and other adapters) only run their file-path→image auto-attach on a paste event — so a streamed path rendered as a literal link/path string instead of the
[Image #N]attachment pill. The model never actually saw the image.Fix: wrap the injected path in DEC 2004 bracketed-paste markers (
\e[200~…\e[201~, reusing thebpStart/bpEndconstants the drop/paste interceptor already defines) at both inject sites:main.go— the in-session Ctrl-V / drag-drop interceptor (the common path).paste.go— thedejima pastecommand; also drops itsRead <path>prefix and no longer auto-submits (the image pill lands in the prompt for the agent's next turn instead of sending a bare image alone).Why it's the right (and small) fix
Capture + upload-into-island was already correct; only the final inject framing was wrong. No new endpoints, no grant routes, no protocol reconstruction — just the paste framing.
Verified live
Against Claude Code v2.1.197 over tmux inside an island:
/path/clip.pngliteral text ❌\e[200~…\e[201~— new[Image #1]attachment pill ✅Same file, same path — only the bracketed-paste framing differs.
Tests
TestBracketedPasteunit test.go build ./...,go test ./cmd/dejima/,golangci-lint run ./cmd/dejima/all clean locally.🤖 Generated with Claude Code