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.