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