acp: Rename `assistant::QuoteSelection` and support it in agent2 threads (#36628)

Cole Miller created

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json              |   4 
assets/keymaps/default-macos.json              |   4 
assets/keymaps/linux/cursor.json               |   4 
assets/keymaps/macos/cursor.json               |   4 
crates/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(-)

Detailed changes

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",

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",

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

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

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::<Vec<_>>();
-
-                        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<Anchor>,
+        message_editor: WeakEntity<MessageEditor>,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Option<Completion> {
+        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::<Vec<_>>();
+
+                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<ContextPickerMode>,

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<Self>) {
+        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>) {
         self.editor.update(cx, |message_editor, cx| {
             message_editor.set_read_only(read_only);

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>) {
+        self.message_editor.update(cx, |message_editor, cx| {
+            message_editor.insert_selections(window, cx);
+        })
+    }
+
     fn render_thread_retry_status_callout(
         &self,
         _window: &mut Window,

crates/agent_ui/src/agent_panel.rs 🔗

@@ -903,6 +903,16 @@ impl AgentPanel {
         }
     }
 
+    fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
+        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<Self>) {
         if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
             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);

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)]

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,
     ]

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 [<number>]`
 
 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`