manage_profiles_modal.rs

  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}