agent_ui: Add icon button to trigger the @-mention completions menu (#42449)

Danilo Leal created

Closes https://github.com/zed-industries/zed/issues/37087

This PR adds an icon button to the footer of the message editor enabling
to trigger and interact with the @-mention completions menu with the
mouse. This is a first step towards making other types of context you
can add in Zed's agent panel more discoverable. Next, I want to improve
the discoverability of images and selections, given that you wouldn't
necessarily know they work in Zed without a clear way to see them. But I
think that for now, this is enough to close the issue above, which had
lots of productive comments and discussion!

<img width="500" height="540" alt="Screenshot 2025-11-11 at 10  46 3@2x"
src="https://github.com/user-attachments/assets/fd028442-6f77-4153-bea1-c0b815da4ac6"
/>

Release Notes:

- agent: Added an icon button in the agent panel that allows to trigger
the @-mention menu (for adding context) now also with the mouse.

Change summary

assets/icons/at_sign.svg                       |  4 +
crates/agent_ui/src/acp/completion_provider.rs |  8 ++
crates/agent_ui/src/acp/message_editor.rs      | 49 ++++++++++++++++++++
crates/agent_ui/src/acp/thread_view.rs         | 25 ++++++++++
crates/icons/src/icons.rs                      |  1 
5 files changed, 85 insertions(+), 2 deletions(-)

Detailed changes

assets/icons/at_sign.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.00156 10.3996C9.32705 10.3996 10.4016 9.32509 10.4016 7.99961C10.4016 6.67413 9.32705 5.59961 8.00156 5.59961C6.67608 5.59961 5.60156 6.67413 5.60156 7.99961C5.60156 9.32509 6.67608 10.3996 8.00156 10.3996Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.4 5.6V8.6C10.4 9.07739 10.5896 9.53523 10.9272 9.8728C11.2648 10.2104 11.7226 10.4 12.2 10.4C12.6774 10.4 13.1352 10.2104 13.4728 9.8728C13.8104 9.53523 14 9.07739 14 8.6V8C14 6.64839 13.5436 5.33636 12.7048 4.27651C11.8661 3.21665 10.694 2.47105 9.37852 2.16051C8.06306 1.84997 6.68129 1.99269 5.45707 2.56554C4.23285 3.13838 3.23791 4.1078 2.63344 5.31672C2.02898 6.52565 1.85041 7.90325 2.12667 9.22633C2.40292 10.5494 3.11782 11.7405 4.15552 12.6065C5.19323 13.4726 6.49295 13.9629 7.84411 13.998C9.19527 14.0331 10.5187 13.611 11.6 12.8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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

@@ -694,14 +694,18 @@ fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -
 }
 
 fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
-    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
+    let path = cx
+        .theme()
+        .syntax()
+        .highlight_id("variable")
+        .map(HighlightId);
     let mut label = CodeLabelBuilder::default();
 
     label.push_str(file_name, None);
     label.push_str(" ", None);
 
     if let Some(directory) = directory {
-        label.push_str(directory, comment_id);
+        label.push_str(directory, path);
     }
 
     label.build()

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

@@ -15,6 +15,7 @@ use editor::{
     EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
     MultiBuffer, ToOffset,
     actions::Paste,
+    code_context_menus::CodeContextMenu,
     display_map::{Crease, CreaseId, FoldId},
     scroll::Autoscroll,
 };
@@ -272,6 +273,15 @@ impl MessageEditor {
         self.editor.read(cx).is_empty(cx)
     }
 
+    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
+        self.editor
+            .read(cx)
+            .context_menu()
+            .borrow()
+            .as_ref()
+            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
+    }
+
     pub fn mentions(&self) -> HashSet<MentionUri> {
         self.mention_set
             .mentions
@@ -836,6 +846,45 @@ impl MessageEditor {
         cx.emit(MessageEditorEvent::Send)
     }
 
+    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let editor = self.editor.clone();
+
+        cx.spawn_in(window, async move |_, cx| {
+            editor
+                .update_in(cx, |editor, window, cx| {
+                    let menu_is_open =
+                        editor.context_menu().borrow().as_ref().is_some_and(|menu| {
+                            matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
+                        });
+
+                    let has_at_sign = {
+                        let snapshot = editor.display_snapshot(cx);
+                        let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
+                        let offset = cursor.to_offset(&snapshot);
+                        if offset > 0 {
+                            snapshot
+                                .buffer_snapshot()
+                                .reversed_chars_at(offset)
+                                .next()
+                                .map(|sign| sign == '@')
+                                .unwrap_or(false)
+                        } else {
+                            false
+                        }
+                    };
+
+                    if menu_is_open && has_at_sign {
+                        return;
+                    }
+
+                    editor.insert("@", window, cx);
+                    editor.show_completions(&editor::actions::ShowCompletions, window, cx);
+                })
+                .log_err();
+        })
+        .detach();
+    }
+
     fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
         self.send(cx);
     }

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

@@ -4188,6 +4188,8 @@ impl AcpThreadView {
                     .justify_between()
                     .child(
                         h_flex()
+                            .gap_0p5()
+                            .child(self.render_add_context_button(cx))
                             .child(self.render_follow_toggle(cx))
                             .children(self.render_burn_mode_toggle(cx)),
                     )
@@ -4502,6 +4504,29 @@ impl AcpThreadView {
             }))
     }
 
+    fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let message_editor = self.message_editor.clone();
+        let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
+
+        IconButton::new("add-context", IconName::AtSign)
+            .icon_size(IconSize::Small)
+            .icon_color(Color::Muted)
+            .when(!menu_visible, |this| {
+                this.tooltip(move |_window, cx| {
+                    Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
+                })
+            })
+            .on_click(cx.listener(move |_this, _, window, cx| {
+                let message_editor_clone = message_editor.clone();
+
+                window.defer(cx, move |window, cx| {
+                    message_editor_clone.update(cx, |message_editor, cx| {
+                        message_editor.trigger_completion_menu(window, cx);
+                    });
+                });
+            }))
+    }
+
     fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
         let workspace = self.workspace.clone();
         MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {