1use std::sync::Arc;
2
3use assistant_settings::{
4 AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
5};
6use fs::Fs;
7use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
8use language_model::LanguageModelRegistry;
9use settings::{Settings as _, SettingsStore, update_settings_file};
10use ui::{
11 ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
12 prelude::*,
13};
14use util::ResultExt as _;
15
16use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
17
18pub struct ProfileSelector {
19 profiles: GroupedAgentProfiles,
20 fs: Arc<dyn Fs>,
21 thread_store: WeakEntity<ThreadStore>,
22 menu_handle: PopoverMenuHandle<ContextMenu>,
23 focus_handle: FocusHandle,
24 _subscriptions: Vec<Subscription>,
25 documentation_side: DocumentationSide,
26}
27
28impl ProfileSelector {
29 pub fn new(
30 fs: Arc<dyn Fs>,
31 thread_store: WeakEntity<ThreadStore>,
32 focus_handle: FocusHandle,
33 documentation_side: DocumentationSide,
34 cx: &mut Context<Self>,
35 ) -> Self {
36 let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
37 this.refresh_profiles(cx);
38 });
39
40 Self {
41 profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
42 fs,
43 thread_store,
44 menu_handle: PopoverMenuHandle::default(),
45 focus_handle,
46 _subscriptions: vec![settings_subscription],
47 documentation_side,
48 }
49 }
50
51 pub fn set_documentation_side(&mut self, side: DocumentationSide, cx: &mut Context<Self>) {
52 self.documentation_side = side;
53 cx.notify();
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 = GroupedAgentProfiles::from_settings(AssistantSettings::get_global(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 = AssistantSettings::get_global(cx);
71 for (profile_id, profile) in self.profiles.builtin.iter() {
72 menu =
73 menu.item(self.menu_entry_for_profile(profile_id.clone(), profile, settings));
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 ));
84 }
85 }
86
87 menu = menu.separator();
88 menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
89 move |window, cx| {
90 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
91 },
92 ));
93
94 menu
95 })
96 }
97
98 fn menu_entry_for_profile(
99 &self,
100 profile_id: AgentProfileId,
101 profile: &AgentProfile,
102 settings: &AssistantSettings,
103 ) -> ContextMenuEntry {
104 let documentation = match profile.name.to_lowercase().as_str() {
105 builtin_profiles::WRITE => Some("Get help to write anything."),
106 builtin_profiles::ASK => Some("Chat about your codebase."),
107 builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
108 _ => None,
109 };
110
111 let entry = ContextMenuEntry::new(profile.name.clone())
112 .toggleable(IconPosition::End, profile_id == settings.default_profile);
113
114 let entry = if let Some(doc_text) = documentation {
115 entry.documentation_aside(self.documentation_side, move |_| {
116 Label::new(doc_text).into_any_element()
117 })
118 } else {
119 entry
120 };
121
122 entry.handler({
123 let fs = self.fs.clone();
124 let thread_store = self.thread_store.clone();
125 let profile_id = profile_id.clone();
126 move |_window, cx| {
127 update_settings_file::<AssistantSettings>(fs.clone(), cx, {
128 let profile_id = profile_id.clone();
129 move |settings, _cx| {
130 settings.set_profile(profile_id.clone());
131 }
132 });
133
134 thread_store
135 .update(cx, |this, cx| {
136 this.load_profile_by_id(profile_id.clone(), cx);
137 })
138 .log_err();
139 }
140 })
141 }
142}
143
144impl Render for ProfileSelector {
145 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
146 let settings = AssistantSettings::get_global(cx);
147 let profile_id = &settings.default_profile;
148 let profile = settings.profiles.get(profile_id);
149
150 let selected_profile = profile
151 .map(|profile| profile.name.clone())
152 .unwrap_or_else(|| "Unknown".into());
153
154 let model_registry = LanguageModelRegistry::read_global(cx);
155 let supports_tools = model_registry
156 .default_model()
157 .map_or(false, |default| default.model.supports_tools());
158
159 if supports_tools {
160 let this = cx.entity().clone();
161 let focus_handle = self.focus_handle.clone();
162 let trigger_button = Button::new("profile-selector-model", selected_profile)
163 .label_size(LabelSize::Small)
164 .color(Color::Muted)
165 .icon(IconName::ChevronDown)
166 .icon_size(IconSize::XSmall)
167 .icon_position(IconPosition::End)
168 .icon_color(Color::Muted);
169
170 PopoverMenu::new("profile-selector")
171 .trigger_with_tooltip(trigger_button, {
172 let focus_handle = focus_handle.clone();
173 move |window, cx| {
174 Tooltip::for_action_in(
175 "Toggle Profile Menu",
176 &ToggleProfileSelector,
177 &focus_handle,
178 window,
179 cx,
180 )
181 }
182 })
183 .anchor(if self.documentation_side == DocumentationSide::Left {
184 gpui::Corner::BottomRight
185 } else {
186 gpui::Corner::BottomLeft
187 })
188 .with_handle(self.menu_handle.clone())
189 .menu(move |window, cx| {
190 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
191 })
192 .into_any_element()
193 } else {
194 Button::new("tools-not-supported-button", "Tools Unsupported")
195 .disabled(true)
196 .label_size(LabelSize::Small)
197 .color(Color::Muted)
198 .tooltip(Tooltip::text("This model does not support tools."))
199 .into_any_element()
200 }
201 }
202}