Skip to content

fix(paste): inject image path as bracketed paste → real image artifact#262

Merged
aoos merged 2 commits into
masterfrom
feat/image-paste-bracketed
Jul 1, 2026
Merged

fix(paste): inject image path as bracketed paste → real image artifact#262
aoos merged 2 commits into
masterfrom
feat/image-paste-bracketed

Conversation

@aoos

@aoos aoos commented Jul 1, 2026

Copy link
Copy Markdown
Owner

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 the bpStart/bpEnd constants 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 — the dejima paste command; also drops its Read <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:

Inject form Renders as
bare path (raw keystrokes) — old behavior /path/clip.png literal text ❌
path wrapped in \e[200~…\e[201~ — new [Image #1] attachment pill ✅

Same file, same path — only the bracketed-paste framing differs.

Tests

  • New TestBracketedPaste unit test.
  • go build ./..., go test ./cmd/dejima/, golangci-lint run ./cmd/dejima/ all clean locally.

🤖 Generated with Claude Code

aoos and others added 2 commits July 1, 2026 19:28
…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
@aoos aoos merged commit f41b805 into master Jul 1, 2026
4 checks passed
@aoos aoos deleted the feat/image-paste-bracketed branch July 1, 2026 19:46
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
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