1mod profile_modal_header;
2
3use std::sync::Arc;
4
5use agent::ContextServerRegistry;
6use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
7use editor::Editor;
8use fs::Fs;
9use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
10use language_model::{LanguageModel, LanguageModelRegistry};
11use settings::SettingsStore;
12use settings::{
13 LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
14};
15use ui::{
16 KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
17};
18use workspace::{ModalView, Workspace};
19
20use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
21use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
22use crate::language_model_selector::{LanguageModelSelector, language_model_selector};
23use crate::{AgentPanel, ManageProfiles};
24
25enum Mode {
26 ChooseProfile(ChooseProfileMode),
27 NewProfile(NewProfileMode),
28 ViewProfile(ViewProfileMode),
29 ConfigureTools {
30 profile_id: AgentProfileId,
31 tool_picker: Entity<ToolPicker>,
32 _subscription: Subscription,
33 },
34 ConfigureMcps {
35 profile_id: AgentProfileId,
36 tool_picker: Entity<ToolPicker>,
37 _subscription: Subscription,
38 },
39 ConfigureDefaultModel {
40 profile_id: AgentProfileId,
41 model_picker: Entity<LanguageModelSelector>,
42 _subscription: Subscription,
43 },
44}
45
46impl Mode {
47 pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
48 let settings = AgentSettings::get_global(cx);
49
50 let mut builtin_profiles = Vec::new();
51 let mut custom_profiles = Vec::new();
52
53 for (profile_id, profile) in settings.profiles.iter() {
54 let entry = ProfileEntry {
55 id: profile_id.clone(),
56 name: profile.name.clone(),
57 navigation: NavigableEntry::focusable(cx),
58 };
59 if builtin_profiles::is_builtin(profile_id) {
60 builtin_profiles.push(entry);
61 } else {
62 custom_profiles.push(entry);
63 }
64 }
65
66 builtin_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
67 custom_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
68
69 Self::ChooseProfile(ChooseProfileMode {
70 builtin_profiles,
71 custom_profiles,
72 add_new_profile: NavigableEntry::focusable(cx),
73 })
74 }
75}
76
77#[derive(Clone)]
78struct ProfileEntry {
79 pub id: AgentProfileId,
80 pub name: SharedString,
81 pub navigation: NavigableEntry,
82}
83
84#[derive(Clone)]
85pub struct ChooseProfileMode {
86 builtin_profiles: Vec<ProfileEntry>,
87 custom_profiles: Vec<ProfileEntry>,
88 add_new_profile: NavigableEntry,
89}
90
91#[derive(Clone)]
92pub struct ViewProfileMode {
93 profile_id: AgentProfileId,
94 fork_profile: NavigableEntry,
95 configure_default_model: NavigableEntry,
96 configure_tools: NavigableEntry,
97 configure_mcps: NavigableEntry,
98 delete_profile: NavigableEntry,
99 cancel_item: NavigableEntry,
100}
101
102#[derive(Clone)]
103pub struct NewProfileMode {
104 name_editor: Entity<Editor>,
105 base_profile_id: Option<AgentProfileId>,
106}
107
108pub struct ManageProfilesModal {
109 fs: Arc<dyn Fs>,
110 context_server_registry: Entity<ContextServerRegistry>,
111 active_model: Option<Arc<dyn LanguageModel>>,
112 focus_handle: FocusHandle,
113 mode: Mode,
114 _settings_subscription: Subscription,
115}
116
117impl ManageProfilesModal {
118 pub fn register(
119 workspace: &mut Workspace,
120 _window: Option<&mut Window>,
121 _cx: &mut Context<Workspace>,
122 ) {
123 workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
124 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
125 let fs = workspace.app_state().fs.clone();
126 let active_model = panel
127 .read(cx)
128 .active_native_agent_thread(cx)
129 .and_then(|thread| thread.read(cx).model().cloned());
130
131 let context_server_registry = panel.read(cx).context_server_registry().clone();
132 workspace.toggle_modal(window, cx, |window, cx| {
133 let mut this = Self::new(fs, active_model, context_server_registry, window, cx);
134
135 if let Some(profile_id) = action.customize_tools.clone() {
136 this.configure_builtin_tools(profile_id, window, cx);
137 }
138
139 this
140 })
141 }
142 });
143 }
144
145 pub fn new(
146 fs: Arc<dyn Fs>,
147 active_model: Option<Arc<dyn LanguageModel>>,
148 context_server_registry: Entity<ContextServerRegistry>,
149 window: &mut Window,
150 cx: &mut Context<Self>,
151 ) -> Self {
152 let focus_handle = cx.focus_handle();
153
154 // Keep this modal in sync with settings changes (including profile deletion).
155 let settings_subscription =
156 cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
157 if matches!(this.mode, Mode::ChooseProfile(_)) {
158 this.mode = Mode::choose_profile(window, cx);
159 this.focus_handle(cx).focus(window, cx);
160 cx.notify();
161 }
162 });
163
164 Self {
165 fs,
166 active_model,
167 context_server_registry,
168 focus_handle,
169 mode: Mode::choose_profile(window, cx),
170 _settings_subscription: settings_subscription,
171 }
172 }
173
174 fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
175 self.mode = Mode::choose_profile(window, cx);
176 self.focus_handle(cx).focus(window, cx);
177 }
178
179 fn new_profile(
180 &mut self,
181 base_profile_id: Option<AgentProfileId>,
182 window: &mut Window,
183 cx: &mut Context<Self>,
184 ) {
185 let name_editor = cx.new(|cx| Editor::single_line(window, cx));
186 name_editor.update(cx, |editor, cx| {
187 editor.set_placeholder_text("Profile name", window, cx);
188 });
189
190 self.mode = Mode::NewProfile(NewProfileMode {
191 name_editor,
192 base_profile_id,
193 });
194 self.focus_handle(cx).focus(window, cx);
195 }
196
197 pub fn view_profile(
198 &mut self,
199 profile_id: AgentProfileId,
200 window: &mut Window,
201 cx: &mut Context<Self>,
202 ) {
203 self.mode = Mode::ViewProfile(ViewProfileMode {
204 profile_id,
205 fork_profile: NavigableEntry::focusable(cx),
206 configure_default_model: NavigableEntry::focusable(cx),
207 configure_tools: NavigableEntry::focusable(cx),
208 configure_mcps: NavigableEntry::focusable(cx),
209 delete_profile: NavigableEntry::focusable(cx),
210 cancel_item: NavigableEntry::focusable(cx),
211 });
212 self.focus_handle(cx).focus(window, cx);
213 }
214
215 fn configure_default_model(
216 &mut self,
217 profile_id: AgentProfileId,
218 window: &mut Window,
219 cx: &mut Context<Self>,
220 ) {
221 let fs = self.fs.clone();
222 let profile_id_for_closure = profile_id.clone();
223
224 let model_picker = cx.new(|cx| {
225 let profile_id = profile_id_for_closure.clone();
226
227 language_model_selector(
228 {
229 let profile_id = profile_id.clone();
230 move |cx| {
231 let settings = AgentSettings::get_global(cx);
232
233 settings
234 .profiles
235 .get(&profile_id)
236 .and_then(|profile| profile.default_model.as_ref())
237 .and_then(|selection| {
238 let registry = LanguageModelRegistry::read_global(cx);
239 let provider_id = language_model::LanguageModelProviderId(
240 gpui::SharedString::from(selection.provider.0.clone()),
241 );
242 let provider = registry.provider(&provider_id)?;
243 let model = provider
244 .provided_models(cx)
245 .iter()
246 .find(|m| m.id().0 == selection.model.as_str())?
247 .clone();
248 Some(language_model::ConfiguredModel { provider, model })
249 })
250 }
251 },
252 {
253 let fs = fs.clone();
254 move |model, cx| {
255 let provider = model.provider_id().0.to_string();
256 let model_id = model.id().0.to_string();
257 let profile_id = profile_id.clone();
258
259 update_settings_file(fs.clone(), cx, move |settings, _cx| {
260 let agent_settings = settings.agent.get_or_insert_default();
261 if let Some(profiles) = agent_settings.profiles.as_mut() {
262 if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
263 profile.default_model = Some(LanguageModelSelection {
264 provider: LanguageModelProviderSetting(provider.clone()),
265 model: model_id.clone(),
266 });
267 }
268 }
269 });
270 }
271 },
272 {
273 let fs = fs.clone();
274 move |model, should_be_favorite, cx| {
275 crate::favorite_models::toggle_in_settings(
276 model,
277 should_be_favorite,
278 fs.clone(),
279 cx,
280 );
281 }
282 },
283 false, // Do not use popover styles for the model picker
284 self.focus_handle.clone(),
285 window,
286 cx,
287 )
288 .modal(false)
289 });
290
291 let dismiss_subscription = cx.subscribe_in(&model_picker, window, {
292 let profile_id = profile_id.clone();
293 move |this, _picker, _: &DismissEvent, window, cx| {
294 this.view_profile(profile_id.clone(), window, cx);
295 }
296 });
297
298 self.mode = Mode::ConfigureDefaultModel {
299 profile_id,
300 model_picker,
301 _subscription: dismiss_subscription,
302 };
303 self.focus_handle(cx).focus(window, cx);
304 }
305
306 fn configure_mcp_tools(
307 &mut self,
308 profile_id: AgentProfileId,
309 window: &mut Window,
310 cx: &mut Context<Self>,
311 ) {
312 let settings = AgentSettings::get_global(cx);
313 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
314 return;
315 };
316
317 let tool_picker = cx.new(|cx| {
318 let delegate = ToolPickerDelegate::mcp_tools(
319 &self.context_server_registry,
320 self.fs.clone(),
321 profile_id.clone(),
322 profile,
323 cx,
324 );
325 ToolPicker::mcp_tools(delegate, window, cx)
326 });
327 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
328 let profile_id = profile_id.clone();
329 move |this, _tool_picker, _: &DismissEvent, window, cx| {
330 this.view_profile(profile_id.clone(), window, cx);
331 }
332 });
333
334 self.mode = Mode::ConfigureMcps {
335 profile_id,
336 tool_picker,
337 _subscription: dismiss_subscription,
338 };
339 self.focus_handle(cx).focus(window, cx);
340 }
341
342 fn configure_builtin_tools(
343 &mut self,
344 profile_id: AgentProfileId,
345 window: &mut Window,
346 cx: &mut Context<Self>,
347 ) {
348 let settings = AgentSettings::get_global(cx);
349 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
350 return;
351 };
352
353 let tool_picker = cx.new(|cx| {
354 let delegate = ToolPickerDelegate::builtin_tools(
355 //todo: This causes the web search tool to show up even it only works when using zed hosted models
356 agent::supported_built_in_tool_names(
357 self.active_model.as_ref().map(|model| model.provider_id()),
358 )
359 .map(|s| s.into())
360 .collect::<Vec<_>>(),
361 self.fs.clone(),
362 profile_id.clone(),
363 profile,
364 cx,
365 );
366 ToolPicker::builtin_tools(delegate, window, cx)
367 });
368 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
369 let profile_id = profile_id.clone();
370 move |this, _tool_picker, _: &DismissEvent, window, cx| {
371 this.view_profile(profile_id.clone(), window, cx);
372 }
373 });
374
375 self.mode = Mode::ConfigureTools {
376 profile_id,
377 tool_picker,
378 _subscription: dismiss_subscription,
379 };
380 self.focus_handle(cx).focus(window, cx);
381 }
382
383 fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
384 match &self.mode {
385 Mode::ChooseProfile { .. } => {}
386 Mode::NewProfile(mode) => {
387 let name = mode.name_editor.read(cx).text(cx);
388
389 let profile_id =
390 AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
391 self.view_profile(profile_id, window, cx);
392 }
393 Mode::ViewProfile(_) => {}
394 Mode::ConfigureTools { .. } => {}
395 Mode::ConfigureMcps { .. } => {}
396 Mode::ConfigureDefaultModel { .. } => {}
397 }
398 }
399
400 fn delete_profile(
401 &mut self,
402 profile_id: AgentProfileId,
403 window: &mut Window,
404 cx: &mut Context<Self>,
405 ) {
406 if builtin_profiles::is_builtin(&profile_id) {
407 self.view_profile(profile_id, window, cx);
408 return;
409 }
410
411 let fs = self.fs.clone();
412
413 update_settings_file(fs, cx, move |settings, _cx| {
414 let Some(agent_settings) = settings.agent.as_mut() else {
415 return;
416 };
417
418 let Some(profiles) = agent_settings.profiles.as_mut() else {
419 return;
420 };
421
422 profiles.shift_remove(profile_id.0.as_ref());
423
424 if agent_settings
425 .default_profile
426 .as_deref()
427 .is_some_and(|default_profile| default_profile == profile_id.0.as_ref())
428 {
429 agent_settings.default_profile = Some(AgentProfileId::default().0);
430 }
431 });
432
433 self.choose_profile(window, cx);
434 }
435
436 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
437 match &self.mode {
438 Mode::ChooseProfile { .. } => {
439 cx.emit(DismissEvent);
440 }
441 Mode::NewProfile(mode) => {
442 if let Some(profile_id) = mode.base_profile_id.clone() {
443 self.view_profile(profile_id, window, cx);
444 } else {
445 self.choose_profile(window, cx);
446 }
447 }
448 Mode::ViewProfile(_) => self.choose_profile(window, cx),
449 Mode::ConfigureTools { profile_id, .. } => {
450 self.view_profile(profile_id.clone(), window, cx)
451 }
452 Mode::ConfigureMcps { profile_id, .. } => {
453 self.view_profile(profile_id.clone(), window, cx)
454 }
455 Mode::ConfigureDefaultModel { profile_id, .. } => {
456 self.view_profile(profile_id.clone(), window, cx)
457 }
458 }
459 }
460}
461
462impl ModalView for ManageProfilesModal {}
463
464impl Focusable for ManageProfilesModal {
465 fn focus_handle(&self, cx: &App) -> FocusHandle {
466 match &self.mode {
467 Mode::ChooseProfile(_) => self.focus_handle.clone(),
468 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
469 Mode::ViewProfile(_) => self.focus_handle.clone(),
470 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
471 Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
472 Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
473 }
474 }
475}
476
477impl EventEmitter<DismissEvent> for ManageProfilesModal {}
478
479impl ManageProfilesModal {
480 fn render_profile(
481 &self,
482 profile: &ProfileEntry,
483 window: &mut Window,
484 cx: &mut Context<Self>,
485 ) -> impl IntoElement + use<> {
486 let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
487
488 div()
489 .id(format!("profile-{}", profile.id))
490 .track_focus(&profile.navigation.focus_handle)
491 .on_action({
492 let profile_id = profile.id.clone();
493 cx.listener(move |this, _: &menu::Confirm, window, cx| {
494 this.view_profile(profile_id.clone(), window, cx);
495 })
496 })
497 .child(
498 ListItem::new(format!("profile-{}", profile.id))
499 .toggle_state(is_focused)
500 .inset(true)
501 .spacing(ListItemSpacing::Sparse)
502 .child(Label::new(profile.name.clone()))
503 .when(is_focused, |this| {
504 this.end_slot(
505 h_flex()
506 .gap_1()
507 .child(
508 Label::new("Customize")
509 .size(LabelSize::Small)
510 .color(Color::Muted),
511 )
512 .child(KeyBinding::for_action_in(
513 &menu::Confirm,
514 &self.focus_handle,
515 cx,
516 )),
517 )
518 })
519 .on_click({
520 let profile_id = profile.id.clone();
521 cx.listener(move |this, _, window, cx| {
522 this.view_profile(profile_id.clone(), window, cx);
523 })
524 }),
525 )
526 }
527
528 fn render_choose_profile(
529 &mut self,
530 mode: ChooseProfileMode,
531 window: &mut Window,
532 cx: &mut Context<Self>,
533 ) -> impl IntoElement {
534 Navigable::new(
535 div()
536 .track_focus(&self.focus_handle(cx))
537 .size_full()
538 .child(ProfileModalHeader::new("Agent Profiles", None))
539 .child(
540 v_flex()
541 .pb_1()
542 .child(ListSeparator)
543 .children(
544 mode.builtin_profiles
545 .iter()
546 .map(|profile| self.render_profile(profile, window, cx)),
547 )
548 .when(!mode.custom_profiles.is_empty(), |this| {
549 this.child(ListSeparator)
550 .child(
551 div().pl_2().pb_1().child(
552 Label::new("Custom Profiles")
553 .size(LabelSize::Small)
554 .color(Color::Muted),
555 ),
556 )
557 .children(
558 mode.custom_profiles
559 .iter()
560 .map(|profile| self.render_profile(profile, window, cx)),
561 )
562 })
563 .child(ListSeparator)
564 .child(
565 div()
566 .id("new-profile")
567 .track_focus(&mode.add_new_profile.focus_handle)
568 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
569 this.new_profile(None, window, cx);
570 }))
571 .child(
572 ListItem::new("new-profile")
573 .toggle_state(
574 mode.add_new_profile
575 .focus_handle
576 .contains_focused(window, cx),
577 )
578 .inset(true)
579 .spacing(ListItemSpacing::Sparse)
580 .start_slot(Icon::new(IconName::Plus))
581 .child(Label::new("Add New Profile"))
582 .on_click({
583 cx.listener(move |this, _, window, cx| {
584 this.new_profile(None, window, cx);
585 })
586 }),
587 ),
588 ),
589 )
590 .into_any_element(),
591 )
592 .map(|mut navigable| {
593 for profile in mode.builtin_profiles {
594 navigable = navigable.entry(profile.navigation);
595 }
596 for profile in mode.custom_profiles {
597 navigable = navigable.entry(profile.navigation);
598 }
599
600 navigable
601 })
602 .entry(mode.add_new_profile)
603 }
604
605 fn render_new_profile(
606 &mut self,
607 mode: NewProfileMode,
608 _window: &mut Window,
609 cx: &mut Context<Self>,
610 ) -> impl IntoElement {
611 let settings = AgentSettings::get_global(cx);
612
613 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
614 settings
615 .profiles
616 .get(base_profile_id)
617 .map(|profile| profile.name.clone())
618 .unwrap_or_else(|| "Unknown".into())
619 });
620
621 v_flex()
622 .id("new-profile")
623 .track_focus(&self.focus_handle(cx))
624 .child(ProfileModalHeader::new(
625 match &base_profile_name {
626 Some(base_profile) => format!("Fork {base_profile}"),
627 None => "New Profile".into(),
628 },
629 match base_profile_name {
630 Some(_) => Some(IconName::Scissors),
631 None => Some(IconName::Plus),
632 },
633 ))
634 .child(ListSeparator)
635 .child(h_flex().p_2().child(mode.name_editor))
636 }
637
638 fn render_view_profile(
639 &mut self,
640 mode: ViewProfileMode,
641 window: &mut Window,
642 cx: &mut Context<Self>,
643 ) -> impl IntoElement {
644 let settings = AgentSettings::get_global(cx);
645
646 let profile_name = settings
647 .profiles
648 .get(&mode.profile_id)
649 .map(|profile| profile.name.clone())
650 .unwrap_or_else(|| "Unknown".into());
651
652 let icon = match mode.profile_id.as_str() {
653 "write" => IconName::Pencil,
654 "ask" => IconName::Chat,
655 _ => IconName::UserRoundPen,
656 };
657
658 Navigable::new(
659 div()
660 .track_focus(&self.focus_handle(cx))
661 .size_full()
662 .child(ProfileModalHeader::new(profile_name, Some(icon)))
663 .child(
664 v_flex()
665 .pb_1()
666 .child(ListSeparator)
667 .child(
668 div()
669 .id("fork-profile")
670 .track_focus(&mode.fork_profile.focus_handle)
671 .on_action({
672 let profile_id = mode.profile_id.clone();
673 cx.listener(move |this, _: &menu::Confirm, window, cx| {
674 this.new_profile(Some(profile_id.clone()), window, cx);
675 })
676 })
677 .child(
678 ListItem::new("fork-profile")
679 .toggle_state(
680 mode.fork_profile
681 .focus_handle
682 .contains_focused(window, cx),
683 )
684 .inset(true)
685 .spacing(ListItemSpacing::Sparse)
686 .start_slot(
687 Icon::new(IconName::Scissors)
688 .size(IconSize::Small)
689 .color(Color::Muted),
690 )
691 .child(Label::new("Fork Profile"))
692 .on_click({
693 let profile_id = mode.profile_id.clone();
694 cx.listener(move |this, _, window, cx| {
695 this.new_profile(
696 Some(profile_id.clone()),
697 window,
698 cx,
699 );
700 })
701 }),
702 ),
703 )
704 .child(
705 div()
706 .id("configure-default-model")
707 .track_focus(&mode.configure_default_model.focus_handle)
708 .on_action({
709 let profile_id = mode.profile_id.clone();
710 cx.listener(move |this, _: &menu::Confirm, window, cx| {
711 this.configure_default_model(
712 profile_id.clone(),
713 window,
714 cx,
715 );
716 })
717 })
718 .child(
719 ListItem::new("model-item")
720 .toggle_state(
721 mode.configure_default_model
722 .focus_handle
723 .contains_focused(window, cx),
724 )
725 .inset(true)
726 .spacing(ListItemSpacing::Sparse)
727 .start_slot(
728 Icon::new(IconName::ZedAssistant)
729 .size(IconSize::Small)
730 .color(Color::Muted),
731 )
732 .child(Label::new("Configure Default Model"))
733 .on_click({
734 let profile_id = mode.profile_id.clone();
735 cx.listener(move |this, _, window, cx| {
736 this.configure_default_model(
737 profile_id.clone(),
738 window,
739 cx,
740 );
741 })
742 }),
743 ),
744 )
745 .child(
746 div()
747 .id("configure-builtin-tools")
748 .track_focus(&mode.configure_tools.focus_handle)
749 .on_action({
750 let profile_id = mode.profile_id.clone();
751 cx.listener(move |this, _: &menu::Confirm, window, cx| {
752 this.configure_builtin_tools(
753 profile_id.clone(),
754 window,
755 cx,
756 );
757 })
758 })
759 .child(
760 ListItem::new("configure-builtin-tools-item")
761 .toggle_state(
762 mode.configure_tools
763 .focus_handle
764 .contains_focused(window, cx),
765 )
766 .inset(true)
767 .spacing(ListItemSpacing::Sparse)
768 .start_slot(
769 Icon::new(IconName::Settings)
770 .size(IconSize::Small)
771 .color(Color::Muted),
772 )
773 .child(Label::new("Configure Built-in Tools"))
774 .on_click({
775 let profile_id = mode.profile_id.clone();
776 cx.listener(move |this, _, window, cx| {
777 this.configure_builtin_tools(
778 profile_id.clone(),
779 window,
780 cx,
781 );
782 })
783 }),
784 ),
785 )
786 .child(
787 div()
788 .id("configure-mcps")
789 .track_focus(&mode.configure_mcps.focus_handle)
790 .on_action({
791 let profile_id = mode.profile_id.clone();
792 cx.listener(move |this, _: &menu::Confirm, window, cx| {
793 this.configure_mcp_tools(profile_id.clone(), window, cx);
794 })
795 })
796 .child(
797 ListItem::new("configure-mcp-tools")
798 .toggle_state(
799 mode.configure_mcps
800 .focus_handle
801 .contains_focused(window, cx),
802 )
803 .inset(true)
804 .spacing(ListItemSpacing::Sparse)
805 .start_slot(
806 Icon::new(IconName::ToolHammer)
807 .size(IconSize::Small)
808 .color(Color::Muted),
809 )
810 .child(Label::new("Configure MCP Tools"))
811 .on_click({
812 let profile_id = mode.profile_id.clone();
813 cx.listener(move |this, _, window, cx| {
814 this.configure_mcp_tools(
815 profile_id.clone(),
816 window,
817 cx,
818 );
819 })
820 }),
821 ),
822 )
823 .child(
824 div()
825 .id("delete-profile")
826 .track_focus(&mode.delete_profile.focus_handle)
827 .on_action({
828 let profile_id = mode.profile_id.clone();
829 cx.listener(move |this, _: &menu::Confirm, window, cx| {
830 this.delete_profile(profile_id.clone(), window, cx);
831 })
832 })
833 .child(
834 ListItem::new("delete-profile")
835 .toggle_state(
836 mode.delete_profile
837 .focus_handle
838 .contains_focused(window, cx),
839 )
840 .inset(true)
841 .spacing(ListItemSpacing::Sparse)
842 .start_slot(
843 Icon::new(IconName::Trash)
844 .size(IconSize::Small)
845 .color(Color::Error),
846 )
847 .child(Label::new("Delete Profile").color(Color::Error))
848 .disabled(builtin_profiles::is_builtin(&mode.profile_id))
849 .on_click({
850 let profile_id = mode.profile_id.clone();
851 cx.listener(move |this, _, window, cx| {
852 this.delete_profile(profile_id.clone(), window, cx);
853 })
854 }),
855 ),
856 )
857 .child(ListSeparator)
858 .child(
859 div()
860 .id("cancel-item")
861 .track_focus(&mode.cancel_item.focus_handle)
862 .on_action({
863 cx.listener(move |this, _: &menu::Confirm, window, cx| {
864 this.cancel(window, cx);
865 })
866 })
867 .child(
868 ListItem::new("cancel-item")
869 .toggle_state(
870 mode.cancel_item
871 .focus_handle
872 .contains_focused(window, cx),
873 )
874 .inset(true)
875 .spacing(ListItemSpacing::Sparse)
876 .start_slot(
877 Icon::new(IconName::ArrowLeft)
878 .size(IconSize::Small)
879 .color(Color::Muted),
880 )
881 .child(Label::new("Go Back"))
882 .end_slot(
883 div().child(
884 KeyBinding::for_action_in(
885 &menu::Cancel,
886 &self.focus_handle,
887 cx,
888 )
889 .size(rems_from_px(12.)),
890 ),
891 )
892 .on_click({
893 cx.listener(move |this, _, window, cx| {
894 this.cancel(window, cx);
895 })
896 }),
897 ),
898 ),
899 )
900 .into_any_element(),
901 )
902 .entry(mode.fork_profile)
903 .entry(mode.configure_default_model)
904 .entry(mode.configure_tools)
905 .entry(mode.configure_mcps)
906 .entry(mode.delete_profile)
907 .entry(mode.cancel_item)
908 }
909}
910
911impl Render for ManageProfilesModal {
912 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
913 let settings = AgentSettings::get_global(cx);
914
915 let go_back_item = div()
916 .id("cancel-item")
917 .track_focus(&self.focus_handle)
918 .on_action({
919 cx.listener(move |this, _: &menu::Confirm, window, cx| {
920 this.cancel(window, cx);
921 })
922 })
923 .child(
924 ListItem::new("cancel-item")
925 .toggle_state(self.focus_handle.contains_focused(window, cx))
926 .inset(true)
927 .spacing(ListItemSpacing::Sparse)
928 .start_slot(
929 Icon::new(IconName::ArrowLeft)
930 .size(IconSize::Small)
931 .color(Color::Muted),
932 )
933 .child(Label::new("Go Back"))
934 .end_slot(
935 div().child(
936 KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle, cx)
937 .size(rems_from_px(12.)),
938 ),
939 )
940 .on_click({
941 cx.listener(move |this, _, window, cx| {
942 this.cancel(window, cx);
943 })
944 }),
945 );
946
947 div()
948 .elevation_3(cx)
949 .w(rems(34.))
950 .key_context("ManageProfilesModal")
951 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
952 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
953 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
954 this.focus_handle(cx).focus(window, cx);
955 }))
956 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
957 .child(match &self.mode {
958 Mode::ChooseProfile(mode) => self
959 .render_choose_profile(mode.clone(), window, cx)
960 .into_any_element(),
961 Mode::NewProfile(mode) => self
962 .render_new_profile(mode.clone(), window, cx)
963 .into_any_element(),
964 Mode::ViewProfile(mode) => self
965 .render_view_profile(mode.clone(), window, cx)
966 .into_any_element(),
967 Mode::ConfigureTools {
968 profile_id,
969 tool_picker,
970 ..
971 } => {
972 let profile_name = settings
973 .profiles
974 .get(profile_id)
975 .map(|profile| profile.name.clone())
976 .unwrap_or_else(|| "Unknown".into());
977
978 v_flex()
979 .pb_1()
980 .child(ProfileModalHeader::new(
981 format!("{profile_name} — Configure Built-in Tools"),
982 Some(IconName::Cog),
983 ))
984 .child(ListSeparator)
985 .child(tool_picker.clone())
986 .child(ListSeparator)
987 .child(go_back_item)
988 .into_any_element()
989 }
990 Mode::ConfigureDefaultModel {
991 profile_id,
992 model_picker,
993 ..
994 } => {
995 let profile_name = settings
996 .profiles
997 .get(profile_id)
998 .map(|profile| profile.name.clone())
999 .unwrap_or_else(|| "Unknown".into());
1000
1001 v_flex()
1002 .pb_1()
1003 .child(ProfileModalHeader::new(
1004 format!("{profile_name} — Configure Default Model"),
1005 Some(IconName::Ai),
1006 ))
1007 .child(ListSeparator)
1008 .child(v_flex().w(rems(34.)).child(model_picker.clone()))
1009 .child(ListSeparator)
1010 .child(go_back_item)
1011 .into_any_element()
1012 }
1013 Mode::ConfigureMcps {
1014 profile_id,
1015 tool_picker,
1016 ..
1017 } => {
1018 let profile_name = settings
1019 .profiles
1020 .get(profile_id)
1021 .map(|profile| profile.name.clone())
1022 .unwrap_or_else(|| "Unknown".into());
1023
1024 v_flex()
1025 .pb_1()
1026 .child(ProfileModalHeader::new(
1027 format!("{profile_name} — Configure MCP Tools"),
1028 Some(IconName::ToolHammer),
1029 ))
1030 .child(ListSeparator)
1031 .child(tool_picker.clone())
1032 .child(ListSeparator)
1033 .child(go_back_item)
1034 .into_any_element()
1035 }
1036 })
1037 }
1038}