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