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, TintColor, 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 .icon(icon)
196 .icon_size(IconSize::XSmall)
197 .icon_position(IconPosition::End)
198 .icon_color(Color::Muted)
199 .selected_style(ButtonStyle::Tinted(TintColor::Accent));
200
201 let disabled = self.disabled;
202
203 let tooltip: Box<dyn Fn(&mut Window, &mut App) -> AnyView> = if disabled {
204 Box::new(Tooltip::text("Disabled until generation is done"))
205 } else {
206 Box::new(Tooltip::element({
207 move |_window, cx| {
208 let container = || h_flex().gap_1().justify_between();
209 v_flex()
210 .gap_1()
211 .child(
212 container()
213 .child(Label::new("Change Profile"))
214 .child(KeyBinding::for_action(&ToggleProfileSelector, cx)),
215 )
216 .child(
217 container()
218 .pt_1()
219 .border_t_1()
220 .border_color(cx.theme().colors().border_variant)
221 .child(Label::new("Cycle Through Profiles"))
222 .child(KeyBinding::for_action(&CycleModeSelector, cx)),
223 )
224 .into_any()
225 }
226 }))
227 };
228
229 PickerPopoverMenu::new(
230 picker,
231 trigger_button,
232 tooltip,
233 gpui::Corner::BottomRight,
234 cx,
235 )
236 .with_handle(self.picker_handle.clone())
237 .render(window, cx)
238 .into_any_element()
239 }
240}
241
242#[derive(Clone)]
243struct ProfileCandidate {
244 id: AgentProfileId,
245 name: SharedString,
246 is_builtin: bool,
247}
248
249#[derive(Clone)]
250struct ProfileMatchEntry {
251 candidate_index: usize,
252 positions: Vec<usize>,
253}
254
255enum ProfilePickerEntry {
256 Header(SharedString),
257 Profile(ProfileMatchEntry),
258}
259
260pub struct ProfilePickerDelegate {
261 fs: Arc<dyn Fs>,
262 provider: Arc<dyn ProfileProvider>,
263 foreground: ForegroundExecutor,
264 background: BackgroundExecutor,
265 candidates: Vec<ProfileCandidate>,
266 string_candidates: Arc<Vec<StringMatchCandidate>>,
267 filtered_entries: Vec<ProfilePickerEntry>,
268 selected_index: usize,
269 hovered_index: Option<usize>,
270 query: String,
271 cancel: Option<Arc<AtomicBool>>,
272 focus_handle: FocusHandle,
273}
274
275impl ProfilePickerDelegate {
276 fn new(
277 fs: Arc<dyn Fs>,
278 provider: Arc<dyn ProfileProvider>,
279 profiles: AvailableProfiles,
280 foreground: ForegroundExecutor,
281 background: BackgroundExecutor,
282 focus_handle: FocusHandle,
283 cx: &mut Context<ProfileSelector>,
284 ) -> Self {
285 let candidates = Self::candidates_from(profiles);
286 let string_candidates = Arc::new(Self::string_candidates(&candidates));
287 let filtered_entries = Self::entries_from_candidates(&candidates);
288
289 let mut this = Self {
290 fs,
291 provider,
292 foreground,
293 background,
294 candidates,
295 string_candidates,
296 filtered_entries,
297 selected_index: 0,
298 hovered_index: None,
299 query: String::new(),
300 cancel: None,
301 focus_handle,
302 };
303
304 this.selected_index = this
305 .index_of_profile(&this.provider.profile_id(cx))
306 .unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));
307
308 this
309 }
310
311 fn refresh_profiles(
312 &mut self,
313 profiles: AvailableProfiles,
314 query: String,
315 cx: &mut Context<Picker<Self>>,
316 ) {
317 self.candidates = Self::candidates_from(profiles);
318 self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));
319 self.query = query;
320
321 if self.query.is_empty() {
322 self.filtered_entries = Self::entries_from_candidates(&self.candidates);
323 } else {
324 let matches = self.search_blocking(&self.query);
325 self.filtered_entries = self.entries_from_matches(matches);
326 }
327
328 self.selected_index = self
329 .index_of_profile(&self.provider.profile_id(cx))
330 .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
331 cx.notify();
332 }
333
334 fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {
335 profiles
336 .into_iter()
337 .map(|(id, name)| ProfileCandidate {
338 is_builtin: builtin_profiles::is_builtin(&id),
339 id,
340 name,
341 })
342 .collect()
343 }
344
345 fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {
346 candidates
347 .iter()
348 .enumerate()
349 .map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))
350 .collect()
351 }
352
353 fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {
354 match candidate.id.as_str() {
355 builtin_profiles::WRITE => Some("Get help to write anything."),
356 builtin_profiles::ASK => Some("Chat about your codebase."),
357 builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
358 _ => None,
359 }
360 }
361
362 fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {
363 let mut entries = Vec::new();
364 let mut inserted_custom_header = false;
365
366 for (idx, candidate) in candidates.iter().enumerate() {
367 if !candidate.is_builtin && !inserted_custom_header {
368 if !entries.is_empty() {
369 entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));
370 }
371 inserted_custom_header = true;
372 }
373
374 entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
375 candidate_index: idx,
376 positions: Vec::new(),
377 }));
378 }
379
380 entries
381 }
382
383 fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {
384 let mut entries = Vec::new();
385 for mat in matches {
386 if self.candidates.get(mat.candidate_id).is_some() {
387 entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
388 candidate_index: mat.candidate_id,
389 positions: mat.positions,
390 }));
391 }
392 }
393 entries
394 }
395
396 fn first_selectable_index(&self) -> Option<usize> {
397 self.filtered_entries
398 .iter()
399 .position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))
400 }
401
402 fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {
403 self.filtered_entries.iter().position(|entry| {
404 matches!(entry, ProfilePickerEntry::Profile(profile) if self
405 .candidates
406 .get(profile.candidate_index)
407 .map(|candidate| &candidate.id == profile_id)
408 .unwrap_or(false))
409 })
410 }
411
412 fn search_blocking(&self, query: &str) -> Vec<StringMatch> {
413 if query.is_empty() {
414 return self
415 .string_candidates
416 .iter()
417 .map(|candidate| StringMatch {
418 candidate_id: candidate.id,
419 score: 0.0,
420 positions: Vec::new(),
421 string: candidate.string.clone(),
422 })
423 .collect();
424 }
425
426 let cancel_flag = AtomicBool::new(false);
427
428 self.foreground.block_on(match_strings(
429 self.string_candidates.as_ref(),
430 query,
431 false,
432 true,
433 100,
434 &cancel_flag,
435 self.background.clone(),
436 ))
437 }
438}
439
440impl PickerDelegate for ProfilePickerDelegate {
441 type ListItem = AnyElement;
442
443 fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {
444 "Search profiles…".into()
445 }
446
447 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
448 let text = if self.candidates.is_empty() {
449 "No profiles.".into()
450 } else {
451 "No profiles match your search.".into()
452 };
453 Some(text)
454 }
455
456 fn match_count(&self) -> usize {
457 self.filtered_entries.len()
458 }
459
460 fn selected_index(&self) -> usize {
461 self.selected_index
462 }
463
464 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
465 self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
466 cx.notify();
467 }
468
469 fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
470 match self.filtered_entries.get(ix) {
471 Some(ProfilePickerEntry::Profile(_)) => true,
472 Some(ProfilePickerEntry::Header(_)) | None => false,
473 }
474 }
475
476 fn update_matches(
477 &mut self,
478 query: String,
479 window: &mut Window,
480 cx: &mut Context<Picker<Self>>,
481 ) -> Task<()> {
482 if query.is_empty() {
483 self.query.clear();
484 self.filtered_entries = Self::entries_from_candidates(&self.candidates);
485 self.selected_index = self
486 .index_of_profile(&self.provider.profile_id(cx))
487 .unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
488 cx.notify();
489 return Task::ready(());
490 }
491
492 if let Some(prev) = &self.cancel {
493 prev.store(true, Ordering::Relaxed);
494 }
495 let cancel = Arc::new(AtomicBool::new(false));
496 self.cancel = Some(cancel.clone());
497
498 let string_candidates = self.string_candidates.clone();
499 let background = self.background.clone();
500 let provider = self.provider.clone();
501 self.query = query.clone();
502
503 let cancel_for_future = cancel;
504
505 cx.spawn_in(window, async move |this, cx| {
506 let matches = match_strings(
507 string_candidates.as_ref(),
508 &query,
509 false,
510 true,
511 100,
512 cancel_for_future.as_ref(),
513 background,
514 )
515 .await;
516
517 this.update_in(cx, |this, _, cx| {
518 if this.delegate.query != query {
519 return;
520 }
521
522 this.delegate.filtered_entries = this.delegate.entries_from_matches(matches);
523 this.delegate.selected_index = this
524 .delegate
525 .index_of_profile(&provider.profile_id(cx))
526 .unwrap_or_else(|| this.delegate.first_selectable_index().unwrap_or(0));
527 cx.notify();
528 })
529 .ok();
530 })
531 }
532
533 fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
534 match self.filtered_entries.get(self.selected_index) {
535 Some(ProfilePickerEntry::Profile(entry)) => {
536 if let Some(candidate) = self.candidates.get(entry.candidate_index) {
537 let profile_id = candidate.id.clone();
538 let fs = self.fs.clone();
539 let provider = self.provider.clone();
540
541 update_settings_file(fs, cx, {
542 let profile_id = profile_id.clone();
543 move |settings, _cx| {
544 settings
545 .agent
546 .get_or_insert_default()
547 .set_profile(profile_id.0);
548 }
549 });
550
551 provider.set_profile(profile_id.clone(), cx);
552
553 telemetry::event!(
554 "agent_profile_switched",
555 profile_id = profile_id.as_str(),
556 source = "picker"
557 );
558 }
559
560 cx.emit(DismissEvent);
561 }
562 _ => {}
563 }
564 }
565
566 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
567 cx.defer_in(window, |picker, window, cx| {
568 picker.set_query("", window, cx);
569 });
570 cx.emit(DismissEvent);
571 }
572
573 fn render_match(
574 &self,
575 ix: usize,
576 selected: bool,
577 _: &mut Window,
578 cx: &mut Context<Picker<Self>>,
579 ) -> Option<Self::ListItem> {
580 match self.filtered_entries.get(ix)? {
581 ProfilePickerEntry::Header(label) => Some(
582 div()
583 .px_2p5()
584 .pb_0p5()
585 .when(ix > 0, |this| {
586 this.mt_1p5()
587 .pt_2()
588 .border_t_1()
589 .border_color(cx.theme().colors().border_variant)
590 })
591 .child(
592 Label::new(label.clone())
593 .size(LabelSize::XSmall)
594 .color(Color::Muted),
595 )
596 .into_any_element(),
597 ),
598 ProfilePickerEntry::Profile(entry) => {
599 let candidate = self.candidates.get(entry.candidate_index)?;
600 let active_id = self.provider.profile_id(cx);
601 let is_active = active_id == candidate.id;
602 let has_documentation = Self::documentation(candidate).is_some();
603
604 Some(
605 div()
606 .id(("profile-picker-item", ix))
607 .when(has_documentation, |this| {
608 this.on_hover(cx.listener(move |picker, hovered, _, cx| {
609 if *hovered {
610 picker.delegate.hovered_index = Some(ix);
611 } else if picker.delegate.hovered_index == Some(ix) {
612 picker.delegate.hovered_index = None;
613 }
614 cx.notify();
615 }))
616 })
617 .child(
618 ListItem::new(candidate.id.0.clone())
619 .inset(true)
620 .spacing(ListItemSpacing::Sparse)
621 .toggle_state(selected)
622 .child(HighlightedLabel::new(
623 candidate.name.clone(),
624 entry.positions.clone(),
625 ))
626 .when(is_active, |this| {
627 this.end_slot(
628 div()
629 .pr_2()
630 .child(Icon::new(IconName::Check).color(Color::Accent)),
631 )
632 }),
633 )
634 .into_any_element(),
635 )
636 }
637 }
638 }
639
640 fn documentation_aside(
641 &self,
642 _window: &mut Window,
643 cx: &mut Context<Picker<Self>>,
644 ) -> Option<DocumentationAside> {
645 use std::rc::Rc;
646
647 let hovered_index = self.hovered_index?;
648 let entry = match self.filtered_entries.get(hovered_index)? {
649 ProfilePickerEntry::Profile(entry) => entry,
650 ProfilePickerEntry::Header(_) => return None,
651 };
652
653 let candidate = self.candidates.get(entry.candidate_index)?;
654 let docs_aside = Self::documentation(candidate)?.to_string();
655
656 let settings = AgentSettings::get_global(cx);
657 let side = match settings.dock {
658 settings::DockPosition::Left => DocumentationSide::Right,
659 settings::DockPosition::Bottom | settings::DockPosition::Right => {
660 DocumentationSide::Left
661 }
662 };
663
664 Some(DocumentationAside {
665 side,
666 render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
667 })
668 }
669
670 fn documentation_aside_index(&self) -> Option<usize> {
671 self.hovered_index
672 }
673
674 fn render_footer(
675 &self,
676 _: &mut Window,
677 cx: &mut Context<Picker<Self>>,
678 ) -> Option<gpui::AnyElement> {
679 let focus_handle = self.focus_handle.clone();
680
681 Some(
682 h_flex()
683 .w_full()
684 .border_t_1()
685 .border_color(cx.theme().colors().border_variant)
686 .p_1p5()
687 .child(
688 Button::new("configure", "Configure")
689 .full_width()
690 .style(ButtonStyle::Outlined)
691 .key_binding(
692 KeyBinding::for_action_in(
693 &ManageProfiles::default(),
694 &focus_handle,
695 cx,
696 )
697 .map(|kb| kb.size(rems_from_px(12.))),
698 )
699 .on_click(|_, window, cx| {
700 window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
701 }),
702 )
703 .into_any(),
704 )
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use fs::FakeFs;
712 use gpui::TestAppContext;
713
714 #[gpui::test]
715 fn entries_include_custom_profiles(_cx: &mut TestAppContext) {
716 let candidates = vec![
717 ProfileCandidate {
718 id: AgentProfileId("write".into()),
719 name: SharedString::from("Write"),
720 is_builtin: true,
721 },
722 ProfileCandidate {
723 id: AgentProfileId("my-custom".into()),
724 name: SharedString::from("My Custom"),
725 is_builtin: false,
726 },
727 ];
728
729 let entries = ProfilePickerDelegate::entries_from_candidates(&candidates);
730
731 assert!(entries.iter().any(|entry| matches!(
732 entry,
733 ProfilePickerEntry::Profile(profile)
734 if candidates[profile.candidate_index].id.as_str() == "my-custom"
735 )));
736 assert!(entries.iter().any(|entry| matches!(
737 entry,
738 ProfilePickerEntry::Header(label) if label.as_ref() == "Custom Profiles"
739 )));
740 }
741
742 #[gpui::test]
743 fn fuzzy_filter_returns_no_results_and_keeps_configure(cx: &mut TestAppContext) {
744 let candidates = vec![ProfileCandidate {
745 id: AgentProfileId("write".into()),
746 name: SharedString::from("Write"),
747 is_builtin: true,
748 }];
749
750 cx.update(|cx| {
751 let focus_handle = cx.focus_handle();
752
753 let delegate = ProfilePickerDelegate {
754 fs: FakeFs::new(cx.background_executor().clone()),
755 provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
756 foreground: cx.foreground_executor().clone(),
757 background: cx.background_executor().clone(),
758 candidates,
759 string_candidates: Arc::new(Vec::new()),
760 filtered_entries: Vec::new(),
761 selected_index: 0,
762 hovered_index: None,
763 query: String::new(),
764 cancel: None,
765 focus_handle,
766 };
767
768 let matches = Vec::new(); // No matches
769 let _entries = delegate.entries_from_matches(matches);
770 });
771 }
772
773 #[gpui::test]
774 fn active_profile_selection_logic_works(cx: &mut TestAppContext) {
775 let candidates = vec![
776 ProfileCandidate {
777 id: AgentProfileId("write".into()),
778 name: SharedString::from("Write"),
779 is_builtin: true,
780 },
781 ProfileCandidate {
782 id: AgentProfileId("ask".into()),
783 name: SharedString::from("Ask"),
784 is_builtin: true,
785 },
786 ];
787
788 cx.update(|cx| {
789 let focus_handle = cx.focus_handle();
790
791 let delegate = ProfilePickerDelegate {
792 fs: FakeFs::new(cx.background_executor().clone()),
793 provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
794 foreground: cx.foreground_executor().clone(),
795 background: cx.background_executor().clone(),
796 candidates,
797 string_candidates: Arc::new(Vec::new()),
798 hovered_index: None,
799 filtered_entries: vec![
800 ProfilePickerEntry::Profile(ProfileMatchEntry {
801 candidate_index: 0,
802 positions: Vec::new(),
803 }),
804 ProfilePickerEntry::Profile(ProfileMatchEntry {
805 candidate_index: 1,
806 positions: Vec::new(),
807 }),
808 ],
809 selected_index: 0,
810 query: String::new(),
811 cancel: None,
812 focus_handle,
813 };
814
815 // Active profile should be found at index 0
816 let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
817 assert_eq!(active_index, Some(0));
818 });
819 }
820
821 struct TestProfileProvider {
822 profile_id: AgentProfileId,
823 }
824
825 impl TestProfileProvider {
826 fn new(profile_id: AgentProfileId) -> Self {
827 Self { profile_id }
828 }
829 }
830
831 impl ProfileProvider for TestProfileProvider {
832 fn profile_id(&self, _cx: &App) -> AgentProfileId {
833 self.profile_id.clone()
834 }
835
836 fn set_profile(&self, _profile_id: AgentProfileId, _cx: &mut App) {}
837
838 fn profiles_supported(&self, _cx: &App) -> bool {
839 true
840 }
841 }
842}