Move threads sidebar into agent panel (#51241)

Max Brunsfeld , Eric Holk , Mikayla Maki , and Anthony Eid created

* [x] Put back persistence of sidebar open state
* [x] when agent panel is docked right, put sidebar on the right side
* [x] remove stale entries from `SidebarsByWindow`

Release Notes:

- N/A

---------

Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

Cargo.lock                                          |  29 
Cargo.toml                                          |   3 
crates/agent_ui/Cargo.toml                          |   1 
crates/agent_ui/src/agent_panel.rs                  | 249 +++++++
crates/agent_ui/src/agent_ui.rs                     |   1 
crates/agent_ui/src/connection_view.rs              |   2 
crates/agent_ui/src/sidebar.rs                      | 497 ++++++--------
crates/debugger_ui/src/tests/stack_frame_list.rs    |   4 
crates/platform_title_bar/src/platform_title_bar.rs |  40 -
crates/sidebar/Cargo.toml                           |  51 -
crates/sidebar/LICENSE-GPL                          |   1 
crates/title_bar/Cargo.toml                         |   1 
crates/title_bar/src/title_bar.rs                   | 125 ---
crates/workspace/src/multi_workspace.rs             | 405 -----------
crates/workspace/src/persistence.rs                 |  10 
crates/workspace/src/persistence/model.rs           |   9 
crates/workspace/src/status_bar.rs                  |  14 
crates/workspace/src/workspace.rs                   |  29 
crates/zed/Cargo.toml                               |   1 
crates/zed/src/visual_test_runner.rs                |  42 -
crates/zed/src/zed.rs                               |  15 
21 files changed, 517 insertions(+), 1,012 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15807,33 +15807,6 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
-[[package]]
-name = "sidebar"
-version = "0.1.0"
-dependencies = [
- "acp_thread",
- "agent",
- "agent-client-protocol",
- "agent_ui",
- "assistant_text_thread",
- "chrono",
- "editor",
- "feature_flags",
- "fs",
- "gpui",
- "language_model",
- "menu",
- "project",
- "recent_projects",
- "serde_json",
- "settings",
- "theme",
- "ui",
- "util",
- "workspace",
- "zed_actions",
-]
-
 [[package]]
 name = "signal-hook"
 version = "0.3.18"
@@ -17660,7 +17633,6 @@ dependencies = [
  "client",
  "cloud_api_types",
  "db",
- "feature_flags",
  "git_ui",
  "gpui",
  "notifications",
@@ -21887,7 +21859,6 @@ dependencies = [
  "settings_profile_selector",
  "settings_ui",
  "shellexpand 2.1.2",
- "sidebar",
  "smol",
  "snippet_provider",
  "snippets_ui",

Cargo.toml 🔗

@@ -173,7 +173,6 @@ members = [
     "crates/settings_profile_selector",
     "crates/settings_ui",
     "crates/shell_command_parser",
-    "crates/sidebar",
     "crates/snippet",
     "crates/snippet_provider",
     "crates/snippets_ui",
@@ -412,7 +411,6 @@ rules_library = { path = "crates/rules_library" }
 scheduler = { path = "crates/scheduler" }
 search = { path = "crates/search" }
 session = { path = "crates/session" }
-sidebar = { path = "crates/sidebar" }
 settings = { path = "crates/settings" }
 settings_content = { path = "crates/settings_content" }
 settings_json = { path = "crates/settings_json" }
@@ -907,7 +905,6 @@ refineable = { codegen-units = 1 }
 release_channel = { codegen-units = 1 }
 reqwest_client = { codegen-units = 1 }
 session = { codegen-units = 1 }
-sidebar = { codegen-units = 1 }
 snippet = { codegen-units = 1 }
 snippets_ui = { codegen-units = 1 }
 story = { codegen-units = 1 }

crates/agent_ui/Cargo.toml 🔗

@@ -132,7 +132,6 @@ languages = { workspace = true, features = ["test-support"] }
 language_model = { workspace = true, "features" = ["test-support"] }
 pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
-
 semver.workspace = true
 reqwest_client.workspace = true
 

crates/agent_ui/src/agent_panel.rs 🔗

@@ -65,9 +65,10 @@ use extension_host::ExtensionStore;
 use fs::Fs;
 use git::repository::validate_worktree_directory;
 use gpui::{
-    Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
-    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
-    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
+    Action, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardItem,
+    Corner, DismissEvent, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle,
+    Focusable, KeyContext, MouseButton, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
+    deferred, prelude::*, pulsating_between,
 };
 use language::LanguageRegistry;
 use language_model::{ConfigurationError, LanguageModelRegistry};
@@ -79,15 +80,17 @@ use search::{BufferSearchBar, buffer_search};
 use settings::{Settings, update_settings_file};
 use theme::ThemeSettings;
 use ui::{
-    Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding,
-    PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
+    Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, Indicator,
+    KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
     utils::WithRemSize,
 };
 use util::ResultExt as _;
 use workspace::{
-    CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
-    WorkspaceId,
+    CollaboratorId, DraggedSelection, DraggedSidebar, DraggedTab, FocusWorkspaceSidebar,
+    MultiWorkspace, SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, ToggleZoom,
+    ToolbarItemView, Workspace, WorkspaceId,
     dock::{DockPosition, Panel, PanelEvent},
+    multi_workspace_enabled,
 };
 use zed_actions::{
     DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
@@ -99,6 +102,55 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
 const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
 const DEFAULT_THREAD_TITLE: &str = "New Thread";
 
+#[derive(Default)]
+struct SidebarsByWindow(
+    collections::HashMap<gpui::WindowId, gpui::WeakEntity<crate::sidebar::Sidebar>>,
+);
+
+impl gpui::Global for SidebarsByWindow {}
+
+pub(crate) fn sidebar_is_open(window: &Window, cx: &App) -> bool {
+    if !multi_workspace_enabled(cx) {
+        return false;
+    }
+    let window_id = window.window_handle().window_id();
+    cx.try_global::<SidebarsByWindow>()
+        .and_then(|sidebars| sidebars.0.get(&window_id)?.upgrade())
+        .is_some_and(|sidebar| sidebar.read(cx).is_open())
+}
+
+fn find_or_create_sidebar_for_window(
+    window: &mut Window,
+    cx: &mut App,
+) -> Option<Entity<crate::sidebar::Sidebar>> {
+    let window_id = window.window_handle().window_id();
+    let multi_workspace = window.root::<MultiWorkspace>().flatten()?;
+
+    if !cx.has_global::<SidebarsByWindow>() {
+        cx.set_global(SidebarsByWindow::default());
+    }
+
+    cx.global_mut::<SidebarsByWindow>()
+        .0
+        .retain(|_, weak| weak.upgrade().is_some());
+
+    let existing = cx
+        .global::<SidebarsByWindow>()
+        .0
+        .get(&window_id)
+        .and_then(|weak| weak.upgrade());
+
+    if let Some(sidebar) = existing {
+        return Some(sidebar);
+    }
+
+    let sidebar = cx.new(|cx| crate::sidebar::Sidebar::new(multi_workspace, window, cx));
+    cx.global_mut::<SidebarsByWindow>()
+        .0
+        .insert(window_id, sidebar.downgrade());
+    Some(sidebar)
+}
+
 fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
     let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
     let key = i64::from(workspace_id).to_string();
@@ -424,6 +476,30 @@ pub fn init(cx: &mut App) {
                             panel.set_start_thread_in(action, cx);
                         });
                     }
+                })
+                .register_action(|workspace, _: &ToggleWorkspaceSidebar, window, cx| {
+                    if !multi_workspace_enabled(cx) {
+                        return;
+                    }
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        if let Some(sidebar) = panel.read(cx).sidebar.clone() {
+                            sidebar.update(cx, |sidebar, cx| {
+                                sidebar.toggle(window, cx);
+                            });
+                        }
+                    }
+                })
+                .register_action(|workspace, _: &FocusWorkspaceSidebar, window, cx| {
+                    if !multi_workspace_enabled(cx) {
+                        return;
+                    }
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        if let Some(sidebar) = panel.read(cx).sidebar.clone() {
+                            sidebar.update(cx, |sidebar, cx| {
+                                sidebar.focus_or_unfocus(workspace, window, cx);
+                            });
+                        }
+                    }
                 });
         },
     )
@@ -820,6 +896,7 @@ pub struct AgentPanel {
     last_configuration_error_telemetry: Option<String>,
     on_boarding_upsell_dismissed: AtomicBool,
     _active_view_observation: Option<Subscription>,
+    pub(crate) sidebar: Option<Entity<crate::sidebar::Sidebar>>,
 }
 
 impl AgentPanel {
@@ -991,7 +1068,6 @@ impl AgentPanel {
         let client = workspace.client().clone();
         let workspace_id = workspace.database_id();
         let workspace = workspace.weak_handle();
-
         let context_server_registry =
             cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
@@ -1149,10 +1225,17 @@ impl AgentPanel {
             last_configuration_error_telemetry: None,
             on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()),
             _active_view_observation: None,
+            sidebar: None,
         };
 
         // Initial sync of agent servers from extensions
         panel.sync_agent_servers_from_extensions(cx);
+
+        cx.defer_in(window, move |this, window, cx| {
+            this.sidebar = find_or_create_sidebar_for_window(window, cx);
+            cx.notify();
+        });
+
         panel
     }
 
@@ -3526,9 +3609,109 @@ impl AgentPanel {
             })
     }
 
