From 1ee07a4bafb368e481f262ecdef826c20db63b40 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 20 Aug 2025 17:31:25 -0400 Subject: [PATCH] acp: Rename `assistant::QuoteSelection` and support it in agent2 threads (#36628) Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- assets/keymaps/linux/cursor.json | 4 +- assets/keymaps/macos/cursor.json | 4 +- .../agent_ui/src/acp/completion_provider.rs | 122 ++++++++++-------- crates/agent_ui/src/acp/message_editor.rs | 54 ++++++-- crates/agent_ui/src/acp/thread_view.rs | 6 + crates/agent_ui/src/agent_panel.rs | 16 ++- crates/agent_ui/src/agent_ui.rs | 6 + crates/agent_ui/src/text_thread_editor.rs | 3 +- docs/src/ai/text-threads.md | 4 +- 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b4efa70572..955e68f5a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -138,7 +138,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -241,7 +241,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ad2ab2ba89..8b18299a91 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -162,7 +162,7 @@ "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -281,7 +281,7 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 1c381b0cf0..2e27158e11 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index fdf9c437cf..1d723bd75b 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index bf0a3f7a5a..3587e5144e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider { confirm: Some(Arc::new(|_, _, _| true)), }), ContextPickerEntry::Action(action) => { - let (new_text, on_action) = match action { - ContextPickerAction::AddSelections => { - const PLACEHOLDER: &str = "selection "; - let selections = selection_ranges(workspace, cx) - .into_iter() - .enumerate() - .map(|(ix, (buffer, range))| { - ( - buffer, - range, - (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), - ) - }) - .collect::>(); - - let new_text: String = PLACEHOLDER.repeat(selections.len()); - - let callback = Arc::new({ - let source_range = source_range.clone(); - move |_, window: &mut Window, cx: &mut App| { - let selections = selections.clone(); - let message_editor = message_editor.clone(); - let source_range = source_range.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_selection( - source_range, - selections, - window, - cx, - ) - }) - .ok(); - }); - false - } - }); - - (new_text, callback) - } - }; - - Some(Completion { - replace_range: source_range, - new_text, - label: CodeLabel::plain(action.label().to_string(), None), - icon_path: Some(action.icon().path().into()), - documentation: None, - source: project::CompletionSource::Custom, - insert_text_mode: None, - // This ensures that when a user accepts this completion, the - // completion menu will still be shown after "@category " is - // inserted - confirm: Some(on_action), - }) + Self::completion_for_action(action, source_range, message_editor, workspace, cx) } } } @@ -359,6 +304,71 @@ impl ContextPickerCompletionProvider { }) } + pub(crate) fn completion_for_action( + action: ContextPickerAction, + source_range: Range, + message_editor: WeakEntity, + workspace: &Entity, + cx: &mut App, + ) -> Option { + let (new_text, on_action) = match action { + ContextPickerAction::AddSelections => { + const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::>(); + + let new_text: String = PLACEHOLDER.repeat(selections.len()); + + let callback = Arc::new({ + let source_range = source_range.clone(); + move |_, window: &mut Window, cx: &mut App| { + let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); + }); + false + } + }); + + (new_text, callback) + } + }; + + Some(Completion { + replace_range: source_range, + new_text, + label: CodeLabel::plain(action.label().to_string(), None), + icon_path: Some(action.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(on_action), + }) + } + fn search( &self, mode: Option, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 5eab1a4e2d..be133808b7 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,6 +1,6 @@ use crate::{ acp::completion_provider::ContextPickerCompletionProvider, - context_picker::fetch_context_picker::fetch_url_content, + context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; @@ -27,7 +27,7 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{Project, ProjectPath, Worktree}; +use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; @@ -561,21 +561,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let path = buffer - .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + // TODO support selections from buffers with no path + let Some(project_path) = buffer.read(cx).project_path(cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; let snapshot = buffer.read(cx).snapshot(); let point_range = selection_range.to_point(&snapshot); let line_range = point_range.start.row..point_range.end.row; let uri = MentionUri::Selection { - path: path.clone(), + path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), + selection_name(&abs_path, &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -587,8 +590,7 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set - .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + self.mention_set.insert_uri(crease_id, uri); } } @@ -948,6 +950,38 @@ impl MessageEditor { .detach(); } + pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let Some(completion) = ContextPickerCompletionProvider::completion_for_action( + ContextPickerAction::AddSelections, + anchor..anchor, + cx.weak_entity(), + &workspace, + cx, + ) else { + return; + }; + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm { + confirm(CompletionIntent::Complete, window, cx); + } + } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { self.editor.update(cx, |message_editor, cx| { message_editor.set_read_only(read_only); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 35da9b8c85..0dfa3d259e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4097,6 +4097,12 @@ impl AcpThreadView { }) } + pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_selections(window, cx); + }) + } + fn render_thread_retry_status_callout( &self, _window: &mut Window, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e2c4acb1ce..65a9da573a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -903,6 +903,16 @@ impl AgentPanel { } } + fn active_thread_view(&self) -> Option<&Entity> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), + ActiveView::Thread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + } + } + fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context) { if cx.has_flag::() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); @@ -3882,7 +3892,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(message_editor) = panel.active_message_editor() { + if let Some(thread_view) = panel.active_thread_view() { + thread_view.update(cx, |thread_view, cx| { + thread_view.insert_selections(window, cx); + }); + } else if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7b6557245f..6084fd6423 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -128,6 +128,12 @@ actions!( ] ); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)] +#[action(namespace = agent)] +#[action(deprecated_aliases = ["assistant::QuoteSelection"])] +/// Quotes the current selection in the agent panel's message editor. +pub struct QuoteSelection; + /// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index a928f7af54..9fbd90c4a6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,4 +1,5 @@ use crate::{ + QuoteSelection, language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; @@ -89,8 +90,6 @@ actions!( CycleMessageRole, /// Inserts the selected text into the active editor. InsertIntoEditor, - /// Quotes the current selection in the assistant conversation. - QuoteSelection, /// Splits the conversation at the current cursor position. Split, ] diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md index 65a5dcba03..ed439252b4 100644 --- a/docs/src/ai/text-threads.md +++ b/docs/src/ai/text-threads.md @@ -16,7 +16,7 @@ To begin, type a message in a `You` block. As you type, the remaining tokens count for the selected model is updated. -Inserting text from an editor is as simple as highlighting the text and running `assistant: quote selection` ({#kb assistant::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. +Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) @@ -148,7 +148,7 @@ Usage: `/terminal []` The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. -This is equivalent to the `assistant: quote selection` command ({#kb assistant::QuoteSelection}). +This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}). Usage: `/selection`