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