profile_selector.rs

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