1use std::sync::Arc;
2
3use assistant_settings::{AgentProfile, AssistantSettings};
4use fs::Fs;
5use gpui::{prelude::*, Action, Entity, FocusHandle, Subscription, WeakEntity};
6use indexmap::IndexMap;
7use settings::{update_settings_file, Settings as _, SettingsStore};
8use ui::{
9 prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu,
10 PopoverMenuHandle,
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.item(ContextMenuEntry::new("Configure Profiles").handler(
98 move |window, cx| {
99 window.dispatch_action(ManageProfiles.boxed_clone(), cx);
100 },
101 ));
102
103 menu
104 })
105 }
106}
107
108impl Render for ProfileSelector {
109 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
110 let settings = AssistantSettings::get_global(cx);
111 let profile_id = &settings.default_profile;
112 let profile = settings.profiles.get(profile_id);
113
114 let selected_profile = profile
115 .map(|profile| profile.name.clone())
116 .unwrap_or_else(|| "Unknown".into());
117
118 let icon = match profile_id.as_ref() {
119 "write" => IconName::Pencil,
120 "ask" => IconName::MessageBubbles,
121 _ => IconName::UserRoundPen,
122 };
123
124 let this = cx.entity().clone();
125 let focus_handle = self.focus_handle.clone();
126 PopoverMenu::new("profile-selector")
127 .menu(move |window, cx| {
128 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
129 })
130 .trigger(
131 ButtonLike::new("profile-selector-button").child(
132 h_flex()
133 .gap_1()
134 .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
135 .child(
136 Label::new(selected_profile)
137 .size(LabelSize::Small)
138 .color(Color::Muted),
139 )
140 .child(
141 Icon::new(IconName::ChevronDown)
142 .size(IconSize::XSmall)
143 .color(Color::Muted),
144 )
145 .child(div().opacity(0.5).children({
146 let focus_handle = focus_handle.clone();
147 KeyBinding::for_action_in(
148 &ToggleProfileSelector,
149 &focus_handle,
150 window,
151 cx,
152 )
153 .map(|kb| kb.size(rems_from_px(10.)))
154 })),
155 ),
156 )
157 .anchor(gpui::Corner::BottomLeft)
158 .with_handle(self.menu_handle.clone())
159 }
160}