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