mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Add existing user agent onboarding flow (#52787)
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A
This commit is contained in:
parent
3d29a0641e
commit
8b0d49f474
6 changed files with 271 additions and 28 deletions
|
|
@ -103,6 +103,29 @@ impl PanelLayout {
|
|||
self.notification_panel_button;
|
||||
}
|
||||
}
|
||||
|
||||
fn backfill_to(&self, user_layout: &PanelLayout, settings: &mut SettingsContent) {
|
||||
if user_layout.agent_dock.is_none() {
|
||||
settings.agent.get_or_insert_default().dock = self.agent_dock;
|
||||
}
|
||||
if user_layout.project_panel_dock.is_none() {
|
||||
settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;
|
||||
}
|
||||
if user_layout.outline_panel_dock.is_none() {
|
||||
settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;
|
||||
}
|
||||
if user_layout.collaboration_panel_dock.is_none() {
|
||||
settings.collaboration_panel.get_or_insert_default().dock =
|
||||
self.collaboration_panel_dock;
|
||||
}
|
||||
if user_layout.git_panel_dock.is_none() {
|
||||
settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
|
||||
}
|
||||
if user_layout.notification_panel_button.is_none() {
|
||||
settings.notification_panel.get_or_insert_default().button =
|
||||
self.notification_panel_button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -219,6 +242,18 @@ impl AgentSettings {
|
|||
WindowLayout::Custom(user_layout)
|
||||
}
|
||||
|
||||
pub fn backfill_editor_layout(fs: Arc<dyn Fs>, cx: &App) {
|
||||
let user_layout = cx
|
||||
.global::<SettingsStore>()
|
||||
.raw_user_settings()
|
||||
.map(|u| PanelLayout::read_from(u.content.as_ref()))
|
||||
.unwrap_or_default();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _cx| {
|
||||
PanelLayout::EDITOR.backfill_to(&user_layout, settings);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_layout(layout: WindowLayout, fs: Arc<dyn Fs>, cx: &App) {
|
||||
let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings());
|
||||
|
||||
|
|
@ -704,6 +739,14 @@ mod tests {
|
|||
use settings::ToolPermissionMode;
|
||||
use settings::ToolPermissionsContent;
|
||||
|
||||
fn set_agent_v2_defaults(cx: &mut gpui::App) {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_default_settings(cx, |defaults| {
|
||||
PanelLayout::AGENT.write_to(defaults);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compiled_regex_case_insensitive() {
|
||||
let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap();
|
||||
|
|
@ -1184,22 +1227,8 @@ mod tests {
|
|||
project::DisableAiSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
|
||||
// The test default settings match the editor layout.
|
||||
let layout = AgentSettings::get_layout(cx);
|
||||
assert!(matches!(layout, WindowLayout::Editor(_)));
|
||||
|
||||
// Switch defaults to the agent layout (simulating the AgentV2 flag).
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_default_settings(cx, |defaults| {
|
||||
defaults.agent.get_or_insert_default().dock = Some(DockPosition::Left);
|
||||
defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Right);
|
||||
defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Right);
|
||||
defaults.collaboration_panel.get_or_insert_default().dock =
|
||||
Some(DockPosition::Right);
|
||||
defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right);
|
||||
defaults.notification_panel.get_or_insert_default().button = Some(false);
|
||||
});
|
||||
});
|
||||
// Test defaults are editor layout; switch to agent V2.
|
||||
set_agent_v2_defaults(cx);
|
||||
|
||||
// Should be Agent with an empty user layout (user hasn't customized).
|
||||
let layout = AgentSettings::get_layout(cx);
|
||||
|
|
@ -1335,17 +1364,7 @@ mod tests {
|
|||
AgentSettings::register(cx);
|
||||
|
||||
// Apply the agent V2 defaults.
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_default_settings(cx, |defaults| {
|
||||
defaults.agent.get_or_insert_default().dock = Some(DockPosition::Left);
|
||||
defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Right);
|
||||
defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Right);
|
||||
defaults.collaboration_panel.get_or_insert_default().dock =
|
||||
Some(DockPosition::Right);
|
||||
defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right);
|
||||
defaults.notification_panel.get_or_insert_default().button = Some(false);
|
||||
});
|
||||
});
|
||||
set_agent_v2_defaults(cx);
|
||||
|
||||
// User has agent=left (matches preset) and project_panel=left (does not)
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
|
|
@ -1392,4 +1411,90 @@ mod tests {
|
|||
assert!(matches!(layout, WindowLayout::Agent(_)));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_backfill_editor_layout(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
// User has only customized project_panel to "right".
|
||||
fs.save(
|
||||
paths::settings_file().as_path(),
|
||||
&serde_json::json!({
|
||||
"project_panel": { "dock": "right" }
|
||||
})
|
||||
.to_string()
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
project::DisableAiSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
|
||||
// Simulate pre-migration state: editor defaults (the old world).
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_default_settings(cx, |defaults| {
|
||||
PanelLayout::EDITOR.write_to(defaults);
|
||||
});
|
||||
});
|
||||
|
||||
// User has only customized project_panel to "right".
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(r#"{ "project_panel": { "dock": "right" } }"#, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Run the one-time backfill while still on old defaults.
|
||||
AgentSettings::backfill_editor_layout(fs.clone(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Read back the file and apply it, then switch to agent V2 defaults.
|
||||
let written = fs.load(paths::settings_file().as_path()).await.unwrap();
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.set_user_settings(&written, cx).unwrap();
|
||||
});
|
||||
|
||||
// The user's project_panel=right should be preserved (they set it).
|
||||
// All other fields should now have the editor preset values
|
||||
// written into user settings.
|
||||
let store = cx.global::<SettingsStore>();
|
||||
let user_layout = store
|
||||
.raw_user_settings()
|
||||
.map(|u| PanelLayout::read_from(u.content.as_ref()))
|
||||
.unwrap_or_default();
|
||||
|
||||
assert_eq!(user_layout.agent_dock, Some(DockPosition::Right));
|
||||
assert_eq!(user_layout.project_panel_dock, Some(DockSide::Right));
|
||||
assert_eq!(user_layout.outline_panel_dock, Some(DockSide::Left));
|
||||
assert_eq!(
|
||||
user_layout.collaboration_panel_dock,
|
||||
Some(DockPosition::Left)
|
||||
);
|
||||
assert_eq!(user_layout.git_panel_dock, Some(DockPosition::Left));
|
||||
assert_eq!(user_layout.notification_panel_button, Some(true));
|
||||
|
||||
// Now switch defaults to agent V2.
|
||||
set_agent_v2_defaults(cx);
|
||||
|
||||
// Even though defaults are now agent, the backfilled user settings
|
||||
// keep everything in the editor layout. The user's experience
|
||||
// hasn't changed.
|
||||
let layout = AgentSettings::get_layout(cx);
|
||||
let WindowLayout::Custom(user_layout) = layout else {
|
||||
panic!(
|
||||
"expected Custom (editor values override agent defaults), got {:?}",
|
||||
layout
|
||||
);
|
||||
};
|
||||
assert_eq!(user_layout.agent_dock, Some(DockPosition::Right));
|
||||
assert_eq!(user_layout.project_panel_dock, Some(DockSide::Right));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ use client::Client;
|
|||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Context, Entity, SharedString, UpdateGlobal, Window, actions};
|
||||
use gpui::{Action, App, Context, Entity, SharedString, UpdateGlobal as _, Window, actions};
|
||||
use language::{
|
||||
LanguageRegistry,
|
||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||
|
|
@ -82,6 +82,7 @@ pub(crate) use thread_history_view::*;
|
|||
use zed_actions;
|
||||
|
||||
pub const DEFAULT_THREAD_TITLE: &str = "New Thread";
|
||||
const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfilled";
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
|
|
@ -354,6 +355,7 @@ pub fn init(
|
|||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
is_new_install: bool,
|
||||
is_eval: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
|
|
@ -427,6 +429,9 @@ pub fn init(
|
|||
})
|
||||
.detach();
|
||||
|
||||
// TODO: remove this field when we're ready remove the feature flag
|
||||
maybe_backfill_editor_layout(fs, is_new_install, false, cx);
|
||||
|
||||
cx.observe_flag::<AgentV2FeatureFlag, _>(|is_enabled, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_default_settings(cx, |defaults| {
|
||||
|
|
@ -453,6 +458,37 @@ pub fn init(
|
|||
.detach();
|
||||
}
|
||||
|
||||
fn maybe_backfill_editor_layout(
|
||||
fs: Arc<dyn Fs>,
|
||||
is_new_install: bool,
|
||||
should_run: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if !should_run {
|
||||
return;
|
||||
}
|
||||
|
||||
let kvp = db::kvp::KeyValueStore::global(cx);
|
||||
let already_backfilled =
|
||||
util::ResultExt::log_err(kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY))
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if !already_backfilled {
|
||||
if !is_new_install {
|
||||
AgentSettings::backfill_editor_layout(fs, cx);
|
||||
}
|
||||
|
||||
db::write_and_log(cx, move || async move {
|
||||
kvp.write_kvp(
|
||||
PARALLEL_AGENT_LAYOUT_BACKFILL_KEY.to_string(),
|
||||
"1".to_string(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||
|
|
@ -624,7 +660,9 @@ mod tests {
|
|||
use super::*;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use db::kvp::KeyValueStore;
|
||||
use editor::actions::AcceptEditPrediction;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
|
|
@ -767,6 +805,100 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
async fn setup_backfill_test(cx: &mut TestAppContext) -> Arc<dyn Fs> {
|
||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
||||
fs.save(
|
||||
paths::settings_file().as_path(),
|
||||
&"{}".into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
AgentSettings::register(cx);
|
||||
DisableAiSettings::register(cx);
|
||||
cx.set_staff(true);
|
||||
});
|
||||
|
||||
fs
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_backfill_sets_kvp_flag(cx: &mut TestAppContext) {
|
||||
let fs = setup_backfill_test(cx).await;
|
||||
|
||||
cx.update(|cx| {
|
||||
let kvp = KeyValueStore::global(cx);
|
||||
assert!(
|
||||
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
maybe_backfill_editor_layout(fs.clone(), false, true, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
||||
assert!(
|
||||
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
"flag should be set after backfill"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_backfill_new_install_sets_flag_without_writing_settings(cx: &mut TestAppContext) {
|
||||
let fs = setup_backfill_test(cx).await;
|
||||
|
||||
cx.update(|cx| {
|
||||
maybe_backfill_editor_layout(fs.clone(), true, true, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
||||
assert!(
|
||||
kvp.read_kvp(PARALLEL_AGENT_LAYOUT_BACKFILL_KEY)
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
"flag should be set even for new installs"
|
||||
);
|
||||
|
||||
let written = fs.load(paths::settings_file().as_path()).await.unwrap();
|
||||
assert_eq!(written.trim(), "{}", "settings file should be unchanged");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_backfill_is_idempotent(cx: &mut TestAppContext) {
|
||||
let fs = setup_backfill_test(cx).await;
|
||||
|
||||
cx.update(|cx| {
|
||||
maybe_backfill_editor_layout(fs.clone(), false, true, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let after_first = fs.load(paths::settings_file().as_path()).await.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
maybe_backfill_editor_layout(fs.clone(), false, true, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let after_second = fs.load(paths::settings_file().as_path()).await.unwrap();
|
||||
assert_eq!(
|
||||
after_first, after_second,
|
||||
"second call should not change settings"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_external_agent_variants() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ pub fn init(cx: &mut App) -> Arc<AgentCliAppState> {
|
|||
prompt_builder,
|
||||
languages.clone(),
|
||||
true,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -598,6 +598,8 @@ fn main() {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let is_new_install = matches!(&installation_id, Some(IdType::New(_)));
|
||||
|
||||
// We should rename these in the future to `first app open`, `first app open for release channel`, and `app open`
|
||||
if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) {
|
||||
match (&system_id, &installation_id) {
|
||||
|
|
@ -683,6 +685,7 @@ fn main() {
|
|||
app_state.client.clone(),
|
||||
prompt_builder.clone(),
|
||||
app_state.languages.clone(),
|
||||
is_new_install,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
|
|||
app_state.client.clone(),
|
||||
prompt_builder,
|
||||
app_state.languages.clone(),
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5045,6 +5045,7 @@ mod tests {
|
|||
app_state.client.clone(),
|
||||
prompt_builder.clone(),
|
||||
app_state.languages.clone(),
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue