1mod profile_modal_header;
2
3use std::sync::Arc;
4
5use assistant_settings::{AgentProfile, AgentProfileId, 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: AgentProfileId,
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: AgentProfileId,
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: AgentProfileId,
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<AgentProfileId>,
83}
84
85pub struct ManageProfilesModal {
86 fs: Arc<dyn Fs>,
87 tools: Entity<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: Entity<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<AgentProfileId>,
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: AgentProfileId,
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: AgentProfileId,
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 = AgentProfileId(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 enable_all_context_servers: base_profile
231 .as_ref()
232 .map(|profile| profile.enable_all_context_servers)
233 .unwrap_or_default(),
234 context_servers: base_profile
235 .map(|profile| profile.context_servers)
236 .unwrap_or_default(),
237 };
238
239 self.create_profile(profile_id.clone(), profile, cx);
240 self.view_profile(profile_id, window, cx);
241 }
242 Mode::ViewProfile(_) => {}
243 Mode::ConfigureTools { .. } => {}
244 }
245 }
246
247 fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
248 match &self.mode {
249 Mode::ChooseProfile { .. } => {
250 cx.emit(DismissEvent);
251 }
252 Mode::NewProfile(mode) => {
253 if let Some(profile_id) = mode.base_profile_id.clone() {
254 self.view_profile(profile_id, window, cx);
255 } else {
256 self.choose_profile(window, cx);
257 }
258 }
259 Mode::ViewProfile(_) => self.choose_profile(window, cx),
260 Mode::ConfigureTools { .. } => {}
261 }
262 }
263
264 fn create_profile(
265 &self,
266 profile_id: AgentProfileId,
267 profile: AgentProfile,
268 cx: &mut Context<Self>,
269 ) {
270 update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
271 move |settings, _cx| {
272 settings.create_profile(profile_id, profile).log_err();
273 }
274 });
275 }
276}
277
278impl ModalView for ManageProfilesModal {}
279
280impl Focusable for ManageProfilesModal {
281 fn focus_handle(&self, cx: &App) -> FocusHandle {
282 match &self.mode {
283 Mode::ChooseProfile(_) => self.focus_handle.clone(),
284 Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
285 Mode::ViewProfile(_) => self.focus_handle.clone(),
286 Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
287 }
288 }
289}
290
291impl EventEmitter<DismissEvent> for ManageProfilesModal {}
292
293impl ManageProfilesModal {
294 fn render_choose_profile(
295 &mut self,
296 mode: ChooseProfileMode,
297 window: &mut Window,
298 cx: &mut Context<Self>,
299 ) -> impl IntoElement {
300 Navigable::new(
301 div()
302 .track_focus(&self.focus_handle(cx))
303 .size_full()
304 .child(ProfileModalHeader::new(
305 "Agent Profiles",
306 IconName::ZedAssistant,
307 ))
308 .child(
309 v_flex()
310 .pb_1()
311 .child(ListSeparator)
312 .children(mode.profiles.iter().map(|profile| {
313 div()
314 .id(SharedString::from(format!("profile-{}", profile.id)))
315 .track_focus(&profile.navigation.focus_handle)
316 .on_action({
317 let profile_id = profile.id.clone();
318 cx.listener(move |this, _: &menu::Confirm, window, cx| {
319 this.view_profile(profile_id.clone(), window, cx);
320 })
321 })
322 .child(
323 ListItem::new(SharedString::from(format!(
324 "profile-{}",
325 profile.id
326 )))
327 .toggle_state(
328 profile
329 .navigation
330 .focus_handle
331 .contains_focused(window, cx),
332 )
333 .inset(true)
334 .spacing(ListItemSpacing::Sparse)
335 .child(Label::new(profile.name.clone()))
336 .end_slot(
337 h_flex()
338 .gap_1()
339 .child(Label::new("Customize").size(LabelSize::Small))
340 .children(KeyBinding::for_action_in(
341 &menu::Confirm,
342 &self.focus_handle,
343 window,
344 cx,
345 )),
346 )
347 .on_click({
348 let profile_id = profile.id.clone();
349 cx.listener(move |this, _, window, cx| {
350 this.view_profile(profile_id.clone(), window, cx);
351 })
352 }),
353 )
354 }))
355 .child(ListSeparator)
356 .child(
357 div()
358 .id("new-profile")
359 .track_focus(&mode.add_new_profile.focus_handle)
360 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
361 this.new_profile(None, window, cx);
362 }))
363 .child(
364 ListItem::new("new-profile")
365 .toggle_state(
366 mode.add_new_profile
367 .focus_handle
368 .contains_focused(window, cx),
369 )
370 .inset(true)
371 .spacing(ListItemSpacing::Sparse)
372 .start_slot(Icon::new(IconName::Plus))
373 .child(Label::new("Add New Profile"))
374 .on_click({
375 cx.listener(move |this, _, window, cx| {
376 this.new_profile(None, window, cx);
377 })
378 }),
379 ),
380 ),
381 )
382 .into_any_element(),
383 )
384 .map(|mut navigable| {
385 for profile in mode.profiles {
386 navigable = navigable.entry(profile.navigation);
387 }
388
389 navigable
390 })
391 .entry(mode.add_new_profile)
392 }
393
394 fn render_new_profile(
395 &mut self,
396 mode: NewProfileMode,
397 _window: &mut Window,
398 cx: &mut Context<Self>,
399 ) -> impl IntoElement {
400 let settings = AssistantSettings::get_global(cx);
401
402 let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
403 settings
404 .profiles
405 .get(base_profile_id)
406 .map(|profile| profile.name.clone())
407 .unwrap_or_else(|| "Unknown".into())
408 });
409
410 v_flex()
411 .id("new-profile")
412 .track_focus(&self.focus_handle(cx))
413 .child(ProfileModalHeader::new(
414 match base_profile_name {
415 Some(base_profile) => format!("Fork {base_profile}"),
416 None => "New Profile".into(),
417 },
418 IconName::Plus,
419 ))
420 .child(ListSeparator)
421 .child(h_flex().p_2().child(mode.name_editor.clone()))
422 }
423
424 fn render_view_profile(
425 &mut self,
426 mode: ViewProfileMode,
427 window: &mut Window,
428 cx: &mut Context<Self>,
429 ) -> impl IntoElement {
430 let settings = AssistantSettings::get_global(cx);
431
432 let profile_name = settings
433 .profiles
434 .get(&mode.profile_id)
435 .map(|profile| profile.name.clone())
436 .unwrap_or_else(|| "Unknown".into());
437
438 Navigable::new(
439 div()
440 .track_focus(&self.focus_handle(cx))
441 .size_full()
442 .child(ProfileModalHeader::new(
443 profile_name,
444 IconName::ZedAssistant,
445 ))
446 .child(
447 v_flex()
448 .pb_1()
449 .child(ListSeparator)
450 .child(
451 div()
452 .id("fork-profile")
453 .track_focus(&mode.fork_profile.focus_handle)
454 .on_action({
455 let profile_id = mode.profile_id.clone();
456 cx.listener(move |this, _: &menu::Confirm, window, cx| {
457 this.new_profile(Some(profile_id.clone()), window, cx);
458 })
459 })
460 .child(
461 ListItem::new("fork-profile")
462 .toggle_state(
463 mode.fork_profile
464 .focus_handle
465 .contains_focused(window, cx),
466 )
467 .inset(true)
468 .spacing(ListItemSpacing::Sparse)
469 .start_slot(Icon::new(IconName::GitBranch))
470 .child(Label::new("Fork Profile"))
471 .on_click({
472 let profile_id = mode.profile_id.clone();
473 cx.listener(move |this, _, window, cx| {
474 this.new_profile(
475 Some(profile_id.clone()),
476 window,
477 cx,
478 );
479 })
480 }),
481 ),
482 )
483 .child(
484 div()
485 .id("configure-tools")
486 .track_focus(&mode.configure_tools.focus_handle)
487 .on_action({
488 let profile_id = mode.profile_id.clone();
489 cx.listener(move |this, _: &menu::Confirm, window, cx| {
490 this.configure_tools(profile_id.clone(), window, cx);
491 })
492 })
493 .child(
494 ListItem::new("configure-tools")
495 .toggle_state(
496 mode.configure_tools
497 .focus_handle
498 .contains_focused(window, cx),
499 )
500 .inset(true)
501 .spacing(ListItemSpacing::Sparse)
502 .start_slot(Icon::new(IconName::Cog))
503 .child(Label::new("Configure Tools"))
504 .on_click({
505 let profile_id = mode.profile_id.clone();
506 cx.listener(move |this, _, window, cx| {
507 this.configure_tools(
508 profile_id.clone(),
509 window,
510 cx,
511 );
512 })
513 }),
514 ),
515 ),
516 )
517 .into_any_element(),
518 )
519 .entry(mode.fork_profile)
520 .entry(mode.configure_tools)
521 }
522}
523
524impl Render for ManageProfilesModal {
525 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
526 let settings = AssistantSettings::get_global(cx);
527
528 div()
529 .elevation_3(cx)
530 .w(rems(34.))
531 .key_context("ManageProfilesModal")
532 .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
533 .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
534 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
535 this.focus_handle(cx).focus(window);
536 }))
537 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
538 .child(match &self.mode {
539 Mode::ChooseProfile(mode) => self
540 .render_choose_profile(mode.clone(), window, cx)
541 .into_any_element(),
542 Mode::NewProfile(mode) => self
543 .render_new_profile(mode.clone(), window, cx)
544 .into_any_element(),
545 Mode::ViewProfile(mode) => self
546 .render_view_profile(mode.clone(), window, cx)
547 .into_any_element(),
548 Mode::ConfigureTools {
549 profile_id,
550 tool_picker,
551 ..
552 } => {
553 let profile_name = settings
554 .profiles
555 .get(profile_id)
556 .map(|profile| profile.name.clone())
557 .unwrap_or_else(|| "Unknown".into());
558
559 div()
560 .child(ProfileModalHeader::new(
561 format!("{profile_name}: Configure Tools"),
562 IconName::Cog,
563 ))
564 .child(ListSeparator)
565 .child(tool_picker.clone())
566 .into_any_element()
567 }
568 })
569 }
570}