diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 50d6f9f5919b572d8c1c7da5002b34ad904c6f1f..17ddd729cbcd613685442625464807aa510694e6 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -35,7 +35,7 @@ use language_model::{ report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelTextStream, Role, }; -use language_model_selector::inline_language_model_selector; +use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, ProjectTransaction}; @@ -1425,6 +1425,7 @@ enum PromptEditorEvent { struct PromptEditor { id: InlineAssistId, editor: Entity, + language_model_selector: Entity, edited_since_done: bool, gutter_dimensions: Arc>, prompt_history: VecDeque, @@ -1438,7 +1439,6 @@ struct PromptEditor { _token_count_subscriptions: Vec, workspace: Option>, show_rate_limit_notice: bool, - fs: Arc, } #[derive(Copy, Clone)] @@ -1589,16 +1589,29 @@ impl Render for PromptEditor { .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) .justify_center() .gap_2() - .child(inline_language_model_selector({ - let fs = self.fs.clone(); - move |model, cx| { - update_settings_file::( - fs.clone(), + .child(LanguageModelSelectorPopoverMenu::new( + self.language_model_selector.clone(), + IconButton::new("context", IconName::SettingsAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(Color::Muted), + move |window, cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + window, cx, - move |settings, _| settings.set_model(model.clone()), - ); - } - })) + ) + }, + gpui::Corner::TopRight, + )) .map(|el| { let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { return el; @@ -1711,8 +1724,21 @@ impl PromptEditor { let mut this = Self { id, - fs, editor: prompt_editor, + language_model_selector: cx.new(|cx| { + let fs = fs.clone(); + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + }, + window, + cx, + ) + }), edited_since_done: false, gutter_dimensions, prompt_history, diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 29016d6c3a740a8f51b83bdeebbc3e3177df9e8c..61afcb3abe25970ad058407c4ceb0946d6f26375 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -19,7 +19,7 @@ use language_model::{ report_assistant_event, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; -use language_model_selector::inline_language_model_selector; +use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use prompt_store::PromptBuilder; use settings::{update_settings_file, Settings}; use std::{ @@ -487,9 +487,9 @@ enum PromptEditorEvent { struct PromptEditor { id: TerminalInlineAssistId, - fs: Arc, height_in_lines: u8, editor: Entity, + language_model_selector: Entity, edited_since_done: bool, prompt_history: VecDeque, prompt_history_ix: Option, @@ -641,16 +641,29 @@ impl Render for PromptEditor { .w_12() .justify_center() .gap_2() - .child(inline_language_model_selector({ - let fs = self.fs.clone(); - move |model, cx| { - update_settings_file::( - fs.clone(), + .child(LanguageModelSelectorPopoverMenu::new( + self.language_model_selector.clone(), + IconButton::new("change-model", IconName::SettingsAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(Color::Muted), + move |window, cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + window, cx, - move |settings, _| settings.set_model(model.clone()), - ); - } - })) + ) + }, + gpui::Corner::TopRight, + )) .children( if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { let error_message = SharedString::from(error.to_string()); @@ -728,9 +741,22 @@ impl PromptEditor { let mut this = Self { id, - fs, height_in_lines: 1, editor: prompt_editor, + language_model_selector: cx.new(|cx| { + let fs = fs.clone(); + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + }, + window, + cx, + ) + }), edited_since_done: false, prompt_history, prompt_history_ix: None, diff --git a/crates/assistant2/src/assistant_model_selector.rs b/crates/assistant2/src/assistant_model_selector.rs index 52d18c97ca65581b105a2fe3d314d259e32bc194..90c88cb54702251c32fb14fee0dd74491a080a15 100644 --- a/crates/assistant2/src/assistant_model_selector.rs +++ b/crates/assistant2/src/assistant_model_selector.rs @@ -1,28 +1,45 @@ use assistant_settings::AssistantSettings; use fs::Fs; -use gpui::FocusHandle; -use language_model_selector::{assistant_language_model_selector, LanguageModelSelector}; +use gpui::{Entity, FocusHandle, SharedString}; +use language_model::LanguageModelRegistry; +use language_model_selector::{ + LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector, +}; use settings::update_settings_file; use std::sync::Arc; -use ui::{prelude::*, PopoverMenuHandle}; +use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip}; pub struct AssistantModelSelector { + selector: Entity, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, - fs: Arc, } impl AssistantModelSelector { pub(crate) fn new( fs: Arc, + menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, - _window: &mut Window, - _cx: &mut App, + window: &mut Window, + cx: &mut App, ) -> Self { Self { - fs, + selector: cx.new(|cx| { + let fs = fs.clone(); + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _cx| settings.set_model(model.clone()), + ); + }, + window, + cx, + ) + }), + menu_handle, focus_handle, - menu_handle: PopoverMenuHandle::default(), } } @@ -32,21 +49,43 @@ impl AssistantModelSelector { } impl Render for AssistantModelSelector { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - assistant_language_model_selector( - self.focus_handle.clone(), - Some(self.menu_handle.clone()), - cx, - { - let fs = self.fs.clone(); - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| settings.set_model(model.clone()), - ); - } + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + let focus_handle = self.focus_handle.clone(); + let model_name = match active_model { + Some(model) => model.name().0, + _ => SharedString::from("No model selected"), + }; + + LanguageModelSelectorPopoverMenu::new( + self.selector.clone(), + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .gap_0p5() + .child( + Label::new(model_name) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .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, + ) }, + gpui::Corner::BottomRight, ) + .with_handle(self.menu_handle.clone()) } } diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index d0ad3bcfb1c9e491b969d8676f99c9e912bf55fd..4a3757d92009f68673bd9b3090652140981481a7 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -857,6 +857,7 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { ContextStrip::new( @@ -880,7 +881,13 @@ impl PromptEditor { context_strip, context_picker_menu_handle, model_selector: cx.new(|cx| { - AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx) + AssistantModelSelector::new( + fs, + model_selector_menu_handle, + prompt_editor.focus_handle(cx), + window, + cx, + ) }), edited_since_done: false, prompt_history, @@ -1005,6 +1012,7 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { ContextStrip::new( @@ -1028,7 +1036,13 @@ impl PromptEditor { context_strip, context_picker_menu_handle, model_selector: cx.new(|cx| { - AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx) + AssistantModelSelector::new( + fs, + model_selector_menu_handle.clone(), + prompt_editor.focus_handle(cx), + window, + cx, + ) }), edited_since_done: false, prompt_history, diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index de5e2a814ef1a71cb0e44a1510c3decfc4b34e24..e0b35acad1b105632feac2cf46e4433d0b8d1e10 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -54,6 +54,7 @@ impl MessageEditor { let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); let context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let editor = cx.new(|cx| { let mut editor = Editor::auto_height(10, window, cx); @@ -106,8 +107,15 @@ impl MessageEditor { context_picker_menu_handle, inline_context_picker, inline_context_picker_menu_handle, - model_selector: cx - .new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)), + model_selector: cx.new(|cx| { + AssistantModelSelector::new( + fs, + model_selector_menu_handle, + editor.focus_handle(cx), + window, + cx, + ) + }), use_tools: false, _subscriptions: subscriptions, } diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index b90626d79df549f7e4d203ff13a7cea653eed234..ad327f46619dcfea0ecaf9d68704d346da6582e2 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -38,7 +38,7 @@ use language_model::{ Role, }; use language_model_selector::{ - assistant_language_model_selector, LanguageModelSelector, ToggleModelSelector, + LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector, }; use multi_buffer::MultiBufferRow; use picker::Picker; @@ -197,7 +197,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>, - language_model_selector: PopoverMenuHandle, + language_model_selector: Entity, + language_model_selector_menu_handle: PopoverMenuHandle, } pub const DEFAULT_TAB_TITLE: &str = "New Chat"; @@ -263,7 +264,7 @@ impl ContextEditor { image_blocks: Default::default(), scroll_position: None, remote_id: None, - fs, + fs: fs.clone(), workspace, project, pending_slash_command_creases: HashMap::default(), @@ -275,7 +276,20 @@ impl ContextEditor { show_accept_terms: false, slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), - language_model_selector: PopoverMenuHandle::default(), + language_model_selector: cx.new(|cx| { + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + }, + window, + cx, + ) + }), + language_model_selector_menu_handle: PopoverMenuHandle::default(), }; this.update_message_headers(cx); this.update_image_blocks(cx); @@ -2375,6 +2389,46 @@ impl ContextEditor { ) } + fn render_language_model_selector(&self, cx: &mut Context) -> 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( + Label::new(model_name) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .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, + ) + }, + gpui::Corner::BottomLeft, + ) + .with_handle(self.language_model_selector_menu_handle.clone()) + } + fn render_last_error(&self, cx: &mut Context) -> Option { let last_error = self.last_error.as_ref()?; @@ -2819,7 +2873,7 @@ impl Render for ContextEditor { None }; - let language_model_selector = self.language_model_selector.clone(); + let language_model_selector = self.language_model_selector_menu_handle.clone(); v_flex() .key_context("ContextEditor") .capture_action(cx.listener(ContextEditor::cancel)) @@ -2872,23 +2926,11 @@ impl Render for ContextEditor { .gap_1() .child(self.render_inject_context_menu(cx)) .child(ui::Divider::vertical()) - .child(div().pl_0p5().child(assistant_language_model_selector( - self.editor().focus_handle(cx), - Some(self.language_model_selector.clone()), - cx, - { - let fs = self.fs.clone(); - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| { - settings.set_model(model.clone()) - }, - ); - } - }, - ))), + .child( + div() + .pl_0p5() + .child(self.render_language_model_selector(cx)), + ), ) .child( h_flex() diff --git a/crates/language_model_selector/src/language_model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs index 83bd9a43dc3cffbae5ab753f1d997320fd9b9de6..73af32816e06a8fd8753678112bf5f9f9ed42fca 100644 --- a/crates/language_model_selector/src/language_model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -1,8 +1,8 @@ -use std::{rc::Rc, sync::Arc}; +use std::sync::Arc; use feature_flags::ZedPro; use gpui::{ - action_with_deprecated_aliases, Action, AnyElement, App, Corner, DismissEvent, Entity, + action_with_deprecated_aliases, Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use language_model::{ @@ -10,10 +10,7 @@ use language_model::{ }; use picker::{Picker, PickerDelegate}; use proto::Plan; -use ui::{ - prelude::*, ButtonLike, IconButtonShape, ListItem, ListItemSpacing, PopoverMenu, - PopoverMenuHandle, Tooltip, -}; +use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; use workspace::ShowConfiguration; action_with_deprecated_aliases!( @@ -31,7 +28,6 @@ pub struct LanguageModelSelector { /// The task used to update the picker's matches when there is a change to /// the language model registry. update_matches_task: Option>, - popover_menu_handle: PopoverMenuHandle, _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -63,7 +59,6 @@ impl LanguageModelSelector { LanguageModelSelector { picker, update_matches_task: None, - popover_menu_handle: PopoverMenuHandle::default(), _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), @@ -73,15 +68,6 @@ impl LanguageModelSelector { } } - pub fn toggle_model_selector( - &mut self, - _: &ToggleModelSelector, - window: &mut Window, - cx: &mut Context, - ) { - self.popover_menu_handle.toggle(window, cx); - } - fn handle_language_model_registry_event( &mut self, _registry: &Entity, @@ -201,6 +187,65 @@ impl Render for LanguageModelSelector { } } +#[derive(IntoElement)] +pub struct LanguageModelSelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + language_model_selector: Entity, + trigger: T, + tooltip: TT, + handle: Option>, + anchor: Corner, +} + +impl LanguageModelSelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + pub fn new( + language_model_selector: Entity, + trigger: T, + tooltip: TT, + anchor: Corner, + ) -> Self { + Self { + language_model_selector, + trigger, + tooltip, + handle: None, + anchor, + } + } + + pub fn with_handle(mut self, handle: PopoverMenuHandle) -> Self { + self.handle = Some(handle); + self + } +} + +impl RenderOnce for LanguageModelSelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let language_model_selector = self.language_model_selector.clone(); + + PopoverMenu::new("model-switcher") + .menu(move |_window, _cx| Some(language_model_selector.clone())) + .trigger_with_tooltip(self.trigger, self.tooltip) + .anchor(self.anchor) + .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) + .offset(gpui::Point { + x: px(0.0), + y: px(-2.0), + }) + } +} + #[derive(Clone)] struct ModelInfo { model: Arc, @@ -482,114 +527,3 @@ impl PickerDelegate for LanguageModelPickerDelegate { ) } } - -pub fn inline_language_model_selector( - on_model_changed: impl Fn(Arc, &App) + 'static, -) -> PopoverMenu { - let on_model_changed = Rc::new(on_model_changed); - PopoverMenu::new("popover-button") - .menu(move |window, cx| { - Some(cx.new(|cx| { - LanguageModelSelector::new( - { - let on_model_changed = on_model_changed.clone(); - move |model, cx| { - on_model_changed(model, cx); - } - }, - window, - cx, - ) - })) - }) - .trigger_with_tooltip( - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted), - move |window, cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - window, - cx, - ) - }, - ) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(-2.0), - }) -} - -pub fn assistant_language_model_selector( - keybinding_target: FocusHandle, - menu_handle: Option>, - cx: &App, - on_model_changed: impl Fn(Arc, &App) + 'static, -) -> PopoverMenu { - let active_model = LanguageModelRegistry::read_global(cx).active_model(); - let model_name = match active_model { - Some(model) => model.name().0, - _ => SharedString::from("No model selected"), - }; - - let on_model_changed = Rc::new(on_model_changed); - - PopoverMenu::new("popover-button") - .menu(move |window, cx| { - Some(cx.new(|cx| { - LanguageModelSelector::new( - { - let on_model_changed = on_model_changed.clone(); - move |model, cx| { - on_model_changed(model, cx); - } - }, - window, - cx, - ) - })) - }) - .trigger_with_tooltip( - ButtonLike::new("active-model") - .style(ButtonStyle::Subtle) - .child( - h_flex() - .gap_0p5() - .child( - Label::new(model_name) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &keybinding_target, - window, - cx, - ) - }, - ) - .anchor(Corner::BottomRight) - .when_some(menu_handle, |el, handle| el.with_handle(handle)) - .offset(gpui::Point { - x: px(0.0), - y: px(-2.0), - }) -}