1use std::rc::Rc;
2use std::sync::Arc;
3
4use acp_thread::{AgentModelInfo, AgentModelSelector};
5use agent_servers::AgentServer;
6use agent_settings::AgentSettings;
7use fs::Fs;
8use gpui::{Entity, FocusHandle};
9use language_model::IconOrSvg;
10use picker::popover_menu::PickerPopoverMenu;
11use settings::Settings as _;
12use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
13use zed_actions::agent::ToggleModelSelector;
14
15use crate::CycleFavoriteModels;
16use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
17
18pub struct AcpModelSelectorPopover {
19 selector: Entity<AcpModelSelector>,
20 menu_handle: PopoverMenuHandle<AcpModelSelector>,
21 focus_handle: FocusHandle,
22}
23
24impl AcpModelSelectorPopover {
25 pub(crate) fn new(
26 selector: Rc<dyn AgentModelSelector>,
27 agent_server: Rc<dyn AgentServer>,
28 fs: Arc<dyn Fs>,
29 menu_handle: PopoverMenuHandle<AcpModelSelector>,
30 focus_handle: FocusHandle,
31 window: &mut Window,
32 cx: &mut Context<Self>,
33 ) -> Self {
34 let focus_handle_clone = focus_handle.clone();
35 Self {
36 selector: cx.new(move |cx| {
37 acp_model_selector(
38 selector,
39 agent_server,
40 fs,
41 focus_handle_clone.clone(),
42 window,
43 cx,
44 )
45 }),
46 menu_handle,
47 focus_handle,
48 }
49 }
50
51 pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
52 self.menu_handle.toggle(window, cx);
53 }
54
55 pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
56 self.selector.read(cx).delegate.active_model()
57 }
58
59 pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
60 self.selector.update(cx, |selector, cx| {
61 selector.delegate.cycle_favorite_models(window, cx);
62 });
63 }
64}
65
66impl Render for AcpModelSelectorPopover {
67 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
68 let model = self.selector.read(cx).delegate.active_model();
69 let model_name = model
70 .as_ref()
71 .map(|model| model.name.clone())
72 .unwrap_or_else(|| SharedString::from("Select a Model"));
73
74 let model_icon = model.as_ref().and_then(|model| model.icon.clone());
75
76 let focus_handle = self.focus_handle.clone();
77
78 let (color, icon) = if self.menu_handle.is_deployed() {
79 (Color::Accent, IconName::ChevronUp)
80 } else {
81 (Color::Muted, IconName::ChevronDown)
82 };
83
84 let tooltip = Tooltip::element({
85 move |_, cx| {
86 let focus_handle = focus_handle.clone();
87 let should_show_cycle_row = !AgentSettings::get_global(cx)
88 .favorite_model_ids()
89 .is_empty();
90
91 v_flex()
92 .gap_1()
93 .child(
94 h_flex()
95 .gap_2()
96 .justify_between()
97 .child(Label::new("Change Model"))
98 .child(KeyBinding::for_action_in(
99 &ToggleModelSelector,
100 &focus_handle,
101 cx,
102 )),
103 )
104 .when(should_show_cycle_row, |this| {
105 this.child(
106 h_flex()
107 .pt_1()
108 .gap_2()
109 .border_t_1()
110 .border_color(cx.theme().colors().border_variant)
111 .justify_between()
112 .child(Label::new("Cycle Favorited Models"))
113 .child(KeyBinding::for_action_in(
114 &CycleFavoriteModels,
115 &focus_handle,
116 cx,
117 )),
118 )
119 })
120 .into_any()
121 }
122 });
123
124 PickerPopoverMenu::new(
125 self.selector.clone(),
126 ButtonLike::new("active-model")
127 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
128 .when_some(model_icon, |this, icon| {
129 this.child(
130 match icon {
131 IconOrSvg::Svg(path) => Icon::from_external_svg(path),
132 IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
133 }
134 .color(color)
135 .size(IconSize::XSmall),
136 )
137 })
138 .child(
139 Label::new(model_name)
140 .color(color)
141 .size(LabelSize::Small)
142 .ml_0p5(),
143 )
144 .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
145 tooltip,
146 gpui::Corner::BottomRight,
147 cx,
148 )
149 .with_handle(self.menu_handle.clone())
150 .render(window, cx)
151 }
152}