profile_selector.rs

  1use crate::{ManageProfiles, ToggleProfileSelector};
  2use agent_settings::{
  3    AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles,
  4    builtin_profiles,
  5};
  6use fs::Fs;
  7use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
  8use settings::{Settings as _, SettingsStore, update_settings_file};
  9use std::sync::Arc;
 10use ui::{
 11    ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
 12    PopoverMenuHandle, TintColor, Tooltip, prelude::*,
 13};
 14
 15/// Trait for types that can provide and manage agent profiles
 16pub trait ProfileProvider {
 17    /// Get the current profile ID
 18    fn profile_id(&self, cx: &App) -> AgentProfileId;
 19
 20    /// Set the profile ID
 21    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App);
 22
 23    /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support)
 24    fn profiles_supported(&self, cx: &App) -> bool;
 25}
 26
 27pub struct ProfileSelector {
 28    profiles: AvailableProfiles,
 29    fs: Arc<dyn Fs>,
 30    provider: Arc<dyn ProfileProvider>,
 31    menu_handle: PopoverMenuHandle<ContextMenu>,
 32    focus_handle: FocusHandle,
 33    _subscriptions: Vec<Subscription>,
 34}
 35
 36impl ProfileSelector {
 37    pub fn new(
 38        fs: Arc<dyn Fs>,
 39        provider: Arc<dyn ProfileProvider>,
 40        focus_handle: FocusHandle,
 41        cx: &mut Context<Self>,
 42    ) -> Self {
 43        let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
 44            this.refresh_profiles(cx);
 45        });
 46
 47        Self {
 48            profiles: AgentProfile::available_profiles(cx),
 49            fs,
 50            provider,
 51            menu_handle: PopoverMenuHandle::default(),
 52            focus_handle,
 53            _subscriptions: vec![settings_subscription],
 54        }
 55    }
 56
 57    pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
 58        self.menu_handle.clone()
 59    }
 60
 61    fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
 62        self.profiles = AgentProfile::available_profiles(cx);
 63    }
 64
 65    fn build_context_menu(
 66        &self,
 67        window: &mut Window,
 68        cx: &mut Context<Self>,
 69    ) -> Entity<ContextMenu> {
 70        ContextMenu::build(window, cx, |mut menu, _window, cx| {
 71            let settings = AgentSettings::get_global(cx);
 72
 73            let mut found_non_builtin = false;
 74            for (profile_id, profile_name) in self.profiles.iter() {
 75                if !builtin_profiles::is_builtin(profile_id) {
 76                    found_non_builtin = true;
 77                    continue;
 78                }
 79                menu = menu.item(self.menu_entry_for_profile(
 80                    profile_id.clone(),
 81                    profile_name,
 82                    settings,
 83                    cx,
 84                ));
 85            }
 86
 87            if found_non_builtin {
 88                menu = menu.separator().header("Custom Profiles");
 89                for (profile_id, profile_name) in self.profiles.iter() {
 90                    if builtin_profiles::is_builtin(profile_id) {
 91                        continue;
 92                    }
 93                    menu = menu.item(self.menu_entry_for_profile(
 94                        profile_id.clone(),
 95                        profile_name,
 96                        settings,
 97                        cx,
 98                    ));
 99                }
100            }
101
102            menu = menu.separator();
103            menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
104                move |window, cx| {
105                    window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
106                },
107            ));
108
109            menu
110        })
111    }
112
113    fn menu_entry_for_profile(
114        &self,
115        profile_id: AgentProfileId,
116        profile_name: &SharedString,
117        settings: &AgentSettings,
118        cx: &App,
119    ) -> ContextMenuEntry {
120        let documentation = match profile_name.to_lowercase().as_str() {
121            builtin_profiles::WRITE => Some("Get help to write anything."),
122            builtin_profiles::ASK => Some("Chat about your codebase."),
123            builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
124            _ => None,
125        };
126        let thread_profile_id = self.provider.profile_id(cx);
127
128        let entry = ContextMenuEntry::new(profile_name.clone())
129            .toggleable(IconPosition::End, profile_id == thread_profile_id);
130
131        let entry = if let Some(doc_text) = documentation {
132            entry.documentation_aside(
133                documentation_side(settings.dock),
134                DocumentationEdge::Top,
135                move |_| Label::new(doc_text).into_any_element(),
136            )
137        } else {
138            entry
139        };
140
141        entry.handler({
142            let fs = self.fs.clone();
143            let provider = self.provider.clone();
144            move |_window, cx| {
145                update_settings_file::<AgentSettings>(fs.clone(), cx, {
146                    let profile_id = profile_id.clone();
147                    move |settings, _cx| {
148                        settings.set_profile(profile_id);
149                    }
150                });
151
152                provider.set_profile(profile_id.clone(), cx);
153            }
154        })
155    }
156}
157
158impl Render for ProfileSelector {
159    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
160        let settings = AgentSettings::get_global(cx);
161        let profile_id = self.provider.profile_id(cx);
162        let profile = settings.profiles.get(&profile_id);
163
164        let selected_profile = profile
165            .map(|profile| profile.name.clone())
166            .unwrap_or_else(|| "Unknown".into());
167
168        if self.provider.profiles_supported(cx) {
169            let this = cx.entity();
170            let focus_handle = self.focus_handle.clone();
171            let trigger_button = Button::new("profile-selector-model", selected_profile)
172                .label_size(LabelSize::Small)
173                .color(Color::Muted)
174                .icon(IconName::ChevronDown)
175                .icon_size(IconSize::XSmall)
176                .icon_position(IconPosition::End)
177                .icon_color(Color::Muted)
178                .selected_style(ButtonStyle::Tinted(TintColor::Accent));
179
180            PopoverMenu::new("profile-selector")
181                .trigger_with_tooltip(trigger_button, {
182                    move |window, cx| {
183                        Tooltip::for_action_in(
184                            "Toggle Profile Menu",
185                            &ToggleProfileSelector,
186                            &focus_handle,
187                            window,
188                            cx,
189                        )
190                    }
191                })
192                .anchor(
193                    if documentation_side(settings.dock) == DocumentationSide::Left {
194                        gpui::Corner::BottomRight
195                    } else {
196                        gpui::Corner::BottomLeft
197                    },
198                )
199                .with_handle(self.menu_handle.clone())
200                .menu(move |window, cx| {
201                    Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
202                })
203                .offset(gpui::Point {
204                    x: px(0.0),
205                    y: px(-2.0),
206                })
207                .into_any_element()
208        } else {
209            Button::new("tools-not-supported-button", "Tools Unsupported")
210                .disabled(true)
211                .label_size(LabelSize::Small)
212                .color(Color::Muted)
213                .tooltip(Tooltip::text("This model does not support tools."))
214                .into_any_element()
215        }
216    }
217}
218
219fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
220    match position {
221        AgentDockPosition::Left => DocumentationSide::Right,
222        AgentDockPosition::Bottom => DocumentationSide::Left,
223        AgentDockPosition::Right => DocumentationSide::Left,
224    }
225}