1use crate::{CycleModeSelector, ManageProfiles, ToggleProfileSelector};
2use agent_settings::{
3 AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
4};
5use fs::Fs;
6use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
7use gpui::{
8 Action, AnyElement, AnyView, App, BackgroundExecutor, Context, DismissEvent, Entity,
9 FocusHandle, Focusable, ForegroundExecutor, SharedString, Subscription, Task, Window,
10};
11use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
12use settings::{Settings as _, SettingsStore, update_settings_file};
13use std::{
14 sync::atomic::Ordering,
15 sync::{Arc, atomic::AtomicBool},
16};
17use ui::{
18 DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
19 ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
20};
21
22/// Trait for types that can provide and manage agent profiles
23pub trait ProfileProvider {
24 /// Get the current profile ID
25 fn profile_id(&self, cx: &App) -> AgentProfileId;
26
27 /// Set the profile ID
28 fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App);
29
30 /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support)
31 fn profiles_supported(&self, cx: &App) -> bool;
32}
33
34pub struct ProfileSelector {
35 profiles: AvailableProfiles,
36 pending_refresh: bool,
37 disabled: bool,
38 fs: Arc<dyn Fs>,
39 provider: Arc<dyn ProfileProvider>,
40 picker: Option<Entity<Picker<ProfilePickerDelegate>>>,
41 picker_handle: PopoverMenuHandle<Picker<ProfilePickerDelegate>>,
42 focus_handle: FocusHandle,
43 _subscriptions: Vec<Subscription>,
44}
45
46impl ProfileSelector {
47 pub fn new(
48 fs: Arc<dyn Fs>,
49 provider: Arc<dyn ProfileProvider>,
50 focus_handle: FocusHandle,
51 cx: &mut Context<Self>,
52 ) -> Self {
53 let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
54 this.pending_refresh = true;
55 cx.notify();
56 });
57
58 Self {
59 profiles: AgentProfile::available_profiles(cx),
60 pending_refresh: false,
61 disabled: false,
62 fs,
63 provider,
64 picker: None,
65 picker_handle: PopoverMenuHandle::default(),
66 focus_handle,
67 _subscriptions: vec![settings_subscription],
68 }
69 }
70
71 pub fn menu_handle(&self) -> PopoverMenuHandle<Picker<ProfilePickerDelegate>> {
72 self.picker_handle.clone()
73 }
74
75 pub fn set_disabled(&mut self, disabled: bool) {
76 self.disabled = disabled;
77 }
78
79 pub fn is_disabled(&self) -> bool {
80 self.disabled
81 }
82
83 pub fn cycle_profile(&mut self, cx: &mut Context<Self>) {
84 if self.disabled {
85 return;
86 }
87
88 if !self.provider.profiles_supported(cx) {
89 return;
90 }
91
92 let profiles = AgentProfile::available_profiles(cx);
93 if profiles.is_empty() {
94 return;
95 }
96
97 let current_profile_id = self.provider.profile_id(cx);
98 let current_index = profiles
99 .keys()
100 .position(|id| id == ¤t_profile_id)
101 .unwrap_or(0);
102
103 let next_index = (current_index + 1) % profiles.len();
104
105 if let Some((next_profile_id, _)) = profiles.get_index(next_index) {
106 self.provider.set_profile(next_profile_id.clone(), cx);
107 }
108 }
109
110 fn ensure_picker(
111 &mut self,
112 window: &mut Window,
113 cx: &mut Context<Self>,
114 ) -> Entity<Picker<ProfilePickerDelegate>> {
115 if self.picker.is_none() {
116 let delegate = ProfilePickerDelegate::new(
117 self.fs.clone(),
118 self.provider.clone(),
119 self.profiles.clone(),
120 cx.foreground_executor().clone(),
121 cx.background_executor().clone(),
122 self.focus_handle.clone(),
123 cx,
124 );
125
126 let picker = cx.new(|cx| {
127 Picker::list(delegate, window, cx)
128 .show_scrollbar(true)
129 .width(rems(18.))
130 .max_height(Some(rems(20.).into()))
131 });
132
133 self.picker = Some(picker);
134 }
135
136 if self.pending_refresh {
137 if let Some(picker) = &self.picker {
138 let profiles = AgentProfile::available_profiles(cx);
139 self.profiles = profiles.clone();
140 picker.update(cx, |picker, cx| {
141 let query = picker.query(cx);
142 picker
143 .delegate
144 .refresh_profiles(profiles.clone(), query, cx);
145 });
146 }
147 self.pending_refresh = false;
148 }
149
150 self.picker.as_ref().unwrap().clone()
151 }
152}
153
154impl Focusable for ProfileSelector {
155 fn focus_handle(&self, cx: &App) -> FocusHandle {
156 if let Some(picker) = &self.picker {
157 picker.focus_handle(cx)
158 } else {
159 self.focus_handle.clone()
160 }
161 }
162}
163
164impl Render for ProfileSelector {
165 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
166 if !self.provider.profiles_supported(cx) {
167 return Button::new("tools-not-supported-button", "Tools Unsupported")
168 .disabled(true)
169 .label_size(LabelSize::Small)
170 .color(Color::Muted)
171 .tooltip(Tooltip::text("This model does not support tools."))
172 .into_any_element();
173 }
174
175 let picker = self.ensure_picker(window, cx);
176
177 let settings = AgentSettings::get_global(cx);
178 let profile_id = self.provider.profile_id(cx);
179 let profile = settings.profiles.get(&profile_id);
180
181 let selected_profile = profile
182 .map(|profile| profile.name.clone())
183 .unwrap_or_else(|| "Unknown".into());
184
185 let icon = if self.picker_handle.is_deployed() {
186 IconName::ChevronUp
187 } else {
188 IconName::ChevronDown
189 };
190
191 let trigger_button = Button::new("profile-selector", selected_profile)
192 .disabled(self.disabled)
193 .label_size(LabelSize::Small)
194 .color(Color::Muted)
195 .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
196
197 let disabled = self.disabled;
198
199 let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = if disabled {
200 Box::new(Tooltip::text("Disabled until generation is done"))
201 } else {
202 Box::new(Tooltip::element({
203 move |_window, cx| {
204 let container = || h_flex().gap_1().justify_between();
205 v_flex()
206 .gap_1()
207 .child(
208 container()
209 .child(Label::new("Change Profile"))
210 .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
211 )
212 .child(
213 container()
214 .pt_1()
215 .border_t_1()
216 .border_color(cx.theme().colors().border_variant)
217 .child(Label::new("Cycle Through Profiles"))
218 .child(KeyBinding::for_action(&CycleModeSelector, cx)),
219 )
220 .into_any()
221 }
222 }))
223 };
224
225 PickerPopoverMenu::new(
226 picker,
227 trigger_button,
228 tooltip,
229 gpui::Corner::BottomRight,
230 cx,
231 )
232 .with_handle(self.picker_handle.clone())
233 .render(window, cx)
234 .into_any_element()
235 }
236}
237
238#[derive(Clone)]
239struct ProfileCandidate {
240 id: AgentProfileId,
241 name: SharedString,
242 is_builtin: bool,
243}
244
245#[derive(Clone)]
246struct ProfileMatchEntry {
247 candidate_index: usize,
248 positions: Vec<usize>,
249}
250
251enum ProfilePickerEntry {
252 Header(SharedString),
253 Profile(ProfileMatchEntry),
254}
255
256pub struct ProfilePickerDelegate {
257 fs: Arc<dyn Fs>,
258 provider: Arc<dyn ProfileProvider>,
259 foreground: ForegroundExecutor,
260 background: BackgroundExecutor,
261 candidates: Vec<ProfileCandidate>,
262 string_candidates: Arc<Vec<StringMatchCandidate>>,
263 filtered_entries: Vec<ProfilePickerEntry>,
264 selected_index: usize,
265 hovered_index: Option<usize>,
266 query: String,
267 cancel: Option<Arc<AtomicBool>>,
268 focus_handle: FocusHandle,
269}
270
271impl ProfilePickerDelegate {
272 fn new(
273 fs: Arc<dyn Fs>,
274 provider: Arc<dyn ProfileProvider>,
275 profiles: AvailableProfiles,
276 foreground: ForegroundExecutor,
277 background: BackgroundExecutor,
278 focus_handle: FocusHandle,
279 cx: &mut Context<ProfileSelector>,
280 ) -> Self {
281 let candidates = Self::candidates_from(profiles);
282 let string_candidates = Arc::new(Self::string_candidates(&candidates));
283 let filtered_entries = Self::entries_from_candidates(&candidates);
284
285 let mut this = Self {
286 fs,
287 provider,
288 foreground,
289 background,
290 candidates,
291 string_candidates,
292 filtered_entries,
293 selected_index: 0,
294 hovered_index: None,
295 query: String::new(),
296 cancel: None,
297 focus_handle,
298 };
299
300 this.selected_index = this
301 .index_of_profile(&this.provider.profile_id(cx))
302 .unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));
303
304 this
305 }
306
307 fn refresh_profiles(
308 &mut self,
309 profiles: AvailableProfiles,
310 query: String,
311 cx: &mut Context<Picker<Self>>,
312 ) {
313 self.candidates = Self::candidates_from(profiles);
314 self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));
315 self.query = query;
316
317 if self.query.is_empty() {
318 self.filtered_entries = Self::entries_from_candidates(&self.candidates);
319 } else {
320 let matches = self.search_blocking(&self.query);
321 self.filtered_entries = self.entries_from_matches(matches);
322 }
323
324 self.selected_index = self
325 .index_of_profile(&self.provider.profile_id(cx))
326 .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
327 cx.notify();
328 }
329
330 fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {
331 profiles
332 .into_iter()
333 .map(|(id, name)| ProfileCandidate {
334 is_builtin: builtin_profiles::is_builtin(&id),
335 id,
336 name,
337 })
338 .collect()
339 }
340
341 fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {
342 candidates
343 .iter()
344 .enumerate()
345 .map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))
346 .collect()
347 }
348
349 fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {
350 match candidate.id.as_str() {
351 builtin_profiles::WRITE => Some("Get help to write anything."),
352 builtin_profiles::ASK => Some("Chat about your codebase."),
353 builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
354 _ => None,
355 }
356 }
357
358 fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {
359 let mut entries = Vec::new();
360 let mut inserted_custom_header = false;
361
362 for (idx, candidate) in candidates.iter().enumerate() {
363 if !candidate.is_builtin && !inserted_custom_header {
364 if !entries.is_empty() {
365 entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));
366 }
367 inserted_custom_header = true;
368 }
369
370 entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
371 candidate_index: idx,
372 positions: Vec::new(),
373 }));
374 }
375
376 entries
377 }
378
379 fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {
380 let mut entries = Vec::new();
381 for mat in matches {
382 if self.candidates.get(mat.candidate_id).is_some() {
383 entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
384 candidate_index: mat.candidate_id,
385 positions: mat.positions,
386 }));
387 }
388 }
389 entries
390 }
391
392 fn first_selectable_index(&self) -> Option<usize> {
393 self.filtered_entries
394 .iter()
395 .position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))
396 }
397
398 fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {
399 self.filtered_entries.iter().position(|entry| {
400 matches!(entry, ProfilePickerEntry::Profile(profile) if self
401 .candidates
402 .get(profile.candidate_index)
403 .map(|candidate| &candidate.id == profile_id)
404 .unwrap_or(false))
405 })
406 }
407
408 fn search_blocking(&self, query: &str) -> Vec<StringMatch> {
409 if query.is_empty() {
410 return self
411 .string_candidates
412 .iter()
413 .map(|candidate| StringMatch {
414 candidate_id: candidate.id,
415 score: 0.0,
416 positions: Vec::new(),
417 string: candidate.string.clone(),
418 })
419 .collect();
420 }
421
422 let cancel_flag = AtomicBool::new(false);
423
424 self.foreground.block_on(match_strings(
425 self.string_candidates.as_ref(),
426 query,
427 false,
428 true,
429 100,
430 &cancel_flag,
431 self.background.clone(),
432 ))
433 }
434}
435
436impl PickerDelegate for ProfilePickerDelegate {
437 type ListItem = AnyElement;
438
439 fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {
440 "Search profiles…".into()
441 }
442
443 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
444 let text = if self.candidates.is_empty() {
445 "No profiles.".into()
446 } else {
447 "No profiles match your search.".into()
448 };
449 Some(text)
450 }
451
452 fn match_count(&self) -> usize {
453 self.filtered_entries.len()
454 }
455
456 fn selected_index(&self) -> usize {
457 self.selected_index
458 }
459
460 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
461 self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
462 cx.notify();
463 }
464
465 fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
466 match self.filtered_entries.get(ix) {
467 Some(ProfilePickerEntry::Profile(_)) => true,
468 Some(ProfilePickerEntry::Header(_)) | None => false,
469 }
470 }
471
472 fn update_matches(
473 &mut self,
474 query: String,
475 window: &mut Window,
476 cx: &mut Context<Picker<Self>>,
477 ) -> Task<()> {
478 if query.is_empty() {
479 self.query.clear();
480 self.filtered_entries = Self::entries_from_candidates(&self.candidates);
481 self.selected_index = self
482 .index_of_profile(&self.provider.profile_id(cx))
483 .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
484 cx.notify();
485 return Task::ready(());
486 }
487
488 if let Some(prev) = &self.cancel {
489 prev.store(true, Ordering::Relaxed);
490 }
491 let cancel = Arc::new(AtomicBool::new(false));
492 self.cancel = Some(cancel.clone());
493
494 let string_candidates = self.string_candidates.clone();
495 let background = self.background.clone();
496 let provider = self.provider.clone();
497 self.query = query.clone();
498
499 let cancel_for_future = cancel;
500
501 cx.spawn_in(window, async move |this, cx| {
502 let matches = match_strings(
503 string_candidates.as_ref(),
504 &query,
505 false,
506 true,
507 100,
508 cancel_for_future.as_ref(),
509 background,
510 )
511 .await;
512
513 this.update_in(cx, |this, _, cx| {
514 if this.delegate.query != query {
515 return;
516 }
517
518 this.delegate.filtered_entries = this.delegate.entries_from_matches(matches);
519 this.delegate.selected_index = this
520 .delegate
521 .index_of_profile(&provider.profile_id(cx))
522 .unwrap_or_else(|| this.delegate.first_selectable_index().unwrap_or(0));
523 cx.notify();
524 })
525 .ok();
526 })
527 }
528
529 fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
530 match self.filtered_entries.get(self.selected_index) {
531 Some(ProfilePickerEntry::Profile(entry)) => {
532 if let Some(candidate) = self.candidates.get(entry.candidate_index) {
533 let profile_id = candidate.id.clone();
534 let fs = self.fs.clone();
535 let provider = self.provider.clone();
536
537 update_settings_file(fs, cx, {
538 let profile_id = profile_id.clone();
539 move |settings, _cx| {
540 settings
541 .agent
542 .get_or_insert_default()
543 .set_profile(profile_id.0);
544 }
545 });
546
547 provider.set_profile(profile_id.clone(), cx);
548
549 telemetry::event!(
550 "agent_profile_switched",
551 profile_id = profile_id.as_str(),
552 source = "picker"
553 );
554 }
555
556 cx.emit(DismissEvent);
557 }
558 _ => {}
559 }
560 }
561
562 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
563 cx.defer_in(window, |picker, window, cx| {
564 picker.set_query("", window, cx);
565 });
566 cx.emit(DismissEvent);
567 }
568
569 fn render_match(
570 &self,
571 ix: usize,
572 selected: bool,
573 _: &mut Window,
574 cx: &mut Context<Picker<Self>>,
575 ) -> Option<Self::ListItem> {
576 match self.filtered_entries.get(ix)? {
577 ProfilePickerEntry::Header(label) => Some(
578 div()
579 .px_2p5()
580 .pb_0p5()
581 .when(ix > 0, |this| {
582 this.mt_1p5()
583 .pt_2()
584 .border_t_1()
585 .border_color(cx.theme().colors().border_variant)
586 })
587 .child(
588 Label::new(label.clone())
589 .size(LabelSize::XSmall)
590 .color(Color::Muted),
591 )
592 .into_any_element(),
593 ),
594 ProfilePickerEntry::Profile(entry) => {
595 let candidate = self.candidates.get(entry.candidate_index)?;
596 let active_id = self.provider.profile_id(cx);
597 let is_active = active_id == candidate.id;
598 let has_documentation = Self::documentation(candidate).is_some();
599
600 Some(
601 div()
602 .id(("profile-picker-item", ix))
603 .when(has_documentation, |this| {
604 this.on_hover(cx.listener(move |picker, hovered, _, cx| {
605 if *hovered {
606 picker.delegate.hovered_index = Some(ix);
607 } else if picker.delegate.hovered_index == Some(ix) {
608 picker.delegate.hovered_index = None;
609 }
610 cx.notify();
611 }))
612 })
613 .child(
614 ListItem::new(candidate.id.0.clone())
615 .inset(true)
616 .spacing(ListItemSpacing::Sparse)
617 .toggle_state(selected)
618 .child(HighlightedLabel::new(
619 candidate.name.clone(),
620 entry.positions.clone(),
621 ))
622 .when(is_active, |this| {
623 this.end_slot(
624 div()
625 .pr_2()
626 .child(Icon::new(IconName::Check).color(Color::Accent)),
627 )
628 }),
629 )
630 .into_any_element(),
631 )
632 }
633 }
634 }
635
636 fn documentation_aside(
637 &self,
638 _window: &mut Window,
639 cx: &mut Context<Picker<Self>>,
640 ) -> Option<DocumentationAside> {
641 use std::rc::Rc;
642
643 let hovered_index = self.hovered_index?;
644 let entry = match self.filtered_entries.get(hovered_index)? {
645 ProfilePickerEntry::Profile(entry) => entry,
646 ProfilePickerEntry::Header(_) => return None,
647 };
648
649 let candidate = self.candidates.get(entry.candidate_index)?;
650 let docs_aside = Self::documentation(candidate)?.to_string();
651
652 let settings = AgentSettings::get_global(cx);
653 let side = match settings.dock {
654 settings::DockPosition::Left => DocumentationSide::Right,
655 settings::DockPosition::Bottom | settings::DockPosition::Right => {
656 DocumentationSide::Left
657 }
658 };
659
660 Some(DocumentationAside {
661 side,
662 render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
663 })
664 }
665
666 fn documentation_aside_index(&self) -> Option<usize> {
667 self.hovered_index
668 }
669
670 fn render_footer(
671 &self,
672 _: &mut Window,
673 cx: &mut Context<Picker<Self>>,
674 ) -> Option<gpui::AnyElement> {
675 let focus_handle = self.focus_handle.clone();
676
677 Some(
678 h_flex()
679 .w_full()
680 .border_t_1()
681 .border_color(cx.theme().colors().border_variant)
682 .p_1p5()
683 .child(
684 Button::new("configure", "Configure")
685 .full_width()
686 .style(ButtonStyle::Outlined)
687 .key_binding(
688 KeyBinding::for_action_in(
689 &ManageProfiles::default(),
690 &focus_handle,
691 cx,
692 )
693 .map(|kb| kb.size(rems_from_px(12.))),
694 )
695 .on_click(|_, window, cx| {
696 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
697 }),
698 )
699 .into_any(),
700 )
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use fs::FakeFs;
708 use gpui::TestAppContext;
709
710 #[gpui::test]
711 fn entries_include_custom_profiles(_cx: &mut TestAppContext) {
712 let candidates = vec![
713 ProfileCandidate {
714 id: AgentProfileId("write".into()),
715 name: SharedString::from("Write"),
716 is_builtin: true,
717 },
718 ProfileCandidate {
719 id: AgentProfileId("my-custom".into()),
720 name: SharedString::from("My Custom"),
721 is_builtin: false,
722 },
723 ];
724
725 let entries = ProfilePickerDelegate::entries_from_candidates(&candidates);
726
727 assert!(entries.iter().any(|entry| matches!(
728 entry,
729 ProfilePickerEntry::Profile(profile)
730 if candidates[profile.candidate_index].id.as_str() == "my-custom"
731 )));
732 assert!(entries.iter().any(|entry| matches!(
733 entry,
734 ProfilePickerEntry::Header(label) if label.as_ref() == "Custom Profiles"
735 )));
736 }
737
738 #[gpui::test]
739 fn fuzzy_filter_returns_no_results_and_keeps_configure(cx: &mut TestAppContext) {
740 let candidates = vec![ProfileCandidate {
741 id: AgentProfileId("write".into()),
742 name: SharedString::from("Write"),
743 is_builtin: true,
744 }];
745
746 cx.update(|cx| {
747 let focus_handle = cx.focus_handle();
748
749 let delegate = ProfilePickerDelegate {
750 fs: FakeFs::new(cx.background_executor().clone()),
751 provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
752 foreground: cx.foreground_executor().clone(),
753 background: cx.background_executor().clone(),
754 candidates,
755 string_candidates: Arc::new(Vec::new()),
756 filtered_entries: Vec::new(),
757 selected_index: 0,
758 hovered_index: None,
759 query: String::new(),
760 cancel: None,
761 focus_handle,
762 };
763
764 let matches = Vec::new(); // No matches
765 let _entries = delegate.entries_from_matches(matches);
766 });
767 }
768
769 #[gpui::test]
770 fn active_profile_selection_logic_works(cx: &mut TestAppContext) {
771 let candidates = vec![
772 ProfileCandidate {
773 id: AgentProfileId("write".into()),
774 name: SharedString::from("Write"),
775 is_builtin: true,
776 },
777 ProfileCandidate {
778 id: AgentProfileId("ask".into()),
779 name: SharedString::from("Ask"),
780 is_builtin: true,
781 },
782 ];
783
784 cx.update(|cx| {
785 let focus_handle = cx.focus_handle();
786
787 let delegate = ProfilePickerDelegate {
788 fs: FakeFs::new(cx.background_executor().clone()),
789 provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
790 foreground: cx.foreground_executor().clone(),
791 background: cx.background_executor().clone(),
792 candidates,
793 string_candidates: Arc::new(Vec::new()),
794 hovered_index: None,
795 filtered_entries: vec![
796 ProfilePickerEntry::Profile(ProfileMatchEntry {
797 candidate_index: 0,
798 positions: Vec::new(),
799 }),
800 ProfilePickerEntry::Profile(ProfileMatchEntry {
801 candidate_index: 1,
802 positions: Vec::new(),
803 }),
804 ],
805 selected_index: 0,
806 query: String::new(),
807 cancel: None,
808 focus_handle,
809 };
810
811 // Active profile should be found at index 0
812 let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
813 assert_eq!(active_index, Some(0));
814 });
815 }
816
817 struct TestProfileProvider {
818 profile_id: AgentProfileId,
819 }
820
821 impl TestProfileProvider {
822 fn new(profile_id: AgentProfileId) -> Self {
823 Self { profile_id }
824 }
825 }
826
827 impl ProfileProvider for TestProfileProvider {
828 fn profile_id(&self, _cx: &App) -> AgentProfileId {
829 self.profile_id.clone()
830 }
831
832 fn set_profile(&self, _profile_id: AgentProfileId, _cx: &mut App) {}
833
834 fn profiles_supported(&self, _cx: &App) -> bool {
835 true
836 }
837 }
838}