profile_selector.rs

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