assistant2: Add keybinding to toggle `ContextPicker` (#22124)

Marshall Bowers created

This PR adds an action and associated keybinding
(<kbd>Cmd+Shift+A</kbd>) to toggle the context picker.

This allows for adding context via the keyboard.

Release Notes:

- N/A

Change summary

assets/keymaps/default-macos.json                  |  4 +
crates/assistant2/src/assistant.rs                 |  1 
crates/assistant2/src/context_strip.rs             | 24 ++++++++++--
crates/assistant2/src/inline_assistant.rs          | 26 ++++++++++++--
crates/assistant2/src/message_editor.rs            | 29 +++++++++++----
crates/assistant2/src/terminal_inline_assistant.rs | 24 +++++++++++-
6 files changed, 87 insertions(+), 21 deletions(-)

Detailed changes

assets/keymaps/default-macos.json 🔗

@@ -225,7 +225,8 @@
     "bindings": {
       "cmd-n": "assistant2::NewThread",
       "cmd-shift-h": "assistant2::OpenHistory",
-      "cmd-shift-m": "assistant2::ToggleModelSelector"
+      "cmd-shift-m": "assistant2::ToggleModelSelector",
+      "cmd-shift-a": "assistant2::ToggleContextPicker"
     }
   },
   {
@@ -618,6 +619,7 @@
     "context": "PromptEditor",
     "use_key_equivalents": true,
     "bindings": {
+      "cmd-shift-a": "assistant2::ToggleContextPicker",
       "ctrl-[": "assistant::CyclePreviousInlineAssist",
       "ctrl-]": "assistant::CycleNextInlineAssist"
     }

crates/assistant2/src/context_strip.rs 🔗

@@ -1,6 +1,6 @@
 use std::rc::Rc;
 
-use gpui::{Model, View, WeakModel, WeakView};
+use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
 use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
 use workspace::Workspace;
 
@@ -8,11 +8,13 @@ use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::thread_store::ThreadStore;
 use crate::ui::ContextPill;
+use crate::ToggleContextPicker;
 
 pub struct ContextStrip {
     context_store: Model<ContextStore>,
     context_picker: View<ContextPicker>,
-    pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
+    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
+    focus_handle: FocusHandle,
 }
 
 impl ContextStrip {
@@ -20,6 +22,8 @@ impl ContextStrip {
         context_store: Model<ContextStore>,
         workspace: WeakView<Workspace>,
         thread_store: Option<WeakModel<ThreadStore>>,
+        focus_handle: FocusHandle,
+        context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         Self {
@@ -32,7 +36,8 @@ impl ContextStrip {
                     cx,
                 )
             }),
-            context_picker_handle: PopoverMenuHandle::default(),
+            context_picker_menu_handle,
+            focus_handle,
         }
     }
 }
