Cargo.lock 🔗
@@ -459,6 +459,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"context_server",
+ "convert_case 0.8.0",
"db",
"editor",
"feature_flags",
Marshall Bowers created
This PR adds support for forking existing profiles from the manage
profiles modal.
https://github.com/user-attachments/assets/5fa9b76c-fafe-4c72-8843-576c4b5ca2f2
Release Notes:
- N/A
Cargo.lock | 1
crates/assistant2/Cargo.toml | 1
crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs | 278
crates/assistant2/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs | 38
crates/assistant2/src/assistant_configuration/profile_picker.rs | 2
crates/ui/src/traits/styled_ext.rs | 2
6 files changed, 283 insertions(+), 39 deletions(-)
@@ -459,6 +459,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"context_server",
+ "convert_case 0.8.0",
"db",
"editor",
"feature_flags",
@@ -31,6 +31,7 @@ clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
+convert_case.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -1,13 +1,21 @@
+mod profile_modal_header;
+
use std::sync::Arc;
-use assistant_settings::AssistantSettings;
+use assistant_settings::{
+ AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
+ ContextServerPresetContent, VersionedAssistantSettingsContent,
+};
use assistant_tool::ToolWorkingSet;
+use convert_case::{Case, Casing as _};
+use editor::Editor;
use fs::Fs;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
-use settings::Settings as _;
-use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
+use settings::{update_settings_file, Settings as _};
+use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace};
+use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles};
@@ -17,8 +25,10 @@ enum Mode {
profile_picker: Entity<ProfilePicker>,
_subscription: Subscription,
},
+ NewProfile(NewProfileMode),
ViewProfile(ViewProfileMode),
ConfigureTools {
+ profile_id: Arc<str>,
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
@@ -57,9 +67,16 @@ impl Mode {
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: Arc<str>,
+ fork_profile: NavigableEntry,
configure_tools: NavigableEntry,
}
+#[derive(Clone)]
+pub struct NewProfileMode {
+ name_editor: Entity<Editor>,
+ base_profile_id: Option<Arc<str>>,
+}
+
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
@@ -104,6 +121,24 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
+ fn new_profile(
+ &mut self,
+ base_profile_id: Option<Arc<str>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let name_editor = cx.new(|cx| Editor::single_line(window, cx));
+ name_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("Profile name", cx);
+ });
+
+ self.mode = Mode::NewProfile(NewProfileMode {
+ name_editor,
+ base_profile_id,
+ });
+ self.focus_handle(cx).focus(window);
+ }
+
pub fn view_profile(
&mut self,
profile_id: Arc<str>,
@@ -112,6 +147,7 @@ impl ManageProfilesModal {
) {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
+ fork_profile: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
@@ -146,21 +182,97 @@ impl ManageProfilesModal {
});
self.mode = Mode::ConfigureTools {
+ profile_id,
tool_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
- fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
+ fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ match &self.mode {
+ Mode::ChooseProfile { .. } => {}
+ Mode::NewProfile(mode) => {
+ let settings = AssistantSettings::get_global(cx);
+
+ let base_profile = mode
+ .base_profile_id
+ .as_ref()
+ .and_then(|profile_id| settings.profiles.get(profile_id).cloned());
+
+ let name = mode.name_editor.read(cx).text(cx);
+ let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
+
+ let profile = AgentProfile {
+ name: name.into(),
+ tools: base_profile
+ .as_ref()
+ .map(|profile| profile.tools.clone())
+ .unwrap_or_default(),
+ context_servers: base_profile
+ .map(|profile| profile.context_servers)
+ .unwrap_or_default(),
+ };
+
+ self.create_profile(profile_id.clone(), profile, cx);
+ self.view_profile(profile_id, window, cx);
+ }
+ Mode::ViewProfile(_) => {}
+ Mode::ConfigureTools { .. } => {}
+ }
+ }
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {}
+ Mode::NewProfile(mode) => {
+ if let Some(profile_id) = mode.base_profile_id.clone() {
+ self.view_profile(profile_id, window, cx);
+ } else {
+ self.choose_profile(window, cx);
+ }
+ }
Mode::ViewProfile(_) => self.choose_profile(window, cx),
Mode::ConfigureTools { .. } => {}
}
}
+
+ fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
+ update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
+ move |settings, _cx| match settings {
+ AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
+ settings,
+ )) => {
+ let profiles = settings.profiles.get_or_insert_default();
+ if profiles.contains_key(&profile_id) {
+ log::error!("profile with ID '{profile_id}' already exists");
+ return;
+ }
+
+ profiles.insert(
+ profile_id,
+ AgentProfileContent {
+ name: profile.name.into(),
+ tools: profile.tools,
+ context_servers: profile
+ .context_servers
+ .into_iter()
+ .map(|(server_id, preset)| {
+ (
+ server_id,
+ ContextServerPresetContent {
+ tools: preset.tools,
+ },
+ )
+ })
+ .collect(),
+ },
+ );
+ }
+ _ => {}
+ }
+ });
+ }
}
impl ModalView for ManageProfilesModal {}
@@ -169,8 +281,9 @@ impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile { profile_picker, .. } => profile_picker.focus_handle(cx),
- Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
+ Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
+ Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
}
}
}
@@ -178,55 +291,122 @@ impl Focusable for ManageProfilesModal {
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
+ fn render_new_profile(
+ &mut self,
+ mode: NewProfileMode,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ v_flex()
+ .id("new-profile")
+ .track_focus(&self.focus_handle(cx))
+ .child(h_flex().p_2().child(mode.name_editor.clone()))
+ }
+
fn render_view_profile(
&mut self,
mode: ViewProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
+ let settings = AssistantSettings::get_global(cx);
+
+ let profile_name = settings
+ .profiles
+ .get(&mode.profile_id)
+ .map(|profile| profile.name.clone())
+ .unwrap_or_else(|| "Unknown".into());
+
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
+ .child(ProfileModalHeader::new(
+ profile_name,
+ IconName::ZedAssistant,
+ ))
.child(
- v_flex().child(
- div()
- .id("configure-tools")
- .track_focus(&mode.configure_tools.focus_handle)
- .on_action({
- let profile_id = mode.profile_id.clone();
- cx.listener(move |this, _: &menu::Confirm, window, cx| {
- this.configure_tools(profile_id.clone(), window, cx);
+ v_flex()
+ .pb_1()
+ .child(ListSeparator)
+ .child(
+ div()
+ .id("fork-profile")
+ .track_focus(&mode.fork_profile.focus_handle)
+ .on_action({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.new_profile(Some(profile_id.clone()), window, cx);
+ })
})
- })
- .child(
- ListItem::new("configure-tools")
- .toggle_state(
- mode.configure_tools
- .focus_handle
- .contains_focused(window, cx),
- )
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .start_slot(Icon::new(IconName::Cog))
- .child(Label::new("Configure Tools"))
- .on_click({
- let profile_id = mode.profile_id.clone();
- cx.listener(move |this, _, window, cx| {
- this.configure_tools(profile_id.clone(), window, cx);
- })
- }),
- ),
- ),
+ .child(
+ ListItem::new("fork-profile")
+ .toggle_state(
+ mode.fork_profile
+ .focus_handle
+ .contains_focused(window, cx),
+ )
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(Icon::new(IconName::GitBranch))
+ .child(Label::new("Fork Profile"))
+ .on_click({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.new_profile(
+ Some(profile_id.clone()),
+ window,
+ cx,
+ );
+ })
+ }),
+ ),
+ )
+ .child(
+ div()
+ .id("configure-tools")
+ .track_focus(&mode.configure_tools.focus_handle)
+ .on_action({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.configure_tools(profile_id.clone(), window, cx);
+ })
+ })
+ .child(
+ ListItem::new("configure-tools")
+ .toggle_state(
+ mode.configure_tools
+ .focus_handle
+ .contains_focused(window, cx),
+ )
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(Icon::new(IconName::Cog))
+ .child(Label::new("Configure Tools"))
+ .on_click({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.configure_tools(
+ profile_id.clone(),
+ window,
+ cx,
+ );
+ })
+ }),
+ ),
+ ),
)
.into_any_element(),
)
+ .entry(mode.fork_profile)
.entry(mode.configure_tools)
}
}
impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let settings = AssistantSettings::get_global(cx);
+
div()
.elevation_3(cx)
.w(rems(34.))
@@ -238,13 +418,37 @@ impl Render for ManageProfilesModal {
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
- Mode::ChooseProfile { profile_picker, .. } => {
- profile_picker.clone().into_any_element()
- }
+ Mode::ChooseProfile { profile_picker, .. } => div()
+ .child(ProfileModalHeader::new("Profiles", IconName::ZedAssistant))
+ .child(ListSeparator)
+ .child(profile_picker.clone())
+ .into_any_element(),
+ Mode::NewProfile(mode) => self
+ .render_new_profile(mode.clone(), window, cx)
+ .into_any_element(),
Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx)
.into_any_element(),
- Mode::ConfigureTools { tool_picker, .. } => tool_picker.clone().into_any_element(),
+ Mode::ConfigureTools {
+ profile_id,
+ tool_picker,
+ ..
+ } => {
+ let profile_name = settings
+ .profiles
+ .get(profile_id)
+ .map(|profile| profile.name.clone())
+ .unwrap_or_else(|| "Unknown".into());
+
+ div()
+ .child(ProfileModalHeader::new(
+ format!("{profile_name}: Configure Tools"),
+ IconName::Cog,
+ ))
+ .child(ListSeparator)
+ .child(tool_picker.clone())
+ .into_any_element()
+ }
})
}
}
@@ -0,0 +1,38 @@
+use ui::prelude::*;
+
+#[derive(IntoElement)]
+pub struct ProfileModalHeader {
+ label: SharedString,
+ icon: IconName,
+}
+
+impl ProfileModalHeader {
+ pub fn new(label: impl Into<SharedString>, icon: IconName) -> Self {
+ Self {
+ label: label.into(),
+ icon,
+ }
+ }
+}
+
+impl RenderOnce for ProfileModalHeader {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ h_flex()
+ .w_full()
+ .px(DynamicSpacing::Base12.rems(cx))
+ .pt(DynamicSpacing::Base08.rems(cx))
+ .pb(DynamicSpacing::Base04.rems(cx))
+ .rounded_t_sm()
+ .gap_1p5()
+ .child(Icon::new(self.icon).size(IconSize::XSmall))
+ .child(
+ h_flex().gap_1().overflow_x_hidden().child(
+ div()
+ .max_w_96()
+ .overflow_x_hidden()
+ .text_ellipsis()
+ .child(Headline::new(self.label).size(HeadlineSize::XSmall)),
+ ),
+ )
+ }
+}
@@ -21,7 +21,7 @@ impl ProfilePicker {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
}
@@ -74,7 +74,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
- fn elevation_3(self, cx: &mut App) -> Self {
+ fn elevation_3(self, cx: &App) -> Self {
elevated(self, cx, ElevationIndex::ModalSurface)
}