+    fn sidebar_info(&self, cx: &App) -> Option<(AnyView, Pixels, bool)> {
+        if !multi_workspace_enabled(cx) {
+            return None;
+        }
+        let sidebar = self.sidebar.as_ref()?;
+        let is_open = sidebar.read(cx).is_open();
+        let width = sidebar.read(cx).width(cx);
+        let view: AnyView = sidebar.clone().into();
+        Some((view, width, is_open))
+    }
+
+    fn render_sidebar_toggle(&self, cx: &Context<Self>) -> Option<AnyElement> {
+        if !multi_workspace_enabled(cx) {
+            return None;
+        }
+        let sidebar = self.sidebar.as_ref()?;
+        let sidebar_read = sidebar.read(cx);
+        if sidebar_read.is_open() {
+            return None;
+        }
+        let has_notifications = sidebar_read.has_notifications(cx);
+
+        Some(
+            IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
+                .icon_size(IconSize::Small)
+                .when(has_notifications, |button| {
+                    button
+                        .indicator(Indicator::dot().color(Color::Accent))
+                        .indicator_border_color(Some(cx.theme().colors().tab_bar_background))
+                })
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
+                })
+                .on_click(|_, window, cx| {
+                    window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+                })
+                .into_any_element(),
+        )
+    }
+
+    fn render_sidebar(&self, cx: &Context<Self>) -> Option<AnyElement> {
+        let (sidebar_view, sidebar_width, is_open) = self.sidebar_info(cx)?;
+        if !is_open {
+            return None;
+        }
+
+        let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
+        let sidebar = self.sidebar.as_ref()?.downgrade();
+
+        let resize_handle = deferred(
+            div()
+                .id("sidebar-resize-handle")
+                .absolute()
+                .when(docked_right, |this| {
+                    this.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
+                })
+                .when(!docked_right, |this| {
+                    this.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
+                })
+                .top(px(0.))
+                .h_full()
+                .w(SIDEBAR_RESIZE_HANDLE_SIZE)
+                .cursor_col_resize()
+                .on_drag(DraggedSidebar, |dragged, _, _, cx| {
+                    cx.stop_propagation();
+                    cx.new(|_| dragged.clone())
+                })
+                .on_mouse_down(MouseButton::Left, |_, _, cx| {
+                    cx.stop_propagation();
+                })
+                .on_mouse_up(MouseButton::Left, move |event, _, cx| {
+                    if event.click_count == 2 {
+                        sidebar
+                            .update(cx, |sidebar, cx| {
+                                sidebar.set_width(None, cx);
+                            })
+                            .ok();
+                        cx.stop_propagation();
+                    }
+                })
+                .occlude(),
+        );
+
+        Some(
+            div()
+                .id("sidebar-container")
+                .relative()
+                .h_full()
+                .w(sidebar_width)
+                .flex_shrink_0()
+                .when(docked_right, |this| this.border_l_1())
+                .when(!docked_right, |this| this.border_r_1())
+                .border_color(cx.theme().colors().border)
+                .child(sidebar_view)
+                .child(resize_handle)
+                .into_any_element(),
+        )
+    }
+
     fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let agent_server_store = self.project.read(cx).agent_server_store().clone();
         let focus_handle = self.focus_handle(cx);
+        let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
 
         let (selected_agent_custom_icon, selected_agent_label) =
             if let AgentType::Custom { name, .. } = &self.selected_agent {
@@ -3991,6 +4174,9 @@ impl AgentPanel {
                         .size_full()
                         .gap(DynamicSpacing::Base04.rems(cx))
                         .pl(DynamicSpacing::Base04.rems(cx))
+                        .when(!docked_right, |this| {
+                            this.children(self.render_sidebar_toggle(cx))
+                        })
                         .child(agent_selector_menu)
                         .child(self.render_start_thread_in_selector(cx)),
                 )
@@ -4007,7 +4193,10 @@ impl AgentPanel {
                                 cx,
                             ))
                         })
-                        .child(self.render_panel_options_menu(window, cx)),
+                        .child(self.render_panel_options_menu(window, cx))
+                        .when(docked_right, |this| {
+                            this.children(self.render_sidebar_toggle(cx))
+                        }),
                 )
                 .into_any_element()
         } else {
@@ -4045,6 +4234,9 @@ impl AgentPanel {
                         .size_full()
                         .gap(DynamicSpacing::Base04.rems(cx))
                         .pl(DynamicSpacing::Base04.rems(cx))
+                        .when(!docked_right, |this| {
+                            this.children(self.render_sidebar_toggle(cx))
+                        })
                         .child(match &self.active_view {
                             ActiveView::History { .. } | ActiveView::Configuration => {
                                 self.render_toolbar_back_button(cx).into_any_element()
@@ -4067,7 +4259,10 @@ impl AgentPanel {
                                 cx,
                             ))
                         })
-                        .child(self.render_panel_options_menu(window, cx)),
+                        .child(self.render_panel_options_menu(window, cx))
+                        .when(docked_right, |this| {
+                            this.children(self.render_sidebar_toggle(cx))
+                        }),
                 )
                 .into_any_element()
         }
@@ -4607,14 +4802,44 @@ impl Render for AgentPanel {
             })
             .children(self.render_trial_end_upsell(window, cx));
 
+        let sidebar = self.render_sidebar(cx);
+        let has_sidebar = sidebar.is_some();
+        let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
+
+        let panel = h_flex()
+            .size_full()
+            .when(has_sidebar, |this| {
+                this.on_drag_move(cx.listener(
+                    move |this, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
+                        if let Some(sidebar) = &this.sidebar {
+                            let width = if docked_right {
+                                e.bounds.right() - e.event.position.x
+                            } else {
+                                e.event.position.x
+                            };
+                            sidebar.update(cx, |sidebar, cx| {
+                                sidebar.set_width(Some(width), cx);
+                            });
+                        }
+                    },
+                ))
+            })
+            .map(|this| {
+                if docked_right {
+                    this.child(content).children(sidebar)
+                } else {
+                    this.children(sidebar).child(content)
+                }
+            });
+
         match self.active_view.which_font_size_used() {
             WhichFontSize::AgentFont => {
                 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
                     .size_full()
-                    .child(content)
+                    .child(panel)
                     .into_any()
             }
-            _ => content.into_any(),
+            _ => panel.into_any(),
         }
     }
 }

crates/agent_ui/src/agent_ui.rs 🔗

@@ -23,6 +23,7 @@ mod mode_selector;
 mod model_selector;
 mod model_selector_popover;
 mod profile_selector;
+pub mod sidebar;
 mod slash_command;
 mod slash_command_picker;
 mod terminal_codegen;

crates/agent_ui/src/connection_view.rs 🔗

@@ -2340,7 +2340,7 @@ impl ConnectionView {
         }
 
         if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
-            multi_workspace.read(cx).is_sidebar_open()
+            crate::agent_panel::sidebar_is_open(window, cx)
                 || self.agent_panel_visible(&multi_workspace, cx)
         } else {
             self.workspace

crates/sidebar/src/sidebar.rs → crates/agent_ui/src/sidebar.rs 🔗

@@ -1,33 +1,32 @@
+use crate::{AgentPanel, AgentPanelEvent, NewThread};
 use acp_thread::ThreadStatus;
 use agent::ThreadStore;
 use agent_client_protocol as acp;
-use agent_ui::{AgentPanel, AgentPanelEvent, NewThread};
+use agent_settings::AgentSettings;
 use chrono::Utc;
+use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorElement, EditorStyle};
 use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
 use gpui::{
-    AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, ListState,
+    Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, FontStyle, ListState,
     Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, list, prelude::*, px,
     relative, rems,
 };
 use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::Event as ProjectEvent;
-use recent_projects::RecentProjects;
 use settings::Settings;
 use std::collections::{HashMap, HashSet};
 use std::mem;
 use theme::{ActiveTheme, ThemeSettings};
-use ui::utils::TRAFFIC_LIGHT_PADDING;
 use ui::{
-    AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, KeyBinding, ListItem,
-    PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, prelude::*,
+    AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem,
+    Tooltip, WithScrollbar, prelude::*,
 };
+use util::ResultExt as _;
 use util::path_list::PathList;
 use workspace::{
-    FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar as WorkspaceSidebar,
-    SidebarEvent, ToggleWorkspaceSidebar, Workspace,
+    MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled,
 };
