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 == ¤t_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. Load more
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 profiles pub 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 == ¤t_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 {