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 //todo: This causes the web search tool to show up even it only works when using zed hosted models
354 let tool_names: Vec<Arc<str>> = agent::supported_built_in_tool_names(
355 self.active_model.as_ref().map(|model| model.provider_id()),
356 cx,
357 )
358 .into_iter()
359 .map(|s| Arc::from(s))
360 .collect();
361
362 let tool_picker = cx.new(|cx| {
363 let delegate = ToolPickerDelegate::builtin_tools(
364 tool_names,
365 self.fs.clone(),
366 profile_id.clone(),
367 profile,
368 cx,
369 );
370 ToolPicker::builtin_tools(delegate, window, cx)
371 });
372 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
373 let profile_id = profile_id.clone();
374 move |this, _tool_picker, _: &DismissEvent, window, cx| {
375 this.view_profile(profile_id.clone(), window, cx);
376 }
377 });
378
379 self.mode = Mode::ConfigureTools {
380 profile_id,
381 tool_picker,
382 _subscription: dismiss_subscription,
383 };
384 self.focus_handle(cx).focus(window, cx);
385 }
386
387 fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
388 match &self.mode {
389 Mode::ChooseProfile { .. } => {}
390 Mode::NewProfile(mode) => {
391 let name = mode.name_editor.read(cx).text(cx);
392
393 let profile_id =
394 AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
395 self.view_profile(profile_id, window, cx);
396 }
397 Mode::ViewProfile(_) => {}
398 Mode::ConfigureTools { .. } => {}
399 Mode::ConfigureMcps { .. } => {}
400 Mode::ConfigureDefaultModel { .. } => {}
401 }
402 }
403
404 fn delete_profile(
405 &mut self,
406 profile_id: AgentProfileId,
407 window: &mut Window,
408 cx: &mut Context<Self>,
409 ) {
410 if builtin_profiles::is_builtin(&profile_id) {
411 self.view_profile(profile_id, window, cx);
412 return;
413 }
414
415 let fs = self.fs.clone();
416
417 update_settings_file(fs, cx, move |settings, _cx| {
418 let Some(agent_settings) = settings.agent.as_mut() else {
419 return;
420 };
421
422 let Some(profiles) = agent_settings.profiles.as_mut() else {
423 return;
424 };
425
426 profiles.shift_remove(profile_id.0.as_ref());
427
428 if agent_settings
429 .default_profile
430 .as_deref()
431 .is_some_and(|default_profile| default_profile == profile_id.0.as_ref())
432 {
433 agent_settings.default_profile = Some(AgentProfileId::default().0);
434 }
435 });
436
437 self.choose_profile(window, cx);
438 }
439
440 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
441 match &self.mode {
442 Mode::ChooseProfile { .. } => {
443 cx.emit(DismissEvent);
444 }
445 Mode::NewProfile(mode) => {
446 if let Some(profile_id) = mode.base_profile_id.clone() {
447 self.view_profile(profile_id, window, cx);
448 } else {
449 self.choose_profile(window, cx);
450 }
451 }
452 Mode::ViewProfile(_) => self.choose_profile(window, cx),
453 Mode::ConfigureTools { profile_id, .. } => {
454 self.view_profile(profile_id.clone(), window, cx)
455 }
456 Mode::ConfigureMcps { profile_id, .. } => {
457 self.view_profile(profile_id.clone(), window, cx)
458 }
459 Mode::ConfigureDefaultModel { profile_id, .. } => {
460 self.view_profile(profile_id.clone(), window, cx)
461 }
462 }
463 }
464}
465
466impl ModalView for ManageProfilesModal {}
467
468impl Focusable for ManageProfilesModal {
469 fn focus_handle(&self, cx: &App) -> FocusHandle {
470 match &self.mode {
471 Mode::ChooseProfile(_) => self.focus_handle.clone(),
472 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
473 Mode::ViewProfile(_) => self.focus_handle.clone(),
474 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
475 Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
476 Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
477 }
478 }
479}
480
481impl EventEmitter<DismissEvent> for ManageProfilesModal {}
482
483impl ManageProfilesModal {
484 fn render_profile(
485 &self,
486 profile: &ProfileEntry,
487 window: &mut Window,
488 cx: &mut Context<Self>,
489 ) -> impl IntoElement + use<> {
490 let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
491
492 div()
493 .id(format!("profile-{}", profile.id))
494 .track_focus(&profile.navigation.focus_handle)
495 .on_action({
496 let profile_id = profile.id.clone();
497 cx.listener(move |this, _: &menu::Confirm, window, cx| {
498 this.view_profile(profile_id.clone(), window, cx);
499 })
500 })
501 .child(
502 ListItem::new(format!("profile-{}", profile.id))
503 .toggle_state(is_focused)
504 .inset(true)
505 .spacing(ListItemSpacing::Sparse)
506 .child(Label::new(profile.name.clone()))
507 .when(is_focused, |this| {
508 this.end_slot(
509 h_flex()
510 .gap_1()
511 .child(
512 Label::new("Customize")
513 .size(LabelSize::Small)
514 .color(Color::Muted),
515 )
516 .child(KeyBinding::for_action_in(
517 &menu::Confirm,
518 &self.focus_handle,
519 cx,
520 )),
521 )
522 })
523 .on_click({
524 let profile_id = profile.id.clone();
525 cx.listener(move |this, _, window, cx| {
526 this.view_profile(profile_id.clone(), window, cx);
527 })
528 }),
529 )
530 }
531
532 fn render_choose_profile(
533 &mut self,
534 mode: ChooseProfileMode,
535 window: &mut Window,
536 cx: &mut Context<Self>,
537 ) -> impl IntoElement {
538 Navigable::new(
539 div()
540 .track_focus(&self.focus_handle(cx))
541 .size_full()
542 .child(ProfileModalHeader::new("Agent Profiles", None))
543 .child(
544 v_flex()
545 .pb_1()
546 .child(ListSeparator)
547 .children(
548 mode.builtin_profiles
549 .iter()
550 .map(|profile| self.render_profile(profile, window, cx)),
551 )
552 .when(!mode.custom_profiles.is_empty(), |this| {
553 this.child(ListSeparator)
554 .child(
555 div().pl_2().pb_1().child(
556 Label::new("Custom Profiles")
557 .size(LabelSize::Small)
558 .color(Color::Muted),
559 ),
560 )
561 .children(
562 mode.custom_profiles
563 .iter()
564 .map(|profile| self.render_profile(profile, window, cx)),
565 )
566 })
567 .child(ListSeparator)
568 .child(
569 div()
570 .id("new-profile")
571 .track_focus(&mode.add_new_profile.focus_handle)
572 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
573 this.new_profile(None, window, cx);
574 }))
575 .child(
576 ListItem::new("new-profile")
577 .toggle_state(
578 mode.add_new_profile
579 .focus_handle
580 .contains_focused(window, cx),
581 )
582 .inset(true)
583 .spacing(ListItemSpacing::Sparse)
584 .start_slot(Icon::new(IconName::Plus))
585 .child(Label::new("Add New Profile"))
586 .on_click({
587 cx.listener(move |this, _, window, cx| {
588 this.new_profile(None, window, cx);
589 })
590 }),
591 ),
592 ),
593 )
594 .into_any_element(),
595 )
596 .map(|mut navigable| {
597 for profile in mode.builtin_profiles {
598 navigable = navigable.entry(profile.navigation);
599 }
600 for profile in mode.custom_profiles {
601 navigable = navigable.entry(profile.navigation);
602 }
603
604 navigable
605 })
606 .entry(mode.add_new_profile)
607 }
608
609 fn render_new_profile(
610 &mut self,
611 mode: NewProfileMode,
612 _window: &mut Window,
613 cx: &mut Context<Self>,
614 ) -> impl IntoElement {
615 let settings = AgentSettings::get_global(cx);
616
617 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
618 settings
619 .profiles
620 .get(base_profile_id)
621 .map(|profile| profile.name.clone())
622 .unwrap_or_else(|| "Unknown".into())
623 });
624
625 v_flex()
626 .id("new-profile")
627 .track_focus(&self.focus_handle(cx))
628 .child(ProfileModalHeader::new(
629 match &base_profile_name {
630 Some(base_profile) => format!("Fork {base_profile}"),
631 None => "New Profile".into(),
632 },
633 match base_profile_name {
634 Some(_) => Some(IconName::Scissors),
635 None => Some(IconName::Plus),
636 },
637 ))
638 .child(ListSeparator)
639 .child(h_flex().p_2().child(mode.name_editor))
640 }
641
642 fn render_view_profile(
643 &mut self,
644 mode: ViewProfileMode,
645 window: &mut Window,
646 cx: &mut Context<Self>,
647 ) -> impl IntoElement {
648 let settings = AgentSettings::get_global(cx);
649
650 let profile_name = settings
651 .profiles
652 .get(&mode.profile_id)
653 .map(|profile| profile.name.clone())
654 .unwrap_or_else(|| "Unknown".into());
655
656 let icon = match mode.profile_id.as_str() {
657 "write" => IconName::Pencil,
658 "ask" => IconName::Chat,
659 _ => IconName::UserRoundPen,
660 };
661
662 Navigable::new(
663 div()
664 .track_focus(&self.focus_handle(cx))
665 .size_full()
666 .child(ProfileModalHeader::new(profile_name, Some(icon)))
667 .child(
668 v_flex()
669 .pb_1()
670 .child(ListSeparator)
671 .child(
672 div()
673 .id("fork-profile")
674 .track_focus(&mode.fork_profile.focus_handle)
675 .on_action({
676 let profile_id = mode.profile_id.clone();
677 cx.listener(move |this, _: &menu::Confirm, window, cx| {
678 this.new_profile(Some(profile_id.clone()), window, cx);
679 })
680 })
681 .child(
682 ListItem::new("fork-profile")
683 .toggle_state(
684 mode.fork_profile
685 .focus_handle
686 .contains_focused(window, cx),
687 )
688 .inset(true)
689 .spacing(ListItemSpacing::Sparse)
690 .start_slot(
691 Icon::new(IconName::Scissors)
692 .size(IconSize::Small)
693 .color(Color::Muted),
694 )
695 .child(Label::new("Fork Profile"))
696 .on_click({
697 let profile_id = mode.profile_id.clone();
698 cx.listener(move |this, _, window, cx| {
699 this.new_profile(
700 Some(profile_id.clone()),
701 window,
702 cx,
703 );
704 })
705 }),
706 ),
707 )
708 .child(
709 div()
710 .id("configure-default-model")
711 .track_focus(&mode.configure_default_model.focus_handle)
712 .on_action({
713 let profile_id = mode.profile_id.clone();
714 cx.listener(move |this, _: &menu::Confirm, window, cx| {
715 this.configure_default_model(
716 profile_id.clone(),
717 window,
718 cx,
719 );
720 })
721 })
722 .child(
723 ListItem::new("model-item")
724 .toggle_state(
725 mode.configure_default_model
726 .focus_handle
727 .contains_focused(window, cx),
728 )
729 .inset(true)
730 .spacing(ListItemSpacing::Sparse)
731 .start_slot(
732 Icon::new(IconName::ZedAssistant)
733 .size(IconSize::Small)
734 .color(Color::Muted),
735 )
736 .child(Label::new("Configure Default Model"))
737 .on_click({
738 let profile_id = mode.profile_id.clone();
739 cx.listener(move |this, _, window, cx| {
740 this.configure_default_model(
741 profile_id.clone(),
742 window,
743 cx,
744 );
745 })
746 }),
747 ),
748 )
749 .child(
750 div()
751 .id("configure-builtin-tools")
752 .track_focus(&mode.configure_tools.focus_handle)
753 .on_action({
754 let profile_id = mode.profile_id.clone();
755 cx.listener(move |this, _: &menu::Confirm, window, cx| {
756 this.configure_builtin_tools(
757 profile_id.clone(),
758 window,
759 cx,
760 );
761 })
762 })
763 .child(
764 ListItem::new("configure-builtin-tools-item")
765 .toggle_state(
766 mode.configure_tools
767 .focus_handle
768 .contains_focused(window, cx),
769 )
770 .inset(true)
771 .spacing(ListItemSpacing::Sparse)
772 .start_slot(
773 Icon::new(IconName::Settings)
774 .size(IconSize::Small)
775 .color(Color::Muted),
776 )
777 .child(Label::new("Configure Built-in Tools"))
778 .on_click({
779 let profile_id = mode.profile_id.clone();
780 cx.listener(move |this, _, window, cx| {
781 this.configure_builtin_tools(
782 profile_id.clone(),
783 window,
784 cx,
785 );
786 })
787 }),
788 ),
789 )
790 .child(
791 div()
792 .id("configure-mcps")
793 .track_focus(&mode.configure_mcps.focus_handle)
794 .on_action({
795 let profile_id = mode.profile_id.clone();
796 cx.listener(move |this, _: &menu::Confirm, window, cx| {
797 this.configure_mcp_tools(profile_id.clone(), window, cx);
798 })
799 })
800 .child(
801 ListItem::new("configure-mcp-tools")
802 .toggle_state(
803 mode.configure_mcps
804 .focus_handle
805 .contains_focused(window, cx),
806 )
807 .inset(true)
808 .spacing(ListItemSpacing::Sparse)
809 .start_slot(
810 Icon::new(IconName::ToolHammer)
811 .size(IconSize::Small)
812 .color(Color::Muted),
813 )
814 .child(Label::new("Configure MCP Tools"))
815 .on_click({
816 let profile_id = mode.profile_id.clone();
817 cx.listener(move |this, _, window, cx| {
818 this.configure_mcp_tools(
819 profile_id.clone(),
820 window,
821 cx,
822 );
823 })
824 }),
825 ),
826 )
827 .child(
828 div()
829 .id("delete-profile")
830 .track_focus(&mode.delete_profile.focus_handle)
831 .on_action({
832 let profile_id = mode.profile_id.clone();
833 cx.listener(move |this, _: &menu::Confirm, window, cx| {
834 this.delete_profile(profile_id.clone(), window, cx);
835 })
836 })
837 .child(
838 ListItem::new("delete-profile")
839 .toggle_state(
840 mode.delete_profile
841 .focus_handle
842 .contains_focused(window, cx),
843 )
844 .inset(true)
845 .spacing(ListItemSpacing::Sparse)
846 .start_slot(
847 Icon::new(IconName::Trash)
848 .size(IconSize::Small)
849 .color(Color::Error),
850 )
851 .child(Label::new("Delete Profile").color(Color::Error))
852 .disabled(builtin_profiles::is_builtin(&mode.profile_id))
853 .on_click({
854 let profile_id = mode.profile_id.clone();
855 cx.listener(move |this, _, window, cx| {
856 this.delete_profile(profile_id.clone(), window, cx);
857 })
858 }),
859 ),
860 )
861 .child(ListSeparator)
862 .child(
863 div()
864 .id("cancel-item")
865 .track_focus(&mode.cancel_item.focus_handle)
866 .on_action({
867 cx.listener(move |this, _: &menu::Confirm, window, cx| {
868 this.cancel(window, cx);
869 })
870 })
871 .child(
872 ListItem::new("cancel-item")
873 .toggle_state(
874 mode.cancel_item
875 .focus_handle
876 .contains_focused(window, cx),
877 )
878 .inset(true)
879 .spacing(ListItemSpacing::Sparse)
880 .start_slot(
881 Icon::new(IconName::ArrowLeft)
882 .size(IconSize::Small)
883 .color(Color::Muted),
884 )
885 .child(Label::new("Go Back"))
886 .end_slot(
887 div().child(
888 KeyBinding::for_action_in(
889 &menu::Cancel,
890 &self.focus_handle,
891 cx,
892 )
893 .size(rems_from_px(12.)),
894 ),
895 )
896 .on_click({
897 cx.listener(move |this, _, window, cx| {
898 this.cancel(window, cx);
899 })
900 }),
901 ),
902 ),
903 )
904 .into_any_element(),
905 )
906 .entry(mode.fork_profile)
907 .entry(mode.configure_default_model)
908 .entry(mode.configure_tools)
909 .entry(mode.configure_mcps)
910 .entry(mode.delete_profile)
911 .entry(mode.cancel_item)
912 }
913}
914
915impl Render for ManageProfilesModal {
916 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
917 let settings = AgentSettings::get_global(cx);
918
919 let go_back_item = div()
920 .id("cancel-item")
921 .track_focus(&self.focus_handle)
922 .on_action({
923 cx.listener(move |this, _: &menu::Confirm, window, cx| {
924 this.cancel(window, cx);
925 })
926 })
927 .child(
928 ListItem::new("cancel-item")
929 .toggle_state(self.focus_handle.contains_focused(window, cx))
930 .inset(true)
931 .spacing(ListItemSpacing::Sparse)
932 .start_slot(
933 Icon::new(IconName::ArrowLeft)
934 .size(IconSize::Small)
935 .color(Color::Muted),
936 )
937 .child(Label::new("Go Back"))
938 .end_slot(
939 div().child(
940 KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle, cx)
941 .size(rems_from_px(12.)),
942 ),
943 )
944 .on_click({
945 cx.listener(move |this, _, window, cx| {
946 this.cancel(window, cx);
947 })
948 }),
949 );
950
951 div()
952 .elevation_3(cx)
953 .w(rems(34.))
954 .key_context("ManageProfilesModal")
955 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
956 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
957 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
958 this.focus_handle(cx).focus(window, cx);
959 }))
960 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
961 .child(match &self.mode {
962 Mode::ChooseProfile(mode) => self
963 .render_choose_profile(mode.clone(), window, cx)
964 .into_any_element(),
965 Mode::NewProfile(mode) => self
966 .render_new_profile(mode.clone(), window, cx)
967 .into_any_element(),
968 Mode::ViewProfile(mode) => self
969 .render_view_profile(mode.clone(), window, cx)
970 .into_any_element(),
971 Mode::ConfigureTools {
972 profile_id,
973 tool_picker,
974 ..
975 } => {
976 let profile_name = settings
977 .profiles
978 .get(profile_id)
979 .map(|profile| profile.name.clone())
980 .unwrap_or_else(|| "Unknown".into());
981
982 v_flex()
983 .pb_1()
984 .child(ProfileModalHeader::new(
985 format!("{profile_name} — Configure Built-in Tools"),
986 Some(IconName::Cog),
987 ))
988 .child(ListSeparator)
989 .child(tool_picker.clone())
990 .child(ListSeparator)
991 .child(go_back_item)
992 .into_any_element()
993 }
994 Mode::ConfigureDefaultModel {
995 profile_id,
996 model_picker,
997 ..
998 } => {
999 let profile_name = settings
1000 .profiles
1001 .get(profile_id)
1002 .map(|profile| profile.name.clone())
1003 .unwrap_or_else(|| "Unknown".into());
1004
1005 v_flex()
1006 .pb_1()
1007 .child(ProfileModalHeader::new(
1008 format!("{profile_name} — Configure Default Model"),
1009 Some(IconName::Ai),
1010 ))
1011 .child(ListSeparator)
1012 .child(v_flex().w(rems(34.)).child(model_picker.clone()))
1013 .child(ListSeparator)
1014 .child(go_back_item)
1015 .into_any_element()
1016 }
1017 Mode::ConfigureMcps {
1018 profile_id,
1019 tool_picker,
1020 ..
1021 } => {
1022 let profile_name = settings
1023 .profiles
1024 .get(profile_id)
1025 .map(|profile| profile.name.clone())
1026 .unwrap_or_else(|| "Unknown".into());
1027
1028 v_flex()
1029 .pb_1()
1030 .child(ProfileModalHeader::new(
1031 format!("{profile_name} — Configure MCP Tools"),
1032 Some(IconName::ToolHammer),
1033 ))
1034 .child(ListSeparator)
1035 .child(tool_picker.clone())
1036 .child(ListSeparator)
1037 .child(go_back_item)
1038 .into_any_element()
1039 }
1040 })
1041 }
1042}