1use std::sync::{Arc, LazyLock};
2
3use anyhow::Result;
4use assistant_settings::{AgentProfile, AssistantSettings};
5use editor::scroll::Autoscroll;
6use editor::Editor;
7use fs::Fs;
8use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity};
9use indexmap::IndexMap;
10use regex::Regex;
11use settings::{update_settings_file, Settings as _, SettingsStore};
12use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
13use util::ResultExt as _;
14use workspace::{create_and_open_local_file, Workspace};
15
16use crate::ThreadStore;
17
18pub struct ProfileSelector {
19 profiles: IndexMap<Arc<str>, AgentProfile>,
20 fs: Arc<dyn Fs>,
21 thread_store: WeakEntity<ThreadStore>,
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 let mut this = Self {
36 profiles: IndexMap::default(),
37 fs,
38 thread_store,
39 _subscriptions: vec![settings_subscription],
40 };
41 this.refresh_profiles(cx);
42
43 this
44 }
45
46 fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
47 let settings = AssistantSettings::get_global(cx);
48
49 self.profiles = settings.profiles.clone();
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 let icon_position = IconPosition::Start;
60
61 menu = menu.header("Profiles");
62 for (profile_id, profile) in self.profiles.clone() {
63 menu = menu.toggleable_entry(
64 profile.name.clone(),
65 profile_id == settings.default_profile,
66 icon_position,
67 None,
68 {
69 let fs = self.fs.clone();
70 let thread_store = self.thread_store.clone();
71 move |_window, cx| {
72 update_settings_file::<AssistantSettings>(fs.clone(), cx, {
73 let profile_id = profile_id.clone();
74 move |settings, _cx| {
75 settings.set_profile(profile_id.clone());
76 }
77 });
78
79 thread_store
80 .update(cx, |this, cx| {
81 this.load_profile_by_id(&profile_id, cx);
82 })
83 .log_err();
84 }
85 },
86 );
87 }
88
89 menu = menu.separator();
90 menu = menu.item(
91 ContextMenuEntry::new("Configure Profiles")
92 .icon(IconName::Pencil)
93 .icon_color(Color::Muted)
94 .handler(move |window, cx| {
95 if let Some(workspace) = window.root().flatten() {
96 let workspace = workspace.downgrade();
97 window
98 .spawn(cx, async |cx| {
99 Self::open_profiles_setting_in_editor(workspace, cx).await
100 })
101 .detach_and_log_err(cx);
102 }
103 }),
104 );
105
106 menu
107 })
108 }
109
110 async fn open_profiles_setting_in_editor(
111 workspace: WeakEntity<Workspace>,
112 cx: &mut AsyncWindowContext,
113 ) -> Result<()> {
114 let settings_editor = workspace
115 .update_in(cx, |_, window, cx| {
116 create_and_open_local_file(paths::settings_file(), window, cx, || {
117 settings::initial_user_settings_content().as_ref().into()
118 })
119 })?
120 .await?
121 .downcast::<Editor>()
122 .unwrap();
123
124 settings_editor
125 .downgrade()
126 .update_in(cx, |editor, window, cx| {
127 let text = editor.buffer().read(cx).snapshot(cx).text();
128
129 let settings = cx.global::<SettingsStore>();
130
131 let edits =
132 settings.edits_for_update::<AssistantSettings>(
133 &text,
134 |settings| match settings {
135 assistant_settings::AssistantSettingsContent::Versioned(settings) => {
136 match settings {
137 assistant_settings::VersionedAssistantSettingsContent::V2(
138 settings,
139 ) => {
140 settings.profiles.get_or_insert_with(IndexMap::default);
141 }
142 assistant_settings::VersionedAssistantSettingsContent::V1(
143 _,
144 ) => {}
145 }
146 }
147 assistant_settings::AssistantSettingsContent::Legacy(_) => {}
148 },
149 );
150
151 if !edits.is_empty() {
152 editor.edit(edits.iter().cloned(), cx);
153 }
154
155 let text = editor.buffer().read(cx).snapshot(cx).text();
156
157 static PROFILES_REGEX: LazyLock<Regex> =
158 LazyLock::new(|| Regex::new(r#"(?P<key>"profiles":)\s*\{"#).unwrap());
159 let range = PROFILES_REGEX.captures(&text).and_then(|captures| {
160 captures
161 .name("key")
162 .map(|inner_match| inner_match.start()..inner_match.end())
163 });
164 if let Some(range) = range {
165 editor.change_selections(
166 Some(Autoscroll::newest()),
167 window,
168 cx,
169 |selections| {
170 selections.select_ranges(vec![range]);
171 },
172 );
173 }
174 })?;
175
176 anyhow::Ok(())
177 }
178}
179
180impl Render for ProfileSelector {
181 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
182 let settings = AssistantSettings::get_global(cx);
183 let profile = settings
184 .profiles
185 .get(&settings.default_profile)
186 .map(|profile| profile.name.clone())
187 .unwrap_or_else(|| "Unknown".into());
188
189 let this = cx.entity().clone();
190 PopoverMenu::new("tool-selector")
191 .menu(move |window, cx| {
192 Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
193 })
194 .trigger_with_tooltip(
195 Button::new("profile-selector-button", profile)
196 .style(ButtonStyle::Filled)
197 .label_size(LabelSize::Small),
198 Tooltip::text("Change Profile"),
199 )
200 .anchor(gpui::Corner::BottomLeft)
201 }
202}