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