terminal: Forward Ctrl+V when clipboard contains images (#42258)

Kingsword created

When running Codex CLI, Claude Code, or other TUI agents in Zed’s
terminal, pasting images wasn’t supported — Zed
treated all clipboard content as plain text and simply pushed it into
the PTY, so the agent never saw the image data.
This change makes terminal pastes behave like they do in a native
terminal: if the clipboard contains an image, Zed now emits a raw Ctrl+V
to the PTY so the agent can read the system clipboard itself.

Release Notes:

- Fixed terminal-launched Codex/Claude sessions by forwarding Ctrl+V for
clipboard images so agents can attach them

Change summary

crates/terminal_view/src/terminal_view.rs | 28 +++++++++++++++++++++---
1 file changed, 24 insertions(+), 4 deletions(-)

Detailed changes

crates/terminal_view/src/terminal_view.rs 🔗

@@ -8,8 +8,8 @@ mod terminal_slash_command;
 use assistant_slash_command::SlashCommandRegistry;
 use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
 use gpui::{
-    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
+    Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
+    Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
     ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
 };
 use persistence::TERMINAL_DB;
@@ -687,12 +687,32 @@ impl TerminalView {
 
     ///Attempt to paste the clipboard into the terminal
     fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
-        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
+        let Some(clipboard) = cx.read_from_clipboard() else {
+            return;
+        };
+
+        if clipboard.entries().iter().any(|entry| match entry {
+            ClipboardEntry::Image(image) => !image.bytes.is_empty(),
+            _ => false,
+        }) {
+            self.forward_ctrl_v(cx);
+            return;
+        }
+
+        if let Some(text) = clipboard.text() {
             self.terminal
-                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
+                .update(cx, |terminal, _cx| terminal.paste(&text));
         }
     }
 
+    /// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
+    /// and attach images using their native workflows.
+    fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
+        self.terminal.update(cx, |term, _| {
+            term.input(vec![0x16]);
+        });
+    }
+
     fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
         self.clear_bell(cx);
         self.terminal.update(cx, |term, _| {