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