agent_ui: Add adjustments to terminal selection as context (#47950)

Danilo Leal created

Follow up to https://github.com/zed-industries/zed/pull/47637

- Removes the requirement for the terminal to be focused to use the
`cmd->` keybinding. This way, we match the behavior of buffer selections
and you can always add what's selected in the terminal inside the agent
panel
- Add number of lines selected in the mention button as well
- Make the "Selection" menu item inside the "Add Context" menu also
observe terminal selections

<img width="420" height="254" alt="Screenshot 2026-01-29 at 1  36@2x"
src="https://github.com/user-attachments/assets/25d2eb00-096f-44d9-8882-273932ca43e4"
/>

Release Notes:

- Agent: Add number of liners selected in the terminal context mention

Change summary

crates/acp_thread/src/mention.rs           | 42 ++++++++++++++++++-----
crates/agent/src/thread.rs                 |  2 
crates/agent_ui/src/acp/message_editor.rs  |  3 +
crates/agent_ui/src/acp/thread_view.rs     | 19 +++++++---
crates/agent_ui/src/completion_provider.rs |  3 +
crates/agent_ui/src/mention_set.rs         |  2 
crates/agent_ui/src/text_thread_editor.rs  | 41 +---------------------
7 files changed, 55 insertions(+), 57 deletions(-)

Detailed changes

crates/acp_thread/src/mention.rs 🔗

@@ -54,7 +54,9 @@ pub enum MentionUri {
     Fetch {
         url: Url,
     },
-    TerminalSelection,
+    TerminalSelection {
+        line_count: u32,
+    },
 }
 
 impl MentionUri {
@@ -201,7 +203,11 @@ impl MentionUri {
                         line_range,
                     })
                 } else if path.starts_with("/agent/terminal-selection") {
-                    Ok(Self::TerminalSelection)
+                    let line_count = single_query_param(&url, "lines")?
+                        .unwrap_or_else(|| "0".to_string())
+                        .parse::<u32>()
+                        .unwrap_or(0);
+                    Ok(Self::TerminalSelection { line_count })
                 } else {
                     bail!("invalid zed url: {:?}", input);
                 }
@@ -224,7 +230,13 @@ impl MentionUri {
             MentionUri::TextThread { name, .. } => name.clone(),
             MentionUri::Rule { name, .. } => name.clone(),
             MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
-            MentionUri::TerminalSelection => "Terminal".to_string(),
+            MentionUri::TerminalSelection { line_count } => {
+                if *line_count == 1 {
+                    "Terminal (1 line)".to_string()
+                } else {
+                    format!("Terminal ({} lines)", line_count)
+                }
+            }
             MentionUri::Selection {
                 abs_path: path,
                 line_range,
@@ -247,7 +259,7 @@ impl MentionUri {
             MentionUri::TextThread { .. } => IconName::Thread.path().into(),
             MentionUri::Rule { .. } => IconName::Reader.path().into(),
             MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
-            MentionUri::TerminalSelection => IconName::Terminal.path().into(),
+            MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
             MentionUri::Selection { .. } => IconName::Reader.path().into(),
             MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
         }
@@ -342,7 +354,12 @@ impl MentionUri {
                 url
             }
             MentionUri::Fetch { url } => url.clone(),
-            MentionUri::TerminalSelection => Url::parse("zed:///agent/terminal-selection").unwrap(),
+            MentionUri::TerminalSelection { line_count } => {
+                let mut url = Url::parse("zed:///agent/terminal-selection").unwrap();
+                url.query_pairs_mut()
+                    .append_pair("lines", &line_count.to_string());
+                url
+            }
         }
     }
 }
@@ -650,13 +667,20 @@ mod tests {
 
     #[test]
     fn test_parse_terminal_selection_uri() {
-        let terminal_uri = "zed:///agent/terminal-selection";
+        let terminal_uri = "zed:///agent/terminal-selection?lines=42";
         let parsed = MentionUri::parse(terminal_uri, PathStyle::local()).unwrap();
         match &parsed {
-            MentionUri::TerminalSelection => {}
-            _ => panic!("Expected Terminal variant"),
+            MentionUri::TerminalSelection { line_count } => {
+                assert_eq!(*line_count, 42);
+            }
+            _ => panic!("Expected TerminalSelection variant"),
         }
         assert_eq!(parsed.to_uri().to_string(), terminal_uri);
-        assert_eq!(parsed.name(), "Terminal");
+        assert_eq!(parsed.name(), "Terminal (42 lines)");
+
+        // Test single line
+        let single_line_uri = "zed:///agent/terminal-selection?lines=1";
+        let parsed_single = MentionUri::parse(single_line_uri, PathStyle::local()).unwrap();
+        assert_eq!(parsed_single.name(), "Terminal (1 line)");
     }
 }

crates/agent/src/thread.rs 🔗

