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