1use std::sync::Arc;
2
3use agent_settings::{
4 AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, GroupedAgentProfiles,
5 builtin_profiles,
6};
7use fs::Fs;
8use gpui::{Action, Empty, 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(AgentSettings::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(AgentSettings::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 = AgentSettings::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: &AgentSettings,
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 entry = ContextMenuEntry::new(profile.name.clone())
114 .toggleable(IconPosition::End, profile_id == settings.default_profile);
115
116 let entry = if let Some(doc_text) = documentation {
117 entry.documentation_aside(documentation_side(settings.dock), move |_| {
118 Label::new(doc_text).into_any_element()
119 })
120 } else {
121 entry
122 };
123
124 entry.handler({
125 let fs = self.fs.clone();
126 let thread_store = self.thread_store.clone();
127 let profile_id = profile_id.clone();
128 move |_window, cx| {
129 update_settings_file::<AgentSettings>(fs.clone(), cx, {
130 let profile_id = profile_id.clone();
131 move |settings, _cx| {
132 settings.set_profile(profile_id.clone());
133 }
134 });
135
136 thread_store
137 .update(cx, |this, cx| {
138 this.load_profile_by_id(profile_id.clone(), cx);
139 })
140 .log_err();
141 }
142 })
143 }
144}
145
146impl Render for ProfileSelector {
147 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
148 let settings = AgentSettings::get_global(cx);
149 let profile_id = &settings.default_profile;
150 let profile = settings.profiles.get(profile_id);
151
152 let selected_profile = profile
153 .map(|profile| profile.name.clone())
154 .unwrap_or_else(|| "Unknown".into());
155
156 let configured_model = self.thread.read(cx).configured_model().or_else(|| {
157 let model_registry = LanguageModelRegistry::read_global(cx);
158 model_registry.default_model()
159 });
160 let Some(configured_model) = configured_model else {
161 return Empty.into_any_element();
162 };
163
164 if configured_model.model.supports_tools() {
165 let this = cx.entity().clone();
166 let focus_handle = self.focus_handle.clone();
167 let trigger_button = Button::new("profile-selector-model", selected_profile)
168 .label_size(LabelSize::Small)
169 .color(Color::Muted)
170 .icon(IconName::ChevronDown)
171 .icon_size(IconSize::XSmall)
172 .icon_position(IconPosition::End)
173 .icon_color(Color::Muted);
174
175 PopoverMenu::new("profile-selector")
176 .trigger_with_tooltip(trigger_button, {
177 let focus_handle = focus_handle.clone();
178 move |window, cx| {
179 Tooltip::for_action_in(
180 "Toggle Profile Menu",
181 &ToggleProfileSelector,
182 &focus_handle,
183 window,
184 cx,
185 )
186 }
187 })
188 .anchor(
189 if documentation_side(settings.dock) == DocumentationSide::Left {
190 gpui::Corner::BottomRight
191 } else {
192 gpui::Corner::BottomLeft
193 },
194 )
195 .with_handle(self.menu_handle.clone())
196 .menu(move |window, cx| {
197 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
198 })
199 .into_any_element()
200 } else {
201 Button::new("tools-not-supported-button", "Tools Unsupported")
202 .disabled(true)
203 .label_size(LabelSize::Small)
204 .color(Color::Muted)
205 .tooltip(Tooltip::text("This model does not support tools."))
206 .into_any_element()
207 }
208 }
209}
210
211fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
212 match position {
213 AgentDockPosition::Left => DocumentationSide::Right,
214 AgentDockPosition::Bottom => DocumentationSide::Left,
215 AgentDockPosition::Right => DocumentationSide::Left,
216 }
217}