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