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