1mod profile_modal_header;
2
3use std::sync::Arc;
4
5use assistant_settings::{
6 AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
7 ContextServerPresetContent, VersionedAssistantSettingsContent,
8};
9use assistant_tool::ToolWorkingSet;
10use convert_case::{Case, Casing as _};
11use editor::Editor;
12use fs::Fs;
13use gpui::{
14 prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
15 WeakEntity,
16};
17use settings::{update_settings_file, Settings as _};
18use ui::{
19 prelude::*, KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry,
20};
21use workspace::{ModalView, Workspace};
22
23use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
24use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
25use crate::{AssistantPanel, ManageProfiles, ThreadStore};
26
27enum Mode {
28 ChooseProfile(ChooseProfileMode),
29 NewProfile(NewProfileMode),
30 ViewProfile(ViewProfileMode),
31 ConfigureTools {
32 profile_id: Arc<str>,
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 = AssistantSettings::get_global(cx);
41
42 let mut profiles = settings.profiles.clone();
43 profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
44
45 let profiles = profiles
46 .into_iter()
47 .map(|(id, profile)| ProfileEntry {
48 id,
49 name: profile.name,
50 navigation: NavigableEntry::focusable(cx),
51 })
52 .collect::<Vec<_>>();
53
54 Self::ChooseProfile(ChooseProfileMode {
55 profiles,
56 add_new_profile: NavigableEntry::focusable(cx),
57 })
58 }
59}
60
61#[derive(Clone)]
62struct ProfileEntry {
63 pub id: Arc<str>,
64 pub name: SharedString,
65 pub navigation: NavigableEntry,
66}
67
68#[derive(Clone)]
69pub struct ChooseProfileMode {
70 profiles: Vec<ProfileEntry>,
71 add_new_profile: NavigableEntry,
72}
73
74#[derive(Clone)]
75pub struct ViewProfileMode {
76 profile_id: Arc<str>,
77 fork_profile: NavigableEntry,
78 configure_tools: NavigableEntry,
79}
80
81#[derive(Clone)]
82pub struct NewProfileMode {
83 name_editor: Entity<Editor>,
84 base_profile_id: Option<Arc<str>>,
85}
86
87pub struct ManageProfilesModal {
88 fs: Arc<dyn Fs>,
89 tools: Arc<ToolWorkingSet>,
90 thread_store: WeakEntity<ThreadStore>,
91 focus_handle: FocusHandle,
92 mode: Mode,
93}
94
95impl ManageProfilesModal {
96 pub fn register(
97 workspace: &mut Workspace,
98 _window: Option<&mut Window>,
99 _cx: &mut Context<Workspace>,
100 ) {
101 workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
102 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
103 let fs = workspace.app_state().fs.clone();
104 let thread_store = panel.read(cx).thread_store();
105 let tools = thread_store.read(cx).tools();
106 let thread_store = thread_store.downgrade();
107 workspace.toggle_modal(window, cx, |window, cx| {
108 Self::new(fs, tools, thread_store, window, cx)
109 })
110 }
111 });
112 }
113
114 pub fn new(
115 fs: Arc<dyn Fs>,
116 tools: Arc<ToolWorkingSet>,
117 thread_store: WeakEntity<ThreadStore>,
118 window: &mut Window,
119 cx: &mut Context<Self>,
120 ) -> Self {
121 let focus_handle = cx.focus_handle();
122
123 Self {
124 fs,
125 tools,
126 thread_store,
127 focus_handle,
128 mode: Mode::choose_profile(window, cx),
129 }
130 }
131
132 fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
133 self.mode = Mode::choose_profile(window, cx);
134 self.focus_handle(cx).focus(window);
135 }
136
137 fn new_profile(
138 &mut self,
139 base_profile_id: Option<Arc<str>>,
140 window: &mut Window,
141 cx: &mut Context<Self>,
142 ) {
143 let name_editor = cx.new(|cx| Editor::single_line(window, cx));
144 name_editor.update(cx, |editor, cx| {
145 editor.set_placeholder_text("Profile name", cx);
146 });
147
148 self.mode = Mode::NewProfile(NewProfileMode {
149 name_editor,
150 base_profile_id,
151 });
152 self.focus_handle(cx).focus(window);
153 }
154
155 pub fn view_profile(
156 &mut self,
157 profile_id: Arc<str>,
158 window: &mut Window,
159 cx: &mut Context<Self>,
160 ) {
161 self.mode = Mode::ViewProfile(ViewProfileMode {
162 profile_id,
163 fork_profile: NavigableEntry::focusable(cx),
164 configure_tools: NavigableEntry::focusable(cx),
165 });
166 self.focus_handle(cx).focus(window);
167 }
168
169 fn configure_tools(
170 &mut self,
171 profile_id: Arc<str>,
172 window: &mut Window,
173 cx: &mut Context<Self>,
174 ) {
175 let settings = AssistantSettings::get_global(cx);
176 let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
177 return;
178 };
179
180 let tool_picker = cx.new(|cx| {
181 let delegate = ToolPickerDelegate::new(
182 self.fs.clone(),
183 self.tools.clone(),
184 self.thread_store.clone(),
185 profile_id.clone(),
186 profile,
187 cx,
188 );
189 ToolPicker::new(delegate, window, cx)
190 });
191 let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
192 let profile_id = profile_id.clone();
193 move |this, _tool_picker, _: &DismissEvent, window, cx| {
194 this.view_profile(profile_id.clone(), window, cx);
195 }
196 });
197
198 self.mode = Mode::ConfigureTools {
199 profile_id,
200 tool_picker,
201 _subscription: dismiss_subscription,
202 };
203 self.focus_handle(cx).focus(window);
204 }
205
206 fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
207 match &self.mode {
208 Mode::ChooseProfile { .. } => {}
209 Mode::NewProfile(mode) => {
210 let settings = AssistantSettings::get_global(cx);
211
212 let base_profile = mode
213 .base_profile_id
214 .as_ref()
215 .and_then(|profile_id| settings.profiles.get(profile_id).cloned());
216
217 let name = mode.name_editor.read(cx).text(cx);
218 let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
219
220 let profile = AgentProfile {
221 name: name.into(),
222 tools: base_profile
223 .as_ref()
224 .map(|profile| profile.tools.clone())
225 .unwrap_or_default(),
226 context_servers: base_profile
227 .map(|profile| profile.context_servers)
228 .unwrap_or_default(),
229 };
230
231 self.create_profile(profile_id.clone(), profile, cx);
232 self.view_profile(profile_id, window, cx);
233 }
234 Mode::ViewProfile(_) => {}
235 Mode::ConfigureTools { .. } => {}
236 }
237 }
238
239 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
240 match &self.mode {
241 Mode::ChooseProfile { .. } => {
242 cx.emit(DismissEvent);
243 }
244 Mode::NewProfile(mode) => {
245 if let Some(profile_id) = mode.base_profile_id.clone() {
246 self.view_profile(profile_id, window, cx);
247 } else {
248 self.choose_profile(window, cx);
249 }
250 }
251 Mode::ViewProfile(_) => self.choose_profile(window, cx),
252 Mode::ConfigureTools { .. } => {}
253 }
254 }
255
256 fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
257 update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
258 move |settings, _cx| match settings {
259 AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
260 settings,
261 )) => {
262 let profiles = settings.profiles.get_or_insert_default();
263 if profiles.contains_key(&profile_id) {
264 log::error!("profile with ID '{profile_id}' already exists");
265 return;
266 }
267
268 profiles.insert(
269 profile_id,
270 AgentProfileContent {
271 name: profile.name.into(),
272 tools: profile.tools,
273 context_servers: profile
274 .context_servers
275 .into_iter()
276 .map(|(server_id, preset)| {
277 (
278 server_id,
279 ContextServerPresetContent {
280 tools: preset.tools,
281 },
282 )
283 })
284 .collect(),
285 },
286 );
287 }
288 _ => {}
289 }
290 });
291 }
292}
293
294impl ModalView for ManageProfilesModal {}
295
296impl Focusable for ManageProfilesModal {
297 fn focus_handle(&self, cx: &App) -> FocusHandle {
298 match &self.mode {
299 Mode::ChooseProfile(_) => self.focus_handle.clone(),
300 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
301 Mode::ViewProfile(_) => self.focus_handle.clone(),
302 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
303 }
304 }
305}
306
307impl EventEmitter<DismissEvent> for ManageProfilesModal {}
308
309impl ManageProfilesModal {
310 fn render_choose_profile(
311 &mut self,
312 mode: ChooseProfileMode,
313 window: &mut Window,
314 cx: &mut Context<Self>,
315 ) -> impl IntoElement {
316 Navigable::new(
317 div()
318 .track_focus(&self.focus_handle(cx))
319 .size_full()
320 .child(ProfileModalHeader::new(
321 "Agent Profiles",
322 IconName::ZedAssistant,
323 ))
324 .child(
325 v_flex()
326 .pb_1()
327 .child(ListSeparator)
328 .children(mode.profiles.iter().map(|profile| {
329 div()
330 .id(SharedString::from(format!("profile-{}", profile.id)))
331 .track_focus(&profile.navigation.focus_handle)
332 .on_action({
333 let profile_id = profile.id.clone();
334 cx.listener(move |this, _: &menu::Confirm, window, cx| {
335 this.view_profile(profile_id.clone(), window, cx);
336 })
337 })
338 .child(
339 ListItem::new(SharedString::from(format!(
340 "profile-{}",
341 profile.id
342 )))
343 .toggle_state(
344 profile
345 .navigation
346 .focus_handle
347 .contains_focused(window, cx),
348 )
349 .inset(true)
350 .spacing(ListItemSpacing::Sparse)
351 .child(Label::new(profile.name.clone()))
352 .end_slot(
353 h_flex()
354 .gap_1()
355 .child(Label::new("Customize").size(LabelSize::Small))
356 .children(KeyBinding::for_action_in(
357 &menu::Confirm,
358 &self.focus_handle,
359 window,
360 cx,
361 )),
362 )
363 .on_click({
364 let profile_id = profile.id.clone();
365 cx.listener(move |this, _, window, cx| {
366 this.new_profile(Some(profile_id.clone()), window, cx);
367 })
368 }),
369 )
370 }))
371 .child(ListSeparator)
372 .child(
373 div()
374 .id("new-profile")
375 .track_focus(&mode.add_new_profile.focus_handle)
376 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
377 this.new_profile(None, window, cx);
378 }))
379 .child(
380 ListItem::new("new-profile")
381 .toggle_state(
382 mode.add_new_profile
383 .focus_handle
384 .contains_focused(window, cx),
385 )
386 .inset(true)
387 .spacing(ListItemSpacing::Sparse)
388 .start_slot(Icon::new(IconName::Plus))
389 .child(Label::new("Add New Profile"))
390 .on_click({
391 cx.listener(move |this, _, window, cx| {
392 this.new_profile(None, window, cx);
393 })
394 }),
395 ),
396 ),
397 )
398 .into_any_element(),
399 )
400 .map(|mut navigable| {
401 for profile in mode.profiles {
402 navigable = navigable.entry(profile.navigation);
403 }
404
405 navigable
406 })
407 .entry(mode.add_new_profile)
408 }
409
410 fn render_new_profile(
411 &mut self,
412 mode: NewProfileMode,
413 _window: &mut Window,
414 cx: &mut Context<Self>,
415 ) -> impl IntoElement {
416 let settings = AssistantSettings::get_global(cx);
417
418 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
419 settings
420 .profiles
421 .get(base_profile_id)
422 .map(|profile| profile.name.clone())
423 .unwrap_or_else(|| "Unknown".into())
424 });
425
426 v_flex()
427 .id("new-profile")
428 .track_focus(&self.focus_handle(cx))
429 .child(ProfileModalHeader::new(
430 match base_profile_name {
431 Some(base_profile) => format!("Fork {base_profile}"),
432 None => "New Profile".into(),
433 },
434 IconName::Plus,
435 ))
436 .child(ListSeparator)
437 .child(h_flex().p_2().child(mode.name_editor.clone()))
438 }
439
440 fn render_view_profile(
441 &mut self,
442 mode: ViewProfileMode,
443 window: &mut Window,
444 cx: &mut Context<Self>,
445 ) -> impl IntoElement {
446 let settings = AssistantSettings::get_global(cx);
447
448 let profile_name = settings
449 .profiles
450 .get(&mode.profile_id)
451 .map(|profile| profile.name.clone())
452 .unwrap_or_else(|| "Unknown".into());
453
454 Navigable::new(
455 div()
456 .track_focus(&self.focus_handle(cx))
457 .size_full()
458 .child(ProfileModalHeader::new(
459 profile_name,
460 IconName::ZedAssistant,
461 ))
462 .child(
463 v_flex()
464 .pb_1()
465 .child(ListSeparator)
466 .child(
467 div()
468 .id("fork-profile")
469 .track_focus(&mode.fork_profile.focus_handle)
470 .on_action({
471 let profile_id = mode.profile_id.clone();
472 cx.listener(move |this, _: &menu::Confirm, window, cx| {
473 this.new_profile(Some(profile_id.clone()), window, cx);
474 })
475 })
476 .child(
477 ListItem::new("fork-profile")
478 .toggle_state(
479 mode.fork_profile
480 .focus_handle
481 .contains_focused(window, cx),
482 )
483 .inset(true)
484 .spacing(ListItemSpacing::Sparse)
485 .start_slot(Icon::new(IconName::GitBranch))
486 .child(Label::new("Fork Profile"))
487 .on_click({
488 let profile_id = mode.profile_id.clone();
489 cx.listener(move |this, _, window, cx| {
490 this.new_profile(
491 Some(profile_id.clone()),
492 window,
493 cx,
494 );
495 })
496 }),
497 ),
498 )
499 .child(
500 div()
501 .id("configure-tools")
502 .track_focus(&mode.configure_tools.focus_handle)
503 .on_action({
504 let profile_id = mode.profile_id.clone();
505 cx.listener(move |this, _: &menu::Confirm, window, cx| {
506 this.configure_tools(profile_id.clone(), window, cx);
507 })
508 })
509 .child(
510 ListItem::new("configure-tools")
511 .toggle_state(
512 mode.configure_tools
513 .focus_handle
514 .contains_focused(window, cx),
515 )
516 .inset(true)
517 .spacing(ListItemSpacing::Sparse)
518 .start_slot(Icon::new(IconName::Cog))
519 .child(Label::new("Configure Tools"))
520 .on_click({
521 let profile_id = mode.profile_id.clone();
522 cx.listener(move |this, _, window, cx| {
523 this.configure_tools(
524 profile_id.clone(),
525 window,
526 cx,
527 );
528 })
529 }),
530 ),
531 ),
532 )
533 .into_any_element(),
534 )
535 .entry(mode.fork_profile)
536 .entry(mode.configure_tools)
537 }
538}
539
540impl Render for ManageProfilesModal {
541 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
542 let settings = AssistantSettings::get_global(cx);
543
544 div()
545 .elevation_3(cx)
546 .w(rems(34.))
547 .key_context("ManageProfilesModal")
548 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
549 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
550 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
551 this.focus_handle(cx).focus(window);
552 }))
553 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
554 .child(match &self.mode {
555 Mode::ChooseProfile(mode) => self
556 .render_choose_profile(mode.clone(), window, cx)
557 .into_any_element(),
558 Mode::NewProfile(mode) => self
559 .render_new_profile(mode.clone(), window, cx)
560 .into_any_element(),
561 Mode::ViewProfile(mode) => self
562 .render_view_profile(mode.clone(), window, cx)
563 .into_any_element(),
564 Mode::ConfigureTools {
565 profile_id,
566 tool_picker,
567 ..
568 } => {
569 let profile_name = settings
570 .profiles
571 .get(profile_id)
572 .map(|profile| profile.name.clone())
573 .unwrap_or_else(|| "Unknown".into());
574
575 div()
576 .child(ProfileModalHeader::new(
577 format!("{profile_name}: Configure Tools"),
578 IconName::Cog,
579 ))
580 .child(ListSeparator)
581 .child(tool_picker.clone())
582 .into_any_element()
583 }
584 })
585 }
586}