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