Files

51 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-1793 — CLI coding agent paste being mangled by Warp
Linear: https://linear.app/warpdotdev/issue/CODE-1793/claude-code-native-image-paste-being-bypassed
## Context
When a CLI coding agent like Claude Code runs as a long-running command in a Warp terminal, Warp intercepts Ctrl+V (and the platform paste action) and converts the clipboard to a shell-escaped text paste that is sent to the PTY. Two different user flows break on Windows as a result:
1. **Raw image data in the clipboard (e.g. screenshot from `Win+Shift+S` / Snipping Tool).** The Windows clipboard has only `CF_DIB`, no `CF_HDROP`. `arboard`'s `file_list()` returns no paths, `plain_text` is empty, and Warp's text-paste path sends nothing. Claude Code's native image-paste handler never gets a chance to run and the user sees nothing happen.
2. **Image file copied from Explorer.** The clipboard has a `CF_HDROP` path. Warp reads the path, shell-escapes it via `ShellFamily::escape`, and writes it as text. PowerShell-family escaping uses backtick escapes, which the CLI agent's path-detection does not recognize; Windows Terminal by contrast pastes the path verbatim and the agent's path-detection attaches the image correctly.
CLI coding agents have their own native clipboard-image paste. The keystroke for raw image data differs per platform: `Ctrl+V` on macOS and Linux, `Alt+V` on Windows (see [anthropics/claude-code#18590](https://github.com/anthropics/claude-code/issues/18590)). For image *paths* pasted as text, agents do their own path-detection on the pasted text and load the file from disk — as long as the path is verbatim.
Relevant code:
- `app/src/terminal/view.rs:14156``TerminalView::paste`. When the input box isn't focused/visible (true during a long-running command like `claude`/`codex`/`opencode`), this reads the clipboard as text and writes it to the PTY, optionally wrapping it in bracketed paste.
- `app/src/terminal/view.rs:7601``TerminalView::read_from_clipboard``clipboard_content_with_escaped_paths` at `app/src/util/clipboard.rs:8`. Converts `ClipboardContent.paths` into a shell-escaped space-joined string; falls back to `plain_text` when there are no paths. Image data on `ClipboardContent.images` is ignored on this path.
- `crates/warpui/src/windowing/winit/windows/clipboard.rs:36` — Windows `read()`. `arboard`'s `file_list()` only returns paths when the clipboard carries `CF_HDROP`; screenshot-tool captures do not.
- `crates/warp_util/src/path.rs:218``ShellFamily::escape`. Uses backtick escapes for PowerShell; the escaped string isn't the form CLI agents recognize as an image path.
- `app/src/terminal/cli_agent_sessions/mod.rs:296``CLIAgentSessionsModel::session(view_id)` gives the active CLI agent (if any) for a terminal.
- `app/src/terminal/cli_agent.rs:108``CLIAgent` enum.
Why the existing paste path can't just "pass-through" Ctrl+V: the `terminal:paste` / Windows `ctrl-v` bindings at `app/src/terminal/view/init.rs` intercept the keystroke and dispatch `TerminalAction::Paste`. If `paste()` returns without writing anything, the agent never sees the keystroke at all.
## Implemented changes
Two changes to `TerminalView::paste` in `app/src/terminal/view.rs`, gated on a new `active_cli_agent_handles_image_paste_natively(ctx)` helper that returns `true` whenever `CLIAgentSessionsModel::as_ref(ctx).session(self.view_id).is_some()` — i.e. any active CLI agent session on this terminal. The paste target must also be the PTY (`!should_paste_in_input`) and the event must not be a middle-click (`!middle_click`), since middle-click is an X11/Linux text-paste convention.
### 1. Forward the native paste keystroke for raw clipboard image data
Before the existing text-paste logic, if we're in a CLI-agent paste and `ctx.clipboard().read().has_image_data()` is true, write the platform-appropriate keystroke the CLI agent expects and return early:
- Windows: `ESC 'v'` (`[0x1b, b'v']`) — `Alt+V`.
- macOS / Linux: `[0x16]``Ctrl+V` (SYN).
This intentionally fires only when raw image data is present. A clipboard that only has file paths (Explorer copy) falls through to #2 — Claude Code's native `Alt+V` handler only reads raw image bytes and errors out ("No image found in clipboard. Use alt+v to paste images.") if we hand it a path-only clipboard.
### 2. Skip shell-escaping on pasted file paths in CLI-agent pastes
`TerminalView::read_from_clipboard` and `TerminalView::middle_click_paste_content` now take `Option<ShellFamily>` instead of `ShellFamily`. `paste()` passes `None` when `is_cli_agent_paste` is true; otherwise it passes `Some(self.shell_family(ctx))` as before. `clipboard_content_with_escaped_paths` already handled `None` by returning paths verbatim, so no changes were needed there.
The net effect: a path like `C:\Users\andy\Pictures\screenshot.png` is sent to the agent exactly as Windows Terminal would paste it — the agent's file-path detection recognizes it and attaches the image. No PowerShell backtick escaping is applied.
### Scope
`active_cli_agent_handles_image_paste_natively` returns `true` for *any* active CLI agent session, including `CLIAgent::Unknown` (user-configured regex matches). The bar to register a session at all is that Warp's CLI-agent detection matched the command; once matched, the coding-agent contract (verbatim paths, optional native image paste) applies uniformly. No per-agent allowlist is maintained.
Everything else (regular text pastes, pastes into Warp's input editor, middle-click, plain shells without an active CLI agent session) continues through the existing `read_from_clipboard` → bracketed-paste path with shell-escaping unchanged.
A feature flag isn't warranted: the change is scoped by the active CLI agent session and reverts the hijack to faithful pass-through behavior, which is strictly closer to what the agent would see running under a plain terminal emulator.
## Testing and validation
Manual verification on Windows (primary platform for the bug):
1. Run `claude` in a Warp terminal until the Claude Code TUI is active.
2. Capture a screenshot with `Win+Shift+S`. Press `Ctrl+V` in Warp. Claude Code should show the `[Image #N]` attachment chip (it receives `Alt+V` and reads the clipboard itself). Previously: nothing happened.
3. In Explorer, copy an image file (`.png`). Press `Ctrl+V`. Claude Code should attach the image via its path-detection on the unescaped path. Previously: PowerShell-escaped path was pasted as text and the agent didn't recognize it.
4. Repeat 23 with `codex` and `opencode` — unescaped file-path paste should attach the image for both. (Raw image data via Alt+V is Claude-specific; Codex/OpenCode paths are the primary case for those agents.)
5. Copy plain text. Press `Ctrl+V`. Text should paste as before; the raw-image early return does not fire and `clipboard_content_with_escaped_paths(..., None)` returns `plain_text` unchanged.
6. Outside any CLI agent (plain `pwsh`), copy a screenshot and Ctrl+V. Behavior is unchanged from today (no CLI agent session → helper returns `false` → shell-escape still applied).
Cross-platform regression checks:
- macOS: `claude` with a screenshot in the clipboard + `Ctrl+V` still attaches the image. On macOS, `Cmd+V` in Warp dispatches the same `TerminalAction::Paste`; the keystroke branch writes `0x16` which matches what macOS Claude Code expects.
- Linux: same as macOS with `Ctrl+V`.
- Middle-click paste on Linux still inserts text (early branch skipped because `middle_click` is true, which also means shell escaping is still applied on that path).
- Pasting into Warp's own input editor (Agent Mode, rich input) is unaffected because `should_paste_in_input` short-circuits before the new branches.
Automated:
- `cargo check -p warp --lib`.
- Existing paste tests in `app/src/terminal/view_test.rs` and `app/src/terminal/input_test.rs` continue to pass — the test helper `read_from_clipboard(ctx)` was updated to pass `Some(ShellFamily::Posix)` to match the new signature, and the new branches only fire when a CLI agent session is active, which those tests don't set up.
## Risks and mitigations
- **Sending `Alt+V` / `Ctrl+V` bytes to a non-Claude TUI that happens to be detected as a CLI agent session.** Only fires when the clipboard actually has raw image data (`has_image_data()` is true), so for normal text/path pastes this branch never runs. Agents that don't handle the keystroke will simply ignore the byte.
- **Future CLI agent updates change the paste keystroke.** The mapping is one `cfg!(windows)` branch inside `paste()`; updating it is a one-line change.
- **Clipboard with both image data and text.** If image data is present we forward the keystroke and don't paste the text. This matches the pre-existing macOS behavior (where the path text was technically inserted but Claude Code attached the image and ignored the text) and is what users asking for native image paste expect.
- **User-defined `CLIAgent::Unknown` regex matches.** These are treated the same as known agents — verbatim paths, keystroke forwarding for raw images. The regex matching is opt-in (user adds the pattern) so the assumption that they want CLI-agent semantics is reasonable.