manage_profiles_modal.rs

  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}