assistant: Add model selector to the Context Editor (#25032)

Danilo Leal created

This PR also removes everything related with the model selector from the
Context Editor toolbar.

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json                     |   1 
assets/keymaps/default-macos.json                     |   1 
crates/assistant/src/assistant_panel.rs               |  29 -
crates/assistant_context_editor/src/context_editor.rs | 170 ++++++------
4 files changed, 94 insertions(+), 107 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -587,6 +587,7 @@
       "save": "workspace::Save",
       "ctrl->": "assistant::QuoteSelection",
       "ctrl-<": "assistant::InsertIntoEditor",
+      "ctrl-alt-/": "assistant::ToggleModelSelector",
       "shift-enter": "assistant::Split",
       "ctrl-r": "assistant::CycleMessageRole",
       "enter": "assistant::ConfirmCommand",

assets/keymaps/default-macos.json 🔗

@@ -218,6 +218,7 @@
       "cmd-s": "workspace::Save",
       "cmd->": "assistant::QuoteSelection",
       "cmd-<": "assistant::InsertIntoEditor",
+      "cmd-alt-/": "assistant::ToggleModelSelector",
       "shift-enter": "assistant::Split",
       "ctrl-r": "assistant::CycleMessageRole",
       "enter": "assistant::ConfirmCommand",

crates/assistant/src/assistant_panel.rs 🔗

@@ -7,7 +7,7 @@ use assistant_context_editor::{
     make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
     ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
     ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
-    ToggleModelSelector, DEFAULT_TAB_TITLE,
+    DEFAULT_TAB_TITLE,
 };
 use assistant_settings::{AssistantDockPosition, AssistantSettings};
 use assistant_slash_command::SlashCommandWorkingSet;
@@ -21,7 +21,6 @@ use gpui::{
 };
 use language::LanguageRegistry;
 use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
-use language_model_selector::LanguageModelSelector;
 use project::Project;
 use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
@@ -29,7 +28,7 @@ use settings::{update_settings_file, Settings};
 use smol::stream::StreamExt;
 use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
-use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
+use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
 use util::{maybe, ResultExt};
 use workspace::DraggedTab;
 use workspace::{
@@ -77,7 +76,6 @@ pub struct AssistantPanel {
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
-    model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
     model_summary_editor: Entity<Editor>,
     authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
     configuration_subscription: Option<Subscription>,
@@ -119,17 +117,9 @@ impl AssistantPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let model_selector_menu_handle = PopoverMenuHandle::default();
         let model_summary_editor = cx.new(|cx| Editor::single_line(window, cx));
-        let context_editor_toolbar = cx.new(|cx| {
-            ContextEditorToolbarItem::new(
-                workspace,
-                model_selector_menu_handle.clone(),
-                model_summary_editor.clone(),
-                window,
-                cx,
-            )
-        });
+        let context_editor_toolbar =
+            cx.new(|_| ContextEditorToolbarItem::new(model_summary_editor.clone()));
 
         let pane = cx.new(|cx| {
             let mut pane = Pane::new(
@@ -331,7 +321,6 @@ impl AssistantPanel {
             languages: workspace.app_state().languages.clone(),
             fs: workspace.app_state().fs.clone(),
             subscriptions,
-            model_selector_menu_handle,
             model_summary_editor,
             authenticate_provider_task: None,
             configuration_subscription: None,
@@ -1054,15 +1043,6 @@ impl AssistantPanel {
         .detach_and_log_err(cx);
     }
 
-    fn toggle_model_selector(
-        &mut self,
-        _: &ToggleModelSelector,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.model_selector_menu_handle.toggle(window, cx);
-    }
-
     pub(crate) fn active_context_editor(&self, cx: &App) -> Option<Entity<ContextEditor>> {
         self.pane
             .read(cx)
@@ -1229,7 +1209,6 @@ impl Render for AssistantPanel {
             }))
             .on_action(cx.listener(AssistantPanel::deploy_history))
             .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
-            .on_action(cx.listener(AssistantPanel::toggle_model_selector))
             .child(registrar.size_full().child(self.pane.clone()))
             .into_any_element()
     }

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -193,6 +193,8 @@ pub struct ContextEditor {
     // the file is opened. In order to keep the worktree alive for the duration of the
     // context editor, we keep a reference here.
     dragged_file_worktrees: Vec<Entity<Worktree>>,
+    language_model_selector: Entity<LanguageModelSelector>,
+    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 }
 
 pub const DEFAULT_TAB_TITLE: &str = "New Chat";
@@ -238,6 +240,22 @@ impl ContextEditor {
             cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
         ];
 
+        let fs_clone = fs.clone();
+        let language_model_selector = cx.new(|cx| {
+            LanguageModelSelector::new(
+                move |model, cx| {
+                    update_settings_file::<AssistantSettings>(
+                        fs_clone.clone(),
+                        cx,
+                        move |settings, _| settings.set_model(model.clone()),
+                    );
+                },
+                window,
+                cx,
+            )
+        });
+
+        let language_model_selector_menu_handle = PopoverMenuHandle::default();
         let sections = context.read(cx).slash_command_output_sections().to_vec();
         let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
         let slash_commands = context.read(cx).slash_commands().clone();
@@ -262,6 +280,8 @@ impl ContextEditor {
             show_accept_terms: false,
             slash_menu_handle: Default::default(),
             dragged_file_worktrees: Vec::new(),
+            language_model_selector,
+            language_model_selector_menu_handle,
         };
         this.update_message_headers(cx);
         this.update_image_blocks(cx);
@@ -2355,13 +2375,62 @@ impl ContextEditor {
         slash_command_picker::SlashCommandSelector::new(
             self.slash_commands.clone(),
             cx.entity().downgrade(),
-            Button::new("trigger", "Add Context")
-                .icon(IconName::Plus)
+            IconButton::new("trigger", IconName::Plus)
                 .icon_size(IconSize::Small)
-                .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start),
-            Tooltip::text("Type / to insert via keyboard"),
+                .icon_color(Color::Muted),
+            move |window, cx| {
+                Tooltip::with_meta(
+                    "Add Context",
+                    None,
+                    "Type / to insert via keyboard",
+                    window,
+                    cx,
+                )
+            },
+        )
+    }
+
+    fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let active_model = LanguageModelRegistry::read_global(cx).active_model();
+        let focus_handle = self.editor().focus_handle(cx).clone();
+        let model_name = match active_model {
+            Some(model) => model.name().0,
+            None => SharedString::from("No model selected"),
+        };
+
+        LanguageModelSelectorPopoverMenu::new(
+            self.language_model_selector.clone(),
+            ButtonLike::new("active-model")
+                .style(ButtonStyle::Subtle)
+                .child(
+                    h_flex()
+                        .gap_0p5()
+                        .child(
+                            div().max_w_32().child(
+                                Label::new(model_name)
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted)
+                                    .text_ellipsis()
+                                    .into_any_element(),
+                            ),
+                        )
+                        .child(
+                            Icon::new(IconName::ChevronDown)
+                                .color(Color::Muted)
+                                .size(IconSize::XSmall),
+                        ),
+                ),
+            move |window, cx| {
+                Tooltip::for_action_in(
+                    "Change Model",
+                    &ToggleModelSelector,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            },
         )
+        .with_handle(self.language_model_selector_menu_handle.clone())
     }
 
     fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@@ -2800,6 +2869,7 @@ impl EventEmitter<SearchEvent> for ContextEditor {}
 impl Render for ContextEditor {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let provider = LanguageModelRegistry::read_global(cx).active_provider();
+
         let accept_terms = if self.show_accept_terms {
             provider.as_ref().and_then(|provider| {
                 provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
@@ -2852,7 +2922,17 @@ impl Render for ContextEditor {
                         .border_t_1()
                         .border_color(cx.theme().colors().border_variant)
                         .bg(cx.theme().colors().editor_background)
-                        .child(h_flex().gap_1().child(self.render_inject_context_menu(cx)))
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .child(self.render_inject_context_menu(cx))
+                                .child(ui::Divider::vertical())
+                                .child(
+                                    div()
+                                        .pl_0p5()
+                                        .child(self.render_language_model_selector(cx)),
+                                ),
+                        )
                         .child(
                             h_flex()
                                 .w_full()
@@ -3163,36 +3243,13 @@ impl FollowableItem for ContextEditor {
 pub struct ContextEditorToolbarItem {
     active_context_editor: Option<WeakEntity<ContextEditor>>,
     model_summary_editor: Entity<Editor>,
-    language_model_selector: Entity<LanguageModelSelector>,
-    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 }
 
 impl ContextEditorToolbarItem {
-    pub fn new(
-        workspace: &Workspace,
-        model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
-        model_summary_editor: Entity<Editor>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
+    pub fn new(model_summary_editor: Entity<Editor>) -> Self {
         Self {
             active_context_editor: None,
             model_summary_editor,
-            language_model_selector: cx.new(|cx| {
-                let fs = workspace.app_state().fs.clone();
-                LanguageModelSelector::new(
-                    move |model, cx| {
-                        update_settings_file::<AssistantSettings>(
-                            fs.clone(),
-                            cx,
-                            move |settings, _| settings.set_model(model.clone()),
-                        );
-                    },
-                    window,
-                    cx,
-                )
-            }),
-            language_model_selector_menu_handle: model_selector_menu_handle,
         }
     }
 
@@ -3263,8 +3320,7 @@ impl Render for ContextEditorToolbarItem {
                         })),
                 ),
             );
-        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
-        let active_model = LanguageModelRegistry::read_global(cx).active_model();
+
         let right_side = h_flex()
             .gap_2()
             // TODO display this in a nicer way, once we have a design for it.
@@ -3280,56 +3336,6 @@ impl Render for ContextEditorToolbarItem {
             //     scan_items_remaining
             //         .map(|remaining_items| format!("Files to scan: {}", remaining_items))
             // })
-            .child(
-                LanguageModelSelectorPopoverMenu::new(
-                    self.language_model_selector.clone(),
-                    ButtonLike::new("active-model")
-                        .style(ButtonStyle::Subtle)
-                        .child(
-                            h_flex()
-                                .w_full()
-                                .gap_0p5()
-                                .child(
-                                    div()
-                                        .overflow_x_hidden()
-                                        .flex_grow()
-                                        .whitespace_nowrap()
-                                        .child(match (active_provider, active_model) {
-                                            (Some(provider), Some(model)) => h_flex()
-                                                .gap_1()
-                                                .child(
-                                                    Icon::new(
-                                                        model
-                                                            .icon()
-                                                            .unwrap_or_else(|| provider.icon()),
-                                                    )
-                                                    .color(Color::Muted)
-                                                    .size(IconSize::XSmall),
-                                                )
-                                                .child(
-                                                    Label::new(model.name().0)
-                                                        .size(LabelSize::Small)
-                                                        .color(Color::Muted),
-                                                )
-                                                .into_any_element(),
-                                            _ => Label::new("No model selected")
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted)
-                                                .into_any_element(),
-                                        }),
-                                )
-                                .child(
-                                    Icon::new(IconName::ChevronDown)
-                                        .color(Color::Muted)
-                                        .size(IconSize::XSmall),
-                                ),
-                        ),
-                    move |window, cx| {
-                        Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
-                    },
-                )
-                .with_handle(self.language_model_selector_menu_handle.clone()),
-            )
             .children(self.render_remaining_tokens(cx));
 
         h_flex()