diff --git a/Cargo.lock b/Cargo.lock index 52963c5f159983af0fd4772a5ef7aaaebcfdcd8c..611fbc0c217886175c666f0fac0530b9ec6e9b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,6 +459,7 @@ dependencies = [ "collections", "command_palette_hooks", "context_server", + "convert_case 0.8.0", "db", "editor", "feature_flags", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index d198bd19ae94aa26cbafeaabe1a5bb78b4e4a7e2..411fe508c08c6baf3a5ebc5a5415ca9569d3e528 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -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 diff --git a/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs index 05fadb1aed30335044b5326ffb792e234214feb6..9c02b27b50331e517e19bb0610f580a5c2e58b65 100644 --- a/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs +++ b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs @@ -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, _subscription: Subscription, }, + NewProfile(NewProfileMode), ViewProfile(ViewProfileMode), ConfigureTools { + profile_id: Arc, tool_picker: Entity, _subscription: Subscription, }, @@ -57,9 +67,16 @@ impl Mode { #[derive(Clone)] pub struct ViewProfileMode { profile_id: Arc, + fork_profile: NavigableEntry, configure_tools: NavigableEntry, } +#[derive(Clone)] +pub struct NewProfileMode { + name_editor: Entity, + base_profile_id: Option>, +} + pub struct ManageProfilesModal { fs: Arc, tools: Arc, @@ -104,6 +121,24 @@ impl ManageProfilesModal { self.focus_handle(cx).focus(window); } + fn new_profile( + &mut self, + base_profile_id: Option>, + window: &mut Window, + cx: &mut Context, + ) { + 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, @@ -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) {} + fn confirm(&mut self, window: &mut Window, cx: &mut Context) { + 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 = 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) { 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, profile: AgentProfile, cx: &mut Context) { + update_settings_file::(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 for ManageProfilesModal {} impl ManageProfilesModal { + fn render_new_profile( + &mut self, + mode: NewProfileMode, + _window: &mut Window, + cx: &mut Context, + ) -> 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, ) -> 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) -> 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() + } }) } } diff --git a/crates/assistant2/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs b/crates/assistant2/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs new file mode 100644 index 0000000000000000000000000000000000000000..401c4a9bfffef679c9247ccbe3ee1de80f89a74d --- /dev/null +++ b/crates/assistant2/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs @@ -0,0 +1,38 @@ +use ui::prelude::*; + +#[derive(IntoElement)] +pub struct ProfileModalHeader { + label: SharedString, + icon: IconName, +} + +impl ProfileModalHeader { + pub fn new(label: impl Into, 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)), + ), + ) + } +} diff --git a/crates/assistant2/src/assistant_configuration/profile_picker.rs b/crates/assistant2/src/assistant_configuration/profile_picker.rs index 3b74570c3d40b5b6d45c956386b323c4d02cb0fe..6ab480df17b37d6edc9f730e8d0883a2baf56cd2 100644 --- a/crates/assistant2/src/assistant_configuration/profile_picker.rs +++ b/crates/assistant2/src/assistant_configuration/profile_picker.rs @@ -21,7 +21,7 @@ impl ProfilePicker { window: &mut Window, cx: &mut Context, ) -> 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 } } } diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs index 76da92d0046bf0dded64540ae4440f541bb0f3c2..57729b64c1de9230392daddda0d221c4b1c30f2a 100644 --- a/crates/ui/src/traits/styled_ext.rs +++ b/crates/ui/src/traits/styled_ext.rs @@ -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) }