From 3c779a6df04873b4060f5a24f8d2e964d6811a58 Mon Sep 17 00:00:00 2001 From: Dom Porada Date: Tue, 15 Sep 2026 14:53:12 +0000 Subject: [PATCH] Add a setting for the Threads Sidebar width (#62883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Objective The Threads Sidebar opens at a hard-coded 300px and can be resized only by dragging. `agent.default_width` controls the Agent Panel rather than the sidebar, leaving no way to configure the sidebar’s preferred width. ## Solution Add `agent.threads_sidebar_default_width` for the width used when no user-chosen sidebar width is stored. The value is clamped to the existing 200–800px drag range. Existing, unresized sidebars follow settings changes immediately. Double-clicking the divider resets the sidebar to the configured width and resumes following subsequent settings changes. The sidebar previously saved its width whenever any sidebar state changed, not only when the divider was dragged. `SerializedSidebar` now records `width_set_by_user`, so settings-driven widths are no longer persisted. Manually resized widths take precedence until the divider is reset. ## Testing - `test_threads_sidebar_default_width` in `crates/agent_settings/src/agent_settings.rs` covers the default, a user override, fallback for `null` and an unset field, and preservation of the Agent Panel width and sidebar position. - Five width-focused tests in `crates/sidebar/src/sidebar_tests.rs` cover reset behavior, live settings updates, clamping, legacy-width restoration, and persistence. They also check that manually resized widths resist settings changes and that resetting resumes following the setting. - To check by hand: set `"agent": { "threads_sidebar_default_width": 360 }`, reset the divider, and confirm the sidebar is 360px wide. Change the setting and confirm the width updates immediately. Drag it wider, restart, and confirm the dragged width is kept. Double-click the divider and confirm it returns to the configured width. Set the value to `5000` and confirm the untouched sidebar clamps to 800px. ## 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 adheres to Zed’s UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Added `agent.threads_sidebar_default_width` for setting the width of the Threads Sidebar. --- assets/settings/default.json | 5 +- crates/agent/src/tool_permissions.rs | 1 + crates/agent_settings/src/agent_settings.rs | 70 +++++- crates/agent_ui/src/agent_ui.rs | 1 + crates/settings_content/src/agent.rs | 10 +- crates/settings_ui/src/page_data.rs | 25 ++- crates/sidebar/src/sidebar.rs | 50 ++++- crates/sidebar/src/sidebar_tests.rs | 237 +++++++++++++++++++- docs/src/ai/parallel-agents.md | 4 + docs/src/reference/all-settings.md | 24 ++ 10 files changed, 410 insertions(+), 17 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 4dc6720bd6..52c048b5c4 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1152,8 +1152,11 @@ // // Default: true "flexible": true, - // Where to position the threads sidebar. Can be 'left' or 'right'. + // Where to position the sidebar holding the agent panel and the list of + // agent threads. Can be 'left' or 'right'. "sidebar_side": "left", + // Default width of the Threads Sidebar. + "threads_sidebar_default_width": 300, // Default fixed width when the agent panel is docked to the left or right and // `flexible` is false. "default_width": 640, diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 9be962547f..188ee5a23f 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -609,6 +609,7 @@ mod tests { show_turn_stats: false, show_merge_conflict_indicator: true, sidebar_side: Default::default(), + threads_sidebar_default_width: px(300.), thinking_display: Default::default(), } } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 05e90208a6..4b83f47f9b 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -10,7 +10,7 @@ use anyhow::Context as _; use collections::{HashSet, IndexMap}; use fs::Fs; use futures::channel::oneshot; -use gpui::{App, Pixels, SharedString}; +use gpui::{App, Pixels, SharedString, px}; use language_model::LanguageModel; use project::DisableAiSettings; use schemars::JsonSchema; @@ -31,6 +31,12 @@ pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = include_str!("prompts/summarize_thread_detailed_prompt.txt"); pub const COMPACTION_PROMPT: &str = include_str!("prompts/compaction_prompt.txt"); +/// Bounds on the width of the threads list. They constrain the configured +/// default as well as the width the user drags to, so that no width the sidebar +/// can hold is able to crowd out the editor or collapse the list. +pub const THREADS_LIST_MIN_WIDTH: Pixels = px(200.0); +pub const THREADS_LIST_MAX_WIDTH: Pixels = px(800.0); + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PanelLayout { pub(crate) agent_dock: Option, @@ -208,6 +214,7 @@ pub struct AgentSettings { pub dock: DockPosition, pub flexible: bool, pub sidebar_side: SidebarDockPosition, + pub threads_sidebar_default_width: Pixels, pub default_width: Pixels, pub default_height: Pixels, pub max_content_width: Option, @@ -760,6 +767,13 @@ impl Settings for AgentSettings { button: agent.button.unwrap(), dock: agent.dock.unwrap(), sidebar_side: agent.sidebar_side.unwrap(), + // Clamped once here so that every reader gets a width the sidebar can + // actually hold, rather than each call site having to remember to. + threads_sidebar_default_width: agent + .threads_sidebar_default_width + .unwrap() + .into_gpui() + .clamp(THREADS_LIST_MIN_WIDTH, THREADS_LIST_MAX_WIDTH), default_width: agent.default_width.unwrap().into_gpui(), default_height: agent.default_height.unwrap().into_gpui(), max_content_width: if agent.limit_content_width.unwrap() { @@ -991,7 +1005,7 @@ fn compile_regex_rules( #[cfg(test)] mod tests { use super::*; - use gpui::{TestAppContext, UpdateGlobal}; + use gpui::{TestAppContext, UpdateGlobal, px}; use serde_json::json; use settings::ToolPermissionMode; use settings::ToolPermissionsContent; @@ -1091,6 +1105,58 @@ mod tests { } } + #[gpui::test] + fn test_threads_sidebar_default_width(cx: &mut gpui::App) { + let store = SettingsStore::test(cx); + cx.set_global(store); + project::DisableAiSettings::register(cx); + AgentSettings::register(cx); + + assert_eq!( + AgentSettings::get_global(cx).threads_sidebar_default_width, + px(300.), + "default.json supplies the Threads Sidebar width" + ); + + SettingsStore::update_global(cx, |store, cx| { + store + .set_user_settings( + r#"{ "agent": { "threads_sidebar_default_width": 360 } }"#, + cx, + ) + .expect("user settings load"); + }); + + let settings = AgentSettings::get_global(cx); + assert_eq!(settings.threads_sidebar_default_width, px(360.)); + assert_eq!( + settings.default_width, + px(640.), + "setting the Threads Sidebar width leaves the agent panel width unchanged" + ); + assert_eq!( + settings.sidebar_side, + SidebarDockPosition::Left, + "setting the Threads Sidebar width leaves its position unchanged" + ); + + for content in [ + r#"{ "agent": { "threads_sidebar_default_width": null } }"#, + r#"{ "agent": {} }"#, + ] { + SettingsStore::update_global(cx, |store, cx| { + store + .set_user_settings(content, cx) + .expect("user settings load"); + }); + assert_eq!( + AgentSettings::get_global(cx).threads_sidebar_default_width, + px(300.), + "an unset width falls back to the default" + ); + } + } + #[gpui::test] fn test_terminal_init_command_filters_empty_without_trimming(cx: &mut gpui::App) { let store = SettingsStore::test(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 28fc3c7c62..64780d7398 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -1011,6 +1011,7 @@ mod tests { show_turn_stats: false, show_merge_conflict_indicator: true, sidebar_side: Default::default(), + threads_sidebar_default_width: px(300.), thinking_display: Default::default(), }; diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 4c39f33ab6..887a70e37b 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -214,10 +214,18 @@ pub struct AgentSettingsContent { /// /// Default: true pub flexible: Option, - /// Where to position the threads sidebar. + /// Where to position the sidebar holding the threads list and the agent panel. /// /// Default: left pub sidebar_side: Option, + /// Default width in pixels for the Threads Sidebar. + /// + /// Values range from 200 to 800, matching the widths the sidebar can be + /// dragged to. Values outside that range are clamped into it. + /// + /// Default: 300 + #[schemars(range(min = 200, max = 800))] + pub threads_sidebar_default_width: Option, /// Default fixed width in pixels when the agent panel is docked to the left or right and /// `flexible` is false. /// diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index bbb952fdf2..f8238aba25 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -6829,7 +6829,7 @@ fn panels_page() -> SettingsPage { ] } - fn agent_panel_section() -> [SettingsPageItem; 7] { + fn agent_panel_section() -> [SettingsPageItem; 8] { [ SettingsPageItem::SectionHeader("Agent Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -6890,6 +6890,29 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Threads Sidebar Default Width", + description: "Default width of the Threads Sidebar. Resizing it takes precedence until you double-click the divider to reset.", + field: Box::new(SettingField { + organization_override: None, + json_path: Some("agent.threads_sidebar_default_width"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .threads_sidebar_default_width + .as_ref() + }, + write: |settings_content, value, _| { + settings_content + .agent + .get_or_insert_default() + .threads_sidebar_default_width = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Agent Panel Default Height", description: "Default height when the agent panel is docked to the bottom.", diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 6e0e2593b1..2b3e1ff3bb 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -4,7 +4,7 @@ use acp_thread::ThreadStatus; use action_log::DiffStats; use agent::{ThreadStore, ZED_AGENT_ID}; use agent_client_protocol::schema::v1 as acp; -use agent_settings::AgentSettings; +use agent_settings::{AgentSettings, THREADS_LIST_MAX_WIDTH, THREADS_LIST_MIN_WIDTH}; use agent_ui::terminal_thread_metadata_store::{ TerminalThreadMetadata, TerminalThreadMetadataStore, terminal_title_prefix, }; @@ -47,7 +47,7 @@ use remote::{RemoteConnectionOptions, same_remote_connection_identity}; use ui::utils::platform_title_bar_height; use serde::{Deserialize, Serialize}; -use settings::Settings as _; +use settings::{Settings as _, SettingsStore}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::mem; @@ -103,10 +103,6 @@ gpui::actions!( ] ); -const DEFAULT_WIDTH: Pixels = px(300.0); -const MIN_WIDTH: Pixels = px(200.0); -const MAX_WIDTH: Pixels = px(800.0); - #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] enum SerializedSidebarView { #[default] @@ -125,6 +121,13 @@ enum NewEntryTarget { struct SerializedSidebar { #[serde(default)] width: Option, + /// Whether `width` came from the user dragging the divider. + /// + /// Legacy state recorded every width without this flag. A width other than + /// the old default of 300 pixels still identifies a manual resize. Only + /// that ambiguous default falls back to `agent.threads_sidebar_default_width`. + #[serde(default)] + width_set_by_user: bool, #[serde(default)] active_view: SerializedSidebarView, } @@ -763,6 +766,11 @@ fn create_worktree_in_workspace( pub struct Sidebar { multi_workspace: WeakEntity, width: Pixels, + /// Whether `width` came from the user rather than from + /// `agent.threads_sidebar_default_width`. Only a user-chosen width is persisted, so + /// that changing the setting is not overridden by a width the user never + /// picked. Serialization runs on many triggers besides resizing. + width_set_by_user: bool, focus_handle: FocusHandle, filter_editor: Entity, rename_editor: Entity, @@ -832,6 +840,15 @@ impl Sidebar { AgentThreadWorktreeLabelFlag::watch(cx); + cx.observe_global::(|this, cx| { + let width = AgentSettings::get_global(cx).threads_sidebar_default_width; + if !this.width_set_by_user && this.width != width { + this.width = width; + cx.notify(); + } + }) + .detach(); + let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Search threads…", window, cx); @@ -917,7 +934,8 @@ impl Sidebar { Self { multi_workspace: multi_workspace.downgrade(), - width: DEFAULT_WIDTH, + width: AgentSettings::get_global(cx).threads_sidebar_default_width, + width_set_by_user: false, focus_handle, filter_editor, rename_editor, @@ -7792,7 +7810,11 @@ impl WorkspaceSidebar for Sidebar { } fn set_width(&mut self, width: Option, cx: &mut Context) { - self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH); + // `None` is the reset gesture, which hands the width back to the setting. + self.width_set_by_user = width.is_some(); + self.width = width + .unwrap_or_else(|| AgentSettings::get_global(cx).threads_sidebar_default_width) + .clamp(THREADS_LIST_MIN_WIDTH, THREADS_LIST_MAX_WIDTH); cx.notify(); } @@ -7832,7 +7854,8 @@ impl WorkspaceSidebar for Sidebar { fn serialized_state(&self, _cx: &App) -> Option { let serialized = SerializedSidebar { - width: Some(f32::from(self.width)), + width: self.width_set_by_user.then(|| f32::from(self.width)), + width_set_by_user: self.width_set_by_user, active_view: match self.view { SidebarView::ThreadList => SerializedSidebarView::ThreadList, SidebarView::Archive(_) => SerializedSidebarView::History, @@ -7848,8 +7871,13 @@ impl WorkspaceSidebar for Sidebar { cx: &mut Context, ) { if let Some(serialized) = serde_json::from_str::(state).log_err() { - if let Some(width) = serialized.width { - self.width = px(width).clamp(MIN_WIDTH, MAX_WIDTH); + // Legacy widths other than the old 300-pixel default imply a manual resize + if let Some(width) = serialized + .width + .filter(|width| serialized.width_set_by_user || *width != 300.0) + { + self.width = px(width).clamp(THREADS_LIST_MIN_WIDTH, THREADS_LIST_MAX_WIDTH); + self.width_set_by_user = true; } if serialized.active_view == SerializedSidebarView::History { cx.defer_in(window, |this, window, cx| { diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 9d1a0abc37..68ef42ea2f 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -14,7 +14,7 @@ use agent_ui::{ }; use chrono::DateTime; use fs::{FakeFs, Fs}; -use gpui::TestAppContext; +use gpui::{TestAppContext, UpdateGlobal}; use pretty_assertions::assert_eq; use project::AgentId; use settings::SettingsStore; @@ -298,6 +298,17 @@ fn setup_sidebar_closed( sidebar } +fn set_threads_sidebar_default_width(width: f32, cx: &mut App) { + SettingsStore::update_global(cx, |store, cx| { + store + .set_user_settings( + &format!(r#"{{"agent": {{"threads_sidebar_default_width": {width}}}}}"#), + cx, + ) + .unwrap(); + }); +} + async fn save_n_test_threads( count: u32, project: &Entity, @@ -871,6 +882,229 @@ async fn test_serialization_round_trip(cx: &mut TestAppContext) { assert_eq!(width1, px(420.0)); } +#[gpui::test] +async fn test_width_reset_returns_configured_default(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + cx.update(|cx| set_threads_sidebar_default_width(360.0, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let sidebar = setup_sidebar(&multi_workspace, cx); + + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width), + px(360.0), + "a fresh sidebar should open at the configured width" + ); + + sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(Some(px(420.0)), cx); + }); + cx.run_until_parked(); + assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.width), px(420.0)); + + sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(None, cx); + }); + cx.run_until_parked(); + + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width), + px(360.0), + "resetting the width should return to the configured width, not a fixed default" + ); +} + +#[gpui::test] +async fn test_width_follows_settings_until_manually_resized(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let sidebar = setup_sidebar(&multi_workspace, cx); + + cx.update(|_window, cx| set_threads_sidebar_default_width(360.0, cx)); + cx.run_until_parked(); + assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.width), px(360.0)); + assert!(!sidebar.read_with(cx, |sidebar, _| sidebar.width_set_by_user)); + + sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(Some(px(420.0)), cx); + }); + cx.update(|_window, cx| set_threads_sidebar_default_width(500.0, cx)); + cx.run_until_parked(); + assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.width), px(420.0)); + + sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(None, cx); + }); + cx.run_until_parked(); + assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.width), px(500.0)); + + for (configured, expected) in [ + (5.0, THREADS_LIST_MIN_WIDTH), + (360.0, px(360.0)), + (5000.0, THREADS_LIST_MAX_WIDTH), + ] { + cx.update(|_window, cx| set_threads_sidebar_default_width(configured, cx)); + cx.run_until_parked(); + assert_eq!(sidebar.read_with(cx, |sidebar, _| sidebar.width), expected); + let serialized = sidebar + .read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)) + .expect("sidebar state is serialized"); + let serialized: SerializedSidebar = + serde_json::from_str(&serialized).expect("sidebar state is valid"); + assert_eq!(serialized.width, None); + assert!(!serialized.width_set_by_user); + } +} + +#[gpui::test] +async fn test_restored_width_preserves_legacy_resizes(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + for (state, expected, width_set_by_user) in [ + (r#"{"width":null}"#, 360.0, false), + (r#"{"width":300.0}"#, 360.0, false), + (r#"{"width":420.0}"#, 420.0, true), + (r#"{"width":300.0,"width_set_by_user":true}"#, 300.0, true), + ] { + cx.update(|_window, cx| set_threads_sidebar_default_width(360.0, cx)); + let sidebar = + cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); + cx.run_until_parked(); + sidebar.update_in(cx, |sidebar, window, cx| { + sidebar.restore_serialized_state(state, window, cx); + }); + cx.run_until_parked(); + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width), + px(expected) + ); + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width_set_by_user), + width_set_by_user + ); + + let serialized = sidebar + .read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)) + .expect("sidebar state is serialized"); + let serialized: SerializedSidebar = + serde_json::from_str(&serialized).expect("sidebar state is valid"); + assert_eq!(serialized.width, width_set_by_user.then_some(expected)); + assert_eq!(serialized.width_set_by_user, width_set_by_user); + + cx.update(|_window, cx| set_threads_sidebar_default_width(500.0, cx)); + cx.run_until_parked(); + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width), + px(if width_set_by_user { expected } else { 500.0 }) + ); + } +} + +#[gpui::test] +async fn test_configured_width_is_clamped_into_range(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + cx.update(|cx| set_threads_sidebar_default_width(5000.0, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let wide_sidebar = + cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); + cx.run_until_parked(); + assert_eq!( + wide_sidebar.read_with(cx, |sidebar, _| sidebar.width), + THREADS_LIST_MAX_WIDTH, + "a configured width above the maximum should be clamped at construction" + ); + + wide_sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(None, cx); + }); + cx.run_until_parked(); + assert_eq!( + wide_sidebar.read_with(cx, |sidebar, _| sidebar.width), + THREADS_LIST_MAX_WIDTH, + "resetting the width should not restore the unclamped configured width" + ); + + cx.update(|_window, cx| set_threads_sidebar_default_width(5.0, cx)); + let narrow_sidebar = + cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); + cx.run_until_parked(); + assert_eq!( + narrow_sidebar.read_with(cx, |sidebar, _| sidebar.width), + THREADS_LIST_MIN_WIDTH, + "a configured width below the minimum should be clamped at construction" + ); +} + +#[gpui::test] +async fn test_only_a_user_chosen_width_is_persisted(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + cx.update(|cx| set_threads_sidebar_default_width(360.0, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let sidebar = setup_sidebar(&multi_workspace, cx); + + // Serialization runs on many triggers besides resizing, so a sidebar the + // user has never resized must not record a width at all. + let untouched = sidebar + .read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)) + .expect("serialized_state should return Some"); + assert!( + !untouched.contains("360"), + "an unresized sidebar should not persist its width, got {untouched}" + ); + + // A legacy width matching the old default is ambiguous, so the setting takes precedence + sidebar.update_in(cx, |sidebar, window, cx| { + sidebar.restore_serialized_state(r#"{"width":300.0}"#, window, cx); + }); + cx.run_until_parked(); + assert_eq!( + sidebar.read_with(cx, |sidebar, _| sidebar.width), + px(360.0), + "the legacy default width falls back to the setting" + ); + + // A width the user picked survives, and keeps surviving across restores. + sidebar.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(Some(px(420.0)), cx); + }); + cx.run_until_parked(); + let resized = sidebar + .read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)) + .expect("serialized_state should return Some"); + + let restored = + cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); + cx.run_until_parked(); + restored.update_in(cx, |sidebar, window, cx| { + sidebar.restore_serialized_state(&resized, window, cx); + }); + cx.run_until_parked(); + assert_eq!( + restored.read_with(cx, |sidebar, _| sidebar.width), + px(420.0), + "a user-chosen width should survive a restore" + ); + + // Resetting gives the width back to the setting, and stops persisting it. + restored.update_in(cx, |sidebar, _window, cx| { + sidebar.set_width(None, cx); + }); + cx.run_until_parked(); + let after_reset = restored + .read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)) + .expect("serialized_state should return Some"); + assert!( + !after_reset.contains("420"), + "resetting should stop persisting the width, got {after_reset}" + ); +} + #[gpui::test] async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppContext) { // A regression test to ensure that restoring a serialized archive view does not panic. @@ -884,6 +1118,7 @@ async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppCon let serialized = serde_json::to_string(&SerializedSidebar { width: Some(400.0), + width_set_by_user: true, active_view: SerializedSidebarView::History, }) .expect("serialization should succeed"); diff --git a/docs/src/ai/parallel-agents.md b/docs/src/ai/parallel-agents.md index 70f0ff4f93..8775bf947d 100644 --- a/docs/src/ai/parallel-agents.md +++ b/docs/src/ai/parallel-agents.md @@ -19,6 +19,10 @@ Terminal Threads also appear as entries in the sidebar alongside agent threads, To focus the sidebar without toggling it, use {#kb multi_workspace::FocusWorkspaceSidebar}. To search your threads, press {#kb agents_sidebar::FocusSidebarFilter} while the sidebar is focused. +To change the sidebar’s default width, open the Settings Editor and search for “Threads Sidebar Default Width”. See [`agent.threads_sidebar_default_width`](../reference/all-settings.md#agent-threads-sidebar-default-width) for values and a configuration example. + +Drag the divider to override the configured width. Double-click it to return to the setting and follow subsequent settings changes. + ### Switching Threads {#switching-threads} Click any thread in the sidebar to switch to it. The Agent Panel updates to show that thread's conversation. diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 4ac82c90cc..cf70ae1c14 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -121,6 +121,30 @@ To use a fixed reset width, disable flexible sizing in the Settings Editor. Or a See [Agent Panel visual customization](../visual-customization.md#agent-panel) for other panel appearance settings. +### Threads Sidebar Default Width {#agent-threads-sidebar-default-width} + +- Description: Default width in pixels of the [Threads Sidebar](../ai/parallel-agents.md#threads-sidebar). +- Setting: `agent.threads_sidebar_default_width` +- Default: `300` + +**Options** + +Numbers from `200` to `800` pixels (inclusive). Values outside this range are clamped to the nearest limit. + +Open the Settings Editor and search for “Threads Sidebar Default Width”. Or add this to your `settings.json`: + +```json [settings] +{ + "agent": { + "threads_sidebar_default_width": 360 + } +} +``` + +If you haven’t manually resized the sidebar, its width follows changes to this setting immediately. A manually resized width takes precedence until you double-click the divider to reset it. After resetting, the sidebar follows this setting again. + +Widths saved by older versions of Zed are preserved if they differ from the previous default of 300 pixels. A saved width of 300 pixels uses this setting instead. + ## Agent UI Font Size - Description: The font size for text in the agent panel. Inherits the UI font size if unset.