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