Factor out language model selector into its own crate (#21113)

Marshall Bowers created

This PR factors the language model selector out into its own
`language_model_selector` crate so that it can be reused in
`assistant2`.

Also renamed it from `ModelSelector` to `LanguageModelSelector` to be a
bit more specific.

Release Notes:

- N/A

Change summary

Cargo.lock                                                    | 15 +
Cargo.toml                                                    |  2 
crates/assistant/Cargo.toml                                   |  1 
crates/assistant/src/assistant.rs                             |  2 
crates/assistant/src/assistant_panel.rs                       | 26 +
crates/assistant/src/inline_assistant.rs                      | 20 +
crates/assistant/src/terminal_inline_assistant.rs             | 21 +
crates/language_model_selector/Cargo.toml                     | 22 +
crates/language_model_selector/LICENSE-GPL                    |  1 
crates/language_model_selector/src/language_model_selector.rs | 57 ++--
10 files changed, 119 insertions(+), 48 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -402,6 +402,7 @@ dependencies = [
  "indoc",
  "language",
  "language_model",
+ "language_model_selector",
  "language_models",
  "languages",
  "log",
@@ -6603,6 +6604,20 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "language_model_selector"
+version = "0.1.0"
+dependencies = [
+ "feature_flags",
+ "gpui",
+ "language_model",
+ "picker",
+ "proto",
+ "ui",
+ "workspace",
+ "zed_actions",
+]
+
 [[package]]
 name = "language_models"
 version = "0.1.0"

Cargo.toml 🔗

@@ -58,6 +58,7 @@ members = [
     "crates/language",
     "crates/language_extension",
     "crates/language_model",
+    "crates/language_model_selector",
     "crates/language_models",
     "crates/language_selector",
     "crates/language_tools",
@@ -236,6 +237,7 @@ journal = { path = "crates/journal" }
 language = { path = "crates/language" }
 language_extension = { path = "crates/language_extension" }
 language_model = { path = "crates/language_model" }
+language_model_selector = { path = "crates/language_model_selector" }
 language_models = { path = "crates/language_models" }
 language_selector = { path = "crates/language_selector" }
 language_tools = { path = "crates/language_tools" }

crates/assistant/Cargo.toml 🔗

@@ -50,6 +50,7 @@ indexed_docs.workspace = true
 indoc.workspace = true
 language.workspace = true
 language_model.workspace = true
+language_model_selector.workspace = true
 language_models.workspace = true
 log.workspace = true
 lsp.workspace = true

crates/assistant/src/assistant.rs 🔗

@@ -5,7 +5,6 @@ pub mod assistant_settings;
 mod context;
 pub mod context_store;
 mod inline_assistant;
-mod model_selector;
 mod patch;
 mod prompt_library;
 mod prompts;
@@ -37,7 +36,6 @@ pub(crate) use inline_assistant::*;
 use language_model::{
     LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
 };
-pub(crate) use model_selector::*;
 pub use patch::*;
 pub use prompts::PromptBuilder;
 use prompts::PromptLoadingParams;

crates/assistant/src/assistant_panel.rs 🔗

@@ -17,9 +17,9 @@ use crate::{
     ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
     DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
     InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
-    MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
-    ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
-    RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
+    MessageMetadata, MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus,
+    QuoteSelection, RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
+    ToggleModelSelector,
 };
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -55,6 +55,7 @@ use language_model::{
     LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
     ZED_CLOUD_PROVIDER_ID,
 };
+use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
 use multi_buffer::MultiBufferRow;
 use picker::{Picker, PickerDelegate};
 use project::lsp_store::LocalLspAdapterDelegate;
@@ -142,7 +143,7 @@ pub struct AssistantPanel {
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
-    model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
+    model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
     model_summary_editor: View<Editor>,
     authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
     configuration_subscription: Option<Subscription>,
@@ -4457,13 +4458,13 @@ pub struct ContextEditorToolbarItem {
     fs: Arc<dyn Fs>,
     active_context_editor: Option<WeakView<ContextEditor>>,
     model_summary_editor: View<Editor>,
-    model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
+    model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
 }
 
 impl ContextEditorToolbarItem {
     pub fn new(
         workspace: &Workspace,
-        model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
+        model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
         model_summary_editor: View<Editor>,
     ) -> Self {
         Self {
@@ -4559,8 +4560,17 @@ impl Render for ContextEditorToolbarItem {
             //         .map(|remaining_items| format!("Files to scan: {}", remaining_items))
             // })
             .child(
-                ModelSelector::new(
-                    self.fs.clone(),
+                LanguageModelSelector::new(
+                    {
+                        let fs = self.fs.clone();
+                        move |model, cx| {
+                            update_settings_file::<AssistantSettings>(
+                                fs.clone(),
+                                cx,
+                                move |settings, _| settings.set_model(model.clone()),
+                            );
+                        }
+                    },
                     ButtonLike::new("active-model")
                         .style(ButtonStyle::Subtle)
                         .child(

crates/assistant/src/inline_assistant.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
     AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
-    CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
+    CyclePreviousInlineAssist, LineDiff, LineOperation, RequestType, StreamingDiff,
 };
 use anyhow::{anyhow, Context as _, Result};
 use client::{telemetry::Telemetry, ErrorExt};
@@ -33,12 +33,13 @@ use language_model::{
     LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
     LanguageModelTextStream, Role,
 };
+use language_model_selector::LanguageModelSelector;
 use language_models::report_assistant_event;
 use multi_buffer::MultiBufferRow;
 use parking_lot::Mutex;
 use project::{CodeAction, ProjectTransaction};
 use rope::Rope;
-use settings::{Settings, SettingsStore};
+use settings::{update_settings_file, Settings, SettingsStore};
 use smol::future::FutureExt;
 use std::{
     cmp,
@@ -1500,8 +1501,17 @@ impl Render for PromptEditor {
                     .justify_center()
                     .gap_2()
                     .child(
-                        ModelSelector::new(
-                            self.fs.clone(),
+                        LanguageModelSelector::new(
+                            {
+                                let fs = self.fs.clone();
+                                move |model, cx| {
+                                    update_settings_file::<AssistantSettings>(
+                                        fs.clone(),
+                                        cx,
+                                        move |settings, _| settings.set_model(model.clone()),
+                                    );
+                                }
+                            },
                             IconButton::new("context", IconName::SettingsAlt)
                                 .shape(IconButtonShape::Square)
                                 .icon_size(IconSize::Small)
@@ -1521,7 +1531,7 @@ impl Render for PromptEditor {
                                     )
                                 }),
                         )
-                        .with_info_text(
+                        .info_text(
                             "Inline edits use context\n\
                             from the currently selected\n\
                             assistant panel tab.",

crates/assistant/src/terminal_inline_assistant.rs 🔗

@@ -1,6 +1,7 @@
+use crate::assistant_settings::AssistantSettings;
 use crate::{
-    humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
-    ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
+    humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType,
+    DEFAULT_CONTEXT_LINES,
 };
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
@@ -19,8 +20,9 @@ use language::Buffer;
 use language_model::{
     LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
 };
+use language_model_selector::LanguageModelSelector;
 use language_models::report_assistant_event;
-use settings::Settings;
+use settings::{update_settings_file, Settings};
 use std::{
     cmp,
     sync::Arc,
@@ -612,8 +614,17 @@ impl Render for PromptEditor {
                     .w_12()
                     .justify_center()
                     .gap_2()
-                    .child(ModelSelector::new(
-                        self.fs.clone(),
+                    .child(LanguageModelSelector::new(
+                        {
+                            let fs = self.fs.clone();
+                            move |model, cx| {
+                                update_settings_file::<AssistantSettings>(
+                                    fs.clone(),
+                                    cx,
+                                    move |settings, _| settings.set_model(model.clone()),
+                                );
+                            }
+                        },
                         IconButton::new("context", IconName::SettingsAlt)
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)

crates/language_model_selector/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "language_model_selector"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/language_model_selector.rs"
+
+[dependencies]
+feature_flags.workspace = true
+gpui.workspace = true
+language_model.workspace = true
+picker.workspace = true
+proto.workspace = true
+ui.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true

crates/assistant/src/model_selector.rs → crates/language_model_selector/src/language_model_selector.rs 🔗

@@ -1,30 +1,27 @@
-use feature_flags::ZedPro;
-
-use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
-use proto::Plan;
-use workspace::ShowConfiguration;
-
 use std::sync::Arc;
 
-use crate::assistant_settings::AssistantSettings;
-use fs::Fs;
-use gpui::{Action, AnyElement, DismissEvent, SharedString, Task};
+use feature_flags::ZedPro;
+use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task};
+use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
 use picker::{Picker, PickerDelegate};
-use settings::update_settings_file;
+use proto::Plan;
 use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
+use workspace::ShowConfiguration;
 
 const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
 
+type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
+
 #[derive(IntoElement)]
-pub struct ModelSelector<T: PopoverTrigger> {
-    handle: Option<PopoverMenuHandle<Picker<ModelPickerDelegate>>>,
-    fs: Arc<dyn Fs>,
+pub struct LanguageModelSelector<T: PopoverTrigger> {
+    handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
+    on_model_changed: OnModelChanged,
     trigger: T,
     info_text: Option<SharedString>,
 }
 
-pub struct ModelPickerDelegate {
-    fs: Arc<dyn Fs>,
+pub struct LanguageModelPickerDelegate {
+    on_model_changed: OnModelChanged,
     all_models: Vec<ModelInfo>,
     filtered_models: Vec<ModelInfo>,
     selected_index: usize,
@@ -38,28 +35,34 @@ struct ModelInfo {
     is_selected: bool,
 }
 
-impl<T: PopoverTrigger> ModelSelector<T> {
-    pub fn new(fs: Arc<dyn Fs>, trigger: T) -> Self {
-        ModelSelector {
+impl<T: PopoverTrigger> LanguageModelSelector<T> {
+    pub fn new(
+        on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
+        trigger: T,
+    ) -> Self {
+        LanguageModelSelector {
             handle: None,
-            fs,
+            on_model_changed: Arc::new(on_model_changed),
             trigger,
             info_text: None,
         }
     }
 
-    pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>) -> Self {
+    pub fn with_handle(
+        mut self,
+        handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
+    ) -> Self {
         self.handle = Some(handle);
         self
     }
 
-    pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
+    pub fn info_text(mut self, text: impl Into<SharedString>) -> Self {
         self.info_text = Some(text.into());
         self
     }
 }
 
-impl PickerDelegate for ModelPickerDelegate {
+impl PickerDelegate for LanguageModelPickerDelegate {
     type ListItem = ListItem;
 
     fn match_count(&self) -> usize {
@@ -137,9 +140,7 @@ impl PickerDelegate for ModelPickerDelegate {
     fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(model_info) = self.filtered_models.get(self.selected_index) {
             let model = model_info.model.clone();
-            update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings, _| {
-                settings.set_model(model.clone())
-            });
+            (self.on_model_changed)(model.clone(), cx);
 
             // Update the selection status
             let selected_model_id = model_info.model.id();
@@ -296,7 +297,7 @@ impl PickerDelegate for ModelPickerDelegate {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
+impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         let selected_provider = LanguageModelRegistry::read_global(cx)
             .active_provider()
@@ -331,8 +332,8 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
             })
             .collect::<Vec<_>>();
 
-        let delegate = ModelPickerDelegate {
-            fs: self.fs.clone(),
+        let delegate = LanguageModelPickerDelegate {
+            on_model_changed: self.on_model_changed.clone(),
             all_models: all_models.clone(),
             filtered_models: all_models,
             selected_index: 0,