Skip to content

fix(tui): forward pastes with bracketed-paste framing when the child enabled mode 2004 - #457

Merged
edwin-zvs merged 1 commit into
mainfrom
fix/bracketed-paste-passthrough
Jun 23, 2026
Merged

edwin-zvs merged 1 commit into
mainfrom
fix/bracketed-paste-passthrough

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Problem

Dragging an image into a Claude Code session running inside construct put the image path into the input as literal text, instead of the [image #N] reference the native Claude Code CLI produces. (Reported behavior: native CLI → [image #1]; construct claude session → raw path.)

Root cause

The construct client does receive the drag correctly as a bracketed-paste event from the outer terminal (CtEvent::Paste). But when it forwards that to the child PTY, dispatch_paste_text sent text.into_bytes() — raw, with no ESC[200~/ESC[201~ framing. Claude Code's drag-image detection (and its multiline-paste guard) only fire on a real bracketed paste, so the path arrived as ordinary typed keystrokes and stayed literal.

The client already models each child terminal with vt100, which tracks DEC private mode 2004 (Screen::bracketed_paste()). It just never consulted that flag when forwarding a paste — effectively a terminal-passthrough bug that bites images hardest.

Fix

  • Add ItemHistory::bracketed_paste_enabled() — reads the live (cached) parser's mode 2004 state. The ESC[?2004h toggle is a CSI sequence, so it already flows through feed_byte into the parser for the interactive harnesses (claude/codex/shell).
  • dispatch_paste_text now routes through encode_paste_for_pty, which wraps the payload in ESC[200~…ESC[201~ only when the target child currently has mode 2004 enabled. Synth/smith sessions with no live parser fall back to raw forwarding (prior behavior).
  • The closing marker is stripped from the payload first so an embedded ESC[201~ can't terminate the paste early (paste-injection guard, as real terminals do).

This is general terminal fidelity, not an image special-case: multiline and shell pastes into a mode-2004 child are now framed too (no premature submit/execute).

Verification

Validated the full chain against the real claude binary in a PTY (not just unit tests):

  1. Claude Code emits ESC[?2004h on startup (byte 19) → construct tracks it.
  2. Bracketed-pasted image path → Claude renders [Image #1].
  3. Raw path → Claude leaves the literal /tmp/...png text.

(2) and (3) reproduce the exact reported discrepancy and confirm the fix closes it.

Tests:

  • pty_render::tests::tracks_child_bracketed_paste_mode — ESC[?2004h/l toggles the tracked state.
  • app::tests::paste_wraps_in_bracketed_markers_only_when_child_enables_mode_2004 — wraps only when mode 2004 is on; strips embedded end-marker.
  • Full agentd-cli suite green (275 + reconnect).

Adds spec 0034-forwarded-pastes-honor-child-bracketed-paste-mode.

Binaries

Code lives entirely in crates/cli → relevant binary is construct:
/Users/moon/agentd/.claude/worktrees/bracketed-paste-passthrough/target/debug/construct

🤖 Generated with Claude Code

…enabled mode 2004

Dragging an image into a Claude Code session running inside construct
landed the image *path* as literal text instead of an `[image #N]`
reference like the native Claude Code CLI produces.

Root cause: the client received the drag correctly as a bracketed-paste
event from the outer terminal, but forwarded it to the child PTY as raw
bytes, dropping the `ESC[200~`/`ESC[201~` framing. Claude Code's
drag-image detection (and its multiline-paste guard) only fire on a real
bracketed paste, so the path was treated as typed keystrokes.

The client already models each child's terminal via vt100, which tracks
DEC mode 2004 (`screen.bracketed_paste()`) — it just never consulted it
when forwarding a paste. Now `dispatch_paste_text` wraps the payload in
the bracketed-paste markers a real terminal would send, but only when the
target child currently has mode 2004 enabled (claude/codex/shell do;
synth/smith sessions fall back to raw, preserving prior behavior). The
closing marker is stripped from the payload first so an embedded
`ESC[201~` can't end the paste early.

Verified end-to-end against the real claude binary in a PTY: a
bracketed-pasted image path renders as `[Image #1]`; the same path sent
raw stays literal text.

Adds spec 0034.
@edwin-zvs
edwin-zvs merged commit c78cd3d into main Jun 23, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the fix/bracketed-paste-passthrough branch June 23, 2026 15:31
@edwin-zvs edwin-zvs mentioned this pull request Jun 23, 2026
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