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