Cargo.lock 🔗
@@ -312,6 +312,7 @@ dependencies = [
"gpui",
"language_model",
"log",
+ "paths",
"project",
"regex",
"schemars",
Mikayla Maki created
Use `AgentSettings::get_layout(cx)` to retrieve the current, exact value of the user's layout settings, and `AgentSettings::set_layout(WindowLayout::agent())` or `AgentSettings::set_layout(cached_user_settings)` to write to them.
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
Cargo.lock | 1
crates/agent_settings/Cargo.toml | 1
crates/agent_settings/src/agent_settings.rs | 367 ++++++++++++++++++++++
crates/settings/src/settings_store.rs | 4
4 files changed, 370 insertions(+), 3 deletions(-)
@@ -312,6 +312,7 @@ dependencies = [
"gpui",
"language_model",
"log",
+ "paths",
"project",
"regex",
"schemars",
@@ -30,6 +30,7 @@ util.workspace = true
[dev-dependencies]
fs.workspace = true
gpui = { workspace = true, features = ["test-support"] }
+paths.workspace = true
serde_json_lenient.workspace = true
serde_json.workspace = true
@@ -5,15 +5,17 @@ use std::sync::{Arc, LazyLock};
use agent_client_protocol::ModelId;
use collections::{HashSet, IndexMap};
+use fs::Fs;
use gpui::{App, Pixels, px};
use language_model::LanguageModel;
use project::DisableAiSettings;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
- DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
- NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SidebarDockPosition,
- SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
+ DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
+ NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent,
+ SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
+ update_settings_file,
};
pub use crate::agent_profile::*;
@@ -22,6 +24,104 @@ pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread
pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
include_str!("prompts/summarize_thread_detailed_prompt.txt");
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct PanelLayout {
+ pub(crate) agent_dock: Option<DockPosition>,
+ pub(crate) project_panel_dock: Option<DockSide>,
+ pub(crate) outline_panel_dock: Option<DockSide>,
+ pub(crate) collaboration_panel_dock: Option<DockPosition>,
+ pub(crate) git_panel_dock: Option<DockPosition>,
+ pub(crate) notification_panel_button: Option<bool>,
+}
+
+impl PanelLayout {
+ const AGENT: Self = Self {
+ agent_dock: Some(DockPosition::Left),
+ project_panel_dock: Some(DockSide::Right),
+ outline_panel_dock: Some(DockSide::Right),
+ collaboration_panel_dock: Some(DockPosition::Right),
+ git_panel_dock: Some(DockPosition::Right),
+ notification_panel_button: Some(false),
+ };
+
+ const EDITOR: Self = Self {
+ agent_dock: Some(DockPosition::Right),
+ project_panel_dock: Some(DockSide::Left),
+ outline_panel_dock: Some(DockSide::Left),
+ collaboration_panel_dock: Some(DockPosition::Left),
+ git_panel_dock: Some(DockPosition::Left),
+ notification_panel_button: Some(true),
+ };
+
+ pub fn is_agent_layout(&self) -> bool {
+ *self == Self::AGENT
+ }
+
+ pub fn is_editor_layout(&self) -> bool {
+ *self == Self::EDITOR
+ }
+
+ fn read_from(content: &SettingsContent) -> Self {
+ Self {
+ agent_dock: content.agent.as_ref().and_then(|a| a.dock),
+ project_panel_dock: content.project_panel.as_ref().and_then(|p| p.dock),
+ outline_panel_dock: content.outline_panel.as_ref().and_then(|p| p.dock),
+ collaboration_panel_dock: content.collaboration_panel.as_ref().and_then(|p| p.dock),
+ git_panel_dock: content.git_panel.as_ref().and_then(|p| p.dock),
+ notification_panel_button: content.notification_panel.as_ref().and_then(|p| p.button),
+ }
+ }
+
+ fn write_to(&self, settings: &mut SettingsContent) {
+ settings.agent.get_or_insert_default().dock = self.agent_dock;
+ settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;
+ settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;
+ settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock;
+ settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
+ settings.notification_panel.get_or_insert_default().button = self.notification_panel_button;
+ }
+
+ fn write_diff_to(&self, current_merged: &PanelLayout, settings: &mut SettingsContent) {
+ if self.agent_dock != current_merged.agent_dock {
+ settings.agent.get_or_insert_default().dock = self.agent_dock;
+ }
+ if self.project_panel_dock != current_merged.project_panel_dock {
+ settings.project_panel.get_or_insert_default().dock = self.project_panel_dock;
+ }
+ if self.outline_panel_dock != current_merged.outline_panel_dock {
+ settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;
+ }
+ if self.collaboration_panel_dock != current_merged.collaboration_panel_dock {
+ settings.collaboration_panel.get_or_insert_default().dock =
+ self.collaboration_panel_dock;
+ }
+ if self.git_panel_dock != current_merged.git_panel_dock {
+ settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
+ }
+ if self.notification_panel_button != current_merged.notification_panel_button {
+ settings.notification_panel.get_or_insert_default().button =
+ self.notification_panel_button;
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum WindowLayout {
+ Editor(Option<PanelLayout>),
+ Agent(Option<PanelLayout>),
+ Custom(PanelLayout),
+}
+
+impl WindowLayout {
+ pub fn agent() -> Self {
+ Self::Agent(None)
+ }
+
+ pub fn editor() -> Self {
+ Self::Editor(None)
+ }
+}
+
#[derive(Clone, Debug, RegisterSetting)]
pub struct AgentSettings {
pub enabled: bool,
@@ -98,6 +198,50 @@ impl AgentSettings {
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
.collect()
}
+
+ pub fn get_layout(cx: &App) -> WindowLayout {
+ let store = cx.global::<SettingsStore>();
+ let merged = store.merged_settings();
+ let user_layout = store
+ .raw_user_settings()
+ .map(|u| PanelLayout::read_from(u.content.as_ref()))
+ .unwrap_or_default();
+ let merged_layout = PanelLayout::read_from(merged);
+
+ if merged_layout.is_agent_layout() {
+ return WindowLayout::Agent(Some(user_layout));
+ }
+
+ if merged_layout.is_editor_layout() {
+ return WindowLayout::Editor(Some(user_layout));
+ }
+
+ WindowLayout::Custom(user_layout)
+ }
+
+ pub fn set_layout(layout: WindowLayout, fs: Arc<dyn Fs>, cx: &App) {
+ let merged = PanelLayout::read_from(cx.global::<SettingsStore>().merged_settings());
+
+ match layout {
+ WindowLayout::Agent(None) => {
+ update_settings_file(fs, cx, move |settings, _cx| {
+ PanelLayout::AGENT.write_diff_to(&merged, settings);
+ });
+ }
+ WindowLayout::Editor(None) => {
+ update_settings_file(fs, cx, move |settings, _cx| {
+ PanelLayout::EDITOR.write_diff_to(&merged, settings);
+ });
+ }
+ WindowLayout::Agent(Some(saved))
+ | WindowLayout::Editor(Some(saved))
+ | WindowLayout::Custom(saved) => {
+ update_settings_file(fs, cx, move |settings, _cx| {
+ saved.write_to(settings);
+ });
+ }
+ }
+ }
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
@@ -555,6 +699,7 @@ fn compile_regex_rules(
#[cfg(test)]
mod tests {
use super::*;
+ use gpui::{TestAppContext, UpdateGlobal};
use serde_json::json;
use settings::ToolPermissionMode;
use settings::ToolPermissionsContent;
@@ -1031,4 +1176,220 @@ mod tests {
let permissions = compile_tool_permissions(Some(content));
assert_eq!(permissions.default, ToolPermissionMode::Deny);
}
+
+ #[gpui::test]
+ fn test_get_layout(cx: &mut gpui::App) {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ 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);
+ });
+ });
+
+ // Should be Agent with an empty user layout (user hasn't customized).
+ let layout = AgentSettings::get_layout(cx);
+ let WindowLayout::Agent(Some(user_layout)) = layout else {
+ panic!("expected Agent(Some), got {:?}", layout);
+ };
+ assert_eq!(user_layout, PanelLayout::default());
+
+ // User explicitly sets agent dock to left (matching the default).
+ // The merged result is still agent, but the user layout captures
+ // only what the user wrote.
+ SettingsStore::update_global(cx, |store, cx| {
+ store
+ .set_user_settings(r#"{ "agent": { "dock": "left" } }"#, cx)
+ .unwrap();
+ });
+
+ let layout = AgentSettings::get_layout(cx);
+ let WindowLayout::Agent(Some(user_layout)) = layout else {
+ panic!("expected Agent(Some), got {:?}", layout);
+ };
+ assert_eq!(user_layout.agent_dock, Some(DockPosition::Left));
+ assert_eq!(user_layout.project_panel_dock, None);
+ assert_eq!(user_layout.outline_panel_dock, None);
+ assert_eq!(user_layout.collaboration_panel_dock, None);
+ assert_eq!(user_layout.git_panel_dock, None);
+ assert_eq!(user_layout.notification_panel_button, None);
+
+ // User sets a combination that doesn't match either preset:
+ // agent on the left but project panel also on the left.
+ SettingsStore::update_global(cx, |store, cx| {
+ store
+ .set_user_settings(
+ r#"{
+ "agent": { "dock": "left" },
+ "project_panel": { "dock": "left" }
+ }"#,
+ cx,
+ )
+ .unwrap();
+ });
+
+ let layout = AgentSettings::get_layout(cx);
+ let WindowLayout::Custom(user_layout) = layout else {
+ panic!("expected Custom, got {:?}", layout);
+ };
+ assert_eq!(user_layout.agent_dock, Some(DockPosition::Left));
+ assert_eq!(user_layout.project_panel_dock, Some(DockSide::Left));
+ }
+
+ #[gpui::test]
+ fn test_set_layout_round_trip(cx: &mut gpui::App) {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ project::DisableAiSettings::register(cx);
+ AgentSettings::register(cx);
+
+ // User has a custom layout: agent on the right with project panel
+ // also on the right. This doesn't match either preset.
+ SettingsStore::update_global(cx, |store, cx| {
+ store
+ .set_user_settings(
+ r#"{
+ "agent": { "dock": "right" },
+ "project_panel": { "dock": "right" }
+ }"#,
+ cx,
+ )
+ .unwrap();
+ });
+
+ let original = AgentSettings::get_layout(cx);
+ let WindowLayout::Custom(ref original_user_layout) = original else {
+ panic!("expected Custom, got {:?}", original);
+ };
+ assert_eq!(original_user_layout.agent_dock, Some(DockPosition::Right));
+ assert_eq!(
+ original_user_layout.project_panel_dock,
+ Some(DockSide::Right)
+ );
+ assert_eq!(original_user_layout.outline_panel_dock, None);
+
+ // Switch to the agent layout. This overwrites the user settings.
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ PanelLayout::AGENT.write_to(settings);
+ });
+ });
+
+ let layout = AgentSettings::get_layout(cx);
+ assert!(matches!(layout, WindowLayout::Agent(_)));
+
+ // Restore the original custom layout.
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ original_user_layout.write_to(settings);
+ });
+ });
+
+ // Should be back to the same custom layout.
+ let restored = AgentSettings::get_layout(cx);
+ let WindowLayout::Custom(restored_user_layout) = restored else {
+ panic!("expected Custom, got {:?}", restored);
+ };
+ assert_eq!(restored_user_layout.agent_dock, Some(DockPosition::Right));
+ assert_eq!(
+ restored_user_layout.project_panel_dock,
+ Some(DockSide::Right)
+ );
+ assert_eq!(restored_user_layout.outline_panel_dock, None);
+ }
+
+ #[gpui::test]
+ async fn test_set_layout_minimal_diff(cx: &mut TestAppContext) {
+ let fs = fs::FakeFs::new(cx.background_executor.clone());
+ fs.save(
+ paths::settings_file().as_path(),
+ &serde_json::json!({
+ "agent": { "dock": "left" },
+ "project_panel": { "dock": "left" }
+ })
+ .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);
+
+ // 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);
+ });
+ });
+
+ // User has agent=left (matches preset) and project_panel=left (does not)
+ SettingsStore::update_global(cx, |store, cx| {
+ store
+ .set_user_settings(
+ r#"{
+ "agent": { "dock": "left" },
+ "project_panel": { "dock": "left" }
+ }"#,
+ cx,
+ )
+ .unwrap();
+ });
+
+ let layout = AgentSettings::get_layout(cx);
+ assert!(matches!(layout, WindowLayout::Custom(_)));
+
+ AgentSettings::set_layout(WindowLayout::agent(), fs.clone(), cx);
+ });
+
+ cx.run_until_parked();
+
+ 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 settings should still have agent=left (preserved)
+ // and now project_panel=right (changed to match preset).
+ 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::Left));
+ assert_eq!(user_layout.project_panel_dock, Some(DockSide::Right));
+ // Other fields weren't in user settings and didn't need changing.
+ assert_eq!(user_layout.outline_panel_dock, None);
+
+ // And the merged result should now match agent.
+ let layout = AgentSettings::get_layout(cx);
+ assert!(matches!(layout, WindowLayout::Agent(_)));
+ });
+ }
}
@@ -370,6 +370,10 @@ impl SettingsStore {
setting_value.set_global_value(value);
}
+ pub fn merged_settings(&self) -> &SettingsContent {
+ &self.merged_settings
+ }
+
/// Get the value of a setting.
///
/// Panics if the given setting type has not been registered, or if there is no