diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 959e0e72f38feadb39da38b7bbc3eed58dcd775e..658b88e0c2a4f0b4203c5f1191c0a49cb4ad6fd5 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -12,14 +12,11 @@ use gpui::{ }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; -use ui::{ - DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem, - ListItemSpacing, prelude::*, -}; +use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*}; use util::ResultExt; use zed_actions::agent::OpenSettings; -use crate::ui::HoldForDefault; +use crate::ui::{HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}; pub type AcpModelSelector = Picker; @@ -236,39 +233,19 @@ impl PickerDelegate for AcpModelPickerDelegate { fn render_match( &self, ix: usize, - selected: bool, + is_focused: bool, _: &mut Window, cx: &mut Context>, ) -> Option { match self.filtered_entries.get(ix)? { - AcpModelPickerEntry::Separator(title) => Some( - div() - .px_2() - .pb_1() - .when(ix > 1, |this| { - this.mt_1() - .pt_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - }) - .child( - Label::new(title) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - ), + AcpModelPickerEntry::Separator(title) => { + Some(ModelSelectorHeader::new(title, ix > 1).into_any_element()) + } AcpModelPickerEntry::Model(model_info) => { let is_selected = Some(model_info) == self.selected_model.as_ref(); let default_model = self.agent_server.default_model(cx); let is_default = default_model.as_ref() == Some(&model_info.id); - let model_icon_color = if is_selected { - Color::Accent - } else { - Color::Muted - }; - Some( div() .id(("model-picker-menu-child", ix)) @@ -284,30 +261,10 @@ impl PickerDelegate for AcpModelPickerDelegate { })) }) .child( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_1p5() - .when_some(model_info.icon, |this, icon| { - this.child( - Icon::new(icon) - .color(model_icon_color) - .size(IconSize::Small) - ) - }) - .child(Label::new(model_info.name.clone()).truncate()), - ) - .end_slot(div().pr_3().when(is_selected, |this| { - this.child( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - })), + ModelSelectorListItem::new(ix, model_info.name.clone()) + .is_focused(is_focused) + .is_selected(is_selected) + .when_some(model_info.icon, |this, icon| this.icon(icon)), ) .into_any_element() ) @@ -343,7 +300,7 @@ impl PickerDelegate for AcpModelPickerDelegate { fn render_footer( &self, _window: &mut Window, - cx: &mut Context>, + _cx: &mut Context>, ) -> Option { let focus_handle = self.focus_handle.clone(); @@ -351,26 +308,7 @@ impl PickerDelegate for AcpModelPickerDelegate { return None; } - Some( - h_flex() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("configure", "Configure") - .full_width() - .style(ButtonStyle::Outlined) - .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(OpenSettings.boxed_clone(), cx); - }), - ) - .into_any(), - ) + Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element()) } } diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 5b5a4513c6dca32e985c966e07ad84e84fc9a872..7e1c35eba45bf9a79d42b59374c8cdb2aa0cac21 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -11,9 +11,11 @@ use language_model::{ }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; -use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*}; +use ui::prelude::*; use zed_actions::agent::OpenSettings; +use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}; + type OnModelChanged = Arc, &mut App) + 'static>; type GetActiveModel = Arc Option + 'static>; @@ -459,28 +461,14 @@ impl PickerDelegate for LanguageModelPickerDelegate { fn render_match( &self, ix: usize, - selected: bool, + is_focused: bool, _: &mut Window, cx: &mut Context>, ) -> Option { match self.filtered_entries.get(ix)? { - LanguageModelPickerEntry::Separator(title) => Some( - div() - .px_2() - .pb_1() - .when(ix > 1, |this| { - this.mt_1() - .pt_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - }) - .child( - Label::new(title) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - ), + LanguageModelPickerEntry::Separator(title) => { + Some(ModelSelectorHeader::new(title, ix > 1).into_any_element()) + } LanguageModelPickerEntry::Model(model_info) => { let active_model = (self.get_active_model)(cx); let active_provider_id = active_model.as_ref().map(|m| m.provider.id()); @@ -489,35 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate { let is_selected = Some(model_info.model.provider_id()) == active_provider_id && Some(model_info.model.id()) == active_model_id; - let model_icon_color = if is_selected { - Color::Accent - } else { - Color::Muted - }; - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_1p5() - .child( - Icon::new(model_info.icon) - .color(model_icon_color) - .size(IconSize::Small), - ) - .child(Label::new(model_info.model.name().0).truncate()), - ) - .end_slot(div().pr_3().when(is_selected, |this| { - this.child( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - })) + ModelSelectorListItem::new(ix, model_info.model.name().0) + .is_focused(is_focused) + .is_selected(is_selected) + .icon(model_info.icon) .into_any_element(), ) } @@ -527,34 +491,15 @@ impl PickerDelegate for LanguageModelPickerDelegate { fn render_footer( &self, _window: &mut Window, - cx: &mut Context>, + _cx: &mut Context>, ) -> Option { - let focus_handle = self.focus_handle.clone(); - if !self.popover_styles { return None; } - Some( - h_flex() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("configure", "Configure") - .full_width() - .style(ButtonStyle::Outlined) - .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(OpenSettings.boxed_clone(), cx); - }), - ) - .into_any(), - ) + let focus_handle = self.focus_handle.clone(); + + Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element()) } } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 6c3d8bc1427092b0d0380cf286da1706337932fe..b484fdb6c6c480f1cffe78eea7a51f635d3906a1 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -4,8 +4,8 @@ mod burn_mode_tooltip; mod claude_code_onboarding_modal; mod end_trial_upsell; mod hold_for_default; +mod model_selector_components; mod onboarding_modal; - mod usage_callout; pub use acp_onboarding_modal::*; @@ -14,6 +14,6 @@ pub use burn_mode_tooltip::*; pub use claude_code_onboarding_modal::*; pub use end_trial_upsell::*; pub use hold_for_default::*; +pub use model_selector_components::*; pub use onboarding_modal::*; - pub use usage_callout::*; diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs new file mode 100644 index 0000000000000000000000000000000000000000..3218daef7c9aadae5cd45b2fc65807d8a32254bd --- /dev/null +++ b/crates/agent_ui/src/ui/model_selector_components.rs @@ -0,0 +1,147 @@ +use gpui::{Action, FocusHandle, prelude::*}; +use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*}; + +#[derive(IntoElement)] +pub struct ModelSelectorHeader { + title: SharedString, + has_border: bool, +} + +impl ModelSelectorHeader { + pub fn new(title: impl Into, has_border: bool) -> Self { + Self { + title: title.into(), + has_border, + } + } +} + +impl RenderOnce for ModelSelectorHeader { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .px_2() + .pb_1() + .when(self.has_border, |this| { + this.mt_1() + .pt_2() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + }) + .child( + Label::new(self.title) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + } +} + +#[derive(IntoElement)] +pub struct ModelSelectorListItem { + index: usize, + title: SharedString, + icon: Option, + is_selected: bool, + is_focused: bool, +} + +impl ModelSelectorListItem { + pub fn new(index: usize, title: impl Into) -> Self { + Self { + index, + title: title.into(), + icon: None, + is_selected: false, + is_focused: false, + } + } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = Some(icon); + self + } + + pub fn is_selected(mut self, is_selected: bool) -> Self { + self.is_selected = is_selected; + self + } + + pub fn is_focused(mut self, is_focused: bool) -> Self { + self.is_focused = is_focused; + self + } +} + +impl RenderOnce for ModelSelectorListItem { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let model_icon_color = if self.is_selected { + Color::Accent + } else { + Color::Muted + }; + + ListItem::new(self.index) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(self.is_focused) + .child( + h_flex() + .w_full() + .gap_1p5() + .when_some(self.icon, |this, icon| { + this.child( + Icon::new(icon) + .color(model_icon_color) + .size(IconSize::Small), + ) + }) + .child(Label::new(self.title).truncate()), + ) + .end_slot(div().pr_2().when(self.is_selected, |this| { + this.child( + Icon::new(IconName::Check) + .color(Color::Accent) + .size(IconSize::Small), + ) + })) + } +} + +#[derive(IntoElement)] +pub struct ModelSelectorFooter { + action: Box, + focus_handle: FocusHandle, +} + +impl ModelSelectorFooter { + pub fn new(action: Box, focus_handle: FocusHandle) -> Self { + Self { + action, + focus_handle, + } + } +} + +impl RenderOnce for ModelSelectorFooter { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let action = self.action; + let focus_handle = self.focus_handle; + + h_flex() + .w_full() + .p_1p5() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child( + Button::new("configure", "Configure") + .full_width() + .style(ButtonStyle::Outlined) + .key_binding( + KeyBinding::for_action_in(action.as_ref(), &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx); + }), + ) + } +}