From d9abcde22a4fc2a17c2bcf4ae5def4e71a04db7e Mon Sep 17 00:00:00 2001 From: Jack Walters <44548099+jackhwalters@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:03:50 +0000 Subject: [PATCH] Add a setting for Threads Sidebar auto-open (#64259) Closes https://github.com/zed-industries/zed/discussions/64266 Opening a folder in an existing window force-opens the Threads Sidebar, and there is no way to prevent it. `open_paths` called `MultiWorkspace::open_sidebar` unconditionally on the reused window, so the sidebar reopened on every such open regardless of whether the user had closed it. This adds `agent.threads_sidebar_auto_open`, defaulting to `true` so current behavior is unchanged. With `false`, the folder still opens in the existing window, but the sidebar stays closed. This path is reached when `default_open_behavior` or `cli_default_open_behavior` is set to `existing_window`. `open_sidebar` was doing double duty: besides showing the sidebar, it pinned the workspace being navigated away from so it survives the switch. When the setting is disabled, `retain_active_workspace` is called instead, so only sidebar visibility changes. https://github.com/user-attachments/assets/fc299df0-27e2-4ac6-98cb-d817d5f71d0e ## Tests Two tests in `multi_workspace_tests.rs` cover both branches: the sidebar opens by default, and stays closed when the setting is disabled while the folder still opens in the same window. Release Notes: - Added `agent.threads_sidebar_auto_open` to control whether opening a folder in an existing window also opens the Threads Sidebar --- assets/settings/default.json | 4 + crates/agent/src/tool_permissions.rs | 1 + crates/agent_settings/src/agent_settings.rs | 2 + crates/agent_ui/src/agent_ui.rs | 1 + crates/settings_content/src/agent.rs | 11 +++ crates/settings_ui/src/page_data.rs | 25 +++++- crates/workspace/src/multi_workspace_tests.rs | 87 +++++++++++++++++++ crates/workspace/src/workspace.rs | 10 ++- docs/src/reference/all-settings.md | 22 +++++ 9 files changed, 161 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index d669b19fe4..02eafc288e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1157,6 +1157,10 @@ "sidebar_side": "left", // Default width of the Threads Sidebar. "threads_sidebar_default_width": 300, + // Whether opening a folder in an existing window automatically opens the + // Threads Sidebar. Applies when `default_open_behavior` or + // `cli_default_open_behavior` is set to "existing_window". + "threads_sidebar_auto_open": true, // 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 188ee5a23f..cd8a02b202 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -610,6 +610,7 @@ mod tests { show_merge_conflict_indicator: true, sidebar_side: Default::default(), threads_sidebar_default_width: px(300.), + threads_sidebar_auto_open: true, thinking_display: Default::default(), } } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 4b83f47f9b..fd64625370 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -215,6 +215,7 @@ pub struct AgentSettings { pub flexible: bool, pub sidebar_side: SidebarDockPosition, pub threads_sidebar_default_width: Pixels, + pub threads_sidebar_auto_open: bool, pub default_width: Pixels, pub default_height: Pixels, pub max_content_width: Option, @@ -774,6 +775,7 @@ impl Settings for AgentSettings { .unwrap() .into_gpui() .clamp(THREADS_LIST_MIN_WIDTH, THREADS_LIST_MAX_WIDTH), + threads_sidebar_auto_open: agent.threads_sidebar_auto_open.unwrap(), 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() { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 64780d7398..f25c16a4d3 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -1012,6 +1012,7 @@ mod tests { show_merge_conflict_indicator: true, sidebar_side: Default::default(), threads_sidebar_default_width: px(300.), + threads_sidebar_auto_open: true, thinking_display: Default::default(), }; diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index bf70fbfd27..dea0240446 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -226,6 +226,17 @@ pub struct AgentSettingsContent { /// Default: 300 #[schemars(range(min = 200, max = 800))] pub threads_sidebar_default_width: Option, + /// Whether opening a folder in an existing window automatically opens the + /// Threads Sidebar. + /// + /// This applies when a folder opens in an existing window instead of a new + /// one, which happens when `default_open_behavior` or + /// `cli_default_open_behavior` is set to `existing_window`. When disabled, + /// the folder still opens in that window, but the sidebar stays closed + /// until you open it. + /// + /// Default: true + pub threads_sidebar_auto_open: 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 f8238aba25..78703221f2 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; 8] { + fn agent_panel_section() -> [SettingsPageItem; 9] { [ SettingsPageItem::SectionHeader("Agent Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -6913,6 +6913,29 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Threads Sidebar Auto Open", + description: "Whether opening a folder in an existing window automatically opens the Threads Sidebar.", + field: Box::new(SettingField { + organization_override: None, + json_path: Some("agent.threads_sidebar_auto_open"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .threads_sidebar_auto_open + .as_ref() + }, + write: |settings_content, value, _| { + settings_content + .agent + .get_or_insert_default() + .threads_sidebar_auto_open = 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/workspace/src/multi_workspace_tests.rs b/crates/workspace/src/multi_workspace_tests.rs index 289d640423..f708991e42 100644 --- a/crates/workspace/src/multi_workspace_tests.rs +++ b/crates/workspace/src/multi_workspace_tests.rs @@ -316,6 +316,93 @@ async fn test_open_new_window_does_not_open_sidebar_on_existing_window(cx: &mut .unwrap(); } +#[gpui::test] +async fn test_open_directory_in_existing_window_opens_sidebar(cx: &mut TestAppContext) { + init_test(cx); + + let app_state = cx.update(AppState::test); + let fs = app_state.fs.as_fake(); + fs.insert_tree(path!("/project_a"), json!({ "file.txt": "" })) + .await; + fs.insert_tree(path!("/project_b"), json!({ "file.txt": "" })) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/project_a").as_ref()], cx).await; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + cx.run_until_parked(); + + cx.update(|cx| { + open_paths( + &[PathBuf::from(path!("/project_b"))], + app_state, + OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + window + .read_with(cx, |mw, _cx| { + assert!( + mw.sidebar_open(), + "adding a directory to an existing window opens the sidebar by default", + ); + assert_eq!(mw.workspaces().count(), 2); + }) + .unwrap(); +} + +#[gpui::test] +async fn test_open_directory_in_existing_window_respects_auto_open_setting( + cx: &mut TestAppContext, +) { + init_test(cx); + + let app_state = cx.update(AppState::test); + let fs = app_state.fs.as_fake(); + fs.insert_tree(path!("/project_a"), json!({ "file.txt": "" })) + .await; + fs.insert_tree(path!("/project_b"), json!({ "file.txt": "" })) + .await; + + cx.update(|cx| { + let mut settings = AgentSettings::get_global(cx).clone(); + settings.threads_sidebar_auto_open = false; + AgentSettings::override_global(settings, cx); + }); + + let project = Project::test(app_state.fs.clone(), [path!("/project_a").as_ref()], cx).await; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + cx.run_until_parked(); + + cx.update(|cx| { + open_paths( + &[PathBuf::from(path!("/project_b"))], + app_state, + OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + window + .read_with(cx, |mw, _cx| { + assert!( + !mw.sidebar_open(), + "the sidebar must stay closed when `threads_sidebar_auto_open` is disabled", + ); + assert_eq!( + mw.workspaces().count(), + 2, + "the directory is still added to the existing window, and the workspace it \ + replaces is retained", + ); + }) + .unwrap(); +} + #[gpui::test] async fn test_open_directory_in_empty_workspace_does_not_open_sidebar(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0f1def9079..b244a36e9b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,6 +43,7 @@ pub use remote::{ }; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; +use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use client::{ ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore, @@ -11168,7 +11169,14 @@ pub fn open_paths( open_options.requesting_window = Some(window); window .update(cx, |multi_workspace, _, cx| { - multi_workspace.open_sidebar(cx); + if AgentSettings::get_global(cx).threads_sidebar_auto_open { + multi_workspace.open_sidebar(cx); + } else { + // Opening the sidebar is also what pins the + // workspace we are about to navigate away from, + // so pin it here to keep it in this window. + multi_workspace.retain_active_workspace(cx); + } }) .log_err(); } diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 86c7293091..1c60421da3 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -145,6 +145,28 @@ If you haven’t manually resized the sidebar, its width follows changes to this 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. +### Threads Sidebar Auto Open {#agent-threads-sidebar-auto-open} + +- Description: Whether opening a folder in an existing window automatically opens the [Threads Sidebar](../ai/parallel-agents.md#threads-sidebar). +- Setting: `agent.threads_sidebar_auto_open` +- Default: `true` + +**Options** + +`true` or `false`. + +This applies when a folder opens in an existing window instead of a new one, which happens when `default_open_behavior` or `cli_default_open_behavior` is set to `existing_window`. With `false`, the folder still opens in that window, but the sidebar stays closed until you open it with {#action multi_workspace::ToggleWorkspaceSidebar}. + +Open the Settings Editor and search for “Threads Sidebar Auto Open”. Or add this to your `settings.json`: + +```json [settings] +{ + "agent": { + "threads_sidebar_auto_open": false + } +} +``` + ## Agent UI Font Size - Description: The font size for text in the agent panel. Inherits the UI font size if unset.