838 lines · 27.6 KB · Rust
RawDownload
use crate::{CycleModeSelector, ManageProfiles, ToggleProfileSelector};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, 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, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,    ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,}; /// 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;} pub struct ProfileSelector {    profiles: AvailableProfiles,    pending_refresh: bool,    disabled: 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,            disabled: 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 set_disabled(&mut self, disabled: bool) {        self.disabled = disabled;    }     pub fn is_disabled(&self) -> bool {        self.disabled    }     pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {        if self.disabled {            return;        }         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);        }    }     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)                    .width(rems(18.))                    .max_height(Some(rems(20.).into()))            });             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.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        };         let trigger_button = Button::new("profile-selector", selected_profile)            .disabled(self.disabled)            .label_size(LabelSize::Small)            .color(Color::Muted)            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));         let disabled = self.disabled;         let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = if disabled {            Box::new(Tooltip::text("Disabled until generation is done"))        } else {            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::Corner::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()    }     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 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 {        self.selected_index    }     fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));        cx.notify();    }     fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {        match self.filtered_entries.get(ix) {            Some(ProfilePickerEntry::Profile(_)) => true,            Some(ProfilePickerEntry::Header(_)) | None => false,        }    }     fn update_matches(        &mut self,        query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        if query.is_empty() {            self.query.clear();            self.filtered_entries = Self::entries_from_candidates(&self.candidates);            self.selected_index = self                .index_of_profile(&self.provider.profile_id(cx))                .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));            cx.notify();            return Task::ready(());        }         if let Some(prev) = &self.cancel {            prev.store(true, Ordering::Relaxed);        }        let cancel = Arc::new(AtomicBool::new(false));        self.cancel = Some(cancel.clone());         let string_candidates = self.string_candidates.clone();        let background = self.background.clone();        let provider = self.provider.clone();        self.query = query.clone();         let cancel_for_future = cancel; 

Lines 1–500 of 838.