1use std::sync::Arc;
2
3use assistant_settings::{
4 AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
5 builtin_profiles,
6};
7use fs::Fs;
8use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
9use language_model::LanguageModelRegistry;
10use settings::{Settings as _, SettingsStore, update_settings_file};
11use ui::{
12 ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
13 prelude::*,
14};
15use util::ResultExt as _;
16
17use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
18
19pub struct ProfileSelector {
20 profiles: GroupedAgentProfiles,
21 fs: Arc<dyn Fs>,
22 thread: Entity<Thread>,
23 thread_store: WeakEntity<ThreadStore>,
24 menu_handle: PopoverMenuHandle<ContextMenu>,
25 focus_handle: FocusHandle,
26 _subscriptions: Vec<Subscription>,
27}
28
29impl ProfileSelector {
30 pub fn new(
31 fs: Arc<dyn Fs>,
32 thread: Entity<Thread>,
33 thread_store: WeakEntity<ThreadStore>,
34 focus_handle: FocusHandle,
35 cx: &mut Context<Self>,
36 ) -> Self {
37 let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
38 this.refresh_profiles(cx);
39 });
40
41 Self {
42 profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
43 fs,
44 thread,
45 thread_store,
46 menu_handle: PopoverMenuHandle::default(),
47 focus_handle,
48 _subscriptions: vec![settings_subscription],
49 }
50 }
51
52 pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
53 self.menu_handle.clone()
54 }
55
56 fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
57 self.profiles = GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx));
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 for (profile_id, profile) in self.profiles.builtin.iter() {
68 menu = menu.item(self.menu_entry_for_profile(
69 profile_id.clone(),
70 profile,
71 settings,
72 cx,
73 ));
74 }
75
76 if !self.profiles.custom.is_empty() {
77 menu = menu.separator().header("Custom Profiles");
78 for (profile_id, profile) in self.profiles.custom.iter() {
79 menu = menu.item(self.menu_entry_for_profile(
80 profile_id.clone(),
81 profile,
82 settings,
83 cx,
84 ));
85 }
86 }
87
88 menu = menu.separator();
89 menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
90 move |window, cx| {
91 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
92 },
93 ));
94
95 menu
96 })
97 }
98
99 fn menu_entry_for_profile(
100 &self,
101 profile_id: AgentProfileId,
102 profile: &AgentProfile,
103 settings: &AssistantSettings,
104 cx: &App,
105 ) -> ContextMenuEntry {
106 let documentation = match profile.name.to_lowercase().as_str() {
107 builtin_profiles::WRITE => Some("Get help to write anything."),
108 builtin_profiles::ASK => Some("Chat about your codebase."),
109 builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
110 _ => None,
111 };
112
113 let current_profile_id = self.thread.read(cx).configured_profile_id();
114
115 let entry = ContextMenuEntry::new(profile.name.clone()).toggleable(
116 IconPosition::End,
117 Some(profile_id.clone()) == current_profile_id,
118 );
119
120 let entry = if let Some(doc_text) = documentation {
121 entry.documentation_aside(documentation_side(settings.dock), move |_| {
122 Label::new(doc_text).into_any_element()
123 })
124 } else {
125 entry
126 };
127
128 entry.handler({
129 let fs = self.fs.clone();
130 let thread_store = self.thread_store.clone();
131 let profile_id = profile_id.clone();
132 let thread = self.thread.clone();
133
134 move |_window, cx| {
135 thread.update(cx, |thread, cx| {
136 thread.set_configured_profile_id(Some(profile_id.clone()), cx);
137 });
138
139 update_settings_file::<AssistantSettings>(fs.clone(), cx, {
140 let profile_id = profile_id.clone();
141 move |settings, _cx| {
142 settings.set_profile(profile_id.clone());
143 }
144 });
145
146 thread_store
147 .update(cx, |this, cx| {
148 this.load_profile_by_id(profile_id.clone(), cx);
149 })
150 .log_err();
151 }
152 })
153 }
154}
155
156impl Render for ProfileSelector {
157 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
158 let settings = AssistantSettings::get_global(cx);
159 let profile_id = self
160 .thread
161 .read(cx)
162 .configured_profile_id()
163 .unwrap_or(settings.default_profile.clone());
164 let profile = settings.profiles.get(&profile_id).cloned();
165
166 let selected_profile = profile
167 .map(|profile| profile.name.clone())
168 .unwrap_or_else(|| "Unknown".into());
169
170 let configured_model = self
171 .thread
172 .read_with(cx, |thread, _cx| thread.configured_model())
173 .or_else(|| {
174 let model_registry = LanguageModelRegistry::read_global(cx);
175 model_registry.default_model()
176 });
177 let supports_tools =
178 configured_model.map_or(false, |default| default.model.supports_tools());
179
180 if supports_tools {
181 let this = cx.entity().clone();
182 let focus_handle = self.focus_handle.clone();
183 let trigger_button = Button::new("profile-selector-model", selected_profile)
184 .label_size(LabelSize::Small)
185 .color(Color::Muted)
186 .icon(IconName::ChevronDown)
187 .icon_size(IconSize::XSmall)
188 .icon_position(IconPosition::End)
189 .icon_color(Color::Muted);
190
191 PopoverMenu::new("profile-selector")
192 .trigger_with_tooltip(trigger_button, {
193 let focus_handle = focus_handle.clone();
194 move |window, cx| {
195 Tooltip::for_action_in(
196 "Toggle Profile Menu",
197 &ToggleProfileSelector,
198 &focus_handle,
199 window,
200 cx,
201 )
202 }
203 })
204 .anchor(
205 if documentation_side(settings.dock) == DocumentationSide::Left {
206 gpui::Corner::BottomRight
207 } else {
208 gpui::Corner::BottomLeft
209 },
210 )
211 .with_handle(self.menu_handle.clone())
212 .menu(move |window, cx| {
213 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
214 })
215 .into_any_element()
216 } else {
217 Button::new("tools-not-supported-button", "Tools Unsupported")
218 .disabled(true)
219 .label_size(LabelSize::Small)
220 .color(Color::Muted)
221 .tooltip(Tooltip::text("This model does not support tools."))
222 .into_any_element()
223 }
224 }
225}
226
227fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
228 match position {
229 AssistantDockPosition::Left => DocumentationSide::Right,
230 AssistantDockPosition::Bottom => DocumentationSide::Left,
231 AssistantDockPosition::Right => DocumentationSide::Left,
232 }
233}