@@ -41,6 +46,7 @@ impl Render for ContextStrip {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let context = self.context_store.read(cx).context();
         let context_picker = self.context_picker.clone();
+        let focus_handle = self.focus_handle.clone();
 
         h_flex()
             .flex_wrap()
@@ -51,7 +57,15 @@ impl Render for ContextStrip {
                     .trigger(
                         IconButton::new("add-context", IconName::Plus)
                             .icon_size(IconSize::Small)
-                            .style(ui::ButtonStyle::Filled),
+                            .style(ui::ButtonStyle::Filled)
+                            .tooltip(move |cx| {
+                                Tooltip::for_action_in(
+                                    "Add Context",
+                                    &ToggleContextPicker,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }),
                     )
                     .attach(gpui::AnchorCorner::TopLeft)
                     .anchor(gpui::AnchorCorner::BottomLeft)
@@ -59,7 +73,7 @@ impl Render for ContextStrip {
                         x: px(0.0),
                         y: px(-16.0),
                     })
-                    .with_handle(self.context_picker_handle.clone()),
+                    .with_handle(self.context_picker_menu_handle.clone()),
             )
             .children(context.iter().map(|context| {
                 ContextPill::new(context.clone()).on_remove({

crates/assistant2/src/inline_assistant.rs 🔗

@@ -1,8 +1,8 @@
 use crate::context::attach_context_to_message;
+use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::context_strip::ContextStrip;
 use crate::thread_store::ThreadStore;
-use crate::AssistantPanel;
 use crate::{
     assistant_settings::AssistantSettings,
     prompts::PromptBuilder,
@@ -10,6 +10,7 @@ use crate::{
     terminal_inline_assistant::TerminalInlineAssistant,
     CycleNextInlineAssist, CyclePreviousInlineAssist, ToggleInlineAssist,
 };
+use crate::{AssistantPanel, ToggleContextPicker};
 use anyhow::{Context as _, Result};
 use client::{telemetry::Telemetry, ErrorExt};
 use collections::{hash_map, HashMap, HashSet, VecDeque};
@@ -60,7 +61,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
 use text::{OffsetRangeExt, ToPoint as _};
 use theme::ThemeSettings;
-use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip};
+use ui::{
+    prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
+};
 use util::{RangeExt, ResultExt};
 use workspace::{dock::Panel, ShowConfiguration};
 use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -1486,6 +1489,7 @@ struct PromptEditor {
     id: InlineAssistId,
     editor: View<Editor>,
     context_strip: View<ContextStrip>,
+    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
     language_model_selector: View<LanguageModelSelector>,
     edited_since_done: bool,
     gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -1607,6 +1611,7 @@ impl Render for PromptEditor {
                     .bg(cx.theme().colors().editor_background)
                     .block_mouse_down()
                     .cursor(CursorStyle::Arrow)
+                    .on_action(cx.listener(Self::toggle_context_picker))
                     .on_action(cx.listener(Self::confirm))
                     .on_action(cx.listener(Self::cancel))
                     .on_action(cx.listener(Self::move_up))
@@ -1747,13 +1752,22 @@ impl PromptEditor {
             editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx)), cx);
             editor
         });
+        let context_picker_menu_handle = PopoverMenuHandle::default();
 
         let mut this = Self {
             id,
-            editor: prompt_editor,
+            editor: prompt_editor.clone(),
             context_strip: cx.new_view(|cx| {
-                ContextStrip::new(context_store, workspace.clone(), thread_store.clone(), cx)
+                ContextStrip::new(
+                    context_store,
+                    workspace.clone(),
+                    thread_store.clone(),
+                    prompt_editor.focus_handle(cx),
+                    context_picker_menu_handle.clone(),
+                    cx,
+                )
             }),
+            context_picker_menu_handle,
             language_model_selector: cx.new_view(|cx| {
                 let fs = fs.clone();
                 LanguageModelSelector::new(
@@ -1911,6 +1925,10 @@ impl PromptEditor {
         }
     }
 
+    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
+        self.context_picker_menu_handle.toggle(cx);
+    }
+
     fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
         match self.codegen.read(cx).status(cx) {
             CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {

crates/assistant2/src/message_editor.rs 🔗

@@ -14,17 +14,19 @@ use ui::{
 use workspace::Workspace;
 
 use crate::assistant_settings::AssistantSettings;
+use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::context_strip::ContextStrip;
 use crate::thread::{RequestKind, Thread};
 use crate::thread_store::ThreadStore;
-use crate::{Chat, ToggleModelSelector};
+use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
 
 pub struct MessageEditor {
     thread: Model<Thread>,
     editor: View<Editor>,
     context_store: Model<ContextStore>,
     context_strip: View<ContextStrip>,
+    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
     language_model_selector: View<LanguageModelSelector>,
     language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
     use_tools: bool,
@@ -39,25 +41,31 @@ impl MessageEditor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let context_store = cx.new_model(|_cx| ContextStore::new());
+        let context_picker_menu_handle = PopoverMenuHandle::default();
+
+        let editor = cx.new_view(|cx| {
+            let mut editor = Editor::auto_height(80, cx);
+            editor.set_placeholder_text("Ask anything, @ to add context", cx);
+            editor.set_show_indent_guides(false, cx);
+
+            editor
+        });
 
         Self {
             thread,
-            editor: cx.new_view(|cx| {
-                let mut editor = Editor::auto_height(80, cx);
-                editor.set_placeholder_text("Ask anything, @ to add context", cx);
-                editor.set_show_indent_guides(false, cx);
-
-                editor
-            }),
+            editor: editor.clone(),
             context_store: context_store.clone(),
             context_strip: cx.new_view(|cx| {
                 ContextStrip::new(
                     context_store,
                     workspace.clone(),
                     Some(thread_store.clone()),
+                    editor.focus_handle(cx),
+                    context_picker_menu_handle.clone(),
                     cx,
                 )
             }),
+            context_picker_menu_handle,
             language_model_selector: cx.new_view(|cx| {
                 let fs = fs.clone();
                 LanguageModelSelector::new(
@@ -80,6 +88,10 @@ impl MessageEditor {
         self.language_model_selector_menu_handle.toggle(cx);
     }
 
+    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
+        self.context_picker_menu_handle.toggle(cx);
+    }
+
     fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
         self.send_to_model(RequestKind::Chat, cx);
     }
@@ -193,6 +205,7 @@ impl Render for MessageEditor {
             .key_context("MessageEditor")
             .on_action(cx.listener(Self::chat))
             .on_action(cx.listener(Self::toggle_model_selector))
+            .on_action(cx.listener(Self::toggle_context_picker))
             .size_full()
             .gap_2()
             .p_2()

crates/assistant2/src/terminal_inline_assistant.rs 🔗

@@ -1,9 +1,11 @@
 use crate::assistant_settings::AssistantSettings;
 use crate::context::attach_context_to_message;
+use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::context_strip::ContextStrip;
 use crate::prompts::PromptBuilder;
 use crate::thread_store::ThreadStore;
+use crate::ToggleContextPicker;
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
 use collections::{HashMap, VecDeque};
@@ -29,7 +31,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
 use terminal::Terminal;
 use terminal_view::TerminalView;
 use theme::ThemeSettings;
-use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
+use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
 use util::ResultExt;
 use workspace::{notifications::NotificationId, Toast, Workspace};
 
@@ -460,6 +462,7 @@ struct PromptEditor {
     height_in_lines: u8,
     editor: View<Editor>,
     context_strip: View<ContextStrip>,
+    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
     language_model_selector: View<LanguageModelSelector>,
     edited_since_done: bool,
     prompt_history: VecDeque<String>,
@@ -580,7 +583,9 @@ impl Render for PromptEditor {
             .size_full()
             .child(
                 h_flex()
+                    .key_context("PromptEditor")
                     .bg(cx.theme().colors().editor_background)
+                    .on_action(cx.listener(Self::toggle_context_picker))
                     .on_action(cx.listener(Self::confirm))
                     .on_action(cx.listener(Self::secondary_confirm))
                     .on_action(cx.listener(Self::cancel))
@@ -674,14 +679,23 @@ impl PromptEditor {
             editor.set_placeholder_text(Self::placeholder_text(cx), cx);
             editor
         });
+        let context_picker_menu_handle = PopoverMenuHandle::default();
 
         let mut this = Self {
             id,
             height_in_lines: 1,
-            editor: prompt_editor,
+            editor: prompt_editor.clone(),
             context_strip: cx.new_view(|cx| {
-                ContextStrip::new(context_store, workspace.clone(), thread_store.clone(), cx)
+                ContextStrip::new(
+                    context_store,
+                    workspace.clone(),
+                    thread_store.clone(),
+                    prompt_editor.focus_handle(cx),
+                    context_picker_menu_handle.clone(),
+                    cx,
+                )
             }),
+            context_picker_menu_handle,
             language_model_selector: cx.new_view(|cx| {
                 let fs = fs.clone();
                 LanguageModelSelector::new(
@@ -790,6 +804,10 @@ impl PromptEditor {
         }
     }
 
+    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
+        self.context_picker_menu_handle.toggle(cx);
+    }
+
     fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
         match &self.codegen.read(cx).status {
             CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {