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:
parent
12a8b850ef
commit
3b158461be
6 changed files with 283 additions and 39 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,18 +291,78 @@ 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(
|
||||||
|
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("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(
|
.child(
|
||||||
v_flex().child(
|
|
||||||
div()
|
div()
|
||||||
.id("configure-tools")
|
.id("configure-tools")
|
||||||
.track_focus(&mode.configure_tools.focus_handle)
|
.track_focus(&mode.configure_tools.focus_handle)
|
||||||
|
@ -213,7 +386,11 @@ impl ManageProfilesModal {
|
||||||
.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.configure_tools(profile_id.clone(), window, cx);
|
this.configure_tools(
|
||||||
|
profile_id.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -221,12 +398,15 @@ impl ManageProfilesModal {
|
||||||
)
|
)
|
||||||
.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()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue