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