language_model_selector: Don't recreate the Picker view each render (#21939)

While working on Assistant2, I noticed that the `LanguageModelSelector`
was recreating its `Picker` view on every single render.

This PR makes it so we create the view once and hold onto it in the
parent view.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-12-12 17:08:48 -05:00 committed by GitHub
parent d7eba54016
commit 9143fd2924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 263 additions and 240 deletions

View file

@ -55,7 +55,7 @@ use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_ID,
}; };
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::lsp_store::LocalLspAdapterDelegate; use project::lsp_store::LocalLspAdapterDelegate;
@ -143,7 +143,7 @@ pub struct AssistantPanel {
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>, model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_summary_editor: View<Editor>, model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>, configuration_subscription: Option<Subscription>,
@ -341,11 +341,12 @@ impl AssistantPanel {
) -> Self { ) -> Self {
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
let model_summary_editor = cx.new_view(Editor::single_line); let model_summary_editor = cx.new_view(Editor::single_line);
let context_editor_toolbar = cx.new_view(|_| { let context_editor_toolbar = cx.new_view(|cx| {
ContextEditorToolbarItem::new( ContextEditorToolbarItem::new(
workspace, workspace,
model_selector_menu_handle.clone(), model_selector_menu_handle.clone(),
model_summary_editor.clone(), model_summary_editor.clone(),
cx,
) )
}); });
@ -4455,23 +4456,36 @@ impl FollowableItem for ContextEditor {
} }
pub struct ContextEditorToolbarItem { pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
active_context_editor: Option<WeakView<ContextEditor>>, active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>, model_summary_editor: View<Editor>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>, language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
} }
impl ContextEditorToolbarItem { impl ContextEditorToolbarItem {
pub fn new( pub fn new(
workspace: &Workspace, workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>, model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_summary_editor: View<Editor>, model_summary_editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
Self { Self {
fs: workspace.app_state().fs.clone(),
active_context_editor: None, active_context_editor: None,
model_summary_editor, model_summary_editor,
model_selector_menu_handle, language_model_selector: cx.new_view(|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()),
);
},
cx,
)
}),
language_model_selector_menu_handle: model_selector_menu_handle,
} }
} }
@ -4560,17 +4574,8 @@ impl Render for ContextEditorToolbarItem {
// .map(|remaining_items| format!("Files to scan: {}", remaining_items)) // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// }) // })
.child( .child(
LanguageModelSelector::new( LanguageModelSelectorPopoverMenu::new(
{ self.language_model_selector.clone(),
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") ButtonLike::new("active-model")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.child( .child(
@ -4616,7 +4621,7 @@ impl Render for ContextEditorToolbarItem {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx) Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}), }),
) )
.with_handle(self.model_selector_menu_handle.clone()), .with_handle(self.language_model_selector_menu_handle.clone()),
) )
.children(self.render_remaining_tokens(cx)); .children(self.render_remaining_tokens(cx));

View file

