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