Detailed changes
@@ -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,
);