1use std::sync::Arc;
2
3use assistant_settings::{AgentProfile, AssistantSettings};
4use fs::Fs;
5use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
6use indexmap::IndexMap;
7use settings::{Settings as _, SettingsStore, update_settings_file};
8use ui::{
9 ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle,
10 prelude::*,
11};
12use util::ResultExt as _;
13
14use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
15
16pub struct ProfileSelector {
17 profiles: IndexMap<Arc<str>, AgentProfile>,
18 fs: Arc<dyn Fs>,
19 thread_store: WeakEntity<ThreadStore>,
20 focus_handle: FocusHandle,
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 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 let mut this = Self {
37 profiles: IndexMap::default(),
38 fs,
39 thread_store,
40 focus_handle,
41 menu_handle: PopoverMenuHandle::default(),
42 _subscriptions: vec![settings_subscription],
43 };
44 this.refresh_profiles(cx);
45
46 this
47 }
48
49 pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
50 self.menu_handle.clone()
51 }
52
53 fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
54 let settings = AssistantSettings::get_global(cx);
55
56 self.profiles = settings.profiles.clone();
57 }
58
59 fn build_context_menu(
60 &self,
61 window: &mut Window,
62 cx: &mut Context<Self>,
63 ) -> Entity<ContextMenu> {
64 ContextMenu::build(window, cx, |mut menu, _window, cx| {
65 let settings = AssistantSettings::get_global(cx);
66 let icon_position = IconPosition::End;
67
68 menu = menu.header("Profiles");
69 for (profile_id, profile) in self.profiles.clone() {
70 menu = menu.toggleable_entry(
71 profile.name.clone(),
72 profile_id == settings.default_profile,
73 icon_position,
74 None,
75 {
76 let fs = self.fs.clone();
77 let thread_store = self.thread_store.clone();
78 move |_window, cx| {
79 update_settings_file::<AssistantSettings>(fs.clone(), cx, {
80 let profile_id = profile_id.clone();
81 move |settings, _cx| {
82 settings.set_profile(profile_id.clone());
83 }
84 });
85
86 thread_store
87 .update(cx, |this, cx| {
88 this.load_profile_by_id(&profile_id, cx);
89 })
90 .log_err();
91 }
92 },
93 );
94 }
95
96 menu = menu.separator();
97 menu = menu.header("Customize Current Profile");
98 menu = menu.item(ContextMenuEntry::new("Tools…").handler({
99 let profile_id = settings.default_profile.clone();
100 move |window, cx| {
101 window.dispatch_action(
102 ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
103 cx,
104 );
105 }
106 }));
107
108 menu = menu.separator();
109 menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
110 move |window, cx| {
111 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
112 },
113 ));
114
115 menu
116 })
117 }
118}
119
120impl Render for ProfileSelector {
121 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
122 let settings = AssistantSettings::get_global(cx);
123 let profile_id = &settings.default_profile;
124 let profile = settings.profiles.get(profile_id);
125
126 let selected_profile = profile
127 .map(|profile| profile.name.clone())
128 .unwrap_or_else(|| "Unknown".into());
129
130 let icon = match profile_id.as_ref() {
131 "write" => IconName::Pencil,
132 "ask" => IconName::MessageBubbles,
133 _ => IconName::UserRoundPen,
134 };
135
136 let this = cx.entity().clone();
137 let focus_handle = self.focus_handle.clone();
138 PopoverMenu::new("profile-selector")
139 .menu(move |window, cx| {
140 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
141 })
142 .trigger(
143 ButtonLike::new("profile-selector-button").child(
144 h_flex()
145 .gap_1()
146 .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
147 .child(
148 Label::new(selected_profile)
149 .size(LabelSize::Small)
150 .color(Color::Muted),
151 )
152 .child(
153 Icon::new(IconName::ChevronDown)
154 .size(IconSize::XSmall)
155 .color(Color::Muted),
156 )
157 .child(div().opacity(0.5).children({
158 let focus_handle = focus_handle.clone();
159 KeyBinding::for_action_in(
160 &ToggleProfileSelector,
161 &focus_handle,
162 window,
163 cx,
164 )
165 .map(|kb| kb.size(rems_from_px(10.)))
166 })),
167 ),
168 )
169 .anchor(gpui::Corner::BottomLeft)
170 .with_handle(self.menu_handle.clone())
171 }
172}