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