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::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
 14use settings::{update_settings_file, Settings as _};
 15use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
 16use workspace::{ModalView, Workspace};
 17
 18use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
 19use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
 20use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
 21use crate::{AssistantPanel, ManageProfiles};
 22
 23enum Mode {
 24    ChooseProfile {
 25        profile_picker: Entity<ProfilePicker>,
 26        _subscription: Subscription,
 27    },
 28    NewProfile(NewProfileMode),
 29    ViewProfile(ViewProfileMode),
 30    ConfigureTools {
 31        profile_id: Arc<str>,
 32        tool_picker: Entity<ToolPicker>,
 33        _subscription: Subscription,
 34    },
 35}
 36
 37impl Mode {
 38    pub fn choose_profile(window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
 39        let this = cx.entity();
 40
 41        let profile_picker = cx.new(|cx| {
 42            let delegate = ProfilePickerDelegate::new(
 43                move |profile_id, window, cx| {
 44                    this.update(cx, |this, cx| {
 45                        this.view_profile(profile_id.clone(), window, cx);
 46                    })
 47                },
 48                cx,
 49            );
 50            ProfilePicker::new(delegate, window, cx)
 51        });
 52        let dismiss_subscription = cx.subscribe_in(
 53            &profile_picker,
 54            window,
 55            |_this, _profile_picker, _: &DismissEvent, _window, cx| {
 56                cx.emit(DismissEvent);
 57            },
 58        );
 59
 60        Self::ChooseProfile {
 61            profile_picker,
 62            _subscription: dismiss_subscription,
 63        }
 64    }
 65}
 66
 67#[derive(Clone)]
 68pub struct ViewProfileMode {
 69    profile_id: Arc<str>,
 70    fork_profile: NavigableEntry,
 71    configure_tools: NavigableEntry,
 72}
 73
 74#[derive(Clone)]
 75pub struct NewProfileMode {
 76    name_editor: Entity<Editor>,
 77    base_profile_id: Option<Arc<str>>,
 78}
 79
 80pub struct ManageProfilesModal {
 81    fs: Arc<dyn Fs>,
 82    tools: Arc<ToolWorkingSet>,
 83    focus_handle: FocusHandle,
 84    mode: Mode,
 85}
 86
 87impl ManageProfilesModal {
 88    pub fn register(
 89        workspace: &mut Workspace,
 90        _window: Option<&mut Window>,
 91        _cx: &mut Context<Workspace>,
 92    ) {
 93        workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
 94            if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 95                let fs = workspace.app_state().fs.clone();
 96                let thread_store = panel.read(cx).thread_store().read(cx);
 97                let tools = thread_store.tools();
 98                workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
 99            }
100        });
101    }
102
103    pub fn new(
104        fs: Arc<dyn Fs>,
105        tools: Arc<ToolWorkingSet>,
106        window: &mut Window,
107        cx: &mut Context<Self>,
108    ) -> Self {
109        let focus_handle = cx.focus_handle();
110
111        Self {
112            fs,
113            tools,
114            focus_handle,
115            mode: Mode::choose_profile(window, cx),
116        }
117    }
118
119    fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
120        self.mode = Mode::choose_profile(window, cx);
121        self.focus_handle(cx).focus(window);
122    }
123
124    fn new_profile(
125        &mut self,
126        base_profile_id: Option<Arc<str>>,
127        window: &mut Window,
128        cx: &mut Context<Self>,
129    ) {
130        let name_editor = cx.new(|cx| Editor::single_line(window, cx));
131        name_editor.update(cx, |editor, cx| {
132            editor.set_placeholder_text("Profile name", cx);
133        });
134
135        self.mode = Mode::NewProfile(NewProfileMode {
136            name_editor,
137            base_profile_id,
138        });
139        self.focus_handle(cx).focus(window);
140    }
141
142    pub fn view_profile(
143        &mut self,
144        profile_id: Arc<str>,
145        window: &mut Window,
146        cx: &mut Context<Self>,
147    ) {
148        self.mode = Mode::ViewProfile(ViewProfileMode {
149            profile_id,
150            fork_profile: NavigableEntry::focusable(cx),
151            configure_tools: NavigableEntry::focusable(cx),
152        });
153        self.focus_handle(cx).focus(window);
154    }
155
156    fn configure_tools(
157        &mut self,
158        profile_id: Arc<str>,
159        window: &mut Window,
160        cx: &mut Context<Self>,
161    ) {
162        let settings = AssistantSettings::get_global(cx);
163        let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
164            return;
165        };
166
167        let tool_picker = cx.new(|cx| {
168            let delegate = ToolPickerDelegate::new(
169                self.fs.clone(),
170                self.tools.clone(),
171                profile_id.clone(),
172                profile,
173                cx,
174            );
175            ToolPicker::new(delegate, window, cx)
176        });
177        let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
178            let profile_id = profile_id.clone();
179            move |this, _tool_picker, _: &DismissEvent, window, cx| {
180                this.view_profile(profile_id.clone(), window, cx);
181            }
182        });
183
184        self.mode = Mode::ConfigureTools {
185            profile_id,
186            tool_picker,
187            _subscription: dismiss_subscription,
188        };
189        self.focus_handle(cx).focus(window);
190    }
191
192    fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
193        match &self.mode {
194            Mode::ChooseProfile { .. } => {}
195            Mode::NewProfile(mode) => {
196                let settings = AssistantSettings::get_global(cx);
197
198                let base_profile = mode
199                    .base_profile_id
200                    .as_ref()
201                    .and_then(|profile_id| settings.profiles.get(profile_id).cloned());
202
203                let name = mode.name_editor.read(cx).text(cx);
204                let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
205
206                let profile = AgentProfile {
207                    name: name.into(),
208                    tools: base_profile
209                        .as_ref()
210                        .map(|profile| profile.tools.clone())
211                        .unwrap_or_default(),
212                    context_servers: base_profile
213                        .map(|profile| profile.context_servers)
214                        .unwrap_or_default(),
215                };
216
217                self.create_profile(profile_id.clone(), profile, cx);
218                self.view_profile(profile_id, window, cx);
219            }
220            Mode::ViewProfile(_) => {}
221            Mode::ConfigureTools { .. } => {}
222        }
223    }
224
225    fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
226        match &self.mode {
227            Mode::ChooseProfile { .. } => {}
228            Mode::NewProfile(mode) => {
229                if let Some(profile_id) = mode.base_profile_id.clone() {
230                    self.view_profile(profile_id, window, cx);
231                } else {
232                    self.choose_profile(window, cx);
233                }
234            }
235            Mode::ViewProfile(_) => self.choose_profile(window, cx),
236            Mode::ConfigureTools { .. } => {}
237        }
238    }
239
240    fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
241        update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
242            move |settings, _cx| match settings {
243                AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
244                    settings,
245                )) => {
246                    let profiles = settings.profiles.get_or_insert_default();
247                    if profiles.contains_key(&profile_id) {
248                        log::error!("profile with ID '{profile_id}' already exists");
249                        return;
250                    }
251
252                    profiles.insert(
253                        profile_id,
254                        AgentProfileContent {
255                            name: profile.name.into(),
256                            tools: profile.tools,
257                            context_servers: profile
258                                .context_servers
259                                .into_iter()
260                                .map(|(server_id, preset)| {
261                                    (
262                                        server_id,
263                                        ContextServerPresetContent {
264                                            tools: preset.tools,
265                                        },
266                                    )
267                                })
268                                .collect(),
269                        },
270                    );
271                }
272                _ => {}
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 { profile_picker, .. } => profile_picker.focus_handle(cx),
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_new_profile(
295        &mut self,
296        mode: NewProfileMode,
297        _window: &mut Window,
298        cx: &mut Context<Self>,
299    ) -> impl IntoElement {
300        v_flex()
301            .id("new-profile")
302            .track_focus(&self.focus_handle(cx))
303            .child(h_flex().p_2().child(mode.name_editor.clone()))
304    }
305
306    fn render_view_profile(
307        &mut self,
308        mode: ViewProfileMode,
309        window: &mut Window,
310        cx: &mut Context<Self>,
311    ) -> impl IntoElement {
312        let settings = AssistantSettings::get_global(cx);
313
314        let profile_name = settings
315            .profiles
316            .get(&mode.profile_id)
317            .map(|profile| profile.name.clone())
318            .unwrap_or_else(|| "Unknown".into());
319
320        Navigable::new(
321            div()
322                .track_focus(&self.focus_handle(cx))
323                .size_full()
324                .child(ProfileModalHeader::new(
325                    profile_name,
326                    IconName::ZedAssistant,
327                ))
328                .child(
329                    v_flex()
330                        .pb_1()
331                        .child(ListSeparator)
332                        .child(
333                            div()
334                                .id("fork-profile")
335                                .track_focus(&mode.fork_profile.focus_handle)
336                                .on_action({
337                                    let profile_id = mode.profile_id.clone();
338                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
339                                        this.new_profile(Some(profile_id.clone()), window, cx);
340                                    })
341                                })
342                                .child(
343                                    ListItem::new("fork-profile")
344                                        .toggle_state(
345                                            mode.fork_profile
346                                                .focus_handle
347                                                .contains_focused(window, cx),
348                                        )
349                                        .inset(true)
350                                        .spacing(ListItemSpacing::Sparse)
351                                        .start_slot(Icon::new(IconName::GitBranch))
352                                        .child(Label::new("Fork Profile"))
353                                        .on_click({
354                                            let profile_id = mode.profile_id.clone();
355                                            cx.listener(move |this, _, window, cx| {
356                                                this.new_profile(
357                                                    Some(profile_id.clone()),
358                                                    window,
359                                                    cx,
360                                                );
361                                            })
362                                        }),
363                                ),
364                        )
365                        .child(
366                            div()
367                                .id("configure-tools")
368                                .track_focus(&mode.configure_tools.focus_handle)
369                                .on_action({
370                                    let profile_id = mode.profile_id.clone();
371                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
372                                        this.configure_tools(profile_id.clone(), window, cx);
373                                    })
374                                })
375                                .child(
376                                    ListItem::new("configure-tools")
377                                        .toggle_state(
378                                            mode.configure_tools
379                                                .focus_handle
380                                                .contains_focused(window, cx),
381                                        )
382                                        .inset(true)
383                                        .spacing(ListItemSpacing::Sparse)
384                                        .start_slot(Icon::new(IconName::Cog))
385                                        .child(Label::new("Configure Tools"))
386                                        .on_click({
387                                            let profile_id = mode.profile_id.clone();
388                                            cx.listener(move |this, _, window, cx| {
389                                                this.configure_tools(
390                                                    profile_id.clone(),
391                                                    window,
392                                                    cx,
393                                                );
394                                            })
395                                        }),
396                                ),
397                        ),
398                )
399                .into_any_element(),
400        )
401        .entry(mode.fork_profile)
402        .entry(mode.configure_tools)
403    }
404}
405
406impl Render for ManageProfilesModal {
407    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
408        let settings = AssistantSettings::get_global(cx);
409
410        div()
411            .elevation_3(cx)
412            .w(rems(34.))
413            .key_context("ManageProfilesModal")
414            .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
415            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
416            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
417                this.focus_handle(cx).focus(window);
418            }))
419            .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
420            .child(match &self.mode {
421                Mode::ChooseProfile { profile_picker, .. } => div()
422                    .child(ProfileModalHeader::new("Profiles", IconName::ZedAssistant))
423                    .child(ListSeparator)
424                    .child(profile_picker.clone())
425                    .into_any_element(),
426                Mode::NewProfile(mode) => self
427                    .render_new_profile(mode.clone(), window, cx)
428                    .into_any_element(),
429                Mode::ViewProfile(mode) => self
430                    .render_view_profile(mode.clone(), window, cx)
431                    .into_any_element(),
432                Mode::ConfigureTools {
433                    profile_id,
434                    tool_picker,
435                    ..
436                } => {
437                    let profile_name = settings
438                        .profiles
439                        .get(profile_id)
440                        .map(|profile| profile.name.clone())
441                        .unwrap_or_else(|| "Unknown".into());
442
443                    div()
444                        .child(ProfileModalHeader::new(
445                            format!("{profile_name}: Configure Tools"),
446                            IconName::Cog,
447                        ))
448                        .child(ListSeparator)
449                        .child(tool_picker.clone())
450                        .into_any_element()
451                }
452            })
453    }
454}