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