@@ -317,7 +317,7 @@ impl UserMessage {
                         MentionUri::Diagnostics { .. } => {
                             write!(&mut diagnostics_context, "\n{}\n", content).ok();
                         }
-                        MentionUri::TerminalSelection => {
+                        MentionUri::TerminalSelection { .. } => {
                             write!(
                                 &mut selection_context,
                                 "\n{}",

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -1017,7 +1017,8 @@ impl MessageEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let mention_uri = MentionUri::TerminalSelection;
+        let line_count = text.lines().count() as u32;
+        let mention_uri = MentionUri::TerminalSelection { line_count };
         let mention_text = mention_uri.as_link().to_string();
 
         let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -6621,7 +6621,7 @@ impl AcpThreadView {
             .map(|active| active.prompt_capabilities.borrow().image)
             .unwrap_or_default();
 
-        let has_selection = workspace
+        let has_editor_selection = workspace
             .upgrade()
             .and_then(|ws| {
                 ws.read(cx)
@@ -6634,6 +6634,13 @@ impl AcpThreadView {
                 })
             });
 
+        let has_terminal_selection = workspace
+            .upgrade()
+            .and_then(|ws| ws.read(cx).panel::<TerminalPanel>(cx))
+            .is_some_and(|panel| !panel.read(cx).terminal_selections(cx).is_empty());
+
+        let has_selection = has_editor_selection || has_terminal_selection;
+
         ContextMenu::build(window, cx, move |menu, _window, _cx| {
             menu.key_context("AddContextMenu")
                 .header("Context")
@@ -6721,10 +6728,10 @@ impl AcpThreadView {
                         .disabled(!has_selection)
                         .handler({
                             move |window, cx| {
-                                message_editor.focus_handle(cx).focus(window, cx);
-                                message_editor.update(cx, |editor, cx| {
-                                    editor.insert_selections(window, cx);
-                                });
+                                window.dispatch_action(
+                                    zed_actions::agent::AddSelectionToThread.boxed_clone(),
+                                    cx,
+                                );
                             }
                         }),
                 )
@@ -6870,7 +6877,7 @@ impl AcpThreadView {
                     cx.open_url(url.as_str());
                 }
                 MentionUri::Diagnostics { .. } => {}
-                MentionUri::TerminalSelection => {}
+                MentionUri::TerminalSelection { .. } => {}
             })
         } else {
             cx.open_url(&url);

crates/agent_ui/src/completion_provider.rs 🔗

@@ -636,7 +636,8 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
                                     };
                                     let offset = start.to_offset(&snapshot);
 
-                                    let mention_uri = MentionUri::TerminalSelection;
+                                    let line_count = terminal_text.lines().count() as u32;
+                                    let mention_uri = MentionUri::TerminalSelection { line_count };
                                     let range = snapshot.anchor_after(offset + terminal_range.start)
                                         ..snapshot.anchor_after(offset + terminal_range.end);
 

crates/agent_ui/src/mention_set.rs 🔗

@@ -249,7 +249,7 @@ impl MentionSet {
                 debug_panic!("unexpected selection URI");
                 Task::ready(Err(anyhow!("unexpected selection URI")))
             }
-            MentionUri::TerminalSelection => {
+            MentionUri::TerminalSelection { .. } => {
                 debug_panic!("unexpected terminal URI");
                 Task::ready(Err(anyhow!("unexpected terminal URI")))
             }

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -65,10 +65,8 @@ use workspace::{
     searchable::{Direction, SearchableItemHandle},
 };
 
-use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
 use workspace::{
     Save, Toast, Workspace,
-    dock::Panel,
     item::{self, FollowableItem, Item},
     notifications::NotificationId,
     pane,
@@ -1498,39 +1496,8 @@ impl TextThreadEditor {
             return;
         };
 
-        // Try terminal selection first (requires focus, so more specific)
-        if let Some(terminal_text) = maybe!({
-            let terminal_panel = workspace.panel::<TerminalPanel>(cx)?;
-
-            if !terminal_panel
-                .read(cx)
-                .focus_handle(cx)
-                .contains_focused(window, cx)
-            {
-                return None;
-            }
-
-            let terminal_view = terminal_panel.read(cx).pane().and_then(|pane| {
-                pane.read(cx)
-                    .active_item()
-                    .and_then(|t| t.downcast::<TerminalView>())
-            })?;
-
-            terminal_view
-                .read(cx)
-                .terminal()
-                .read(cx)
-                .last_content
-                .selection_text
-                .clone()
-        }) {
-            if !terminal_text.is_empty() {
-                agent_panel_delegate.quote_terminal_text(workspace, terminal_text, window, cx);
-                return;
-            }
-        }
-
-        // Try editor selection
+        // Get buffer info for the delegate call (even if empty, AcpThreadView ignores these
+        // params and calls insert_selections which handles both terminal and buffer)
         if let Some((selections, buffer)) = maybe!({
             let editor = workspace
                 .active_item(cx)
@@ -1551,9 +1518,7 @@ impl TextThreadEditor {
             });
             Some((selections, buffer))
         }) {
-            if !selections.is_empty() {
-                agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
-            }
+            agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
         }
     }