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