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

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
This commit is contained in:
Danilo Leal 2026-01-29 14:26:23 -03:00 committed by GitHub
parent ad24b3f0a0
commit 263e0004ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 55 additions and 57 deletions

View file

@ -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)");
}
}

View file

@ -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{}",

View file

@ -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| {

View file

@ -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);

View file

@ -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);

View file

@ -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")))
}

View file

@ -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);
}
}