1mod profile_modal_header;
2
3use std::sync::Arc;
4
5use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
6use assistant_tool::ToolWorkingSet;
7use editor::Editor;
8use fs::Fs;
9use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
10use settings::Settings as _;
11use ui::{
12 KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
13};
14use workspace::{ModalView, Workspace};
15
16use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
17use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
18use crate::{AgentPanel, ManageProfiles};
19
20use super::tool_picker::ToolPickerMode;
21
22enum Mode {
23 ChooseProfile(ChooseProfileMode),
24 NewProfile(NewProfileMode),
25 ViewProfile(ViewProfileMode),
26 ConfigureTools {
27 profile_id: AgentProfileId,
28 tool_picker: Entity<ToolPicker>,
29 _subscription: Subscription,
30 },
31 ConfigureMcps {
32 profile_id: AgentProfileId,
33 tool_picker: Entity<ToolPicker>,
34 _subscription: Subscription,
35 },
36}
37
38impl Mode {
39 pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
40 let settings = AgentSettings::get_global(cx);
41
42 let mut builtin_profiles = Vec::new();
43 let mut custom_profiles = Vec::new();
44
45 for (profile_id, profile) in settings.profiles.iter() {
46 let entry = ProfileEntry {
47 id: profile_id.clone(),
48 name: profile.name.clone(),
49 navigation: NavigableEntry::focusable(cx),
50 };
51 if builtin_profiles::is_builtin(profile_id) {
52 builtin_profiles.push(entry);
53 } else {
54 custom_profiles.push(entry);
55 }
56 }
57
58 builtin_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
59 custom_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
60
61 Self::ChooseProfile(ChooseProfileMode {
62 builtin_profiles,
63 custom_profiles,
64 add_new_profile: NavigableEntry::focusable(cx),
65 })
66 }
67}
68
69#[derive(Clone)]
70struct ProfileEntry {
71 pub id: AgentProfileId,
72 pub name: SharedString,
73 pub navigation: NavigableEntry,
74}
75
76#[derive(Clone)]
77pub struct ChooseProfileMode {
78 builtin_profiles: Vec<ProfileEntry>,
79 custom_profiles: Vec<ProfileEntry>,
80 add_new_profile: NavigableEntry,
81}
82
83#[derive(Clone)]
84pub struct ViewProfileMode {
85 profile_id: AgentProfileId,
86 fork_profile: NavigableEntry,
87 configure_tools: NavigableEntry,
88 configure_mcps: NavigableEntry,
89 cancel_item: NavigableEntry,
90}
91
92#[derive(Clone)]
93pub struct NewProfileMode {
94 name_editor: Entity<Editor>,
95 base_profile_id: Option<AgentProfileId>,
96}
97
98pub struct ManageProfilesModal {
99 fs: Arc<dyn Fs>,
100 tools: Entity<ToolWorkingSet>,
101 focus_handle: FocusHandle,
102 mode: Mode,
103}
104
105impl ManageProfilesModal {
106 pub fn register(
107 workspace: &mut Workspace,
108 _window: Option<&mut Window>,
109 _cx: &mut Context<Workspace>,
110 ) {
111 workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
112 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
113 let fs = workspace.app_state().fs.clone();
114 let thread_store = panel.read(cx).thread_store();
115 let tools = thread_store.read(cx).tools();
116 workspace.toggle_modal(window, cx, |window, cx| {
117 let mut this = Self::new(fs, tools, window, cx);
118
119 if let Some(profile_id) = action.customize_tools.clone() {
120 this.configure_builtin_tools(profile_id, window, cx);
121 }
122
123 this
124 })
125 }
126 });
127 }
128
129 pub fn new(
130 fs: Arc<dyn Fs>,
131 tools: Entity<ToolWorkingSet>,
132 window: &mut Window,
133 cx: &mut Context<Self>,
134 ) -> Self {
135 let focus_handle = cx.focus_handle();
136
137 Self {
138 fs,
139 tools,
140 focus_handle,
141 mode: Mode::choose_profile(window, cx),
142 }
143 }
144
145 fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
146 self.mode = Mode::choose_profile(window, cx);
147 self.focus_handle(cx).focus(window);
148 }
149
150 fn new_profile(
151 &mut self,
152 base_profile_id: Option<AgentProfileId>,
153 window: &mut Window,
154 cx: &mut Context<Self>,
155 ) {
156 let name_editor = cx.new(|cx| Editor::single_line(window, cx));
157 name_editor.update(cx, |editor, cx| {
158 editor.set_placeholder_text("Profile name", window, cx);
159 });
160
161 self.mode = Mode::NewProfile(NewProfileMode {
162 name_editor,
163 base_profile_id,
164 });
165 self.focus_handle(cx).focus(window);
166 }
167
168 pub fn view_profile(
169 &mut self,
170 profile_id: AgentProfileId,
171 window: &mut Window,
172 cx: &mut Context<Self>,
173 ) {
174 self.mode = Mode::ViewProfile(ViewProfileMode {
175 profile_id,
176 fork_profile: NavigableEntry::focusable(cx),
177 configure_tools: NavigableEntry::focusable(cx),
178 configure_mcps: NavigableEntry::focusable(cx),
179 cancel_item: NavigableEntry::focusable(cx),
180 });
181 self.focus_handle(cx).focus(window);
182 }
183
184 fn configure_mcp_tools(
185 &mut self,
186 profile_id: AgentProfileId,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 let settings = AgentSettings::get_global(cx);
191 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
192 return;
193 };
194
195 let tool_picker = cx.new(|cx| {
196 let delegate = ToolPickerDelegate::new(
197 ToolPickerMode::McpTools,
198 self.fs.clone(),
199 self.tools.clone(),
200 profile_id.clone(),
201 profile,
202 cx,
203 );
204 ToolPicker::mcp_tools(delegate, window, cx)
205 });
206 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
207 let profile_id = profile_id.clone();
208 move |this, _tool_picker, _: &DismissEvent, window, cx| {
209 this.view_profile(profile_id.clone(), window, cx);
210 }
211 });
212
213 self.mode = Mode::ConfigureMcps {
214 profile_id,
215 tool_picker,
216 _subscription: dismiss_subscription,
217 };
218 self.focus_handle(cx).focus(window);
219 }
220
221 fn configure_builtin_tools(
222 &mut self,
223 profile_id: AgentProfileId,
224 window: &mut Window,
225 cx: &mut Context<Self>,
226 ) {
227 let settings = AgentSettings::get_global(cx);
228 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
229 return;
230 };
231
232 let tool_picker = cx.new(|cx| {
233 let delegate = ToolPickerDelegate::new(
234 ToolPickerMode::BuiltinTools,
235 self.fs.clone(),
236 self.tools.clone(),
237 profile_id.clone(),
238 profile,
239 cx,
240 );
241 ToolPicker::builtin_tools(delegate, window, cx)
242 });
243 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
244 let profile_id = profile_id.clone();
245 move |this, _tool_picker, _: &DismissEvent, window, cx| {
246 this.view_profile(profile_id.clone(), window, cx);
247 }
248 });
249
250 self.mode = Mode::ConfigureTools {
251 profile_id,
252 tool_picker,
253 _subscription: dismiss_subscription,
254 };
255 self.focus_handle(cx).focus(window);
256 }
257
258 fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
259 match &self.mode {
260 Mode::ChooseProfile { .. } => {}
261 Mode::NewProfile(mode) => {
262 let name = mode.name_editor.read(cx).text(cx);
263
264 let profile_id =
265 AgentProfile::create(name, mode.base_profile_id.clone(), self.fs.clone(), cx);
266 self.view_profile(profile_id, window, cx);
267 }
268 Mode::ViewProfile(_) => {}
269 Mode::ConfigureTools { .. } => {}
270 Mode::ConfigureMcps { .. } => {}
271 }
272 }
273
274 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
275 match &self.mode {
276 Mode::ChooseProfile { .. } => {
277 cx.emit(DismissEvent);
278 }
279 Mode::NewProfile(mode) => {
280 if let Some(profile_id) = mode.base_profile_id.clone() {
281 self.view_profile(profile_id, window, cx);
282 } else {
283 self.choose_profile(window, cx);
284 }
285 }
286 Mode::ViewProfile(_) => self.choose_profile(window, cx),
287 Mode::ConfigureTools { profile_id, .. } => {
288 self.view_profile(profile_id.clone(), window, cx)
289 }
290 Mode::ConfigureMcps { profile_id, .. } => {
291 self.view_profile(profile_id.clone(), window, cx)
292 }
293 }
294 }
295}
296
297impl ModalView for ManageProfilesModal {}
298
299impl Focusable for ManageProfilesModal {
300 fn focus_handle(&self, cx: &App) -> FocusHandle {
301 match &self.mode {
302 Mode::ChooseProfile(_) => self.focus_handle.clone(),
303 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
304 Mode::ViewProfile(_) => self.focus_handle.clone(),
305 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
306 Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
307 }
308 }
309}
310
311impl EventEmitter<DismissEvent> for ManageProfilesModal {}
312
313impl ManageProfilesModal {
314 fn render_profile(
315 &self,
316 profile: &ProfileEntry,
317 window: &mut Window,
318 cx: &mut Context<Self>,
319 ) -> impl IntoElement + use<> {
320 let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
321
322 div()
323 .id(SharedString::from(format!("profile-{}", profile.id)))
324 .track_focus(&profile.navigation.focus_handle)
325 .on_action({
326 let profile_id = profile.id.clone();
327 cx.listener(move |this, _: &menu::Confirm, window, cx| {
328 this.view_profile(profile_id.clone(), window, cx);
329 })
330 })
331 .child(
332 ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
333 .toggle_state(is_focused)
334 .inset(true)
335 .spacing(ListItemSpacing::Sparse)
336 .child(Label::new(profile.name.clone()))
337 .when(is_focused, |this| {
338 this.end_slot(
339 h_flex()
340 .gap_1()
341 .child(
342 Label::new("Customize")
343 .size(LabelSize::Small)
344 .color(Color::Muted),
345 )
346 .children(KeyBinding::for_action_in(
347 &menu::Confirm,
348 &self.focus_handle,
349 window,
350 cx,
351 )),
352 )
353 })
354 .on_click({
355 let profile_id = profile.id.clone();
356 cx.listener(move |this, _, window, cx| {
357 this.view_profile(profile_id.clone(), window, cx);
358 })
359 }),
360 )
361 }
362
363 fn render_choose_profile(
364 &mut self,
365 mode: ChooseProfileMode,
366 window: &mut Window,
367 cx: &mut Context<Self>,
368 ) -> impl IntoElement {
369 Navigable::new(
370 div()
371 .track_focus(&self.focus_handle(cx))
372 .size_full()
373 .child(ProfileModalHeader::new("Agent Profiles", None))
374 .child(
375 v_flex()
376 .pb_1()
377 .child(ListSeparator)
378 .children(
379 mode.builtin_profiles
380 .iter()
381 .map(|profile| self.render_profile(profile, window, cx)),
382 )
383 .when(!mode.custom_profiles.is_empty(), |this| {
384 this.child(ListSeparator)
385 .child(
386 div().pl_2().pb_1().child(
387 Label::new("Custom Profiles")
388 .size(LabelSize::Small)
389 .color(Color::Muted),
390 ),
391 )
392 .children(
393 mode.custom_profiles
394 .iter()
395 .map(|profile| self.render_profile(profile, window, cx)),
396 )
397 })
398 .child(ListSeparator)
399 .child(
400 div()
401 .id("new-profile")
402 .track_focus(&mode.add_new_profile.focus_handle)
403 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
404 this.new_profile(None, window, cx);
405 }))
406 .child(
407 ListItem::new("new-profile")
408 .toggle_state(
409 mode.add_new_profile
410 .focus_handle
411 .contains_focused(window, cx),
412 )
413 .inset(true)
414 .spacing(ListItemSpacing::Sparse)
415 .start_slot(Icon::new(IconName::Plus))
416 .child(Label::new("Add New Profile"))
417 .on_click({
418 cx.listener(move |this, _, window, cx| {
419 this.new_profile(None, window, cx);
420 })
421 }),
422 ),
423 ),
424 )
425 .into_any_element(),
426 )
427 .map(|mut navigable| {
428 for profile in mode.builtin_profiles {
429 navigable = navigable.entry(profile.navigation);
430 }
431 for profile in mode.custom_profiles {
432 navigable = navigable.entry(profile.navigation);
433 }
434
435 navigable
436 })
437 .entry(mode.add_new_profile)
438 }
439
440 fn render_new_profile(
441 &mut self,
442 mode: NewProfileMode,
443 _window: &mut Window,
444 cx: &mut Context<Self>,
445 ) -> impl IntoElement {
446 let settings = AgentSettings::get_global(cx);
447
448 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
449 settings
450 .profiles
451 .get(base_profile_id)
452 .map(|profile| profile.name.clone())
453 .unwrap_or_else(|| "Unknown".into())
454 });
455
456 v_flex()
457 .id("new-profile")
458 .track_focus(&self.focus_handle(cx))
459 .child(ProfileModalHeader::new(
460 match &base_profile_name {
461 Some(base_profile) => format!("Fork {base_profile}"),
462 None => "New Profile".into(),
463 },
464 match base_profile_name {
465 Some(_) => Some(IconName::Scissors),
466 None => Some(IconName::Plus),
467 },
468 ))
469 .child(ListSeparator)
470 .child(h_flex().p_2().child(mode.name_editor))
471 }
472
473 fn render_view_profile(
474 &mut self,
475 mode: ViewProfileMode,
476 window: &mut Window,
477 cx: &mut Context<Self>,
478 ) -> impl IntoElement {
479 let settings = AgentSettings::get_global(cx);
480
481 let profile_name = settings
482 .profiles
483 .get(&mode.profile_id)
484 .map(|profile| profile.name.clone())
485 .unwrap_or_else(|| "Unknown".into());
486
487 let icon = match mode.profile_id.as_str() {
488 "write" => IconName::Pencil,
489 "ask" => IconName::Chat,
490 _ => IconName::UserRoundPen,
491 };
492
493 Navigable::new(
494 div()
495 .track_focus(&self.focus_handle(cx))
496 .size_full()
497 .child(ProfileModalHeader::new(profile_name, Some(icon)))
498 .child(
499 v_flex()
500 .pb_1()
501 .child(ListSeparator)
502 .child(
503 div()
504 .id("fork-profile")
505 .track_focus(&mode.fork_profile.focus_handle)
506 .on_action({
507 let profile_id = mode.profile_id.clone();
508 cx.listener(move |this, _: &menu::Confirm, window, cx| {
509 this.new_profile(Some(profile_id.clone()), window, cx);
510 })
511 })
512 .child(
513 ListItem::new("fork-profile")
514 .toggle_state(
515 mode.fork_profile
516 .focus_handle
517 .contains_focused(window, cx),
518 )
519 .inset(true)
520 .spacing(ListItemSpacing::Sparse)
521 .start_slot(
522 Icon::new(IconName::Scissors)
523 .size(IconSize::Small)
524 .color(Color::Muted),
525 )
526 .child(Label::new("Fork Profile"))
527 .on_click({
528 let profile_id = mode.profile_id.clone();
529 cx.listener(move |this, _, window, cx| {
530 this.new_profile(
531 Some(profile_id.clone()),
532 window,
533 cx,
534 );
535 })
536 }),
537 ),
538 )
539 .child(
540 div()
541 .id("configure-builtin-tools")
542 .track_focus(&mode.configure_tools.focus_handle)
543 .on_action({
544 let profile_id = mode.profile_id.clone();
545 cx.listener(move |this, _: &menu::Confirm, window, cx| {
546 this.configure_builtin_tools(
547 profile_id.clone(),
548 window,
549 cx,
550 );
551 })
552 })
553 .child(
554 ListItem::new("configure-builtin-tools-item")
555 .toggle_state(
556 mode.configure_tools
557 .focus_handle
558 .contains_focused(window, cx),
559 )
560 .inset(true)
561 .spacing(ListItemSpacing::Sparse)
562 .start_slot(
563 Icon::new(IconName::Settings)
564 .size(IconSize::Small)
565 .color(Color::Muted),
566 )
567 .child(Label::new("Configure Built-in Tools"))
568 .on_click({
569 let profile_id = mode.profile_id.clone();
570 cx.listener(move |this, _, window, cx| {
571 this.configure_builtin_tools(
572 profile_id.clone(),
573 window,
574 cx,
575 );
576 })
577 }),
578 ),
579 )
580 .child(
581 div()
582 .id("configure-mcps")
583 .track_focus(&mode.configure_mcps.focus_handle)
584 .on_action({
585 let profile_id = mode.profile_id.clone();
586 cx.listener(move |this, _: &menu::Confirm, window, cx| {
587 this.configure_mcp_tools(profile_id.clone(), window, cx);
588 })
589 })
590 .child(
591 ListItem::new("configure-mcp-tools")
592 .toggle_state(
593 mode.configure_mcps
594 .focus_handle
595 .contains_focused(window, cx),
596 )
597 .inset(true)
598 .spacing(ListItemSpacing::Sparse)
599 .start_slot(
600 Icon::new(IconName::ToolHammer)
601 .size(IconSize::Small)
602 .color(Color::Muted),
603 )
604 .child(Label::new("Configure MCP Tools"))
605 .on_click({
606 let profile_id = mode.profile_id.clone();
607 cx.listener(move |this, _, window, cx| {
608 this.configure_mcp_tools(
609 profile_id.clone(),
610 window,
611 cx,
612 );
613 })
614 }),
615 ),
616 )
617 .child(ListSeparator)
618 .child(
619 div()
620 .id("cancel-item")
621 .track_focus(&mode.cancel_item.focus_handle)
622 .on_action({
623 cx.listener(move |this, _: &menu::Confirm, window, cx| {
624 this.cancel(window, cx);
625 })
626 })
627 .child(
628 ListItem::new("cancel-item")
629 .toggle_state(
630 mode.cancel_item
631 .focus_handle
632 .contains_focused(window, cx),
633 )
634 .inset(true)
635 .spacing(ListItemSpacing::Sparse)
636 .start_slot(
637 Icon::new(IconName::ArrowLeft)
638 .size(IconSize::Small)
639 .color(Color::Muted),
640 )
641 .child(Label::new("Go Back"))
642 .end_slot(
643 div().children(
644 KeyBinding::for_action_in(
645 &menu::Cancel,
646 &self.focus_handle,
647 window,
648 cx,
649 )
650 .map(|kb| kb.size(rems_from_px(12.))),
651 ),
652 )
653 .on_click({
654 cx.listener(move |this, _, window, cx| {
655 this.cancel(window, cx);
656 })
657 }),
658 ),
659 ),
660 )
661 .into_any_element(),
662 )
663 .entry(mode.fork_profile)
664 .entry(mode.configure_tools)
665 .entry(mode.configure_mcps)
666 .entry(mode.cancel_item)
667 }
668}
669
670impl Render for ManageProfilesModal {
671 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
672 let settings = AgentSettings::get_global(cx);
673
674 let go_back_item = div()
675 .id("cancel-item")
676 .track_focus(&self.focus_handle)
677 .on_action({
678 cx.listener(move |this, _: &menu::Confirm, window, cx| {
679 this.cancel(window, cx);
680 })
681 })
682 .child(
683 ListItem::new("cancel-item")
684 .toggle_state(self.focus_handle.contains_focused(window, cx))
685 .inset(true)
686 .spacing(ListItemSpacing::Sparse)
687 .start_slot(
688 Icon::new(IconName::ArrowLeft)
689 .size(IconSize::Small)
690 .color(Color::Muted),
691 )
692 .child(Label::new("Go Back"))
693 .end_slot(
694 div().children(
695 KeyBinding::for_action_in(
696 &menu::Cancel,
697 &self.focus_handle,
698 window,
699 cx,
700 )
701 .map(|kb| kb.size(rems_from_px(12.))),
702 ),
703 )
704 .on_click({
705 cx.listener(move |this, _, window, cx| {
706 this.cancel(window, cx);
707 })
708 }),
709 );
710
711 div()
712 .elevation_3(cx)
713 .w(rems(34.))
714 .key_context("ManageProfilesModal")
715 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
716 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
717 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
718 this.focus_handle(cx).focus(window);
719 }))
720 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
721 .child(match &self.mode {
722 Mode::ChooseProfile(mode) => self
723 .render_choose_profile(mode.clone(), window, cx)
724 .into_any_element(),
725 Mode::NewProfile(mode) => self
726 .render_new_profile(mode.clone(), window, cx)
727 .into_any_element(),
728 Mode::ViewProfile(mode) => self
729 .render_view_profile(mode.clone(), window, cx)
730 .into_any_element(),
731 Mode::ConfigureTools {
732 profile_id,
733 tool_picker,
734 ..
735 } => {
736 let profile_name = settings
737 .profiles
738 .get(profile_id)
739 .map(|profile| profile.name.clone())
740 .unwrap_or_else(|| "Unknown".into());
741
742 v_flex()
743 .pb_1()
744 .child(ProfileModalHeader::new(
745 format!("{profile_name} — Configure Built-in Tools"),
746 Some(IconName::Cog),
747 ))
748 .child(ListSeparator)
749 .child(tool_picker.clone())
750 .child(ListSeparator)
751 .child(go_back_item)
752 .into_any_element()
753 }
754 Mode::ConfigureMcps {
755 profile_id,
756 tool_picker,
757 ..
758 } => {
759 let profile_name = settings
760 .profiles
761 .get(profile_id)
762 .map(|profile| profile.name.clone())
763 .unwrap_or_else(|| "Unknown".into());
764
765 v_flex()
766 .pb_1()
767 .child(ProfileModalHeader::new(
768 format!("{profile_name} — Configure MCP Tools"),
769 Some(IconName::ToolHammer),
770 ))
771 .child(ListSeparator)
772 .child(tool_picker.clone())
773 .child(ListSeparator)
774 .child(go_back_item)
775 .into_any_element()
776 }
777 })
778 }
779}