profile_selector.rs

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