@ -33,7 +33,7 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role, LanguageModelTextStream, Role,
}; };
use language_model_selector::LanguageModelSelector; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event; use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -1358,8 +1358,8 @@ enum PromptEditorEvent {
struct PromptEditor { struct PromptEditor {
id: InlineAssistId, id: InlineAssistId,
fs: Arc<dyn Fs>,
editor: View<Editor>, editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool, edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>, gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
@ -1500,43 +1500,27 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center() .justify_center()
.gap_2() .gap_2()
.child( .child(LanguageModelSelectorPopoverMenu::new(
LanguageModelSelector::new( self.language_model_selector.clone(),
{ IconButton::new("context", IconName::SettingsAlt)
let fs = self.fs.clone(); .shape(IconButtonShape::Square)
move |model, cx| { .icon_size(IconSize::Small)
update_settings_file::<AssistantSettings>( .icon_color(Color::Muted)
fs.clone(), .tooltip(move |cx| {
cx, Tooltip::with_meta(
move |settings, _| settings.set_model(model.clone()), format!(
); "Using {}",
} LanguageModelRegistry::read_global(cx)
}, .active_model()
IconButton::new("context", IconName::SettingsAlt) .map(|model| model.name().0)
.shape(IconButtonShape::Square) .unwrap_or_else(|| "No model selected".into()),
.icon_size(IconSize::Small) ),
.icon_color(Color::Muted) None,
.tooltip(move |cx| { "Change Model",
Tooltip::with_meta( cx,
format!( )
"Using {}", }),
LanguageModelRegistry::read_global(cx) ))
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
)
.info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",
),
)
.map(|el| { .map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el; return el;
@ -1642,6 +1626,19 @@ impl PromptEditor {
let mut this = Self { let mut this = Self {
id, id,
editor: prompt_editor, editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false, edited_since_done: false,
gutter_dimensions, gutter_dimensions,
prompt_history, prompt_history,
@ -1650,7 +1647,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
codegen, codegen,
fs,
pending_token_count: Task::ready(Ok(())), pending_token_count: Task::ready(Ok(())),
token_counts: None, token_counts: None,
_token_count_subscriptions: token_count_subscriptions, _token_count_subscriptions: token_count_subscriptions,

View file

@ -20,7 +20,7 @@ use language::Buffer;
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
}; };
use language_model_selector::LanguageModelSelector; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event; use language_models::report_assistant_event;
use settings::{update_settings_file, Settings}; use settings::{update_settings_file, Settings};
use std::{ use std::{
@ -476,9 +476,9 @@ enum PromptEditorEvent {
struct PromptEditor { struct PromptEditor {
id: TerminalInlineAssistId, id: TerminalInlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8, height_in_lines: u8,
editor: View<Editor>, editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool, edited_since_done: bool,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>, prompt_history_ix: Option<usize>,
@ -614,17 +614,8 @@ impl Render for PromptEditor {
.w_12() .w_12()
.justify_center() .justify_center()
.gap_2() .gap_2()
.child(LanguageModelSelector::new( .child(LanguageModelSelectorPopoverMenu::new(
{ self.language_model_selector.clone(),
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) IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -718,6 +709,19 @@ impl PromptEditor {
id, id,
height_in_lines: 1, height_in_lines: 1,
editor: prompt_editor, editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false, edited_since_done: false,
prompt_history, prompt_history,
prompt_history_ix: None, prompt_history_ix: None,
@ -725,7 +729,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
codegen, codegen,
fs,
pending_token_count: Task::ready(Ok(())), pending_token_count: Task::ready(Ok(())),
token_count: None, token_count: None,
_token_count_subscriptions: token_count_subscriptions, _token_count_subscriptions: token_count_subscriptions,

View file

@ -31,7 +31,7 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role, LanguageModelTextStream, Role,
}; };
use language_model_selector::LanguageModelSelector; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event; use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -1454,8 +1454,8 @@ enum PromptEditorEvent {
struct PromptEditor { struct PromptEditor {
id: InlineAssistId, id: InlineAssistId,
fs: Arc<dyn Fs>,
editor: View<Editor>, editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool, edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>, gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
@ -1589,43 +1589,27 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center() .justify_center()
.gap_2() .gap_2()
.child( .child(LanguageModelSelectorPopoverMenu::new(
LanguageModelSelector::new( self.language_model_selector.clone(),
{ IconButton::new("context", IconName::SettingsAlt)
let fs = self.fs.clone(); .shape(IconButtonShape::Square)
move |model, cx| { .icon_size(IconSize::Small)
update_settings_file::<AssistantSettings>( .icon_color(Color::Muted)
fs.clone(), .tooltip(move |cx| {
cx, Tooltip::with_meta(
move |settings, _| settings.set_model(model.clone()), format!(
); "Using {}",
} LanguageModelRegistry::read_global(cx)
}, .active_model()
IconButton::new("context", IconName::SettingsAlt) .map(|model| model.name().0)
.shape(IconButtonShape::Square) .unwrap_or_else(|| "No model selected".into()),
.icon_size(IconSize::Small) ),
.icon_color(Color::Muted) None,
.tooltip(move |cx| { "Change Model",
Tooltip::with_meta( cx,
format!( )
"Using {}", }),
LanguageModelRegistry::read_global(cx) ))
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
)
.info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",
),
)
.map(|el| { .map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el; return el;
@ -1714,6 +1698,19 @@ impl PromptEditor {
let mut this = Self { let mut this = Self {
id, id,
editor: prompt_editor, editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false, edited_since_done: false,
gutter_dimensions, gutter_dimensions,
prompt_history, prompt_history,
@ -1722,7 +1719,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
codegen, codegen,
fs,
show_rate_limit_notice: false, show_rate_limit_notice: false,
}; };
this.subscribe_to_editor(cx); this.subscribe_to_editor(cx);

View file

@ -3,7 +3,7 @@ use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView}; use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
@ -25,6 +25,7 @@ pub struct MessageEditor {
next_context_id: ContextId, next_context_id: ContextId,
context_picker: View<ContextPicker>, context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>, pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
use_tools: bool, use_tools: bool,
} }
@ -47,6 +48,14 @@ impl MessageEditor {
next_context_id: ContextId(0), next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)), context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(), context_picker_handle: PopoverMenuHandle::default(),
language_model_selector: cx.new_view(|cx| {
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
cx,
)
}),
use_tools: false, use_tools: false,
} }
} }
@ -120,10 +129,8 @@ impl MessageEditor {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model(); let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelector::new( LanguageModelSelectorPopoverMenu::new(
|model, _cx| { self.language_model_selector.clone(),
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model") ButtonLike::new("active-model")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.child( .child(

View file

@ -17,7 +17,7 @@ use language::Buffer;
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
}; };
use language_model_selector::LanguageModelSelector; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event; use language_models::report_assistant_event;
use settings::{update_settings_file, Settings}; use settings::{update_settings_file, Settings};
use std::{cmp, sync::Arc, time::Instant}; use std::{cmp, sync::Arc, time::Instant};
@ -439,9 +439,9 @@ enum PromptEditorEvent {
struct PromptEditor { struct PromptEditor {
id: TerminalInlineAssistId, id: TerminalInlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8, height_in_lines: u8,
editor: View<Editor>, editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool, edited_since_done: bool,
prompt_history: VecDeque<String>, prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>, prompt_history_ix: Option<usize>,
@ -575,17 +575,8 @@ impl Render for PromptEditor {
.w_12() .w_12()
.justify_center() .justify_center()
.gap_2() .gap_2()
.child(LanguageModelSelector::new( .child(LanguageModelSelectorPopoverMenu::new(
{ self.language_model_selector.clone(),
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) IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -665,6 +656,19 @@ impl PromptEditor {
id, id,
height_in_lines: 1, height_in_lines: 1,
editor: prompt_editor, editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false, edited_since_done: false,
prompt_history, prompt_history,
prompt_history_ix: None, prompt_history_ix: None,
@ -672,7 +676,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(), editor_subscriptions: Vec::new(),
codegen, codegen,
fs,
}; };
this.count_lines(cx); this.count_lines(cx);
this.subscribe_to_editor(cx); this.subscribe_to_editor(cx);

View file

@ -1,7 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use feature_flags::ZedPro; use feature_flags::ZedPro;
use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task}; use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
View, WeakView,
};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use proto::Plan; use proto::Plan;
@ -12,19 +15,101 @@ const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>; type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
#[derive(IntoElement)] pub struct LanguageModelSelector {
pub struct LanguageModelSelector<T: PopoverTrigger> { picker: View<Picker<LanguageModelPickerDelegate>>,
handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
on_model_changed: OnModelChanged,
trigger: T,
info_text: Option<SharedString>,
} }
pub struct LanguageModelPickerDelegate { impl LanguageModelSelector {
on_model_changed: OnModelChanged, pub fn new(
all_models: Vec<ModelInfo>, on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
filtered_models: Vec<ModelInfo>, cx: &mut ViewContext<Self>,
selected_index: usize, ) -> Self {
let on_model_changed = Arc::new(on_model_changed);
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let icon = provider.icon();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.view().downgrade(),
on_model_changed: on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
LanguageModelSelector { picker }
}
}
impl EventEmitter<DismissEvent> for LanguageModelSelector {}
impl FocusableView for LanguageModelSelector {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for LanguageModelSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(IntoElement)]
pub struct LanguageModelSelectorPopoverMenu<T>
where
T: PopoverTrigger,
{
language_model_selector: View<LanguageModelSelector>,
trigger: T,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
}
impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
pub fn new(language_model_selector: View<LanguageModelSelector>, trigger: T) -> Self {
Self {
language_model_selector,
trigger,
handle: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
self.handle = Some(handle);
self
}
}
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let language_model_selector = self.language_model_selector.clone();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(language_model_selector.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -32,34 +117,14 @@ struct ModelInfo {
model: Arc<dyn LanguageModel>, model: Arc<dyn LanguageModel>,
icon: IconName, icon: IconName,
availability: LanguageModelAvailability, availability: LanguageModelAvailability,
is_selected: bool,
} }
impl<T: PopoverTrigger> LanguageModelSelector<T> { pub struct LanguageModelPickerDelegate {
pub fn new( language_model_selector: WeakView<LanguageModelSelector>,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static, on_model_changed: OnModelChanged,
trigger: T, all_models: Vec<ModelInfo>,
) -> Self { filtered_models: Vec<ModelInfo>,
LanguageModelSelector { selected_index: usize,
handle: None,
on_model_changed: Arc::new(on_model_changed),
trigger,
info_text: None,
}
}
pub fn with_handle(
mut self,
handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
) -> Self {
self.handle = Some(handle);
self
}
pub fn info_text(mut self, text: impl Into<SharedString>) -> Self {
self.info_text = Some(text.into());
self
}
} }
impl PickerDelegate for LanguageModelPickerDelegate { impl PickerDelegate for LanguageModelPickerDelegate {
@ -142,23 +207,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let model = model_info.model.clone(); let model = model_info.model.clone();
(self.on_model_changed)(model.clone(), cx); (self.on_model_changed)(model.clone(), cx);
// Update the selection status
let selected_model_id = model_info.model.id();
let selected_provider_id = model_info.model.provider_id();
for model in &mut self.all_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
for model in &mut self.filtered_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
} }
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {} fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.language_model_selector
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> { fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let configured_models_count = LanguageModelRegistry::global(cx) let configured_models_count = LanguageModelRegistry::global(cx)
@ -195,6 +252,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let model_info = self.filtered_models.get(ix)?; let model_info = self.filtered_models.get(ix)?;
let provider_name: String = model_info.model.provider_name().0.clone().into(); let provider_name: String = model_info.model.provider_name().0.clone().into();
let active_provider_id = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let active_model_id = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
Some( Some(
ListItem::new(ix) ListItem::new(ix)
.inset(true) .inset(true)
@ -235,7 +303,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
}), }),
), ),
) )
.end_slot(div().when(model_info.is_selected, |this| { .end_slot(div().when(is_selected, |this| {
this.child( this.child(
Icon::new(IconName::Check) Icon::new(IconName::Check)
.color(Color::Accent) .color(Color::Accent)
@ -296,58 +364,3 @@ impl PickerDelegate for LanguageModelPickerDelegate {
) )
} }
} }
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let selected_model = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
on_model_changed: self.on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
}