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 .child(KeyBinding::for_action_in(
356 &menu::Confirm,
357 &self.focus_handle,
358 cx,
359 )),
360 )
361 })
362 .on_click({
363 let profile_id = profile.id.clone();
364 cx.listener(move |this, _, window, cx| {
365 this.view_profile(profile_id.clone(), window, cx);
366 })
367 }),
368 )
369 }
370
371 fn render_choose_profile(
372 &mut self,
373 mode: ChooseProfileMode,
374 window: &mut Window,
375 cx: &mut Context<Self>,
376 ) -> impl IntoElement {
377 Navigable::new(
378 div()
379 .track_focus(&self.focus_handle(cx))
380 .size_full()
381 .child(ProfileModalHeader::new("Agent Profiles", None))
382 .child(
383 v_flex()
384 .pb_1()
385 .child(ListSeparator)
386 .children(
387 mode.builtin_profiles
388 .iter()
389 .map(|profile| self.render_profile(profile, window, cx)),
390 )
391 .when(!mode.custom_profiles.is_empty(), |this| {
392 this.child(ListSeparator)
393 .child(
394 div().pl_2().pb_1().child(
395 Label::new("Custom Profiles")
396 .size(LabelSize::Small)
397 .color(Color::Muted),
398 ),
399 )
400 .children(
401 mode.custom_profiles
402 .iter()
403 .map(|profile| self.render_profile(profile, window, cx)),
404 )
405 })
406 .child(ListSeparator)
407 .child(
408 div()
409 .id("new-profile")
410 .track_focus(&mode.add_new_profile.focus_handle)
411 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
412 this.new_profile(None, window, cx);
413 }))
414 .child(
415 ListItem::new("new-profile")
416 .toggle_state(
417 mode.add_new_profile
418 .focus_handle
419 .contains_focused(window, cx),
420 )
421 .inset(true)
422 .spacing(ListItemSpacing::Sparse)
423 .start_slot(Icon::new(IconName::Plus))
424 .child(Label::new("Add New Profile"))
425 .on_click({
426 cx.listener(move |this, _, window, cx| {
427 this.new_profile(None, window, cx);
428 })
429 }),
430 ),
431 ),
432 )
433 .into_any_element(),
434 )
435 .map(|mut navigable| {
436 for profile in mode.builtin_profiles {
437 navigable = navigable.entry(profile.navigation);
438 }
439 for profile in mode.custom_profiles {
440 navigable = navigable.entry(profile.navigation);
441 }
442
443 navigable
444 })
445 .entry(mode.add_new_profile)
446 }
447
448 fn render_new_profile(
449 &mut self,
450 mode: NewProfileMode,
451 _window: &mut Window,
452 cx: &mut Context<Self>,
453 ) -> impl IntoElement {
454 let settings = AgentSettings::get_global(cx);
455
456 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
457 settings
458 .profiles
459 .get(base_profile_id)
460 .map(|profile| profile.name.clone())
461 .unwrap_or_else(|| "Unknown".into())
462 });
463
464 v_flex()
465 .id("new-profile")
466 .track_focus(&self.focus_handle(cx))
467 .child(ProfileModalHeader::new(
468 match &base_profile_name {
469 Some(base_profile) => format!("Fork {base_profile}"),
470 None => "New Profile".into(),
471 },
472 match base_profile_name {
473 Some(_) => Some(IconName::Scissors),
474 None => Some(IconName::Plus),
475 },
476 ))
477 .child(ListSeparator)
478 .child(h_flex().p_2().child(mode.name_editor))
479 }
480
481 fn render_view_profile(
482 &mut self,
483 mode: ViewProfileMode,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) -> impl IntoElement {
487 let settings = AgentSettings::get_global(cx);
488
489 let profile_name = settings
490 .profiles
491 .get(&mode.profile_id)
492 .map(|profile| profile.name.clone())
493 .unwrap_or_else(|| "Unknown".into());
494
495 let icon = match mode.profile_id.as_str() {
496 "write" => IconName::Pencil,
497 "ask" => IconName::Chat,
498 _ => IconName::UserRoundPen,
499 };
500
501 Navigable::new(
502 div()
503 .track_focus(&self.focus_handle(cx))
504 .size_full()
505 .child(ProfileModalHeader::new(profile_name, Some(icon)))
506 .child(
507 v_flex()
508 .pb_1()
509 .child(ListSeparator)
510 .child(
511 div()
512 .id("fork-profile")
513 .track_focus(&mode.fork_profile.focus_handle)
514 .on_action({
515 let profile_id = mode.profile_id.clone();
516 cx.listener(move |this, _: &menu::Confirm, window, cx| {
517 this.new_profile(Some(profile_id.clone()), window, cx);
518 })
519 })
520 .child(
521 ListItem::new("fork-profile")
522 .toggle_state(
523 mode.fork_profile
524 .focus_handle
525 .contains_focused(window, cx),
526 )
527 .inset(true)
528 .spacing(ListItemSpacing::Sparse)
529 .start_slot(
530 Icon::new(IconName::Scissors)
531 .size(IconSize::Small)
532 .color(Color::Muted),
533 )
534 .child(Label::new("Fork Profile"))
535 .on_click({
536 let profile_id = mode.profile_id.clone();
537 cx.listener(move |this, _, window, cx| {
538 this.new_profile(
539 Some(profile_id.clone()),
540 window,
541 cx,
542 );
543 })
544 }),
545 ),
546 )
547 .child(
548 div()
549 .id("configure-builtin-tools")
550 .track_focus(&mode.configure_tools.focus_handle)
551 .on_action({
552 let profile_id = mode.profile_id.clone();
553 cx.listener(move |this, _: &menu::Confirm, window, cx| {
554 this.configure_builtin_tools(
555 profile_id.clone(),
556 window,
557 cx,
558 );
559 })
560 })
561 .child(
562 ListItem::new("configure-builtin-tools-item")
563 .toggle_state(
564 mode.configure_tools
565 .focus_handle
566 .contains_focused(window, cx),
567 )
568 .inset(true)
569 .spacing(ListItemSpacing::Sparse)
570 .start_slot(
571 Icon::new(IconName::Settings)
572 .size(IconSize::Small)
573 .color(Color::Muted),
574 )
575 .child(Label::new("Configure Built-in Tools"))
576 .on_click({
577 let profile_id = mode.profile_id.clone();
578 cx.listener(move |this, _, window, cx| {
579 this.configure_builtin_tools(
580 profile_id.clone(),
581 window,
582 cx,
583 );
584 })
585 }),
586 ),
587 )
588 .child(
589 div()
590 .id("configure-mcps")
591 .track_focus(&mode.configure_mcps.focus_handle)
592 .on_action({
593 let profile_id = mode.profile_id.clone();
594 cx.listener(move |this, _: &menu::Confirm, window, cx| {
595 this.configure_mcp_tools(profile_id.clone(), window, cx);
596 })
597 })
598 .child(
599 ListItem::new("configure-mcp-tools")
600 .toggle_state(
601 mode.configure_mcps
602 .focus_handle
603 .contains_focused(window, cx),
604 )
605 .inset(true)
606 .spacing(ListItemSpacing::Sparse)
607 .start_slot(
608 Icon::new(IconName::ToolHammer)
609 .size(IconSize::Small)
610 .color(Color::Muted),
611 )
612 .child(Label::new("Configure MCP Tools"))
613 .on_click({
614 let profile_id = mode.profile_id.clone();
615 cx.listener(move |this, _, window, cx| {
616 this.configure_mcp_tools(
617 profile_id.clone(),
618 window,
619 cx,
620 );
621 })
622 }),
623 ),
624 )
625 .child(ListSeparator)
626 .child(
627 div()
628 .id("cancel-item")
629 .track_focus(&mode.cancel_item.focus_handle)
630 .on_action({
631 cx.listener(move |this, _: &menu::Confirm, window, cx| {
632 this.cancel(window, cx);
633 })
634 })
635 .child(
636 ListItem::new("cancel-item")
637 .toggle_state(
638 mode.cancel_item
639 .focus_handle
640 .contains_focused(window, cx),
641 )
642 .inset(true)
643 .spacing(ListItemSpacing::Sparse)
644 .start_slot(
645 Icon::new(IconName::ArrowLeft)
646 .size(IconSize::Small)
647 .color(Color::Muted),
648 )
649 .child(Label::new("Go Back"))
650 .end_slot(
651 div().child(
652 KeyBinding::for_action_in(
653 &menu::Cancel,
654 &self.focus_handle,
655 cx,
656 )
657 .size(rems_from_px(12.)),
658 ),
659 )
660 .on_click({
661 cx.listener(move |this, _, window, cx| {
662 this.cancel(window, cx);
663 })
664 }),
665 ),
666 ),
667 )
668 .into_any_element(),
669 )
670 .entry(mode.fork_profile)
671 .entry(mode.configure_tools)
672 .entry(mode.configure_mcps)
673 .entry(mode.cancel_item)
674 }
675}
676
677impl Render for ManageProfilesModal {
678 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
679 let settings = AgentSettings::get_global(cx);
680
681 let go_back_item = div()
682 .id("cancel-item")
683 .track_focus(&self.focus_handle)
684 .on_action({
685 cx.listener(move |this, _: &menu::Confirm, window, cx| {
686 this.cancel(window, cx);
687 })
688 })
689 .child(
690 ListItem::new("cancel-item")
691 .toggle_state(self.focus_handle.contains_focused(window, cx))
692 .inset(true)
693 .spacing(ListItemSpacing::Sparse)
694 .start_slot(
695 Icon::new(IconName::ArrowLeft)
696 .size(IconSize::Small)
697 .color(Color::Muted),
698 )
699 .child(Label::new("Go Back"))
700 .end_slot(
701 div().child(
702 KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle, cx)
703 .size(rems_from_px(12.)),
704 ),
705 )
706 .on_click({
707 cx.listener(move |this, _, window, cx| {
708 this.cancel(window, cx);
709 })
710 }),
711 );
712
713 div()
714 .elevation_3(cx)
715 .w(rems(34.))
716 .key_context("ManageProfilesModal")
717 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
718 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
719 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
720 this.focus_handle(cx).focus(window);
721 }))
722 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
723 .child(match &self.mode {
724 Mode::ChooseProfile(mode) => self
725 .render_choose_profile(mode.clone(), window, cx)
726 .into_any_element(),
727 Mode::NewProfile(mode) => self
728 .render_new_profile(mode.clone(), window, cx)
729 .into_any_element(),
730 Mode::ViewProfile(mode) => self
731 .render_view_profile(mode.clone(), window, cx)
732 .into_any_element(),
733 Mode::ConfigureTools {
734 profile_id,
735 tool_picker,
736 ..
737 } => {
738 let profile_name = settings
739 .profiles
740 .get(profile_id)
741 .map(|profile| profile.name.clone())
742 .unwrap_or_else(|| "Unknown".into());
743
744 v_flex()
745 .pb_1()
746 .child(ProfileModalHeader::new(
747 format!("{profile_name} — Configure Built-in Tools"),
748 Some(IconName::Cog),
749 ))
750 .child(ListSeparator)
751 .child(tool_picker.clone())
752 .child(ListSeparator)
753 .child(go_back_item)
754 .into_any_element()
755 }
756 Mode::ConfigureMcps {
757 profile_id,
758 tool_picker,
759 ..
760 } => {
761 let profile_name = settings
762 .profiles
763 .get(profile_id)
764 .map(|profile| profile.name.clone())
765 .unwrap_or_else(|| "Unknown".into());
766
767 v_flex()
768 .pb_1()
769 .child(ProfileModalHeader::new(
770 format!("{profile_name} — Configure MCP Tools"),
771 Some(IconName::ToolHammer),
772 ))
773 .child(ListSeparator)
774 .child(tool_picker.clone())
775 .child(ListSeparator)
776 .child(go_back_item)
777 .into_any_element()
778 }
779 })
780 }
781}