profile_selector.rs

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