assistant2: Add support for forking existing profiles (#27627)

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
This commit is contained in:
Marshall Bowers 2025-03-27 16:17:42 -04:00 committed by GitHub
parent 12a8b850ef
commit 3b158461be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 283 additions and 39 deletions

1
Cargo.lock generated
View file

@ -459,6 +459,7 @@ dependencies = [
"collections", "collections",
"command_palette_hooks", "command_palette_hooks",
"context_server", "context_server",
"convert_case 0.8.0",
"db", "db",
"editor", "editor",
"feature_flags", "feature_flags",

View file

@ -31,6 +31,7 @@ clock.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
context_server.workspace = true context_server.workspace = true
convert_case.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true

View file

@ -1,13 +1,21 @@
mod profile_modal_header;
use std::sync::Arc; use std::sync::Arc;
use assistant_settings::AssistantSettings; use assistant_settings::{
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs; use fs::Fs;
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription}; use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use settings::Settings as _; use settings::{update_settings_file, Settings as _};
use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry}; use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace}; 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::profile_picker::{ProfilePicker, ProfilePickerDelegate};
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles}; use crate::{AssistantPanel, ManageProfiles};
@ -17,8 +25,10 @@ enum Mode {
profile_picker: Entity<ProfilePicker>, profile_picker: Entity<ProfilePicker>,
_subscription: Subscription, _subscription: Subscription,
}, },
NewProfile(NewProfileMode),
ViewProfile(ViewProfileMode), ViewProfile(ViewProfileMode),
ConfigureTools { ConfigureTools {
profile_id: Arc<str>,
tool_picker: Entity<ToolPicker>, tool_picker: Entity<ToolPicker>,
_subscription: Subscription, _subscription: Subscription,
}, },
@ -57,9 +67,16 @@ impl Mode {
#[derive(Clone)] #[derive(Clone)]
pub struct ViewProfileMode { pub struct ViewProfileMode {
profile_id: Arc<str>, profile_id: Arc<str>,
fork_profile: NavigableEntry,
configure_tools: NavigableEntry, configure_tools: NavigableEntry,
} }
#[derive(Clone)]
pub struct NewProfileMode {
name_editor: Entity<Editor>,
base_profile_id: Option<Arc<str>>,
}
pub struct ManageProfilesModal { pub struct ManageProfilesModal {
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>, tools: Arc<ToolWorkingSet>,
@ -104,6 +121,24 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window); 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( pub fn view_profile(
&mut self, &mut self,
profile_id: Arc<str>, profile_id: Arc<str>,
@ -112,6 +147,7 @@ impl ManageProfilesModal {
) { ) {
self.mode = Mode::ViewProfile(ViewProfileMode { self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id, profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx), configure_tools: NavigableEntry::focusable(cx),
}); });
self.focus_handle(cx).focus(window); self.focus_handle(cx).focus(window);
@ -146,21 +182,97 @@ impl ManageProfilesModal {
}); });
self.mode = Mode::ConfigureTools { self.mode = Mode::ConfigureTools {
profile_id,
tool_picker, tool_picker,
_subscription: dismiss_subscription, _subscription: dismiss_subscription,
}; };
self.focus_handle(cx).focus(window); 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>) { fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode { match &self.mode {
Mode::ChooseProfile { .. } => {} 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::ViewProfile(_) => self.choose_profile(window, cx),
Mode::ConfigureTools { .. } => {} 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 {} impl ModalView for ManageProfilesModal {}
@ -169,8 +281,9 @@ impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode { match &self.mode {
Mode::ChooseProfile { profile_picker, .. } => profile_picker.focus_handle(cx), 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::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 EventEmitter<DismissEvent> for ManageProfilesModal {}
impl 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( fn render_view_profile(
&mut self, &mut self,
mode: ViewProfileMode, mode: ViewProfileMode,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> 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( Navigable::new(
div() div()
.track_focus(&self.focus_handle(cx)) .track_focus(&self.focus_handle(cx))
.size_full() .size_full()
.child(ProfileModalHeader::new(
profile_name,
IconName::ZedAssistant,
))
.child( .child(
v_flex().child( v_flex()
div() .pb_1()
.id("configure-tools") .child(ListSeparator)
.track_focus(&mode.configure_tools.focus_handle) .child(
.on_action({ div()
let profile_id = mode.profile_id.clone(); .id("fork-profile")
cx.listener(move |this, _: &menu::Confirm, window, cx| { .track_focus(&mode.fork_profile.focus_handle)
this.configure_tools(profile_id.clone(), window, cx); .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(
.child( ListItem::new("fork-profile")
ListItem::new("configure-tools") .toggle_state(
.toggle_state( mode.fork_profile
mode.configure_tools .focus_handle
.focus_handle .contains_focused(window, cx),
.contains_focused(window, cx), )
) .inset(true)
.inset(true) .spacing(ListItemSpacing::Sparse)
.spacing(ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::GitBranch))
.start_slot(Icon::new(IconName::Cog)) .child(Label::new("Fork Profile"))
.child(Label::new("Configure Tools")) .on_click({
.on_click({ let profile_id = mode.profile_id.clone();
let profile_id = mode.profile_id.clone(); cx.listener(move |this, _, window, cx| {
cx.listener(move |this, _, window, cx| { this.new_profile(
this.configure_tools(profile_id.clone(), window, cx); 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(), .into_any_element(),
) )
.entry(mode.fork_profile)
.entry(mode.configure_tools) .entry(mode.configure_tools)
} }
} }
impl Render for ManageProfilesModal { impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
div() div()
.elevation_3(cx) .elevation_3(cx)
.w(rems(34.)) .w(rems(34.))
@ -238,13 +418,37 @@ impl Render for ManageProfilesModal {
})) }))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode { .child(match &self.mode {
Mode::ChooseProfile { profile_picker, .. } => { Mode::ChooseProfile { profile_picker, .. } => div()
profile_picker.clone().into_any_element() .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 Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx) .render_view_profile(mode.clone(), window, cx)
.into_any_element(), .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()
}
}) })
} }
} }

View file

@ -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)),
),
)
}
}

View file

@ -21,7 +21,7 @@ impl ProfilePicker {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> 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 } Self { picker }
} }
} }

View file

@ -74,7 +74,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
/// ///
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs /// 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) elevated(self, cx, ElevationIndex::ModalSurface)
} }