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