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