profile_selector.rs

  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}