-use zed_actions::OpenRecent;
 use zed_actions::editor::{MoveDown, MoveUp};
 
 actions!(
@@ -44,6 +43,27 @@ const DEFAULT_WIDTH: Pixels = px(320.0);
 const MIN_WIDTH: Pixels = px(200.0);
 const MAX_WIDTH: Pixels = px(800.0);
 const DEFAULT_THREADS_SHOWN: usize = 5;
+const SIDEBAR_STATE_KEY: &str = "sidebar_state";
+
+fn read_sidebar_open_state(multi_workspace_id: u64) -> bool {
+    KEY_VALUE_STORE
+        .scoped(SIDEBAR_STATE_KEY)
+        .read(&multi_workspace_id.to_string())
+        .log_err()
+        .flatten()
+        .and_then(|json| serde_json::from_str::<bool>(&json).ok())
+        .unwrap_or(false)
+}
+
+async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) {
+    if let Ok(json) = serde_json::to_string(&is_open) {
+        KEY_VALUE_STORE
+            .scoped(SIDEBAR_STATE_KEY)
+            .write(multi_workspace_id.to_string(), json)
+            .await
+            .log_err();
+    }
+}
 
 #[derive(Clone, Debug)]
 struct ActiveThreadInfo {
@@ -173,6 +193,8 @@ fn workspace_path_list_and_label(
 
 pub struct Sidebar {
     multi_workspace: WeakEntity<MultiWorkspace>,
+    persistence_key: Option<u64>,
+    is_open: bool,
     width: Pixels,
     focus_handle: FocusHandle,
     filter_editor: Entity<Editor>,
@@ -186,11 +208,8 @@ pub struct Sidebar {
     active_entry_index: Option<usize>,
     collapsed_groups: HashSet<PathList>,
     expanded_groups: HashMap<PathList, usize>,
-    recent_projects_popover_handle: PopoverMenuHandle<RecentProjects>,
 }
 
-impl EventEmitter<SidebarEvent> for Sidebar {}
-
 impl Sidebar {
     pub fn new(
         multi_workspace: Entity<MultiWorkspace>,
@@ -212,7 +231,6 @@ impl Sidebar {
             window,
             |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
                 MultiWorkspaceEvent::ActiveWorkspaceChanged => {
-                    this.focused_thread = None;
                     this.update_entries(cx);
                 }
                 MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
@@ -270,8 +288,15 @@ impl Sidebar {
             this.update_entries(cx);
         });
 
+        let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0);
+        let is_open = persistence_key
+            .map(read_sidebar_open_state)
+            .unwrap_or(false);
+
         Self {
             multi_workspace: multi_workspace.downgrade(),
+            persistence_key,
+            is_open,
             width: DEFAULT_WIDTH,
             focus_handle,
             filter_editor,
@@ -282,7 +307,6 @@ impl Sidebar {
             active_entry_index: None,
             collapsed_groups: HashSet::new(),
             expanded_groups: HashMap::new(),
-            recent_projects_popover_handle: PopoverMenuHandle::default(),
         }
     }
 
@@ -334,31 +358,10 @@ impl Sidebar {
         cx.subscribe_in(
             agent_panel,
             window,
-            |this, agent_panel, event: &AgentPanelEvent, _window, cx| match event {
-                AgentPanelEvent::ActiveViewChanged => {
-                    match agent_panel.read(cx).active_connection_view() {
-                        Some(thread) => {
-                            if let Some(session_id) = thread.read(cx).parent_id(cx) {
-                                this.focused_thread = Some(session_id);
-                            }
-                        }
-                        None => {
-                            this.focused_thread = None;
-                        }
-                    }
-                    this.update_entries(cx);
-                }
-                AgentPanelEvent::ThreadFocused => {
-                    let new_focused = agent_panel
-                        .read(cx)
-                        .active_connection_view()
-                        .and_then(|thread| thread.read(cx).parent_id(cx));
-                    if new_focused.is_some() && new_focused != this.focused_thread {
-                        this.focused_thread = new_focused;
-                        this.update_entries(cx);
-                    }
-                }
-                AgentPanelEvent::BackgroundThreadChanged => {
+            |this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event {
+                AgentPanelEvent::ActiveViewChanged
+                | AgentPanelEvent::ThreadFocused
+                | AgentPanelEvent::BackgroundThreadChanged => {
                     this.update_entries(cx);
                 }
             },
@@ -419,6 +422,12 @@ impl Sidebar {
         let workspaces = mw.workspaces().to_vec();
         let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned();
 
+        self.focused_thread = active_workspace
+            .as_ref()
+            .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx))
+            .and_then(|panel| panel.read(cx).active_connection_view().cloned())
+            .and_then(|cv| cv.read(cx).parent_id(cx));
+
         let thread_store = ThreadStore::try_global(cx);
         let query = self.filter_editor.read(cx).text(cx);
 
@@ -657,7 +666,7 @@ impl Sidebar {
         let Some(multi_workspace) = self.multi_workspace.upgrade() else {
             return;
         };
-        if !multi_workspace.read(cx).multi_workspace_enabled(cx) {
+        if !multi_workspace_enabled(cx) {
             return;
         }
 
@@ -885,8 +894,6 @@ impl Sidebar {
             return;
         };
 
-        self.focused_thread = None;
-
         multi_workspace.update(cx, |multi_workspace, cx| {
             multi_workspace.activate(workspace.clone(), cx);
         });
@@ -1173,48 +1180,6 @@ impl Sidebar {
             .into_any_element()
     }
 
-    fn render_recent_projects_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        let workspace = self
-            .multi_workspace
-            .upgrade()
-            .map(|mw| mw.read(cx).workspace().downgrade());
-
-        let focus_handle = workspace
-            .as_ref()
-            .and_then(|ws| ws.upgrade())
-            .map(|w| w.read(cx).focus_handle(cx))
-            .unwrap_or_else(|| cx.focus_handle());
-
-        let popover_handle = self.recent_projects_popover_handle.clone();
-
-        PopoverMenu::new("sidebar-recent-projects-menu")
-            .with_handle(popover_handle)
-            .menu(move |window, cx| {
-                workspace.as_ref().map(|ws| {
-                    RecentProjects::popover(ws.clone(), false, focus_handle.clone(), window, cx)
-                })
-            })
-            .trigger_with_tooltip(
-                IconButton::new("open-project", IconName::OpenFolder)
-                    .icon_size(IconSize::Small)
-                    .selected_style(ButtonStyle::Tinted(TintColor::Accent)),
-                |_window, cx| {
-                    Tooltip::for_action(
-                        "Recent Projects",
-                        &OpenRecent {
-                            create_new_window: false,
-                        },
-                        cx,
-                    )
-                },
-            )
-            .anchor(gpui::Corner::TopLeft)
-            .offset(gpui::Point {
-                x: px(0.0),
-                y: px(2.0),
-            })
-    }
-
     fn render_filter_input(&self, cx: &mut Context<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
         let text_style = TextStyle {
@@ -1343,26 +1308,66 @@ impl Sidebar {
     }
 }
 
-impl WorkspaceSidebar for Sidebar {
-    fn width(&self, _cx: &App) -> Pixels {
-        self.width
+impl Sidebar {
+    pub fn is_open(&self) -> bool {
+        self.is_open
     }
 
-    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
-        self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH);
+    pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
+        if self.is_open == open {
+            return;
+        }
+        self.is_open = open;
         cx.notify();
+        if let Some(key) = self.persistence_key {
+            let is_open = self.is_open;
+            cx.background_spawn(async move {
+                save_sidebar_open_state(key, is_open).await;
+            })
+            .detach();
+        }
     }
 
-    fn has_notifications(&self, _cx: &App) -> bool {
-        !self.contents.notified_threads.is_empty()
+    pub fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let new_state = !self.is_open;
+        self.set_open(new_state, cx);
+        if new_state {
+            cx.focus_self(window);
+        }
+    }
+
+    pub fn focus_or_unfocus(
+        &mut self,
+        workspace: &mut Workspace,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.is_open {
+            let sidebar_is_focused = self.focus_handle(cx).contains_focused(window, cx);
+            if sidebar_is_focused {
+                let active_pane = workspace.active_pane().clone();
+                let pane_focus = active_pane.read(cx).focus_handle(cx);
+                window.focus(&pane_focus, cx);
+            } else {
+                cx.focus_self(window);
+            }
+        } else {
+            self.set_open(true, cx);
+            cx.focus_self(window);
+        }
     }
 
-    fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
-        self.recent_projects_popover_handle.toggle(window, cx);
+    pub fn width(&self, _cx: &App) -> Pixels {
+        self.width
+    }
+
+    pub fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
+        self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH);
+        cx.notify();
     }
 
-    fn is_recent_projects_popover_deployed(&self) -> bool {
-        self.recent_projects_popover_handle.is_deployed()
+    pub fn has_notifications(&self, _cx: &App) -> bool {
+        !self.contents.notified_threads.is_empty()
     }
 }
 
@@ -1374,18 +1379,9 @@ impl Focusable for Sidebar {
 
 impl Render for Sidebar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let titlebar_height = ui::utils::platform_title_bar_height(window);
         let ui_font = theme::setup_ui_font(window, cx);
-        let is_focused = self.focus_handle.is_focused(window)
-            || self.filter_editor.focus_handle(cx).is_focused(window);
         let has_query = self.has_filter_query(cx);
 
-        let focus_tooltip_label = if is_focused {
-            "Focus Workspace"
-        } else {
-            "Focus Sidebar"
-        };
-
         v_flex()
             .id("workspace-sidebar")
             .key_context("WorkspaceSidebar")
@@ -1401,69 +1397,26 @@ impl Render for Sidebar {
             .on_action(cx.listener(Self::collapse_selected_entry))
             .on_action(cx.listener(Self::cancel))
             .font(ui_font)
-            .h_full()
-            .w(self.width)
+            .size_full()
             .bg(cx.theme().colors().surface_background)
-            .border_r_1()
-            .border_color(cx.theme().colors().border)
-            .child(
-                h_flex()
-                    .flex_none()
-                    .h(titlebar_height)
-                    .w_full()
-                    .mt_px()
-                    .pb_px()
-                    .pr_1()
-                    .when_else(
-                        cfg!(target_os = "macos") && !window.is_fullscreen(),
-                        |this| this.pl(px(TRAFFIC_LIGHT_PADDING)),
-                        |this| this.pl_2(),
-                    )
-                    .justify_between()
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
-                    .child({
-                        let focus_handle_toggle = self.focus_handle.clone();
-                        let focus_handle_focus = self.focus_handle.clone();
-                        IconButton::new("close-sidebar", IconName::WorkspaceNavOpen)
-                            .icon_size(IconSize::Small)
-                            .tooltip(Tooltip::element(move |_, cx| {
-                                v_flex()
-                                    .gap_1()
-                                    .child(
-                                        h_flex()
-                                            .gap_2()
-                                            .justify_between()
-                                            .child(Label::new("Close Sidebar"))
-                                            .child(KeyBinding::for_action_in(
-                                                &ToggleWorkspaceSidebar,
-                                                &focus_handle_toggle,
-                                                cx,
-                                            )),
-                                    )
-                                    .child(
-                                        h_flex()
-                                            .pt_1()
-                                            .gap_2()
-                                            .border_t_1()
-                                            .border_color(cx.theme().colors().border_variant)
-                                            .justify_between()
-                                            .child(Label::new(focus_tooltip_label))
-                                            .child(KeyBinding::for_action_in(
-                                                &FocusWorkspaceSidebar,
-                                                &focus_handle_focus,
-                                                cx,
-                                            )),
-                                    )
-                                    .into_any_element()
-                            }))
-                            .on_click(cx.listener(|_this, _, _window, cx| {
-                                cx.emit(SidebarEvent::Close);
-                            }))
-                    })
-                    .child(self.render_recent_projects_button(cx)),
-            )
-            .child(
+            .child({
+                let docked_right =
+                    AgentSettings::get_global(cx).dock == settings::DockPosition::Right;
+                let render_close_button = || {
+                    IconButton::new("sidebar-close-toggle", IconName::WorkspaceNavOpen)
+                        .icon_size(IconSize::Small)
+                        .tooltip(move |_, cx| {
+                            Tooltip::for_action(
+                                "Close Threads Sidebar",
+                                &ToggleWorkspaceSidebar,
+                                cx,
+                            )
+                        })
+                        .on_click(|_, window, cx| {
+                            window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+                        })
+                };
+
                 h_flex()
                     .flex_none()
                     .px_2p5()
@@ -1471,6 +1424,7 @@ impl Render for Sidebar {
                     .gap_2()
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
+                    .when(!docked_right, |this| this.child(render_close_button()))
                     .child(
                         Icon::new(IconName::MagnifyingGlass)
                             .size(IconSize::Small)
@@ -1487,8 +1441,9 @@ impl Render for Sidebar {
                                     this.update_entries(cx);
                                 })),
                         )
-                    }),
-            )
+                    })
+                    .when(docked_right, |this| this.child(render_close_button()))
+            })
             .child(
                 v_flex()
                     .flex_1()
@@ -1509,26 +1464,24 @@ impl Render for Sidebar {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::test_support::{active_session_id, open_thread_with_connection, send_message};
     use acp_thread::StubAgentConnection;
     use agent::ThreadStore;
-    use agent_ui::test_support::{active_session_id, open_thread_with_connection, send_message};
     use assistant_text_thread::TextThreadStore;
     use chrono::DateTime;
     use feature_flags::FeatureFlagAppExt as _;
     use fs::FakeFs;
     use gpui::TestAppContext;
-    use settings::SettingsStore;
     use std::sync::Arc;
     use util::path_list::PathList;
 
     fn init_test(cx: &mut TestAppContext) {
+        crate::test_support::init_test(cx);
         cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            editor::init(cx);
             cx.update_flags(false, vec!["agent-v2".into()]);
             ThreadStore::init_global(cx);
+            language_model::LanguageModelRegistry::test(cx);
+            prompt_store::init(cx);
         });
     }
 
@@ -1569,14 +1522,33 @@ mod tests {
         multi_workspace: &Entity<MultiWorkspace>,
         cx: &mut gpui::VisualTestContext,
     ) -> Entity<Sidebar> {
-        let multi_workspace = multi_workspace.clone();
-        let sidebar =
-            cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx)));
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.register_sidebar(sidebar.clone(), window, cx);
+        let (sidebar, _panel) = setup_sidebar_with_agent_panel(multi_workspace, cx);
+        sidebar
+    }
+
+    fn setup_sidebar_with_agent_panel(
+        multi_workspace: &Entity<MultiWorkspace>,
+        cx: &mut gpui::VisualTestContext,
+    ) -> (Entity<Sidebar>, Entity<AgentPanel>) {
+        let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
+        let project = workspace.read_with(cx, |ws, _cx| ws.project().clone());
+        let panel = add_agent_panel(&workspace, &project, cx);
+        workspace.update_in(cx, |workspace, window, cx| {
+            workspace.right_dock().update(cx, |dock, cx| {
+                if let Some(panel_ix) = dock.panel_index_for_type::<AgentPanel>() {
+                    dock.activate_panel(panel_ix, window, cx);
+                }
+                dock.set_open(true, window, cx);
+            });
         });
         cx.run_until_parked();
-        sidebar
+        let sidebar = panel.read_with(cx, |panel, _cx| {
+            panel
+                .sidebar
+                .clone()
+                .expect("AgentPanel should have created a sidebar")
+        });
+        (sidebar, panel)
     }
 
     async fn save_n_test_threads(
@@ -1623,16 +1595,10 @@ mod tests {
         cx.run_until_parked();
     }
 
-    fn open_and_focus_sidebar(
-        sidebar: &Entity<Sidebar>,
-        multi_workspace: &Entity<MultiWorkspace>,
-        cx: &mut gpui::VisualTestContext,
-    ) {
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.toggle_sidebar(window, cx);
-        });
+    fn open_and_focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
         cx.run_until_parked();
-        sidebar.update_in(cx, |_, window, cx| {
+        sidebar.update_in(cx, |sidebar, window, cx| {
+            sidebar.set_open(true, cx);
             cx.focus_self(window);
         });
         cx.run_until_parked();
@@ -1886,7 +1852,7 @@ mod tests {
         assert!(entries.iter().any(|e| e.contains("View More (12)")));
 
         // Focus and navigate to View More, then confirm to expand by one batch
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         for _ in 0..7 {
             cx.dispatch_action(SelectNext);
         }
@@ -2169,7 +2135,7 @@ mod tests {
         // Entries: [header, thread3, thread2, thread1]
         // Focusing the sidebar does not set a selection; select_next/select_previous
         // handle None gracefully by starting from the first or last entry.
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
 
         // First SelectNext from None starts at index 0
@@ -2218,7 +2184,7 @@ mod tests {
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
         cx.run_until_parked();
 
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
 
         // SelectLast jumps to the end
         cx.dispatch_action(SelectLast);
@@ -2241,7 +2207,7 @@ mod tests {
 
         // Open the sidebar so it's rendered, then focus it to trigger focus_in.
         // focus_in no longer sets a default selection.
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
 
         // Manually set a selection, blur, then refocus — selection should be preserved
@@ -2273,6 +2239,9 @@ mod tests {
         });
         cx.run_until_parked();
 
+        // Add an agent panel to workspace 1 so the sidebar renders when it's active.
+        setup_sidebar_with_agent_panel(&multi_workspace, cx);
+
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
         save_n_test_threads(1, &path_list, cx).await;
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
@@ -2299,7 +2268,7 @@ mod tests {
         );
 
         // Focus the sidebar and manually select the header (index 0)
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         sidebar.update_in(cx, |sidebar, _window, _cx| {
             sidebar.selection = Some(0);
         });
@@ -2342,7 +2311,7 @@ mod tests {
         assert!(entries.iter().any(|e| e.contains("View More (3)")));
 
         // Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6)
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         for _ in 0..7 {
             cx.dispatch_action(SelectNext);
         }
@@ -2377,7 +2346,7 @@ mod tests {
         );
 
         // Focus sidebar and manually select the header (index 0). Press left to collapse.
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         sidebar.update_in(cx, |sidebar, _window, _cx| {
             sidebar.selection = Some(0);
         });
@@ -2417,7 +2386,7 @@ mod tests {
         cx.run_until_parked();
 
         // Focus sidebar (selection starts at None), then navigate down to the thread (child)
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         cx.dispatch_action(SelectNext);
         cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
@@ -2452,7 +2421,7 @@ mod tests {
         );
 
         // Focus sidebar — focus_in does not set a selection
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
 
         // First SelectNext from None starts at index 0 (header)
@@ -2485,7 +2454,7 @@ mod tests {
         cx.run_until_parked();
 
         // Focus sidebar (selection starts at None), navigate down to the thread (index 1)
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         cx.dispatch_action(SelectNext);
         cx.dispatch_action(SelectNext);
         assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
@@ -2505,24 +2474,6 @@ mod tests {
         );
     }
 
-    async fn init_test_project_with_agent_panel(
-        worktree_path: &str,
-        cx: &mut TestAppContext,
-    ) -> Entity<project::Project> {
-        agent_ui::test_support::init_test(cx);
-        cx.update(|cx| {
-            cx.update_flags(false, vec!["agent-v2".into()]);
-            ThreadStore::init_global(cx);
-            language_model::LanguageModelRegistry::test(cx);
-        });
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(worktree_path, serde_json::json!({ "src": {} }))
-            .await;
-        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
-        project::Project::test(fs, [worktree_path.as_ref()], cx).await
-    }
-
     fn add_agent_panel(
         workspace: &Entity<Workspace>,
         project: &Entity<project::Project>,
@@ -2536,23 +2487,12 @@ mod tests {
         })
     }
 
-    fn setup_sidebar_with_agent_panel(
-        multi_workspace: &Entity<MultiWorkspace>,
-        project: &Entity<project::Project>,
-        cx: &mut gpui::VisualTestContext,
-    ) -> (Entity<Sidebar>, Entity<AgentPanel>) {
-        let sidebar = setup_sidebar(multi_workspace, cx);
-        let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
-        let panel = add_agent_panel(&workspace, project, cx);
-        (sidebar, panel)
-    }
-
     #[gpui::test]
     async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
-        let project = init_test_project_with_agent_panel("/my-project", cx).await;
+        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, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+        let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
 
@@ -2595,10 +2535,10 @@ mod tests {
 
     #[gpui::test]
     async fn test_background_thread_completion_triggers_notification(cx: &mut TestAppContext) {
-        let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
+        let project_a = init_test_project("/project-a", cx).await;
         let (multi_workspace, cx) = cx
             .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
-        let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
+        let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
 
         let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
 
@@ -2802,7 +2742,7 @@ mod tests {
         );
 
         // User types a search query to filter down.
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         type_in_search(&sidebar, "alpha", cx);
         assert_eq!(
             visible_entries_as_strings(&sidebar, cx),
@@ -3125,7 +3065,7 @@ mod tests {
 
         // User focuses the sidebar and collapses the group using keyboard:
         // manually select the header, then press CollapseSelectedEntry to collapse.
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
         sidebar.update_in(cx, |sidebar, _window, _cx| {
             sidebar.selection = Some(0);
         });
@@ -3175,7 +3115,7 @@ mod tests {
         }
         cx.run_until_parked();
 
-        open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+        open_and_focus_sidebar(&sidebar, cx);
 
         // User types "fix" — two threads match.
         type_in_search(&sidebar, "fix", cx);
@@ -3352,10 +3292,10 @@ mod tests {
 
     #[gpui::test]
     async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext) {
-        let project = init_test_project_with_agent_panel("/my-project", cx).await;
+        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, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+        let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
 
@@ -3400,10 +3340,10 @@ mod tests {
 
     #[gpui::test]
     async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
-        let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
+        let project_a = init_test_project("/project-a", cx).await;
         let (multi_workspace, cx) = cx
             .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
-        let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
+        let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
 
         let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
 
@@ -3432,7 +3372,8 @@ mod tests {
         let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone());
 
         // ── 1. Initial state: no focused thread ──────────────────────────────
-        // Workspace B is active (just added), so its header is the active entry.
+        // Workspace B is active (just added) and has no thread, so its header
+        // is the active entry.
         sidebar.read_with(cx, |sidebar, _cx| {
             assert_eq!(
                 sidebar.focused_thread, None,
@@ -3447,6 +3388,7 @@ mod tests {
             );
         });
 
+        // ── 2. Click thread in workspace A via sidebar ───────────────────────
         sidebar.update_in(cx, |sidebar, window, cx| {
             sidebar.activate_thread(
                 acp_thread::AgentSessionInfo {
@@ -3490,6 +3432,7 @@ mod tests {
             );
         });
 
+        // ── 3. Open thread in workspace B, then click it via sidebar ─────────
         let connection_b = StubAgentConnection::new();
         connection_b.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
             acp::ContentChunk::new("Thread B".into()),
@@ -3501,6 +3444,16 @@ mod tests {
         save_thread_to_store(&session_id_b, &path_list_b, cx).await;
         cx.run_until_parked();
 
+        // Opening a thread in a non-active workspace should NOT change
+        // focused_thread — it's derived from the active workspace.
+        sidebar.read_with(cx, |sidebar, _cx| {
+            assert_eq!(
+                sidebar.focused_thread.as_ref(),
+                Some(&session_id_a),
+                "Opening a thread in a non-active workspace should not affect focused_thread"
+            );
+        });
+
         // Workspace A is currently active. Click a thread in workspace B,
         // which also triggers a workspace switch.
         sidebar.update_in(cx, |sidebar, window, cx| {
@@ -3535,25 +3488,30 @@ mod tests {
             );
         });
 
+        // ── 4. Switch workspace → focused_thread reflects new workspace ──────
         multi_workspace.update_in(cx, |mw, window, cx| {
             mw.activate_next_workspace(window, cx);
         });
         cx.run_until_parked();
 
+        // Workspace A is now active. Its agent panel still has session_id_a
+        // loaded, so focused_thread should reflect that.
         sidebar.read_with(cx, |sidebar, _cx| {
             assert_eq!(
-                sidebar.focused_thread, None,
-                "External workspace switch should clear focused_thread"
+                sidebar.focused_thread.as_ref(),
+                Some(&session_id_a),
+                "Switching workspaces should derive focused_thread from the new active workspace"
             );
             let active_entry = sidebar
                 .active_entry_index
                 .and_then(|ix| sidebar.contents.entries.get(ix));
             assert!(
-                matches!(active_entry, Some(ListEntry::ProjectHeader { .. })),
-                "Active entry should be the workspace header after external switch"
+                matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_a),
+                "Active entry should be workspace_a's active thread"
             );
         });
 
+        // ── 5. Opening a thread in a non-active workspace is ignored ──────────
         let connection_b2 = StubAgentConnection::new();
         connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
             acp::ContentChunk::new("New thread".into()),
@@ -3564,69 +3522,48 @@ mod tests {
         save_thread_to_store(&session_id_b2, &path_list_b, cx).await;
         cx.run_until_parked();
 
+        // Workspace A is still active, so focused_thread stays on session_id_a.
         sidebar.read_with(cx, |sidebar, _cx| {
             assert_eq!(
                 sidebar.focused_thread.as_ref(),
-                Some(&session_id_b2),
-                "Opening a thread externally should set focused_thread"
-            );
-        });
-
-        workspace_b.update_in(cx, |workspace, window, cx| {
-            workspace.focus_handle(cx).focus(window, cx);
-        });
-        cx.run_until_parked();
-
-        sidebar.read_with(cx, |sidebar, _cx| {
-            assert_eq!(
-                sidebar.focused_thread.as_ref(),
-                Some(&session_id_b2),
-                "Defocusing the sidebar should not clear focused_thread"
+                Some(&session_id_a),
+                "Opening a thread in a non-active workspace should not affect focused_thread"
             );
         });
 
+        // ── 6. Activating workspace B shows its active thread ────────────────
         sidebar.update_in(cx, |sidebar, window, cx| {
             sidebar.activate_workspace(&workspace_b, window, cx);
         });
         cx.run_until_parked();
 
+        // Workspace B is now active with session_id_b2 loaded.
         sidebar.read_with(cx, |sidebar, _cx| {
             assert_eq!(
-                sidebar.focused_thread, None,
-                "Clicking a workspace header should clear focused_thread"
+                sidebar.focused_thread.as_ref(),
+                Some(&session_id_b2),
+                "Activating workspace_b should show workspace_b's active thread"
             );
             let active_entry = sidebar
                 .active_entry_index
                 .and_then(|ix| sidebar.contents.entries.get(ix));
             assert!(
-                matches!(active_entry, Some(ListEntry::ProjectHeader { .. })),
-                "Active entry should be the workspace header"
+                matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_b2),
+                "Active entry should be workspace_b's active thread"
             );
         });
 
-        // ── 8. Focusing the agent panel thread restores focused_thread ────
-        // Workspace B still has session_id_b2 loaded in the agent panel.
-        // Clicking into the thread (simulated by focusing its view) should
-        // set focused_thread via the ThreadFocused event.
-        panel_b.update_in(cx, |panel, window, cx| {
-            if let Some(thread_view) = panel.active_connection_view() {
-                thread_view.read(cx).focus_handle(cx).focus(window, cx);
-            }
+        // ── 7. Switching back to workspace A reflects its thread ─────────────
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.activate_next_workspace(window, cx);
         });
         cx.run_until_parked();
 
         sidebar.read_with(cx, |sidebar, _cx| {
             assert_eq!(
                 sidebar.focused_thread.as_ref(),
-                Some(&session_id_b2),
-                "Focusing the agent panel thread should set focused_thread"
-            );
-            let active_entry = sidebar
-                .active_entry_index
-                .and_then(|ix| sidebar.contents.entries.get(ix));
-            assert!(
-                matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_b2),
-                "Active entry should be the focused thread"
+                Some(&session_id_a),
+                "Switching back to workspace_a should show its active thread"
             );
         });
     }

crates/debugger_ui/src/tests/stack_frame_list.rs 🔗

@@ -1211,7 +1211,9 @@ async fn test_stack_frame_filter_persistence(
     cx.run_until_parked();
 
     let workspace_id = workspace
-        .update(cx, |workspace, _window, cx| workspace.database_id(cx))
+        .update(cx, |workspace, _window, cx| {
+            workspace.active_workspace_database_id(cx)
+        })
         .ok()
         .flatten()
         .expect("workspace id has to be some for this test to work properly");

crates/platform_title_bar/src/platform_title_bar.rs 🔗

@@ -31,8 +31,6 @@ pub struct PlatformTitleBar {
     children: SmallVec<[AnyElement; 2]>,
     should_move: bool,
     system_window_tabs: Entity<SystemWindowTabs>,
-    workspace_sidebar_open: bool,
-    sidebar_has_notifications: bool,
 }
 
 impl PlatformTitleBar {
@@ -46,8 +44,6 @@ impl PlatformTitleBar {
             children: SmallVec::new(),
             should_move: false,
             system_window_tabs,
-            workspace_sidebar_open: false,
-            sidebar_has_notifications: false,
         }
     }
 
@@ -74,28 +70,6 @@ impl PlatformTitleBar {
         SystemWindowTabs::init(cx);
     }
 
-    pub fn is_workspace_sidebar_open(&self) -> bool {
-        self.workspace_sidebar_open
-    }
-
-    pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
-        self.workspace_sidebar_open = open;
-        cx.notify();
-    }
-
-    pub fn sidebar_has_notifications(&self) -> bool {
-        self.sidebar_has_notifications
-    }
-
-    pub fn set_sidebar_has_notifications(
-        &mut self,
-        has_notifications: bool,
-        cx: &mut Context<Self>,
-    ) {
-        self.sidebar_has_notifications = has_notifications;
-        cx.notify();
-    }
-
     pub fn is_multi_workspace_enabled(cx: &App) -> bool {
         cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
     }
@@ -110,9 +84,6 @@ impl Render for PlatformTitleBar {
         let close_action = Box::new(workspace::CloseWindow);
         let children = mem::take(&mut self.children);
 
-        let is_multiworkspace_sidebar_open =
-            PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
-
         let title_bar = h_flex()
             .window_control_area(WindowControlArea::Drag)
             .w_full()
@@ -161,9 +132,7 @@ impl Render for PlatformTitleBar {
             .map(|this| {
                 if window.is_fullscreen() {
                     this.pl_2()
-                } else if self.platform_style == PlatformStyle::Mac
-                    && !is_multiworkspace_sidebar_open
-                {
+                } else if self.platform_style == PlatformStyle::Mac {
                     this.pl(px(TRAFFIC_LIGHT_PADDING))
                 } else {
                     this.pl_2()
@@ -175,10 +144,9 @@ impl Render for PlatformTitleBar {
                     .when(!(tiling.top || tiling.right), |el| {
                         el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
                     })
-                    .when(
-                        !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
-                        |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
-                    )
+                    .when(!(tiling.top || tiling.left), |el| {
+                        el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                    })
                     // this border is to avoid a transparent gap in the rounded corners
                     .mt(px(-1.))
                     .mb(px(-1.))

crates/sidebar/Cargo.toml 🔗

@@ -1,51 +0,0 @@
-[package]
-name = "sidebar"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/sidebar.rs"
-
-[features]
-default = []
-
-[dependencies]
-acp_thread.workspace = true
-agent.workspace = true
-agent-client-protocol.workspace = true
-agent_ui.workspace = true
-chrono.workspace = true
-editor.workspace = true
-feature_flags.workspace = true
-fs.workspace = true
-gpui.workspace = true
-menu.workspace = true
-project.workspace = true
-recent_projects.workspace = true
-settings.workspace = true
-theme.workspace = true
-ui.workspace = true
-util.workspace = true
-workspace.workspace = true
-zed_actions.workspace = true
-
-[dev-dependencies]
-acp_thread = { workspace = true, features = ["test-support"] }
-agent = { workspace = true, features = ["test-support"] }
-agent_ui = { workspace = true, features = ["test-support"] }
-assistant_text_thread = { workspace = true, features = ["test-support"] }
-editor.workspace = true
-language_model = { workspace = true, features = ["test-support"] }
-serde_json.workspace = true
-feature_flags.workspace = true
-fs = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-project = { workspace = true, features = ["test-support"] }
-settings = { workspace = true, features = ["test-support"] }
-workspace = { workspace = true, features = ["test-support"] }
-recent_projects = { workspace = true, features = ["test-support"] }

crates/title_bar/Cargo.toml 🔗

@@ -38,7 +38,6 @@ chrono.workspace = true
 client.workspace = true
 cloud_api_types.workspace = true
 db.workspace = true
-feature_flags.workspace = true
 git_ui.workspace = true
 gpui = { workspace = true, features = ["screen-capture"] }
 notifications.workspace = true

crates/title_bar/src/title_bar.rs 🔗

@@ -24,16 +24,13 @@ use auto_update::AutoUpdateStatus;
 use call::ActiveCall;
 use client::{Client, UserStore, zed_urls};
 use cloud_api_types::Plan;
-use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
 use gpui::{
     Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable,
     InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
     StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
 };
 use onboarding_banner::OnboardingBanner;
-use project::{
-    DisableAiSettings, Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
-};
+use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
 use remote::RemoteConnectionOptions;
 use settings::Settings;
 use settings::WorktreeId;
@@ -47,8 +44,7 @@ use ui::{
 use update_version::UpdateVersion;
 use util::ResultExt;
 use workspace::{
-    MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace,
-    notifications::NotifyResultExt,
+    MultiWorkspace, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt,
 };
 use zed_actions::OpenRemote;
 
@@ -174,7 +170,6 @@ impl Render for TitleBar {
                     let mut render_project_items = title_bar_settings.show_branch_name
                         || title_bar_settings.show_project_items;
                     title_bar
-                        .children(self.render_workspace_sidebar_toggle(window, cx))
                         .when_some(
                             self.application_menu.clone().filter(|_| !show_menus),
                             |title_bar, menu| {
@@ -357,7 +352,6 @@ impl TitleBar {
 
         // Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar.
         {
-            let platform_titlebar = platform_titlebar.clone();
             let window_handle = window.window_handle();
             cx.spawn(async move |this: WeakEntity<TitleBar>, cx| {
                 let Some(multi_workspace_handle) = window_handle.downcast::<MultiWorkspace>()
@@ -370,26 +364,8 @@ impl TitleBar {
                         return;
                     };
 
-                    let is_open = multi_workspace.read(cx).is_sidebar_open();
-                    let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx);
-                    platform_titlebar.update(cx, |titlebar, cx| {
-                        titlebar.set_workspace_sidebar_open(is_open, cx);
-                        titlebar.set_sidebar_has_notifications(has_notifications, cx);
-                    });
-
-                    let platform_titlebar = platform_titlebar.clone();
-                    let subscription = cx.observe(&multi_workspace, move |mw, cx| {
-                        let is_open = mw.read(cx).is_sidebar_open();
-                        let has_notifications = mw.read(cx).sidebar_has_notifications(cx);
-                        platform_titlebar.update(cx, |titlebar, cx| {
-                            titlebar.set_workspace_sidebar_open(is_open, cx);
-                            titlebar.set_sidebar_has_notifications(has_notifications, cx);
-                        });
-                    });
-
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |this, _| {
-                            this._subscriptions.push(subscription);
                             this.multi_workspace = Some(multi_workspace.downgrade());
                         });
                     }
@@ -686,46 +662,7 @@ impl TitleBar {
         )
     }
 
-    fn render_workspace_sidebar_toggle(
-        &self,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<AnyElement> {
-        if !cx.has_flag::<AgentV2FeatureFlag>() || DisableAiSettings::get_global(cx).disable_ai {
-            return None;
-        }
-
-        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
-
-        if is_sidebar_open {
-            return None;
-        }
-
-        let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
-
-        Some(
-            IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
-                .icon_size(IconSize::Small)
-                .when(has_notifications, |button| {
-                    button
-                        .indicator(Indicator::dot().color(Color::Accent))
-                        .indicator_border_color(Some(cx.theme().colors().title_bar_background))
-                })
-                .tooltip(move |_, cx| {
-                    Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
-                })
-                .on_click(|_, window, cx| {
-                    window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
-                })
-                .into_any_element(),
-        )
-    }
-
-    pub fn render_project_name(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
+    pub fn render_project_name(&self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let workspace = self.workspace.clone();
 
         let name = self.effective_active_worktree(cx).map(|worktree| {
@@ -741,19 +678,6 @@ impl TitleBar {
             "Open Recent Project".to_string()
         };
 
-        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
-
-        if is_sidebar_open {
-            return self
-                .render_project_name_with_sidebar_popover(
-                    window,
-                    display_name,
-                    is_project_selected,
-                    cx,
-                )
-                .into_any_element();
-        }
-
         let focus_handle = workspace
             .upgrade()
             .map(|w| w.read(cx).focus_handle(cx))
@@ -793,49 +717,6 @@ impl TitleBar {
             .into_any_element()
     }
 
-    fn render_project_name_with_sidebar_popover(
-        &self,
-        _window: &Window,
-        display_name: String,
-        is_project_selected: bool,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let multi_workspace = self.multi_workspace.clone();
-
-        let is_popover_deployed = multi_workspace
-            .as_ref()
-            .and_then(|mw| mw.upgrade())
-            .map(|mw| mw.read(cx).is_recent_projects_popover_deployed(cx))
-            .unwrap_or(false);
-
-        Button::new("project_name_trigger", display_name)
-            .label_size(LabelSize::Small)
-            .when(self.worktree_count(cx) > 1, |this| {
-                this.icon(IconName::ChevronDown)
-                    .icon_color(Color::Muted)
-                    .icon_size(IconSize::XSmall)
-            })
-            .toggle_state(is_popover_deployed)
-            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-            .when(!is_project_selected, |s| s.color(Color::Muted))
-            .tooltip(move |_window, cx| {
-                Tooltip::for_action(
-                    "Recent Projects",
-                    &zed_actions::OpenRecent {
-                        create_new_window: false,
-                    },
-                    cx,
-                )
-            })
-            .on_click(move |_, window, cx| {
-                if let Some(mw) = multi_workspace.as_ref().and_then(|mw| mw.upgrade()) {
-                    mw.update(cx, |mw, cx| {
-                        mw.toggle_recent_projects_popover(window, cx);
-                    });
-                }
-            })
-    }
-
     pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
         let effective_worktree = self.effective_active_worktree(cx)?;
         let repository = self.get_repository_for_worktree(&effective_worktree, cx)?;

crates/workspace/src/multi_workspace.rs 🔗

@@ -1,9 +1,8 @@
 use anyhow::Result;
 use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
 use gpui::{
-    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
-    ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
-    actions, deferred, px,
+    App, Context, Entity, EntityId, EventEmitter, Focusable, ManagedView, Pixels, Render,
+    Subscription, Task, Tiling, Window, WindowId, actions, px,
 };
 use project::{DisableAiSettings, Project};
 use settings::Settings;
@@ -12,11 +11,12 @@ use std::path::PathBuf;
 use ui::prelude::*;
 use util::ResultExt;
 
-const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
+pub const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
 
 use crate::{
     CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast,
     Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId,
+    persistence::model::MultiWorkspaceId,
 };
 
 actions!(
@@ -41,31 +41,6 @@ pub enum MultiWorkspaceEvent {
     WorkspaceRemoved(EntityId),
 }
 
-pub enum SidebarEvent {
-    Open,
-    Close,
-}
-
-pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
-    fn width(&self, cx: &App) -> Pixels;
-    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
-    fn has_notifications(&self, cx: &App) -> bool;
-    fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App);
-    fn is_recent_projects_popover_deployed(&self) -> bool;
-}
-
-pub trait SidebarHandle: 'static + Send + Sync {
-    fn width(&self, cx: &App) -> Pixels;
-    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
-    fn focus_handle(&self, cx: &App) -> FocusHandle;
-    fn focus(&self, window: &mut Window, cx: &mut App);
-    fn has_notifications(&self, cx: &App) -> bool;
-    fn to_any(&self) -> AnyView;
-    fn entity_id(&self) -> EntityId;
-    fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App);
-    fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool;
-}
-
 #[derive(Clone)]
 pub struct DraggedSidebar;
 
@@ -75,54 +50,11 @@ impl Render for DraggedSidebar {
     }
 }
 
-impl<T: Sidebar> SidebarHandle for Entity<T> {
-    fn width(&self, cx: &App) -> Pixels {
-        self.read(cx).width(cx)
-    }
-
-    fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
-        self.update(cx, |this, cx| this.set_width(width, cx))
-    }
-
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.read(cx).focus_handle(cx)
-    }
-
-    fn focus(&self, window: &mut Window, cx: &mut App) {
-        let handle = self.read(cx).focus_handle(cx);
-        window.focus(&handle, cx);
-    }
-
-    fn has_notifications(&self, cx: &App) -> bool {
-        self.read(cx).has_notifications(cx)
-    }
-
-    fn to_any(&self) -> AnyView {
-        self.clone().into()
-    }
-
-    fn entity_id(&self) -> EntityId {
-        Entity::entity_id(self)
-    }
-
-    fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
-        self.update(cx, |this, cx| {
-            this.toggle_recent_projects_popover(window, cx);
-        });
-    }
-
-    fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool {
-        self.read(cx).is_recent_projects_popover_deployed()
-    }
-}
-
 pub struct MultiWorkspace {
     window_id: WindowId,
     workspaces: Vec<Entity<Workspace>>,
+    database_id: Option<MultiWorkspaceId>,
     active_workspace_index: usize,
-    sidebar: Option<Box<dyn SidebarHandle>>,
-    sidebar_open: bool,
-    _sidebar_subscription: Option<Subscription>,
     pending_removal_tasks: Vec<Task<()>>,
     _serialize_task: Option<Task<()>>,
     _create_task: Option<Task<()>>,
@@ -131,6 +63,10 @@ pub struct MultiWorkspace {
 
 impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
 
+pub fn multi_workspace_enabled(cx: &App) -> bool {
+    cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
+}
+
 impl MultiWorkspace {
     pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
@@ -145,142 +81,17 @@ impl MultiWorkspace {
             }
         });
         let quit_subscription = cx.on_app_quit(Self::app_will_quit);
-        let settings_subscription =
-            cx.observe_global_in::<settings::SettingsStore>(window, |this, window, cx| {
-                if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open {
-                    this.close_sidebar(window, cx);
-                }
-            });
         Self::subscribe_to_workspace(&workspace, cx);
         Self {
             window_id: window.window_handle().window_id(),
+            database_id: None,
             workspaces: vec![workspace],
             active_workspace_index: 0,
-            sidebar: None,
-            sidebar_open: false,
-            _sidebar_subscription: None,
             pending_removal_tasks: Vec::new(),
             _serialize_task: None,
             _create_task: None,
-            _subscriptions: vec![
-                release_subscription,
-                quit_subscription,
-                settings_subscription,
-            ],
-        }
-    }
-
-    pub fn register_sidebar<T: Sidebar>(
-        &mut self,
-        sidebar: Entity<T>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let subscription =
-            cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
-                SidebarEvent::Open => this.toggle_sidebar(window, cx),
-                SidebarEvent::Close => {
-                    this.close_sidebar(window, cx);
-                }
-            });
-        self.sidebar = Some(Box::new(sidebar));
-        self._sidebar_subscription = Some(subscription);
-    }
-
-    pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
-        self.sidebar.as_deref()
-    }
-
-    pub fn sidebar_open(&self) -> bool {
-        self.sidebar_open && self.sidebar.is_some()
-    }
-
-    pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
-        self.sidebar
-            .as_ref()
-            .map_or(false, |s| s.has_notifications(cx))
-    }
-
-    pub fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
-        if let Some(sidebar) = &self.sidebar {
-            sidebar.toggle_recent_projects_popover(window, cx);
-        }
-    }
-
-    pub fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool {
-        self.sidebar
-            .as_ref()
-            .map_or(false, |s| s.is_recent_projects_popover_deployed(cx))
-    }
-
-    pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
-        cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
-    }
-
-    pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
-            return;
-        }
-
-        if self.sidebar_open {
-            self.close_sidebar(window, cx);
-        } else {
-            self.open_sidebar(cx);
-            if let Some(sidebar) = &self.sidebar {
-                sidebar.focus(window, cx);
-            }
-        }
-    }
-
-    pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
-            return;
-        }
-
-        if self.sidebar_open {
-            let sidebar_is_focused = self
-                .sidebar
-                .as_ref()
-                .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
-
-            if sidebar_is_focused {
-                let pane = self.workspace().read(cx).active_pane().clone();
-                let pane_focus = pane.read(cx).focus_handle(cx);
-                window.focus(&pane_focus, cx);
-            } else if let Some(sidebar) = &self.sidebar {
-                sidebar.focus(window, cx);
-            }
-        } else {
-            self.open_sidebar(cx);
-            if let Some(sidebar) = &self.sidebar {
-                sidebar.focus(window, cx);
-            }
-        }
-    }
-
-    pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
-        self.sidebar_open = true;
-        for workspace in &self.workspaces {
-            workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(true, cx);
-            });
-        }
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.sidebar_open = false;
-        for workspace in &self.workspaces {
-            workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(false, cx);
-            });
+            _subscriptions: vec![release_subscription, quit_subscription],
         }
-        let pane = self.workspace().read(cx).active_pane().clone();
-        let pane_focus = pane.read(cx).focus_handle(cx);
-        window.focus(&pane_focus, cx);
-        self.serialize(cx);
-        cx.notify();
     }
 
     pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
@@ -318,10 +129,6 @@ impl MultiWorkspace {
         .detach();
     }
 
-    pub fn is_sidebar_open(&self) -> bool {
-        self.sidebar_open
-    }
-
     pub fn workspace(&self) -> &Entity<Workspace> {
         &self.workspaces[self.active_workspace_index]
     }
@@ -335,7 +142,7 @@ impl MultiWorkspace {
     }
 
     pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
+        if !multi_workspace_enabled(cx) {
             self.workspaces[0] = workspace;
             self.active_workspace_index = 0;
             cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
@@ -371,11 +178,6 @@ impl MultiWorkspace {
         if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
             index
         } else {
-            if self.sidebar_open {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.set_workspace_sidebar_open(true, cx);
-                });
-            }
             Self::subscribe_to_workspace(&workspace, cx);
             self.workspaces.push(workspace.clone());
             cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
@@ -384,6 +186,14 @@ impl MultiWorkspace {
         }
     }
 
+    pub fn database_id(&self) -> Option<MultiWorkspaceId> {
+        self.database_id
+    }
+
+    pub fn set_database_id(&mut self, id: Option<MultiWorkspaceId>) {
+        self.database_id = id;
+    }
+
     pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
         debug_assert!(
             index < self.workspaces.len(),
@@ -421,7 +231,6 @@ impl MultiWorkspace {
         let window_id = self.window_id;
         let state = crate::persistence::model::MultiWorkspaceState {
             active_workspace_id: self.workspace().read(cx).database_id(),
-            sidebar_open: self.sidebar_open,
         };
         self._serialize_task = Some(cx.background_spawn(async move {
             crate::persistence::write_multi_workspace_state(window_id, state).await;
@@ -540,7 +349,7 @@ impl MultiWorkspace {
         self.workspace().read(cx).items_of_type::<T>(cx)
     }
 
-    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
+    pub fn active_workspace_database_id(&self, cx: &App) -> Option<WorkspaceId> {
         self.workspace().read(cx).database_id()
     }
 
@@ -583,7 +392,7 @@ impl MultiWorkspace {
     }
 
     pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
+        if !multi_workspace_enabled(cx) {
             return;
         }
         let app_state = self.workspace().read(cx).app_state().clone();
@@ -692,7 +501,7 @@ impl MultiWorkspace {
     ) -> Task<Result<()>> {
         let workspace = self.workspace().clone();
 
-        if self.multi_workspace_enabled(cx) {
+        if multi_workspace_enabled(cx) {
             workspace.update(cx, |workspace, cx| {
                 workspace.open_workspace_for_paths(true, paths, window, cx)
             })
@@ -719,57 +528,6 @@ impl MultiWorkspace {
 
 impl Render for MultiWorkspace {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let multi_workspace_enabled = self.multi_workspace_enabled(cx);
-
-        let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
-            self.sidebar.as_ref().map(|sidebar_handle| {
-                let weak = cx.weak_entity();
-
-                let sidebar_width = sidebar_handle.width(cx);
-                let resize_handle = deferred(
-                    div()
-                        .id("sidebar-resize-handle")
-                        .absolute()
-                        .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
-                        .top(px(0.))
-                        .h_full()
-                        .w(SIDEBAR_RESIZE_HANDLE_SIZE)
-                        .cursor_col_resize()
-                        .on_drag(DraggedSidebar, |dragged, _, _, cx| {
-                            cx.stop_propagation();
-                            cx.new(|_| dragged.clone())
-                        })
-                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
-                            cx.stop_propagation();
-                        })
-                        .on_mouse_up(MouseButton::Left, move |event, _, cx| {
-                            if event.click_count == 2 {
-                                weak.update(cx, |this, cx| {
-                                    if let Some(sidebar) = this.sidebar.as_mut() {
-                                        sidebar.set_width(None, cx);
-                                    }
-                                })
-                                .ok();
-                                cx.stop_propagation();
-                            }
-                        })
-                        .occlude(),
-                );
-
-                div()
-                    .id("sidebar-container")
-                    .relative()
-                    .h_full()
-                    .w(sidebar_width)
-                    .flex_shrink_0()
-                    .child(sidebar_handle.to_any())
-                    .child(resize_handle)
-                    .into_any_element()
-            })
-        } else {
-            None
-        };
-
         let ui_font = theme::setup_ui_font(window, cx);
         let text_color = cx.theme().colors().text;
 
@@ -799,32 +557,6 @@ impl Render for MultiWorkspace {
                         this.activate_previous_workspace(window, cx);
                     },
                 ))
-                .when(self.multi_workspace_enabled(cx), |this| {
-                    this.on_action(cx.listener(
-                        |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
-                            this.toggle_sidebar(window, cx);
-                        },
-                    ))
-                    .on_action(cx.listener(
-                        |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
-                            this.focus_sidebar(window, cx);
-                        },
-                    ))
-                })
-                .when(
-                    self.sidebar_open() && self.multi_workspace_enabled(cx),
-                    |this| {
-                        this.on_drag_move(cx.listener(
-                            |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
-                                if let Some(sidebar) = &this.sidebar {
-                                    let new_width = e.event.position.x;
-                                    sidebar.set_width(Some(new_width), cx);
-                                }
-                            },
-                        ))
-                        .children(sidebar)
-                    },
-                )
                 .child(
                     div()
                         .flex()
@@ -837,98 +569,9 @@ impl Render for MultiWorkspace {
             window,
             cx,
             Tiling {
-                left: multi_workspace_enabled && self.sidebar_open,
+                left: false,
                 ..Tiling::default()
             },
         )
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use fs::FakeFs;
-    use gpui::TestAppContext;
-    use settings::SettingsStore;
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            DisableAiSettings::register(cx);
-            cx.update_flags(false, vec!["agent-v2".into()]);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, [], cx).await;
-
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(mw.multi_workspace_enabled(cx));
-        });
-
-        multi_workspace.update_in(cx, |mw, _window, cx| {
-            mw.open_sidebar(cx);
-            assert!(mw.is_sidebar_open());
-        });
-
-        cx.update(|_window, cx| {
-            DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
-        });
-        cx.run_until_parked();
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(
-                !mw.is_sidebar_open(),
-                "Sidebar should be closed when disable_ai is true"
-            );
-            assert!(
-                !mw.multi_workspace_enabled(cx),
-                "Multi-workspace should be disabled when disable_ai is true"
-            );
-        });
-
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.toggle_sidebar(window, cx);
-        });
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert!(
-                !mw.is_sidebar_open(),
-                "Sidebar should remain closed when toggled with disable_ai true"
-            );
-        });
-
-        cx.update(|_window, cx| {
-            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
-        });
-        cx.run_until_parked();
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(
-                mw.multi_workspace_enabled(cx),
-                "Multi-workspace should be enabled after re-enabling AI"
-            );
-            assert!(
-                !mw.is_sidebar_open(),
-                "Sidebar should still be closed after re-enabling AI (not auto-opened)"
-            );
-        });
-
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.toggle_sidebar(window, cx);
-        });
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert!(
-                mw.is_sidebar_open(),
-                "Sidebar should open when toggled after re-enabling AI"
-            );
-        });
-    }
-}

