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