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