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