988 lines · 34.6 KB · Rust
RawDownload
use crate::{    CycleModeSelector, ManageProfiles, ToggleProfileSelector, ui::documentation_aside_side,};use agent_settings::{    AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,};use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate, match_strings};use gpui::{    Action, AnyElement, AnyView, App, BackgroundExecutor, Context, DismissEvent, Empty, Entity,    FocusHandle, Focusable, ForegroundExecutor, SharedString, Subscription, Task, Window,};use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};use settings::{Settings as _, SettingsStore, update_settings_file};use std::{    sync::atomic::Ordering,    sync::{Arc, atomic::AtomicBool},};use ui::{    DocumentationAside, HighlightedLabel, KeyBinding, LabelSize, ListItem, ListItemSpacing,    PopoverMenuHandle, TintColor, Tooltip, prelude::*,};use workspace::ToggleWorktreeSecurity; /// Trait for types that can provide and manage agent profilespub trait ProfileProvider {    /// Get the current profile ID    fn profile_id(&self, cx: &App) -> AgentProfileId;     /// Set the profile ID    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App);     /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support)    fn profiles_supported(&self, cx: &App) -> bool;     /// Check if there is a model selected in the current context.    fn model_selected(&self, cx: &App) -> bool;     /// Whether the current workspace is restricted (has untrusted worktrees).    ///    /// In a restricted workspace, profiles that enable tools forbidden in    /// restricted mode are flagged, and the active built-in `write`/`ask`    /// profiles are downgraded to `minimal`.    fn is_restricted(&self, _cx: &App) -> bool {        false    }     /// Whether the active profile has been downgraded to `minimal` because the    /// workspace is restricted (i.e. the user selected `write`/`ask`, but those    /// profiles aren't honored while restricted).    fn profile_downgraded(&self, _cx: &App) -> bool {        false    }} pub struct ProfileSelector {    profiles: AvailableProfiles,    pending_refresh: bool,    fs: Arc<dyn Fs>,    provider: Arc<dyn ProfileProvider>,    picker: Option<Entity<Picker<ProfilePickerDelegate>>>,    picker_handle: PopoverMenuHandle<Picker<ProfilePickerDelegate>>,    focus_handle: FocusHandle,    _subscriptions: Vec<Subscription>,} impl ProfileSelector {    pub fn new(        fs: Arc<dyn Fs>,        provider: Arc<dyn ProfileProvider>,        focus_handle: FocusHandle,        cx: &mut Context<Self>,    ) -> Self {        let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {            this.pending_refresh = true;            cx.notify();        });         Self {            profiles: AgentProfile::available_profiles(cx),            pending_refresh: false,            fs,            provider,            picker: None,            picker_handle: PopoverMenuHandle::default(),            focus_handle,            _subscriptions: vec![settings_subscription],        }    }     pub fn menu_handle(&self) -> PopoverMenuHandle<Picker<ProfilePickerDelegate>> {        self.picker_handle.clone()    }     pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {        if !self.provider.profiles_supported(cx) {            return;        }         let profiles = AgentProfile::available_profiles(cx);        if profiles.is_empty() {            return;        }         let current_profile_id = self.provider.profile_id(cx);        let current_index = profiles            .keys()            .position(|id| id == &current_profile_id)            .unwrap_or(0);         let next_index = (current_index + 1) % profiles.len();         if let Some((next_profile_id, _)) = profiles.get_index(next_index) {            self.provider.set_profile(next_profile_id.clone(), cx);            telemetry::event!(                "Agent Profile Switched",                profile_id = next_profile_id.as_str(),                source = "cycle"            );            cx.notify();        }    }     fn ensure_picker(        &mut self,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Entity<Picker<ProfilePickerDelegate>> {        if self.picker.is_none() {            let delegate = ProfilePickerDelegate::new(                self.fs.clone(),                self.provider.clone(),                self.profiles.clone(),                cx.foreground_executor().clone(),                cx.background_executor().clone(),                self.focus_handle.clone(),                cx,            );             let picker = cx.new(|cx| {                Picker::list(delegate, window, cx)                    .show_scrollbar(true)                    .initial_width(rems(18.))            });             self.picker = Some(picker);        }         if self.pending_refresh {            if let Some(picker) = &self.picker {                let profiles = AgentProfile::available_profiles(cx);                self.profiles = profiles.clone();                picker.update(cx, |picker, cx| {                    let query = picker.query(cx);                    picker                        .delegate                        .refresh_profiles(profiles.clone(), query, cx);                });            }            self.pending_refresh = false;        }         self.picker.as_ref().unwrap().clone()    }} impl Focusable for ProfileSelector {    fn focus_handle(&self, cx: &App) -> FocusHandle {        if let Some(picker) = &self.picker {            picker.focus_handle(cx)        } else {            self.focus_handle.clone()        }    }} impl Render for ProfileSelector {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        if !self.provider.model_selected(cx) {            return Empty.into_any_element();        }         if !self.provider.profiles_supported(cx) {            return Button::new("tools-not-supported-button", "Tools Unsupported")                .disabled(true)                .label_size(LabelSize::Small)                .color(Color::Muted)                .tooltip(Tooltip::text("This model does not support tools."))                .into_any_element();        }         let picker = self.ensure_picker(window, cx);         let settings = AgentSettings::get_global(cx);        let profile_id = self.provider.profile_id(cx);        let profile = settings.profiles.get(&profile_id);         let selected_profile = profile            .map(|profile| profile.name.clone())            .unwrap_or_else(|| "Unknown".into());         let icon = if self.picker_handle.is_deployed() {            IconName::ChevronUp        } else {            IconName::ChevronDown        };         // Warn when the active profile is affected by a restricted workspace:        // either it was downgraded to `minimal`, or it still enables tools that        // are forbidden while restricted.        let show_warning = self.provider.is_restricted(cx)            && (self.provider.profile_downgraded(cx)                || !ProfilePickerDelegate::restricted_forbidden_tools(&profile_id, cx).is_empty());         let trigger_button = Button::new("profile-selector", selected_profile)            .label_size(LabelSize::Small)            .color(Color::Muted)            .when(show_warning, |this| {                this.start_icon(                    Icon::new(IconName::Warning)                        .size(IconSize::XSmall)                        .color(Color::Warning),                )            })            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));         let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = Box::new(Tooltip::element({            move |_window, cx| {                let container = || h_flex().gap_1().justify_between();                v_flex()                    .gap_1()                    .child(                        container()                            .child(Label::new("Change Profile"))                            .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),                    )                    .child(                        container()                            .pt_1()                            .border_t_1()                            .border_color(cx.theme().colors().border_variant)                            .child(Label::new("Cycle Through Profiles"))                            .child(KeyBinding::for_action(&CycleModeSelector, cx)),                    )                    .into_any()            }        }));         PickerPopoverMenu::new(            picker,            trigger_button,            tooltip,            gpui::Anchor::BottomRight,            cx,        )        .with_handle(self.picker_handle.clone())        .render(window, cx)        .into_any_element()    }} #[derive(Clone)]struct ProfileCandidate {    id: AgentProfileId,    name: SharedString,    is_builtin: bool,} #[derive(Clone)]struct ProfileMatchEntry {    candidate_index: usize,    positions: Vec<usize>,} enum ProfilePickerEntry {    Header(SharedString),    Profile(ProfileMatchEntry),} pub struct ProfilePickerDelegate {    fs: Arc<dyn Fs>,    provider: Arc<dyn ProfileProvider>,    foreground: ForegroundExecutor,    background: BackgroundExecutor,    candidates: Vec<ProfileCandidate>,    string_candidates: Arc<Vec<StringMatchCandidate>>,    filtered_entries: Vec<ProfilePickerEntry>,    selected_index: usize,    hovered_index: Option<usize>,    query: String,    cancel: Option<Arc<AtomicBool>>,    focus_handle: FocusHandle,} impl ProfilePickerDelegate {    fn new(        fs: Arc<dyn Fs>,        provider: Arc<dyn ProfileProvider>,        profiles: AvailableProfiles,        foreground: ForegroundExecutor,        background: BackgroundExecutor,        focus_handle: FocusHandle,        cx: &mut Context<ProfileSelector>,    ) -> Self {        let candidates = Self::candidates_from(profiles);        let string_candidates = Arc::new(Self::string_candidates(&candidates));        let filtered_entries = Self::entries_from_candidates(&candidates);         let mut this = Self {            fs,            provider,            foreground,            background,            candidates,            string_candidates,            filtered_entries,            selected_index: 0,            hovered_index: None,            query: String::new(),            cancel: None,            focus_handle,        };         this.selected_index = this            .index_of_profile(&this.provider.profile_id(cx))            .unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));         this    }     fn refresh_profiles(        &mut self,        profiles: AvailableProfiles,        query: String,        cx: &mut Context<Picker<Self>>,    ) {        self.candidates = Self::candidates_from(profiles);        self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));        self.query = query;         if self.query.is_empty() {            self.filtered_entries = Self::entries_from_candidates(&self.candidates);        } else {            let matches = self.search_blocking(&self.query);            self.filtered_entries = self.entries_from_matches(matches);        }         self.selected_index = self            .index_of_profile(&self.provider.profile_id(cx))            .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));        cx.notify();    }     fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {        profiles            .into_iter()            .map(|(id, name)| ProfileCandidate {                is_builtin: builtin_profiles::is_builtin(&id),                id,                name,            })            .collect()    }     fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {        candidates            .iter()            .enumerate()            .map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))            .collect()    }     /// Tools enabled by a profile that are forbidden while the workspace is    /// restricted. Returns an empty list for profiles that are safe to use.    fn restricted_forbidden_tools(profile_id: &AgentProfileId, cx: &App) -> Vec<SharedString> {        let Some(profile) = AgentSettings::get_global(cx).profiles.get(profile_id) else {            return Vec::new();        };        profile            .tools            .iter()            .filter(|(name, enabled)| {                **enabled && !agent::tool_allowed_in_restricted_mode(name.as_ref())            })            .map(|(name, _)| SharedString::from(name.to_string()))            .collect()    }     fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {        match candidate.id.as_str() {            builtin_profiles::WRITE => Some("Get help to write anything."),            builtin_profiles::ASK => Some("Chat about your codebase."),            builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),            _ => None,        }    }     fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {        let mut entries = Vec::new();        let mut inserted_custom_header = false;         for (idx, candidate) in candidates.iter().enumerate() {            if !candidate.is_builtin && !inserted_custom_header {                if !entries.is_empty() {                    entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));                }                inserted_custom_header = true;            }             entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {                candidate_index: idx,                positions: Vec::new(),            }));        }         entries    }     fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {        let mut entries = Vec::new();        for mat in matches {            if self.candidates.get(mat.candidate_id).is_some() {                entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {                    candidate_index: mat.candidate_id,                    positions: mat.positions,                }));            }        }        entries    }     fn first_selectable_index(&self) -> Option<usize> {        self.filtered_entries            .iter()            .position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))    }     fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {        self.filtered_entries.iter().position(|entry| {            matches!(entry, ProfilePickerEntry::Profile(profile) if self                .candidates                .get(profile.candidate_index)                .map(|candidate| &candidate.id == profile_id)                .unwrap_or(false))        })    }     fn search_blocking(&self, query: &str) -> Vec<StringMatch> {        if query.is_empty() {            return self                .string_candidates                .iter()                .map(|candidate| StringMatch {                    candidate_id: candidate.id,                    score: 0.0,                    positions: Vec::new(),                    string: candidate.string.clone(),                })                .collect();        }         let cancel_flag = AtomicBool::new(false);         self.foreground.block_on(match_strings(            self.string_candidates.as_ref(),            query,            false,            true,            100,            &cancel_flag,            self.background.clone(),        ))    }} impl PickerDelegate for ProfilePickerDelegate {    type ListItem = AnyElement;     fn name() -> &'static str {        "profile selector"    }     fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {        "Search profiles…".into()    }     fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {        let text = if self.candidates.is_empty() {            "No profiles.".into()        } else {            "No profiles match your search.".into()        };        Some(text)    }     fn match_count(&self) -> usize {        self.filtered_entries.len()    }     fn selected_index(&self) -> usize {

Lines 1–500 of 988.