crates/workspace/src/persistence.rs 🔗

@@ -341,6 +341,7 @@ pub fn read_serialized_multi_workspaces(
                 .map(read_multi_workspace_state)
                 .unwrap_or_default();
             model::SerializedMultiWorkspace {
+                id: window_id.map(|id| model::MultiWorkspaceId(id.as_u64())),
                 workspaces: group,
                 state,
             }
@@ -3877,7 +3878,6 @@ mod tests {
             window_10,
             MultiWorkspaceState {
                 active_workspace_id: Some(WorkspaceId(2)),
-                sidebar_open: true,
             },
         )
         .await;
@@ -3886,7 +3886,6 @@ mod tests {
             window_20,
             MultiWorkspaceState {
                 active_workspace_id: Some(WorkspaceId(3)),
-                sidebar_open: false,
             },
         )
         .await;
@@ -3924,23 +3923,20 @@ mod tests {
         // Should produce 3 groups: window 10, window 20, and the orphan.
         assert_eq!(results.len(), 3);
 
-        // Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open.
+        // Window 10 group: 2 workspaces, active_workspace_id = 2.
         let group_10 = &results[0];
         assert_eq!(group_10.workspaces.len(), 2);
         assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
-        assert_eq!(group_10.state.sidebar_open, true);
 
-        // Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed.
+        // Window 20 group: 1 workspace, active_workspace_id = 3.
         let group_20 = &results[1];
         assert_eq!(group_20.workspaces.len(), 1);
         assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
-        assert_eq!(group_20.state.sidebar_open, false);
 
         // Orphan group: no window_id, so state is default.
         let group_none = &results[2];
         assert_eq!(group_none.workspaces.len(), 1);
         assert_eq!(group_none.state.active_workspace_id, None);
-        assert_eq!(group_none.state.sidebar_open, false);
     }
 
     #[gpui::test]

crates/workspace/src/persistence/model.rs 🔗

@@ -63,18 +63,19 @@ pub struct SessionWorkspace {
 #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
 pub struct MultiWorkspaceState {
     pub active_workspace_id: Option<WorkspaceId>,
-    pub sidebar_open: bool,
 }
 
-/// The serialized state of a single MultiWorkspace window from a previous session:
-/// all workspaces that shared the window, which one was active, and whether the
-/// sidebar was open.
+/// The serialized state of a single MultiWorkspace window from a previous session.
 #[derive(Debug, Clone)]
 pub struct SerializedMultiWorkspace {
+    pub id: Option<MultiWorkspaceId>,
     pub workspaces: Vec<SessionWorkspace>,
     pub state: MultiWorkspaceState,
 }
 
+#[derive(Debug, Clone, Copy)]
+pub struct MultiWorkspaceId(pub u64);
+
 #[derive(Debug, PartialEq, Clone)]
 pub(crate) struct SerializedWorkspace {
     pub(crate) id: WorkspaceId,

crates/workspace/src/status_bar.rs 🔗

@@ -34,7 +34,6 @@ pub struct StatusBar {
     right_items: Vec<Box<dyn StatusItemViewHandle>>,
     active_pane: Entity<Pane>,
     _observe_active_pane: Subscription,
-    workspace_sidebar_open: bool,
 }
 
 impl Render for StatusBar {
@@ -52,10 +51,9 @@ impl Render for StatusBar {
                     .when(!(tiling.bottom || tiling.right), |el| {
                         el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
                     })
-                    .when(
-                        !(tiling.bottom || tiling.left) && !self.workspace_sidebar_open,
-                        |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING),
-                    )
+                    .when(!(tiling.bottom || tiling.left), |el| {
+                        el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
+                    })
                     // This border is to avoid a transparent gap in the rounded corners
                     .mb(px(-1.))
                     .border_b(px(1.0))
@@ -91,17 +89,11 @@ impl StatusBar {
             _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| {
                 this.update_active_pane_item(window, cx)
             }),
-            workspace_sidebar_open: false,
         };
         this.update_active_pane_item(window, cx);
         this
     }
 
-    pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
-        self.workspace_sidebar_open = open;
-        cx.notify();
-    }
-
     pub fn add_left_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
     where
         T: 'static + StatusItemView,

crates/workspace/src/workspace.rs 🔗

@@ -28,8 +28,8 @@ pub use crate::notifications::NotificationFrame;
 pub use dock::Panel;
 pub use multi_workspace::{
     DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent,
-    NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent,
-    SidebarHandle, ToggleWorkspaceSidebar,
+    NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow,
+    SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, multi_workspace_enabled,
 };
 pub use path_list::{PathList, SerializedPathList};
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -80,8 +80,8 @@ use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
 pub use persistence::{
     DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
     model::{
-        DockStructure, ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation,
-        SessionWorkspace,
+        DockStructure, ItemId, MultiWorkspaceId, SerializedMultiWorkspace,
+        SerializedWorkspaceLocation, SessionWorkspace,
     },
     read_serialized_multi_workspaces,
 };
@@ -2154,12 +2154,6 @@ impl Workspace {
         &self.status_bar
     }
 
-    pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) {
-        self.status_bar.update(cx, |status_bar, cx| {
-            status_bar.set_workspace_sidebar_open(open, cx);
-        });
-    }
-
     pub fn status_bar_visible(&self, cx: &App) -> bool {
         StatusBarSettings::get_global(cx).show
     }
