profile_selector.rs

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