manage_profiles_modal.rs

  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}