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