From 263e0004cefedc7cbdde24d24b971a5e03e41a60 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Thu, 29 Jan 2026 14:26:23 -0300
Subject: [PATCH] agent_ui: Add adjustments to terminal selection as context
(#47950)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
Release Notes:
- Agent: Add number of liners selected in the terminal context mention
---
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(-)
diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs
index edfbe3443fef822ce34950f1630be6862df00b54..6550e6f4ade781c165fe6671c9150004149f60da 100644
--- a/crates/acp_thread/src/mention.rs
+++ b/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::()
+ .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)");
}
}
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index bb5c09c38e3836dfdaba15436ad280f2a0cd5910..6c7c532a7984350fba2f4e6079f851482365c2e8 100644
--- a/crates/agent/src/thread.rs
+++ b/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{}",
diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs
index f6d1eba70f84cc98b05ac9566d0260bd61996da8..bbcc0c1bbf6eba0ebeca0f7a97b732ca2721e8c3 100644
--- a/crates/agent_ui/src/acp/message_editor.rs
+++ b/crates/agent_ui/src/acp/message_editor.rs
@@ -1017,7 +1017,8 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context,
) {
- 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| {
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 5f1b52d701f0337c8450726595543b9193298ea1..b739823be0a168d51c0a36cc9e07d49da9277345 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/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::(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);
diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs
index 8b6f6f560c278c03205f790e9819fcf87484d0aa..0195522df6b1801f6200f09bfc0a255963470695 100644
--- a/crates/agent_ui/src/completion_provider.rs
+++ b/crates/agent_ui/src/completion_provider.rs
@@ -636,7 +636,8 @@ impl PromptCompletionProvider {
};
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);
diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs
index 2bd8e6d502dafc879b468a1c47735c0a57ab7aa6..07a7841be764b34381f88087e1d7f6c447d9d910 100644
--- a/crates/agent_ui/src/mention_set.rs
+++ b/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")))
}
diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs
index 4be047c47d491f7121e3f2322d6edcdbcc7baebe..9f3234ef0489c8576f5d1fd1db95232d6a8b6d1c 100644
--- a/crates/agent_ui/src/text_thread_editor.rs
+++ b/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::(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::())
- })?;
-
- 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);
}
}