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