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