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, 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}