@@ -8184,7 +8178,11 @@ pub async fn restore_multiworkspace(
     app_state: Arc<AppState>,
     cx: &mut AsyncApp,
 ) -> anyhow::Result<MultiWorkspaceRestoreResult> {
-    let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
+    let SerializedMultiWorkspace {
+        workspaces,
+        state,
+        id: window_id,
+    } = multi_workspace;
     let mut group_iter = workspaces.into_iter();
     let first = group_iter
         .next()
@@ -8248,6 +8246,7 @@ pub async fn restore_multiworkspace(
     if let Some(target_id) = state.active_workspace_id {
         window_handle
             .update(cx, |multi_workspace, window, cx| {
+                multi_workspace.set_database_id(window_id);
                 let target_index = multi_workspace
                     .workspaces()
                     .iter()
@@ -8269,14 +8268,6 @@ pub async fn restore_multiworkspace(
             .ok();
     }
 
-    if state.sidebar_open {
-        window_handle
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.open_sidebar(cx);
-            })
-            .ok();
-    }
-
     window_handle
         .update(cx, |_, window, _cx| {
             window.activate_window();

crates/zed/Cargo.toml 🔗

@@ -182,7 +182,6 @@ settings.workspace = true
 settings_profile_selector.workspace = true
 settings_ui.workspace = true
 shellexpand.workspace = true
-sidebar.workspace = true
 smol.workspace = true
 snippet_provider.workspace = true
 snippets_ui.workspace = true

crates/zed/src/visual_test_runner.rs 🔗

@@ -103,8 +103,8 @@ use {
     feature_flags::FeatureFlagAppExt as _,
     git_ui::project_diff::ProjectDiff,
     gpui::{
-        App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString, VisualTestAppContext,
-        WindowBounds, WindowHandle, WindowOptions, point, px, size,
+        Action as _, App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString,
+        VisualTestAppContext, WindowBounds, WindowHandle, WindowOptions, point, px, size,
     },
     image::RgbaImage,
     project_panel::ProjectPanel,
@@ -2649,22 +2649,6 @@ fn run_multi_workspace_sidebar_visual_tests(
 
     cx.run_until_parked();
 
-    // Create the sidebar and register it on the MultiWorkspace
-    let sidebar = multi_workspace_window
-        .update(cx, |_multi_workspace, window, cx| {
-            let multi_workspace_handle = cx.entity();
-            cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
-        })
-        .context("Failed to create sidebar")?;
-
-    multi_workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.register_sidebar(sidebar.clone(), window, cx);
-        })
-        .context("Failed to register sidebar")?;
-
-    cx.run_until_parked();
-
     // Save test threads to the ThreadStore for each workspace
     let save_tasks = multi_workspace_window
         .update(cx, |multi_workspace, _window, cx| {
@@ -2742,8 +2726,8 @@ fn run_multi_workspace_sidebar_visual_tests(
 
     // Open the sidebar
     multi_workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.toggle_sidebar(window, cx);
+        .update(cx, |_multi_workspace, window, cx| {
+            window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx);
         })
         .context("Failed to toggle sidebar")?;
 
@@ -3181,24 +3165,10 @@ edition = "2021"
 
     cx.run_until_parked();
 
-    // Create and register the workspace sidebar
-    let sidebar = workspace_window
-        .update(cx, |_multi_workspace, window, cx| {
-            let multi_workspace_handle = cx.entity();
-            cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
-        })
-        .context("Failed to create sidebar")?;
-
-    workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.register_sidebar(sidebar.clone(), window, cx);
-        })
-        .context("Failed to register sidebar")?;
-
     // Open the sidebar
     workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.toggle_sidebar(window, cx);
+        .update(cx, |_multi_workspace, window, cx| {
+            window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx);
         })
         .context("Failed to toggle sidebar")?;
 

crates/zed/src/zed.rs 🔗

@@ -68,7 +68,6 @@ use settings::{
     initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
     update_settings_file,
 };
-use sidebar::Sidebar;
 use std::time::Duration;
 use std::{
     borrow::Cow,
@@ -389,20 +388,6 @@ pub fn initialize_workspace(
                 })
                 .unwrap_or(true)
         });
-
-        let window_handle = window.window_handle();
-        let multi_workspace_handle = cx.entity();
-        cx.defer(move |cx| {
-            window_handle
-                .update(cx, |_, window, cx| {
-                    let sidebar =
-                        cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
-                    multi_workspace_handle.update(cx, |multi_workspace, cx| {
-                        multi_workspace.register_sidebar(sidebar, window, cx);
-                    });
-                })
-                .ok();
-        });
     })
     .detach();