1use std::sync::Arc;
2
3use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
4use fs::Fs;
5use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
6use indexmap::IndexMap;
7use language_model::LanguageModelRegistry;
8use settings::{Settings as _, SettingsStore, update_settings_file};
9use ui::{
10 ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
11 prelude::*,
12};
13use util::ResultExt as _;
14
15use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
16
17pub struct ProfileSelector {
18 profiles: IndexMap<AgentProfileId, AgentProfile>,
19 fs: Arc<dyn Fs>,
20 thread_store: WeakEntity<ThreadStore>,
21 focus_handle: FocusHandle,
22 menu_handle: PopoverMenuHandle<ContextMenu>,
23 _subscriptions: Vec<Subscription>,
24}
25
26impl ProfileSelector {
27 pub fn new(
28 fs: Arc<dyn Fs>,
29 thread_store: WeakEntity<ThreadStore>,
30 focus_handle: FocusHandle,
31 cx: &mut Context<Self>,
32 ) -> Self {
33 let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
34 this.refresh_profiles(cx);
35 });
36
37 let mut this = Self {
38 profiles: IndexMap::default(),
39 fs,
40 thread_store,
41 focus_handle,
42 menu_handle: PopoverMenuHandle::default(),
43 _subscriptions: vec![settings_subscription],
44 };
45 this.refresh_profiles(cx);
46
47 this
48 }
49
50 pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
51 self.menu_handle.clone()
52 }
53
54 fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
55 let settings = AssistantSettings::get_global(cx);
56
57 self.profiles = settings.profiles.clone();
58 }
59
60 fn build_context_menu(
61 &self,
62 window: &mut Window,
63 cx: &mut Context<Self>,
64 ) -> Entity<ContextMenu> {
65 ContextMenu::build(window, cx, |mut menu, _window, cx| {
66 let settings = AssistantSettings::get_global(cx);
67 let icon_position = IconPosition::End;
68
69 menu = menu.header("Profiles");
70 for (profile_id, profile) in self.profiles.clone() {
71 menu = menu.toggleable_entry(
72 profile.name.clone(),
73 profile_id == settings.default_profile,
74 icon_position,
75 None,
76 {
77 let fs = self.fs.clone();
78 let thread_store = self.thread_store.clone();
79 move |_window, cx| {
80 update_settings_file::<AssistantSettings>(fs.clone(), cx, {
81 let profile_id = profile_id.clone();
82 move |settings, _cx| {
83 settings.set_profile(profile_id.clone());
84 }
85 });
86
87 thread_store
88 .update(cx, |this, cx| {
89 this.load_profile_by_id(profile_id.clone(), cx);
90 })
91 .log_err();
92 }
93 },
94 );
95 }
96
97 menu = menu.separator();
98 menu = menu.header("Customize Current Profile");
99 menu = menu.item(ContextMenuEntry::new("Tools…").handler({
100 let profile_id = settings.default_profile.clone();
101 move |window, cx| {
102 window.dispatch_action(
103 ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
104 cx,
105 );
106 }
107 }));
108
109 menu = menu.separator();
110 menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
111 move |window, cx| {
112 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
113 },
114 ));
115
116 menu
117 })
118 }
119}
120
121impl Render for ProfileSelector {
122 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
123 let settings = AssistantSettings::get_global(cx);
124 let profile_id = &settings.default_profile;
125 let profile = settings.profiles.get(profile_id);
126
127 let selected_profile = profile
128 .map(|profile| profile.name.clone())
129 .unwrap_or_else(|| "Unknown".into());
130
131 let model_registry = LanguageModelRegistry::read_global(cx);
132 let supports_tools = model_registry
133 .default_model()
134 .map_or(false, |default| default.model.supports_tools());
135
136 let icon = match profile_id.as_str() {
137 "write" => IconName::Pencil,
138 "ask" => IconName::MessageBubbles,
139 _ => IconName::UserRoundPen,
140 };
141
142 let this = cx.entity().clone();
143 let focus_handle = self.focus_handle.clone();
144 PopoverMenu::new("profile-selector")
145 .menu(move |window, cx| {
146 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
147 })
148 .trigger(if supports_tools {
149 ButtonLike::new("profile-selector-button").child(
150 h_flex()
151 .gap_1()
152 .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
153 .child(
154 Label::new(selected_profile)
155 .size(LabelSize::Small)
156 .color(Color::Muted),
157 )
158 .child(
159 Icon::new(IconName::ChevronDown)
160 .size(IconSize::XSmall)
161 .color(Color::Muted),
162 )
163 .child(div().opacity(0.5).children({
164 let focus_handle = focus_handle.clone();
165 KeyBinding::for_action_in(
166 &ToggleProfileSelector,
167 &focus_handle,
168 window,
169 cx,
170 )
171 .map(|kb| kb.size(rems_from_px(10.)))
172 })),
173 )
174 } else {
175 ButtonLike::new("tools-not-supported-button")
176 .disabled(true)
177 .child(
178 h_flex().gap_1().child(
179 Label::new("No Tools")
180 .size(LabelSize::Small)
181 .color(Color::Muted),
182 ),
183 )
184 .tooltip(Tooltip::text("The current model does not support tools."))
185 })
186 .anchor(gpui::Corner::BottomLeft)
187 .with_handle(self.menu_handle.clone())
188 }
189}