Revert "New multi workspace (#47795)" (#48776)

Finn Evers created

Preparing this just in case.

Release Notes:

- N/A

Change summary

Cargo.lock                                                            |   31 
Cargo.toml                                                            |    3 
assets/icons/workspace_nav_closed.svg                                 |    5 
assets/icons/workspace_nav_open.svg                                   |    5 
assets/keymaps/default-linux.json                                     |    8 
assets/keymaps/default-macos.json                                     |    8 
assets/keymaps/default-windows.json                                   |    8 
crates/agent_ui/src/acp.rs                                            |    2 
crates/agent_ui/src/acp/message_editor.rs                             |   19 
crates/agent_ui/src/acp/thread_view.rs                                |  198 
crates/agent_ui/src/agent_diff.rs                                     |    8 
crates/agent_ui/src/agent_panel.rs                                    |  320 
crates/agent_ui/src/agent_ui.rs                                       |    8 
crates/agent_ui/src/inline_prompt_editor.rs                           |   11 
crates/agent_ui/src/mention_set.rs                                    |   17 
crates/agent_ui/src/ui/agent_notification.rs                          |   18 
crates/collab/tests/integration/channel_guest_tests.rs                |   24 
crates/collab/tests/integration/editor_tests.rs                       |   64 
crates/collab/tests/integration/following_tests.rs                    |   12 
crates/collab/tests/integration/git_tests.rs                          |   32 
crates/collab/tests/integration/remote_editing_collaboration_tests.rs |   24 
crates/collab/tests/integration/test_server.rs                        |   54 
crates/collab_ui/src/collab_panel.rs                                  |   27 
crates/copilot_ui/src/sign_in.rs                                      |    4 
crates/db/src/kvp.rs                                                  |  127 
crates/debugger_ui/src/tests.rs                                       |   69 
crates/debugger_ui/src/tests/attach_modal.rs                          |   54 
crates/debugger_ui/src/tests/new_process_modal.rs                     |   32 
crates/debugger_ui/src/tests/stack_frame_list.rs                      |    6 
crates/dev_container/Cargo.toml                                       |    5 
crates/dev_container/src/devcontainer_api.rs                          |  566 
crates/dev_container/src/lib.rs                                       |   81 
crates/diagnostics/src/buffer_diagnostics.rs                          |    2 
crates/edit_prediction_ui/src/edit_prediction_button.rs               |    2 
crates/editor/src/editor.rs                                           |   22 
crates/editor/src/element.rs                                          |   21 
crates/editor/src/hover_popover.rs                                    |    2 
crates/editor/src/test/editor_lsp_test_context.rs                     |   25 
crates/feature_flags/src/flags.rs                                     |    4 
crates/file_finder/src/file_finder.rs                                 |    7 
crates/file_finder/src/file_finder_tests.rs                           |   14 
crates/git_ui/src/commit_view.rs                                      |    2 
crates/git_ui/src/file_diff_view.rs                                   |    9 
crates/git_ui/src/git_panel.rs                                        |    3 
crates/git_ui/src/project_diff.rs                                     |    3 
crates/git_ui/src/worktree_picker.rs                                  |   50 
crates/icons/src/icons.rs                                             |    2 
crates/inspector_ui/Cargo.toml                                        |    1 
crates/inspector_ui/src/inspector.rs                                  |    5 
crates/journal/src/journal.rs                                         |   25 
crates/keymap_editor/src/keymap_editor.rs                             |    2 
crates/miniprofiler_ui/src/miniprofiler_ui.rs                         |   27 
crates/onboarding/src/onboarding.rs                                   |   11 
crates/outline/src/outline.rs                                         |   24 
crates/platform_title_bar/Cargo.toml                                  |    1 
crates/platform_title_bar/src/platform_title_bar.rs                   |   65 
crates/platform_title_bar/src/platforms.rs                            |    1 
crates/platform_title_bar/src/platforms/platform_mac.rs               |   10 
crates/project_panel/src/project_panel.rs                             |   23 
crates/recent_projects/Cargo.toml                                     |    2 
crates/recent_projects/src/disconnected_overlay.rs                    |    6 
crates/recent_projects/src/recent_projects.rs                         |  210 
crates/recent_projects/src/remote_connections.rs                      |  177 
crates/recent_projects/src/remote_servers.rs                          |  119 
crates/recent_projects/src/wsl_picker.rs                              |    4 
crates/repl/src/repl_sessions_ui.rs                                   |    5 
crates/rules_library/src/rules_library.rs                             |   14 
crates/search/src/search.rs                                           |    2 
crates/session/src/session.rs                                         |   14 
crates/settings_profile_selector/src/settings_profile_selector.rs     |    9 
crates/settings_ui/src/settings_ui.rs                                 |  283 
crates/sidebar/Cargo.toml                                             |   43 
crates/sidebar/LICENSE-GPL                                            |    1 
crates/sidebar/src/sidebar.rs                                         | 1304 
crates/title_bar/Cargo.toml                                           |    1 
crates/title_bar/src/project_dropdown.rs                              |   64 
crates/title_bar/src/title_bar.rs                                     |  102 
crates/ui/src/components/thread_item.rs                               |   56 
crates/ui/src/utils.rs                                                |    2 
crates/ui/src/utils/constants.rs                                      |   27 
crates/vim/src/command.rs                                             |   32 
crates/vim/src/normal/mark.rs                                         |   10 
crates/vim/src/normal/repeat.rs                                       |    4 
crates/vim/src/normal/search.rs                                       |    2 
crates/vim/src/state.rs                                               |   18 
crates/vim/src/vim.rs                                                 |    8 
crates/workspace/Cargo.toml                                           |    1 
crates/workspace/src/history_manager.rs                               |   11 
crates/workspace/src/multi_workspace.rs                               |  513 
crates/workspace/src/notifications.rs                                 |   77 
crates/workspace/src/pane.rs                                          |    7 
crates/workspace/src/persistence.rs                                   |  457 
crates/workspace/src/persistence/model.rs                             |   28 
crates/workspace/src/welcome.rs                                       |   25 
crates/workspace/src/workspace.rs                                     |  726 
crates/zed/Cargo.toml                                                 |    2 
crates/zed/src/main.rs                                                |  294 
crates/zed/src/visual_test_runner.rs                                  |  319 
crates/zed/src/zed.rs                                                 | 1092 
crates/zed/src/zed/migrate.rs                                         |   19 
crates/zed/src/zed/open_listener.rs                                   |  108 
101 files changed, 1,859 insertions(+), 6,552 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4942,7 +4942,6 @@ checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
 name = "dev_container"
 version = "0.1.0"
 dependencies = [
- "fs",
  "futures 0.3.31",
  "gpui",
  "http 1.3.1",
@@ -4952,12 +4951,10 @@ dependencies = [
  "node_runtime",
  "paths",
  "picker",
- "project",
  "serde",
  "serde_json",
  "settings",
  "smol",
- "theme",
  "ui",
  "util",
  "workspace",
@@ -8495,6 +8492,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "language",
+ "platform_title_bar",
  "project",
  "serde_json",
  "serde_json_lenient",
@@ -12382,7 +12380,6 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
 name = "platform_title_bar"
 version = "0.1.0"
 dependencies = [
- "feature_flags",
  "gpui",
  "settings",
  "smallvec",
@@ -15349,30 +15346,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_ui",
- "db",
- "editor",
- "feature_flags",
- "fs",
- "fuzzy",
- "gpui",
- "picker",
- "project",
- "recent_projects",
- "serde_json",
- "settings",
- "theme",
- "ui",
- "ui_input",
- "util",
- "workspace",
-]
-
 [[package]]
 name = "signal-hook"
 version = "0.3.18"
@@ -17252,7 +17225,6 @@ dependencies = [
  "cloud_api_types",
  "collections",
  "db",
- "feature_flags",
  "git_ui",
  "gpui",
  "http_client",
@@ -21138,7 +21110,6 @@ dependencies = [
  "settings_profile_selector",
  "settings_ui",
  "shellexpand 2.1.2",
- "sidebar",
  "smol",
  "snippet_provider",
  "snippets_ui",

Cargo.toml 🔗

@@ -155,7 +155,6 @@ members = [
     "crates/schema_generator",
     "crates/search",
     "crates/session",
-    "crates/sidebar",
     "crates/settings",
     "crates/settings_content",
     "crates/settings_json",
@@ -397,7 +396,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" }
@@ -857,7 +855,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 }

assets/icons/workspace_nav_closed.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.2" width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
-<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
-</svg>

assets/icons/workspace_nav_open.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
-<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
-</svg>

assets/keymaps/default-linux.json 🔗

@@ -596,7 +596,6 @@
       "ctrl-alt-b": "workspace::ToggleRightDock",
       "ctrl-b": "workspace::ToggleLeftDock",
       "ctrl-j": "workspace::ToggleBottomDock",
-      "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
       "ctrl-alt-y": "workspace::ToggleAllDocks",
       "ctrl-alt-0": "workspace::ResetActiveDockSize",
       // For 0px parameter, uses UI font size value.
@@ -656,13 +655,6 @@
       "ctrl-w": "workspace::CloseActiveDock",
     },
   },
-  {
-    "context": "WorkspaceSidebar",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
-    },
-  },
   {
     "context": "Workspace && debugger_running",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -657,7 +657,6 @@
       "cmd-alt-b": "workspace::ToggleRightDock",
       "cmd-r": "workspace::ToggleRightDock",
       "cmd-j": "workspace::ToggleBottomDock",
-      "cmd-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
       "alt-cmd-y": "workspace::ToggleAllDocks",
       // For 0px parameter, uses UI font size value.
       "ctrl-alt-0": "workspace::ResetActiveDockSize",
@@ -717,13 +716,6 @@
       // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
     },
   },
-  {
-    "context": "WorkspaceSidebar",
-    "use_key_equivalents": true,
-    "bindings": {
-      "cmd-n": "multi_workspace::NewWorkspaceInWindow",
-    },
-  },
   {
     "context": "Workspace && debugger_running",
     "use_key_equivalents": true,

assets/keymaps/default-windows.json 🔗

@@ -591,7 +591,6 @@
       "ctrl-alt-b": "workspace::ToggleRightDock",
       "ctrl-b": "workspace::ToggleLeftDock",
       "ctrl-j": "workspace::ToggleBottomDock",
-      "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
       "ctrl-shift-y": "workspace::ToggleAllDocks",
       "alt-r": "workspace::ResetActiveDockSize",
       // For 0px parameter, uses UI font size value.
@@ -660,13 +659,6 @@
       "f5": "debugger::Continue",
     },
   },
-  {
-    "context": "WorkspaceSidebar",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
-    },
-  },
   {
     "context": "ApplicationMenu",
     "use_key_equivalents": true,

crates/agent_ui/src/acp.rs 🔗

@@ -5,7 +5,7 @@ mod mode_selector;
 mod model_selector;
 mod model_selector_popover;
 mod thread_history;
-pub(crate) mod thread_view;
+mod thread_view;
 
 pub use mode_selector::ModeSelector;
 pub use model_selector::AcpModelSelector;

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -815,13 +815,8 @@ impl MessageEditor {
         }
 
         if self.prompt_capabilities.borrow().image
-            && let Some(task) = paste_images_as_context(
-                self.editor.clone(),
-                self.mention_set.clone(),
-                self.workspace.clone(),
-                window,
-                cx,
-            )
+            && let Some(task) =
+                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
         {
             task.detach();
             return;
@@ -1089,7 +1084,6 @@ impl MessageEditor {
 
         let editor = self.editor.clone();
         let mention_set = self.mention_set.clone();
-        let workspace = self.workspace.clone();
 
         let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
             files: true,
@@ -1140,14 +1134,7 @@ impl MessageEditor {
                     images.push(gpui::Image::from_bytes(format, content));
                 }
 
-                crate::mention_set::insert_images_as_context(
-                    images,
-                    editor,
-                    mention_set,
-                    workspace,
-                    cx,
-                )
-                .await;
+                crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await;
                 Ok(())
             })
             .detach_and_log_err(cx);

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -57,9 +57,7 @@ use ui::{
 };
 use util::defer;
 use util::{ResultExt, size::format_file_size, time::duration_alt_display};
-use workspace::{
-    CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,
-};
+use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId};
 use zed_actions::agent::{Chat, ToggleModelSelector};
 use zed_actions::assistant::OpenRulesLibrary;
 
@@ -1988,30 +1986,9 @@ impl AcpServerView {
         self.show_notification(caption, icon, window, cx);
     }
 
-    fn agent_is_visible(&self, window: &Window, cx: &App) -> bool {
-        if window.is_window_active() {
-            let workspace_is_foreground = window
-                .root::<MultiWorkspace>()
-                .flatten()
-                .and_then(|mw| {
-                    let mw = mw.read(cx);
-                    self.workspace.upgrade().map(|ws| mw.workspace() == &ws)
-                })
-                .unwrap_or(true);
-
-            if workspace_is_foreground {
-                if let Some(workspace) = self.workspace.upgrade() {
-                    return AgentPanel::is_visible(&workspace, cx);
-                }
-            }
-        }
-
-        false
-    }
-
     fn play_notification_sound(&self, window: &Window, cx: &mut App) {
         let settings = AgentSettings::get_global(cx);
-        if settings.play_sound_when_agent_done && !self.agent_is_visible(window, cx) {
+        if settings.play_sound_when_agent_done && !window.is_window_active() {
             Audio::play_sound(Sound::AgentDone, cx);
         }
     }
@@ -2029,7 +2006,14 @@ impl AcpServerView {
 
         let settings = AgentSettings::get_global(cx);
 
-        let should_notify = !self.agent_is_visible(window, cx);
+        let window_is_inactive = !window.is_window_active();
+        let panel_is_hidden = self
+            .workspace
+            .upgrade()
+            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
+            .unwrap_or(true);
+
+        let should_notify = window_is_inactive || panel_is_hidden;
 
         if !should_notify {
             return;
@@ -2092,22 +2076,19 @@ impl AcpServerView {
                 .push(cx.subscribe_in(&pop_up, window, {
                     |this, _, event, window, cx| match event {
                         AgentNotificationEvent::Accepted => {
-                            let Some(handle) = window.window_handle().downcast::<MultiWorkspace>()
-                            else {
-                                log::error!("root view should be a MultiWorkspace");
-                                return;
-                            };
+                            let handle = window.window_handle();
                             cx.activate(true);
 
                             let workspace_handle = this.workspace.clone();
 
+                            // If there are multiple Zed windows, activate the correct one.
                             cx.defer(move |cx| {
                                 handle
-                                    .update(cx, |multi_workspace, window, cx| {
+                                    .update(cx, |_view, window, _cx| {
                                         window.activate_window();
+
                                         if let Some(workspace) = workspace_handle.upgrade() {
-                                            multi_workspace.activate(workspace.clone(), cx);
-                                            workspace.update(cx, |workspace, cx| {
+                                            workspace.update(_cx, |workspace, cx| {
                                                 workspace.focus_panel::<AgentPanel>(window, cx);
                                             });
                                         }
@@ -2132,12 +2113,12 @@ impl AcpServerView {
                 .push({
                     let pop_up_weak = pop_up.downgrade();
 
-                    cx.observe_window_activation(window, move |this, window, cx| {
-                        if this.agent_is_visible(window, cx)
+                    cx.observe_window_activation(window, move |_, window, cx| {
+                        if window.is_window_active()
                             && let Some(pop_up) = pop_up_weak.upgrade()
                         {
-                            pop_up.update(cx, |notification, cx| {
-                                notification.dismiss(cx);
+                            pop_up.update(cx, |_, cx| {
+                                cx.emit(AgentNotificationEvent::Dismissed);
                             });
                         }
                     })
@@ -2388,7 +2369,6 @@ pub(crate) mod tests {
     use action_log::ActionLog;
     use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
     use agent_client_protocol::SessionId;
-    use assistant_text_thread::TextThreadStore;
     use editor::MultiBufferOffset;
     use fs::FakeFs;
     use gpui::{EventEmitter, TestAppContext, VisualTestContext};
@@ -2398,7 +2378,7 @@ pub(crate) mod tests {
     use std::any::Any;
     use std::path::Path;
     use std::rc::Rc;
-    use workspace::{Item, MultiWorkspace};
+    use workspace::Item;
 
     use super::*;
 
@@ -2698,138 +2678,6 @@ pub(crate) mod tests {
         );
     }
 
-    #[gpui::test]
-    async fn test_notification_when_workspace_is_background_in_multi_workspace(
-        cx: &mut TestAppContext,
-    ) {
-        init_test(cx);
-
-        // Enable multi-workspace feature flag and init globals needed by AgentPanel
-        let fs = FakeFs::new(cx.executor());
-
-        cx.update(|cx| {
-            cx.update_flags(true, vec!["agent-v2".to_string()]);
-            agent::ThreadStore::init_global(cx);
-            language_model::LanguageModelRegistry::test(cx);
-            <dyn Fs>::set_global(fs.clone(), cx);
-        });
-
-        let project1 = Project::test(fs.clone(), [], cx).await;
-
-        // Create a MultiWorkspace window with one workspace
-        let multi_workspace_handle =
-            cx.add_window(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
-
-        // Get workspace 1 (the initial workspace)
-        let workspace1 = multi_workspace_handle
-            .read_with(cx, |mw, _cx| mw.workspace().clone())
-            .unwrap();
-
-        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
-
-        workspace1.update_in(cx, |workspace, window, cx| {
-            let text_thread_store =
-                cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
-            let panel =
-                cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
-            workspace.add_panel(panel, window, cx);
-
-            // Open the dock and activate the agent panel so it's visible
-            workspace.focus_panel::<crate::AgentPanel>(window, cx);
-        });
-
-        cx.run_until_parked();
-
-        cx.read(|cx| {
-            assert!(
-                crate::AgentPanel::is_visible(&workspace1, cx),
-                "AgentPanel should be visible in workspace1's dock"
-            );
-        });
-
-        // Set up thread view in workspace 1
-        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
-
-        let agent = StubAgentServer::default_response();
-        let thread_view = cx.update(|window, cx| {
-            cx.new(|cx| {
-                AcpServerView::new(
-                    Rc::new(agent),
-                    None,
-                    None,
-                    workspace1.downgrade(),
-                    project1.clone(),
-                    Some(thread_store),
-                    None,
-                    history,
-                    window,
-                    cx,
-                )
-            })
-        });
-        cx.run_until_parked();
-
-        let message_editor = message_editor(&thread_view, cx);
-        message_editor.update_in(cx, |editor, window, cx| {
-            editor.set_text("Hello", window, cx);
-        });
-
-        // Create a second workspace and switch to it.
-        // This makes workspace1 the "background" workspace.
-        let project2 = Project::test(fs, [], cx).await;
-        multi_workspace_handle
-            .update(cx, |mw, window, cx| {
-                let workspace2 = cx.new(|cx| Workspace::test_new(project2, window, cx));
-                mw.activate(workspace2, cx);
-            })
-            .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace1 is no longer the active workspace
-        multi_workspace_handle
-            .read_with(cx, |mw, _cx| {
-                assert_eq!(mw.active_workspace_index(), 1);
-                assert_ne!(mw.workspace(), &workspace1);
-            })
-            .unwrap();
-
-        // Window is active, agent panel is visible in workspace1, but workspace1
-        // is in the background. The notification should show because the user
-        // can't actually see the agent panel.
-        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
-
-        cx.run_until_parked();
-
-        assert!(
-            cx.windows()
-                .iter()
-                .any(|window| window.downcast::<AgentNotification>().is_some()),
-            "Expected notification when workspace is in background within MultiWorkspace"
-        );
-
-        // Also verify: clicking "View Panel" should switch to workspace1.
-        cx.windows()
-            .iter()
-            .find_map(|window| window.downcast::<AgentNotification>())
-            .unwrap()
-            .update(cx, |window, _, cx| window.accept(cx))
-            .unwrap();
-
-        cx.run_until_parked();
-
-        multi_workspace_handle
-            .read_with(cx, |mw, _cx| {
-                assert_eq!(
-                    mw.workspace(),
-                    &workspace1,
-                    "Expected workspace1 to become the active workspace after accepting notification"
-                );
-            })
-            .unwrap();
-    }
-
     #[gpui::test]
     async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
         init_test(cx);
@@ -2992,18 +2840,18 @@ pub(crate) mod tests {
         }
     }
 
-    pub(crate) struct StubAgentServer<C> {
+    struct StubAgentServer<C> {
         connection: C,
     }
 
     impl<C> StubAgentServer<C> {
-        pub(crate) fn new(connection: C) -> Self {
+        fn new(connection: C) -> Self {
             Self { connection }
         }
     }
 
     impl StubAgentServer<StubAgentConnection> {
-        pub(crate) fn default_response() -> Self {
+        fn default_response() -> Self {
             let conn = StubAgentConnection::new();
             conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
                 acp::ContentChunk::new("Default response".into()),

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1352,10 +1352,10 @@ impl AgentDiff {
                     self.update_reviewing_editors(workspace, window, cx);
                 }
             }
-            AcpThreadEvent::Stopped => {
-                self.update_reviewing_editors(workspace, window, cx);
-            }
-            AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => {
+            AcpThreadEvent::Stopped
+            | AcpThreadEvent::Error
+            | AcpThreadEvent::LoadError(_)
+            | AcpThreadEvent::Refusal => {
                 self.update_reviewing_editors(workspace, window, cx);
             }
             AcpThreadEvent::TitleUpdated

crates/agent_ui/src/agent_panel.rs 🔗

@@ -81,50 +81,10 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
 const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
 const DEFAULT_THREAD_TITLE: &str = "New Thread";
 
-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();
-    scope
-        .read(&key)
-        .log_err()
-        .flatten()
-        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
-}
-
-async fn save_serialized_panel(
-    workspace_id: workspace::WorkspaceId,
-    panel: SerializedAgentPanel,
-) -> Result<()> {
-    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
-    let key = i64::from(workspace_id).to_string();
-    scope.write(key, serde_json::to_string(&panel)?).await?;
-    Ok(())
-}
-
-/// Migration: reads the original single-panel format stored under the
-/// `"agent_panel"` KVP key before per-workspace keying was introduced.
-fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
-    KEY_VALUE_STORE
-        .read_kvp(AGENT_PANEL_KEY)
-        .log_err()
-        .flatten()
-        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Serialize, Deserialize, Debug)]
 struct SerializedAgentPanel {
     width: Option<Pixels>,
     selected_agent: Option<AgentType>,
-    #[serde(default)]
-    last_active_thread: Option<SerializedActiveThread>,
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-struct SerializedActiveThread {
-    session_id: String,
-    agent_type: AgentType,
-    title: Option<String>,
-    cwd: Option<std::path::PathBuf>,
 }
 
 pub fn init(cx: &mut App) {
@@ -468,7 +428,6 @@ pub struct AgentPanel {
     focus_handle: FocusHandle,
     active_view: ActiveView,
     previous_view: Option<ActiveView>,
-    _active_view_observation: Option<Subscription>,
     new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -485,45 +444,19 @@ pub struct AgentPanel {
 }
 
 impl AgentPanel {
-    fn serialize(&mut self, cx: &mut App) {
-        let workspace_id = self
-            .workspace
-            .read_with(cx, |workspace, _| workspace.database_id())
-            .ok()
-            .flatten();
-
-        let Some(workspace_id) = workspace_id else {
-            return;
-        };
-
+    fn serialize(&mut self, cx: &mut Context<Self>) {
         let width = self.width;
         let selected_agent = self.selected_agent.clone();
-
-        let last_active_thread = self.active_agent_thread(cx).map(|thread| {
-            let thread = thread.read(cx);
-            let title = thread.title();
-            SerializedActiveThread {
-                session_id: thread.session_id().0.to_string(),
-                agent_type: self.selected_agent.clone(),
-                title: if title.as_ref() != DEFAULT_THREAD_TITLE {
-                    Some(title.to_string())
-                } else {
-                    None
-                },
-                cwd: None,
-            }
-        });
-
         self.pending_serialization = Some(cx.background_spawn(async move {
-            save_serialized_panel(
-                workspace_id,
-                SerializedAgentPanel {
-                    width,
-                    selected_agent: Some(selected_agent),
-                    last_active_thread,
-                },
-            )
-            .await?;
+            KEY_VALUE_STORE
+                .write_kvp(
+                    AGENT_PANEL_KEY.into(),
+                    serde_json::to_string(&SerializedAgentPanel {
+                        width,
+                        selected_agent: Some(selected_agent),
+                    })?,
+                )
+                .await?;
             anyhow::Ok(())
         }));
     }
@@ -539,18 +472,16 @@ impl AgentPanel {
                 Ok(prompt_store) => prompt_store.await.ok(),
                 Err(_) => None,
             };
-            let workspace_id = workspace
-                .read_with(cx, |workspace, _| workspace.database_id())
-                .ok()
-                .flatten();
-
-            let serialized_panel = cx
-                .background_spawn(async move {
-                    workspace_id
-                        .and_then(read_serialized_panel)
-                        .or_else(read_legacy_serialized_panel)
-                })
-                .await;
+            let serialized_panel = if let Some(panel) = cx
+                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                serde_json::from_str::<SerializedAgentPanel>(&panel).log_err()
+            } else {
+                None
+            };
 
             let slash_commands = Arc::new(SlashCommandWorkingSet::default());
             let text_thread_store = workspace
@@ -569,30 +500,15 @@ impl AgentPanel {
                 let panel =
                     cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
 
-                if let Some(serialized_panel) = &serialized_panel {
+                if let Some(serialized_panel) = serialized_panel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width.map(|w| w.round());
-                        if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
+                        if let Some(selected_agent) = serialized_panel.selected_agent {
                             panel.selected_agent = selected_agent;
                         }
                         cx.notify();
                     });
                 }
-
-                if let Some(thread_info) = serialized_panel.and_then(|p| p.last_active_thread) {
-                    let agent_type = thread_info.agent_type.clone();
-                    let session_info = AgentSessionInfo {
-                        session_id: acp::SessionId::new(thread_info.session_id),
-                        cwd: thread_info.cwd,
-                        title: thread_info.title.map(SharedString::from),
-                        updated_at: None,
-                        meta: None,
-                    };
-                    panel.update(cx, |panel, cx| {
-                        panel.selected_agent = agent_type;
-                        panel.load_agent_thread(session_info, window, cx);
-                    });
-                }
                 panel
             })?;
 
@@ -600,7 +516,7 @@ impl AgentPanel {
         })
     }
 
-    pub(crate) fn new(
+    fn new(
         workspace: &Workspace,
         text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
@@ -730,7 +646,6 @@ impl AgentPanel {
             focus_handle: cx.focus_handle(),
             context_server_registry,
             previous_view: None,
-            _active_view_observation: None,
             new_thread_menu_handle: PopoverMenuHandle::default(),
             agent_panel_menu_handle: PopoverMenuHandle::default(),
             agent_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -799,7 +714,7 @@ impl AgentPanel {
         &self.context_server_registry
     }
 
-    pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
+    pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool {
         let workspace_read = workspace.read(cx);
 
         workspace_read
@@ -807,13 +722,15 @@ impl AgentPanel {
             .map(|panel| {
                 let panel_id = Entity::entity_id(&panel);
 
-                workspace_read.all_docks().iter().any(|dock| {
+                let is_visible = workspace_read.all_docks().iter().any(|dock| {
                     dock.read(cx)
                         .visible_panel()
                         .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
-                })
+                });
+
+                !is_visible
             })
-            .unwrap_or(false)
+            .unwrap_or(true)
     }
 
     pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
@@ -1106,7 +1023,6 @@ impl AgentPanel {
             ActiveView::Configuration | ActiveView::History { .. } => {
                 if let Some(previous_view) = self.previous_view.take() {
                     self.active_view = previous_view;
-                    cx.emit(AgentPanelEvent::ActiveViewChanged);
 
                     match &self.active_view {
                         ActiveView::AgentThread { thread_view } => {
@@ -1503,7 +1419,7 @@ impl AgentPanel {
         }
     }
 
-    pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
+    pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
         match &self.active_view {
             ActiveView::AgentThread { thread_view, .. } => thread_view
                 .read(cx)
@@ -1559,21 +1475,9 @@ impl AgentPanel {
             self.active_view = new_view;
         }
 
-        self._active_view_observation = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                Some(cx.observe(thread_view, |this, _, cx| {
-                    cx.emit(AgentPanelEvent::ActiveViewChanged);
-                    this.serialize(cx);
-                    cx.notify();
-                }))
-            }
-            _ => None,
-        };
-
         if focus {
             self.focus_handle(cx).focus(window, cx);
         }
-        cx.emit(AgentPanelEvent::ActiveViewChanged);
     }
 
     fn populate_recently_updated_menu_section(
@@ -1846,12 +1750,7 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
     AgentSettings::get_global(cx).dock.into()
 }
 
-pub enum AgentPanelEvent {
-    ActiveViewChanged,
-}
-
 impl EventEmitter<PanelEvent> for AgentPanel {}
-impl EventEmitter<AgentPanelEvent> for AgentPanel {}
 
 impl Panel for AgentPanel {
     fn persistent_name() -> &'static str {
@@ -1894,14 +1793,7 @@ impl Panel for AgentPanel {
             DockPosition::Left | DockPosition::Right => self.width = size,
             DockPosition::Bottom => self.height = size,
         }
-        let this = cx.weak_entity();
-        cx.defer(move |cx| {
-            if let Some(this) = this.upgrade() {
-                this.update(cx, |this, cx| {
-                    this.serialize(cx);
-                });
-            }
-        });
+        self.serialize(cx);
         cx.notify();
     }
 
@@ -3392,151 +3284,3 @@ impl AgentPanel {
         self.active_thread_view()
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::acp::thread_view::tests::{StubAgentServer, init_test};
-    use assistant_text_thread::TextThreadStore;
-    use feature_flags::FeatureFlagAppExt;
-    use fs::FakeFs;
-    use gpui::{TestAppContext, VisualTestContext};
-    use project::Project;
-    use workspace::{MultiWorkspace, Workspace};
-
-    #[gpui::test]
-    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.update(|cx| {
-            cx.update_flags(true, vec!["agent-v2".to_string()]);
-            agent::ThreadStore::init_global(cx);
-            language_model::LanguageModelRegistry::test(cx);
-        });
-
-        // --- Create a MultiWorkspace window with two workspaces ---
-        let fs = FakeFs::new(cx.executor());
-        let project_a = Project::test(fs.clone(), [], cx).await;
-        let project_b = Project::test(fs, [], cx).await;
-
-        let multi_workspace =
-            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
-
-        let workspace_a = multi_workspace
-            .read_with(cx, |multi_workspace, _cx| {
-                multi_workspace.workspace().clone()
-            })
-            .unwrap();
-
-        let workspace_b = multi_workspace
-            .update(cx, |multi_workspace, window, cx| {
-                let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
-                multi_workspace.activate(workspace.clone(), cx);
-                workspace
-            })
-            .unwrap();
-
-        workspace_a.update(cx, |workspace, _cx| {
-            workspace.set_random_database_id();
-        });
-        workspace_b.update(cx, |workspace, _cx| {
-            workspace.set_random_database_id();
-        });
-
-        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
-
-        // --- Set up workspace A: width=300, with an active thread ---
-        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
-            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
-            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
-        });
-
-        panel_a.update(cx, |panel, _cx| {
-            panel.width = Some(px(300.0));
-        });
-
-        panel_a.update_in(cx, |panel, window, cx| {
-            panel.open_external_thread_with_server(
-                Rc::new(StubAgentServer::default_response()),
-                window,
-                cx,
-            );
-        });
-
-        cx.run_until_parked();
-
-        panel_a.read_with(cx, |panel, cx| {
-            assert!(
-                panel.active_agent_thread(cx).is_some(),
-                "workspace A should have an active thread after connection"
-            );
-        });
-
-        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
-
-        // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
-        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
-            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
-            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
-        });
-
-        panel_b.update(cx, |panel, _cx| {
-            panel.width = Some(px(400.0));
-            panel.selected_agent = AgentType::ClaudeCode;
-        });
-
-        // --- Serialize both panels ---
-        panel_a.update(cx, |panel, cx| panel.serialize(cx));
-        panel_b.update(cx, |panel, cx| panel.serialize(cx));
-        cx.run_until_parked();
-
-        // --- Load fresh panels for each workspace and verify independent state ---
-        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
-
-        let async_cx = cx.update(|window, cx| window.to_async(cx));
-        let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
-            .await
-            .expect("panel A load should succeed");
-        cx.run_until_parked();
-
-        let async_cx = cx.update(|window, cx| window.to_async(cx));
-        let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
-            .await
-            .expect("panel B load should succeed");
-        cx.run_until_parked();
-
-        // Workspace A should restore its thread, width, and agent type
-        loaded_a.read_with(cx, |panel, _cx| {
-            assert_eq!(
-                panel.width,
-                Some(px(300.0)),
-                "workspace A width should be restored"
-            );
-            assert_eq!(
-                panel.selected_agent, agent_type_a,
-                "workspace A agent type should be restored"
-            );
-            assert!(
-                panel.active_thread_view().is_some(),
-                "workspace A should have its active thread restored"
-            );
-        });
-
-        // Workspace B should restore its own width and agent type, with no thread
-        loaded_b.read_with(cx, |panel, _cx| {
-            assert_eq!(
-                panel.width,
-                Some(px(400.0)),
-                "workspace B width should be restored"
-            );
-            assert_eq!(
-                panel.selected_agent,
-                AgentType::ClaudeCode,
-                "workspace B agent type should be restored"
-            );
-            assert!(
-                panel.active_thread_view().is_none(),
-                "workspace B should have no active thread"
-            );
-        });
-    }
-}

crates/agent_ui/src/agent_ui.rs 🔗

@@ -49,7 +49,7 @@ use std::any::TypeId;
 use workspace::Workspace;
 
 use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
-pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate};
+pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
 use crate::agent_registry_ui::AgentRegistryPage;
 pub use crate::inline_assistant::InlineAssistant;
 pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
@@ -422,12 +422,6 @@ fn update_command_palette_filter(cx: &mut App) {
                 filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
             }
         }
-
-        if agent_v2_enabled {
-            filter.show_namespace("multi_workspace");
-        } else {
-            filter.hide_namespace("multi_workspace");
-        }
     });
 }
 

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -417,13 +417,8 @@ impl<T: 'static> PromptEditor<T> {
 
     fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
         if inline_assistant_model_supports_images(cx)
-            && let Some(task) = paste_images_as_context(
-                self.editor.clone(),
-                self.mention_set.clone(),
-                self.workspace.clone(),
-                window,
-                cx,
-            )
+            && let Some(task) =
+                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
         {
             task.detach();
         }
@@ -443,7 +438,7 @@ impl<T: 'static> PromptEditor<T> {
                 self.mention_set
                     .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 
-                if let Some(workspace) = Workspace::for_window(window, cx) {
+                if let Some(workspace) = window.root::<Workspace>().flatten() {
                     workspace.update(cx, |workspace, cx| {
                         let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 

crates/agent_ui/src/mention_set.rs 🔗

@@ -297,9 +297,8 @@ impl MentionSet {
         self.mentions.insert(crease_id, (mention_uri, task.clone()));
 
         // Notify the user if we failed to load the mentioned context
-        let workspace = workspace.downgrade();
-        cx.spawn(async move |this, mut cx| {
-            let result = task.await.notify_workspace_async_err(workspace, &mut cx);
+        cx.spawn_in(window, async move |this, cx| {
+            let result = task.await.notify_async_err(cx);
             drop(tx);
             if result.is_none() {
                 this.update(cx, |this, cx| {
@@ -645,7 +644,6 @@ pub(crate) async fn insert_images_as_context(
     images: Vec<gpui::Image>,
     editor: Entity<Editor>,
     mention_set: Entity<MentionSet>,
-    workspace: WeakEntity<Workspace>,
     cx: &mut gpui::AsyncWindowContext,
 ) {
     if images.is_empty() {
@@ -720,11 +718,7 @@ pub(crate) async fn insert_images_as_context(
             mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
         });
 
-        if task
-            .await
-            .notify_workspace_async_err(workspace.clone(), cx)
-            .is_none()
-        {
+        if task.await.notify_async_err(cx).is_none() {
             editor.update(cx, |editor, cx| {
                 editor.edit([(start_anchor..end_anchor, "")], cx);
             });
@@ -738,12 +732,11 @@ pub(crate) async fn insert_images_as_context(
 pub(crate) fn paste_images_as_context(
     editor: Entity<Editor>,
     mention_set: Entity<MentionSet>,
-    workspace: WeakEntity<Workspace>,
     window: &mut Window,
     cx: &mut App,
 ) -> Option<Task<()>> {
     let clipboard = cx.read_from_clipboard()?;
-    Some(window.spawn(cx, async move |mut cx| {
+    Some(window.spawn(cx, async move |cx| {
         use itertools::Itertools;
         let (mut images, paths) = clipboard
             .into_entries()
@@ -790,7 +783,7 @@ pub(crate) fn paste_images_as_context(
         })
         .ok();
 
-        insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
+        insert_images_as_context(images, editor, mention_set, cx).await;
     }))
 }
 

crates/agent_ui/src/ui/agent_notification.rs 🔗

@@ -75,16 +75,6 @@ pub enum AgentNotificationEvent {
 
 impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
 
-impl AgentNotification {
-    pub fn accept(&mut self, cx: &mut Context<Self>) {
-        cx.emit(AgentNotificationEvent::Accepted);
-    }
-
-    pub fn dismiss(&mut self, cx: &mut Context<Self>) {
-        cx.emit(AgentNotificationEvent::Dismissed);
-    }
-}
-
 impl Render for AgentNotification {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let ui_font = theme::setup_ui_font(window, cx);
@@ -184,14 +174,14 @@ impl Render for AgentNotification {
                             .style(ButtonStyle::Tinted(ui::TintColor::Accent))
                             .full_width()
                             .on_click({
-                                cx.listener(move |this, _event, _, cx| {
-                                    this.accept(cx);
+                                cx.listener(move |_this, _event, _, cx| {
+                                    cx.emit(AgentNotificationEvent::Accepted);
                                 })
                             }),
                     )
                     .child(Button::new("dismiss", "Dismiss").full_width().on_click({
-                        cx.listener(move |this, _event, _, cx| {
-                            this.dismiss(cx);
+                        cx.listener(move |_, _event, _, cx| {
+                            cx.emit(AgentNotificationEvent::Dismissed);
                         })
                     })),
             )

crates/collab/tests/integration/channel_guest_tests.rs 🔗

@@ -34,11 +34,9 @@ async fn test_channel_guests(
     cx_a.executor().run_until_parked();
 
     // Client B joins channel A as a guest
-    cx_b.update(|cx| {
-        workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
-    })
-    .await
-    .unwrap();
+    cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
+        .await
+        .unwrap();
 
     // b should be following a in the shared project.
     // B is a guest,
@@ -78,11 +76,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
         .await;
 
     let project_a = client_a.build_test_project(cx_a).await;
-    cx_a.update(|cx| {
-        workspace::join_channel(channel_id, client_a.app_state.clone(), None, None, cx)
-    })
-    .await
-    .unwrap();
+    cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
+        .await
+        .unwrap();
 
     // Client A shares a project in the channel
     active_call_a
@@ -92,11 +88,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     cx_a.run_until_parked();
 
     // Client B joins channel A as a guest
-    cx_b.update(|cx| {
-        workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
-    })
-    .await
-    .unwrap();
+    cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
+        .await
+        .unwrap();
     cx_a.run_until_parked();
 
     // client B opens 1.txt as a guest

crates/collab/tests/integration/editor_tests.rs 🔗

@@ -19,8 +19,7 @@ use fs::Fs;
 use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
 use git::repository::repo_path;
 use gpui::{
-    App, AppContext as _, Entity, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext,
-    VisualTestContext,
+    App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,
 };
 use indoc::indoc;
 use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
@@ -52,7 +51,7 @@ use std::{
 };
 use text::Point;
 use util::{path, rel_path::rel_path, uri};
-use workspace::{CloseIntent, MultiWorkspace, Workspace};
+use workspace::{CloseIntent, Workspace};
 
 #[gpui::test(iterations = 10)]
 async fn test_host_disconnect(
@@ -96,46 +95,34 @@ async fn test_host_disconnect(
 
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
 
-    let window_b = cx_b.add_window(|window, cx| {
-        let workspace = cx.new(|cx| {
-            Workspace::new(
-                None,
-                project_b.clone(),
-                client_b.app_state.clone(),
-                window,
-                cx,
-            )
-        });
-        MultiWorkspace::new(workspace, cx)
+    let workspace_b = cx_b.add_window(|window, cx| {
+        Workspace::new(
+            None,
+            project_b.clone(),
+            client_b.app_state.clone(),
+            window,
+            cx,
+        )
     });
-    let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
-    let workspace_b = window_b
-        .root(cx_b)
-        .unwrap()
-        .read_with(cx_b, |multi_workspace, _| {
-            multi_workspace.workspace().clone()
-        });
+    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
+    let workspace_b_view = workspace_b.root(cx_b).unwrap();
 
-    let editor_b: Entity<Editor> = workspace_b
-        .update_in(cx_b, |workspace, window, cx| {
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, window, cx| {
             workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
         })
+        .unwrap()
         .await
         .unwrap()
         .downcast::<Editor>()
         .unwrap();
 
     //TODO: focus
-    assert!(
-        cx_b.update_window_entity(&editor_b, |editor: &mut Editor, window, _| editor
-            .is_focused(window))
-    );
-    editor_b.update_in(cx_b, |editor: &mut Editor, window, cx| {
-        editor.insert("X", window, cx)
-    });
+    assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
+    editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
 
     cx_b.update(|_, cx| {
-        assert!(workspace_b.read(cx).is_edited());
+        assert!(workspace_b_view.read(cx).is_edited());
     });
 
     // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
@@ -153,16 +140,19 @@ async fn test_host_disconnect(
     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
 
     // Ensure client B's edited state is reset and that the whole window is blurred.
-    workspace_b.update(cx_b, |workspace, cx| {
-        assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
-        assert!(!workspace.is_edited());
-    });
+    workspace_b
+        .update(cx_b, |workspace, _, cx| {
+            assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
+            assert!(!workspace.is_edited());
+        })
+        .unwrap();
 
     // Ensure client B is not prompted to save edits when closing window after disconnecting.
-    let can_close: bool = workspace_b
-        .update_in(cx_b, |workspace, window, cx| {
+    let can_close = workspace_b
+        .update(cx_b, |workspace, window, cx| {
             workspace.prepare_to_close(CloseIntent::Quit, window, cx)
         })
+        .unwrap()
         .await
         .unwrap();
     assert!(can_close);

crates/collab/tests/integration/following_tests.rs 🔗

@@ -17,7 +17,7 @@ use serde_json::json;
 use settings::SettingsStore;
 use text::{Point, ToPoint};
 use util::{path, rel_path::rel_path, test::sample_text};
-use workspace::{CollaboratorId, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _};
+use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
 
 use super::TestClient;
 
@@ -1555,9 +1555,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
     let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
 
     let workspace_b_project_a = window_b_project_a
-        .downcast::<MultiWorkspace>()
+        .downcast::<Workspace>()
         .unwrap()
-        .read_with(cx_b, |mw, _| mw.workspace().clone())
+        .root(cx_b)
         .unwrap();
 
     // assert that b is following a in project a in w.rs
@@ -1657,9 +1657,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
         .unwrap();
     let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
     let workspace_a_project_b = window_a_project_b
-        .downcast::<MultiWorkspace>()
+        .downcast::<Workspace>()
         .unwrap()
-        .read_with(cx_a, |mw, _| mw.workspace().clone())
+        .root(cx_a)
         .unwrap();
 
     executor.run_until_parked();
@@ -2144,7 +2144,7 @@ pub(crate) async fn join_channel(
     client: &TestClient,
     cx: &mut TestAppContext,
 ) -> anyhow::Result<()> {
-    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, None, cx))
+    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
         .await
 }
 

crates/collab/tests/integration/git_tests.rs 🔗

@@ -3,11 +3,11 @@ use std::path::Path;
 use call::ActiveCall;
 use git::status::{FileStatus, StatusCode, TrackedStatus};
 use git_ui::project_diff::ProjectDiff;
-use gpui::{AppContext as _, TestAppContext, VisualTestContext};
+use gpui::{TestAppContext, VisualTestContext};
 use project::ProjectPath;
 use serde_json::json;
 use util::{path, rel_path::rel_path};
-use workspace::{MultiWorkspace, Workspace};
+use workspace::Workspace;
 
 //
 use crate::TestServer;
@@ -57,25 +57,17 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
     cx_b.update(editor::init);
     cx_b.update(git_ui::init);
     let project_b = client_b.join_remote_project(project_id, cx_b).await;
-    let window_b = cx_b.add_window(|window, cx| {
-        let workspace = cx.new(|cx| {
-            Workspace::new(
-                None,
-                project_b.clone(),
-                client_b.app_state.clone(),
-                window,
-                cx,
-            )
-        });
-        MultiWorkspace::new(workspace, cx)
+    let workspace_b = cx_b.add_window(|window, cx| {
+        Workspace::new(
+            None,
+            project_b.clone(),
+            client_b.app_state.clone(),
+            window,
+            cx,
+        )
     });
-    let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
-    let workspace_b = window_b
-        .root(cx_b)
-        .unwrap()
-        .read_with(cx_b, |multi_workspace, _| {
-            multi_workspace.workspace().clone()
-        });
+    let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
+    let workspace_b = workspace_b.root(cx_b).unwrap();
 
     cx_b.update(|window, cx| {
         window

crates/collab/tests/integration/remote_editing_collaboration_tests.rs 🔗

@@ -8,9 +8,7 @@ use editor::{Editor, EditorMode, MultiBuffer};
 use extension::ExtensionHostProxy;
 use fs::{FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
-use gpui::{
-    AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
-};
+use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext};
 use http_client::BlockedHttpClient;
 use language::{
     FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
@@ -665,7 +663,7 @@ async fn test_remote_server_debugger(
 
     let workspace_window = cx_a
         .window_handle()
-        .downcast::<workspace::MultiWorkspace>()
+        .downcast::<workspace::Workspace>()
         .unwrap();
 
     let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
@@ -673,16 +671,13 @@ async fn test_remote_server_debugger(
     debug_panel.update(cx_a, |debug_panel, cx| {
         assert_eq!(
             debug_panel.active_session().unwrap().read(cx).session(cx),
-            session.clone()
+            session
         )
     });
 
-    session.update(
-        cx_a,
-        |session: &mut project::debugger::session::Session, _| {
-            assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
-        },
-    );
+    session.update(cx_a, |session, _| {
+        assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
+    });
 
     let shutdown_session = workspace.update(cx_a, |workspace, cx| {
         workspace.project().update(cx, |project, cx| {
@@ -777,7 +772,7 @@ async fn test_slow_adapter_startup_retries(
 
     let workspace_window = cx_a
         .window_handle()
-        .downcast::<workspace::MultiWorkspace>()
+        .downcast::<workspace::Workspace>()
         .unwrap();
 
     let count = Arc::new(AtomicUsize::new(0));
@@ -809,10 +804,7 @@ async fn test_slow_adapter_startup_retries(
     .unwrap();
     cx_a.run_until_parked();
 
-    let client = session.update(
-        cx_a,
-        |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
-    );
+    let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
     client
         .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
             reason: dap::StoppedEventReason::Pause,

crates/collab/tests/integration/test_server.rs 🔗

@@ -45,7 +45,7 @@ use std::{
     },
 };
 use util::path;
-use workspace::{MultiWorkspace, Workspace, WorkspaceStore};
+use workspace::{Workspace, WorkspaceStore};
 
 use livekit_client::test::TestServer as LivekitTestServer;
 
@@ -843,7 +843,7 @@ impl TestClient {
         channel_id: ChannelId,
         cx: &'a mut TestAppContext,
     ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
-        cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, None, cx))
+        cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
             .await
             .unwrap();
         cx.run_until_parked();
@@ -897,19 +897,10 @@ impl TestClient {
         project: &Entity<Project>,
         cx: &'a mut TestAppContext,
     ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
-        let app_state = self.app_state.clone();
-        let project = project.clone();
-        let window = cx.add_window(|window, cx| {
+        cx.add_window_view(|window, cx| {
             window.activate_window();
-            let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
-            MultiWorkspace::new(workspace, cx)
-        });
-        let cx = VisualTestContext::from_window(*window, cx).into_mut();
-        cx.run_until_parked();
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
-        (workspace, cx)
+            Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
+        })
     }
 
     pub async fn build_test_workspace<'a>(
@@ -917,33 +908,19 @@ impl TestClient {
         cx: &'a mut TestAppContext,
     ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
         let project = self.build_test_project(cx).await;
-        let app_state = self.app_state.clone();
-        let window = cx.add_window(|window, cx| {
+        cx.add_window_view(|window, cx| {
             window.activate_window();
-            let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
-            MultiWorkspace::new(workspace, cx)
-        });
-        let cx = VisualTestContext::from_window(*window, cx).into_mut();
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
-        (workspace, cx)
+            Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
+        })
     }
 
     pub fn active_workspace<'a>(
         &'a self,
         cx: &'a mut TestAppContext,
     ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
-        let window = cx.update(|cx| {
-            cx.active_window()
-                .unwrap()
-                .downcast::<MultiWorkspace>()
-                .unwrap()
-        });
+        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
 
-        let entity = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
+        let entity = window.root(cx).unwrap();
         let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut();
         // it might be nice to try and cleanup these at the end of each test.
         (entity, cx)
@@ -954,15 +931,8 @@ pub fn open_channel_notes(
     channel_id: ChannelId,
     cx: &mut VisualTestContext,
 ) -> Task<anyhow::Result<Entity<ChannelView>>> {
-    let window = cx.update(|_, cx| {
-        cx.active_window()
-            .unwrap()
-            .downcast::<MultiWorkspace>()
-            .unwrap()
-    });
-    let entity = window
-        .read_with(cx, |mw, _| mw.workspace().clone())
-        .unwrap();
+    let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
+    let entity = window.root(cx).unwrap();
 
     cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx))
 }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -36,8 +36,7 @@ use ui::{
 };
 use util::{ResultExt, TryFutureExt, maybe};
 use workspace::{
-    CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare,
-    ShareProject, Workspace,
+    CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
     notifications::{DetachAndPromptErr, NotifyResultExt},
 };
@@ -121,7 +120,6 @@ pub fn init(cx: &mut App) {
 
             if let Some(room) = ActiveCall::global(cx).read(cx).room() {
                 let romo_id_fut = room.read(cx).room_id();
-                let workspace_handle = cx.weak_entity();
                 cx.spawn(async move |workspace, cx| {
                     let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
                     workspace.update(cx, |workspace, cx| {
@@ -136,7 +134,7 @@ pub fn init(cx: &mut App) {
                         );
                     })
                 })
-                .detach_and_notify_err(workspace_handle, window, cx);
+                .detach_and_notify_err(window, cx);
             } else {
                 workspace.show_error(&"There’s no active call; join one first.", cx);
             }
@@ -2179,13 +2177,12 @@ impl CollabPanel {
                 &["Remove", "Cancel"],
                 cx,
             );
-            let workspace = self.workspace.clone();
-            cx.spawn_in(window, async move |this, mut cx| {
+            cx.spawn_in(window, async move |this, cx| {
                 if answer.await? == 0 {
                     channel_store
                         .update(cx, |channels, _| channels.remove_channel(channel_id))
                         .await
-                        .notify_workspace_async_err(workspace, &mut cx);
+                        .notify_async_err(cx);
                     this.update_in(cx, |_, window, cx| cx.focus_self(window))
                         .ok();
                 }
@@ -2214,13 +2211,12 @@ impl CollabPanel {
             &["Remove", "Cancel"],
             cx,
         );
-        let workspace = self.workspace.clone();
-        cx.spawn_in(window, async move |_, mut cx| {
+        cx.spawn_in(window, async move |_, cx| {
             if answer.await? == 0 {
                 user_store
                     .update(cx, |store, cx| store.remove_contact(user_id, cx))
                     .await
-                    .notify_workspace_async_err(workspace, &mut cx);
+                    .notify_async_err(cx);
             }
             anyhow::Ok(())
         })
@@ -2271,15 +2267,13 @@ impl CollabPanel {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
-
-        let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
+        let Some(handle) = window.window_handle().downcast::<Workspace>() else {
             return;
         };
         workspace::join_channel(
             channel_id,
             workspace.read(cx).app_state().clone(),
             Some(handle),
-            Some(self.workspace.clone()),
             cx,
         )
         .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
@@ -2322,13 +2316,12 @@ impl CollabPanel {
                             .full_width()
                             .on_click(cx.listener(|this, _, window, cx| {
                                 let client = this.client.clone();
-                                let workspace = this.workspace.clone();
-                                cx.spawn_in(window, async move |_, mut cx| {
+                                cx.spawn_in(window, async move |_, cx| {
                                     client
-                                        .connect(true, &mut cx)
+                                        .connect(true, cx)
                                         .await
                                         .into_response()
-                                        .notify_workspace_async_err(workspace, &mut cx);
+                                        .notify_async_err(cx);
                                 })
                                 .detach()
                             })),

crates/copilot_ui/src/sign_in.rs 🔗

@@ -35,7 +35,7 @@ pub fn initiate_sign_out(copilot: Entity<Copilot>, window: &mut Window, cx: &mut
                 cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
             }
             Err(err) => cx.update(|window, cx| {
-                if let Some(workspace) = Workspace::for_window(window, cx) {
+                if let Some(workspace) = window.root::<Workspace>().flatten() {
                     workspace.update(cx, |workspace, cx| {
                         workspace.show_error(&err, cx);
                     })
@@ -82,7 +82,7 @@ fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Win
 fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
     const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
 
-    let Some(workspace) = Workspace::for_window(window, cx) else {
+    let Some(workspace) = window.root::<Workspace>().flatten() else {
         return;
     };
 

crates/db/src/kvp.rs 🔗

@@ -1,4 +1,3 @@
-use anyhow::Context as _;
 use gpui::App;
 use sqlez_macros::sql;
 use util::ResultExt as _;
@@ -14,22 +13,12 @@ pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnect
 impl Domain for KeyValueStore {
     const NAME: &str = stringify!(KeyValueStore);
 
-    const MIGRATIONS: &[&str] = &[
-        sql!(
-            CREATE TABLE IF NOT EXISTS kv_store(
-                key TEXT PRIMARY KEY,
-                value TEXT NOT NULL
-            ) STRICT;
-        ),
-        sql!(
-            CREATE TABLE IF NOT EXISTS scoped_kv_store(
-                namespace TEXT NOT NULL,
-                key TEXT NOT NULL,
-                value TEXT NOT NULL,
-                PRIMARY KEY(namespace, key)
-            ) STRICT;
-        ),
-    ];
+    const MIGRATIONS: &[&str] = &[sql!(
+        CREATE TABLE IF NOT EXISTS kv_store(
+            key TEXT PRIMARY KEY,
+            value TEXT NOT NULL
+        ) STRICT;
+    )];
 }
 
 crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
@@ -80,64 +69,6 @@ impl KeyValueStore {
             DELETE FROM kv_store WHERE key = (?)
         }
     }
-
-    pub fn scoped<'a>(&'a self, namespace: &'a str) -> ScopedKeyValueStore<'a> {
-        ScopedKeyValueStore {
-            store: self,
-            namespace,
-        }
-    }
-}
-
-pub struct ScopedKeyValueStore<'a> {
-    store: &'a KeyValueStore,
-    namespace: &'a str,
-}
-
-impl ScopedKeyValueStore<'_> {
-    pub fn read(&self, key: &str) -> anyhow::Result<Option<String>> {
-        self.store.select_row_bound::<(&str, &str), String>(
-            "SELECT value FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
-        )?((self.namespace, key))
-        .context("Failed to read from scoped_kv_store")
-    }
-
-    pub async fn write(&self, key: String, value: String) -> anyhow::Result<()> {
-        let namespace = self.namespace.to_owned();
-        self.store
-            .write(move |connection| {
-                connection.exec_bound::<(&str, &str, &str)>(
-                    "INSERT OR REPLACE INTO scoped_kv_store(namespace, key, value) VALUES ((?), (?), (?))",
-                )?((&namespace, &key, &value))
-                .context("Failed to write to scoped_kv_store")
-            })
-            .await
-    }
-
-    pub async fn delete(&self, key: String) -> anyhow::Result<()> {
-        let namespace = self.namespace.to_owned();
-        self.store
-            .write(move |connection| {
-                connection.exec_bound::<(&str, &str)>(
-                    "DELETE FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
-                )?((&namespace, &key))
-                .context("Failed to delete from scoped_kv_store")
-            })
-            .await
-    }
-
-    pub async fn delete_all(&self) -> anyhow::Result<()> {
-        let namespace = self.namespace.to_owned();
-        self.store
-            .write(move |connection| {
-                connection
-                    .exec_bound::<&str>("DELETE FROM scoped_kv_store WHERE namespace = (?)")?(
-                    &namespace,
-                )
-                .context("Failed to delete_all from scoped_kv_store")
-            })
-            .await
-    }
 }
 
 #[cfg(test)]
@@ -168,52 +99,6 @@ mod tests {
         db.delete_kvp("key-1".to_string()).await.unwrap();
         assert_eq!(db.read_kvp("key-1").unwrap(), None);
     }
-
-    #[gpui::test]
-    async fn test_scoped_kvp() {
-        let db = KeyValueStore::open_test_db("test_scoped_kvp").await;
-
-        let scope_a = db.scoped("namespace-a");
-        let scope_b = db.scoped("namespace-b");
-
-        // Reading a missing key returns None
-        assert_eq!(scope_a.read("key-1").unwrap(), None);
-
-        // Writing and reading back a key works
-        scope_a
-            .write("key-1".to_string(), "value-a1".to_string())
-            .await
-            .unwrap();
-        assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
-
-        // Two namespaces with the same key don't collide
-        scope_b
-            .write("key-1".to_string(), "value-b1".to_string())
-            .await
-            .unwrap();
-        assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
-        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
-
-        // delete removes a single key without affecting others in the namespace
-        scope_a
-            .write("key-2".to_string(), "value-a2".to_string())
-            .await
-            .unwrap();
-        scope_a.delete("key-1".to_string()).await.unwrap();
-        assert_eq!(scope_a.read("key-1").unwrap(), None);
-        assert_eq!(scope_a.read("key-2").unwrap(), Some("value-a2".to_string()));
-        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
-
-        // delete_all removes all keys in a namespace without affecting other namespaces
-        scope_a
-            .write("key-3".to_string(), "value-a3".to_string())
-            .await
-            .unwrap();
-        scope_a.delete_all().await.unwrap();
-        assert_eq!(scope_a.read("key-2").unwrap(), None);
-        assert_eq!(scope_a.read("key-3").unwrap(), None);
-        assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
-    }
 }
 
 pub struct GlobalKeyValueStore(ThreadSafeConnection);

crates/debugger_ui/src/tests.rs 🔗

@@ -8,7 +8,7 @@ use project::{Project, debugger::session::Session};
 use settings::SettingsStore;
 use task::SharedTaskContext;
 use terminal_view::terminal_panel::TerminalPanel;
-use workspace::MultiWorkspace;
+use workspace::Workspace;
 
 use crate::{debugger_panel::DebugPanel, session::DebugSession};
 
@@ -52,16 +52,14 @@ pub fn init_test(cx: &mut gpui::TestAppContext) {
 pub async fn init_test_workspace(
     project: &Entity<Project>,
     cx: &mut TestAppContext,
-) -> WindowHandle<MultiWorkspace> {
+) -> WindowHandle<Workspace> {
     let workspace_handle =
-        cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
     let debugger_panel = workspace_handle
-        .update(cx, |multi, window, cx| {
-            multi.workspace().update(cx, |_workspace, cx| {
-                cx.spawn_in(window, async move |this, cx| {
-                    DebugPanel::load(this, cx).await
-                })
+        .update(cx, |_, window, cx| {
+            cx.spawn_in(window, async move |this, cx| {
+                DebugPanel::load(this, cx).await
             })
         })
         .unwrap()
@@ -69,10 +67,9 @@ pub async fn init_test_workspace(
         .expect("Failed to load debug panel");
 
     let terminal_panel = workspace_handle
-        .update(cx, |multi, window, cx| {
-            let weak_workspace = multi.workspace().downgrade();
-            cx.spawn_in(window, async move |_, cx| {
-                TerminalPanel::load(weak_workspace, cx.clone()).await
+        .update(cx, |_, window, cx| {
+            cx.spawn_in(window, async |this, cx| {
+                TerminalPanel::load(this, cx.clone()).await
             })
         })
         .unwrap()
@@ -80,11 +77,9 @@ pub async fn init_test_workspace(
         .expect("Failed to load terminal panel");
 
     workspace_handle
-        .update(cx, |multi, window, cx| {
-            multi.workspace().update(cx, |workspace, cx| {
-                workspace.add_panel(debugger_panel, window, cx);
-                workspace.add_panel(terminal_panel, window, cx);
-            });
+        .update(cx, |workspace, window, cx| {
+            workspace.add_panel(debugger_panel, window, cx);
+            workspace.add_panel(terminal_panel, window, cx);
         })
         .unwrap();
     workspace_handle
@@ -92,45 +87,39 @@ pub async fn init_test_workspace(
 
 #[track_caller]
 pub fn active_debug_session_panel(
-    workspace: WindowHandle<MultiWorkspace>,
+    workspace: WindowHandle<Workspace>,
     cx: &mut TestAppContext,
 ) -> Entity<DebugSession> {
     workspace
-        .update(cx, |multi, _window, cx| {
-            multi.workspace().update(cx, |workspace, cx| {
-                let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
-                debug_panel
-                    .update(cx, |this, _| this.active_session())
-                    .unwrap()
-            })
+        .update(cx, |workspace, _window, cx| {
+            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
+            debug_panel
+                .update(cx, |this, _| this.active_session())
+                .unwrap()
         })
         .unwrap()
 }
 
 pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
-    workspace: &WindowHandle<MultiWorkspace>,
+    workspace: &WindowHandle<Workspace>,
     cx: &mut gpui::TestAppContext,
     config: DebugTaskDefinition,
     configure: T,
 ) -> Result<Entity<Session>> {
     let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
-    workspace.update(cx, |multi, window, cx| {
-        multi.workspace().update(cx, |workspace, cx| {
-            workspace.start_debug_session(
-                config.to_scenario(),
-                SharedTaskContext::default(),
-                None,
-                None,
-                window,
-                cx,
-            )
-        })
+    workspace.update(cx, |workspace, window, cx| {
+        workspace.start_debug_session(
+            config.to_scenario(),
+            SharedTaskContext::default(),
+            None,
+            None,
+            window,
+            cx,
+        )
     })?;
     cx.run_until_parked();
     let session = workspace.read_with(cx, |workspace, cx| {
         workspace
-            .workspace()
-            .read(cx)
             .panel::<DebugPanel>(cx)
             .and_then(|panel| panel.read(cx).active_session())
             .map(|session| session.read(cx).running_state().read(cx).session())
@@ -142,7 +131,7 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
 }
 
 pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
-    workspace: &WindowHandle<MultiWorkspace>,
+    workspace: &WindowHandle<Workspace>,
     cx: &mut gpui::TestAppContext,
     configure: T,
 ) -> Result<Entity<Session>> {

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

@@ -60,13 +60,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
     // assert we didn't show the attach modal
     workspace
         .update(cx, |workspace, _window, cx| {
-            assert!(
-                workspace
-                    .workspace()
-                    .read(cx)
-                    .active_modal::<AttachModal>(cx)
-                    .is_none()
-            );
+            assert!(workspace.active_modal::<AttachModal>(cx).is_none());
         })
         .unwrap();
 }
@@ -103,9 +97,9 @@ async fn test_show_attach_modal_and_select_process(
             });
         });
     let attach_modal = workspace
-        .update(cx, |multi, window, cx| {
-            let workspace_handle = multi.workspace().downgrade();
-            multi.toggle_modal(window, cx, |window, cx| {
+        .update(cx, |workspace, window, cx| {
+            let workspace_handle = cx.weak_entity();
+            workspace.toggle_modal(window, cx, |window, cx| {
                 AttachModal::with_processes(
                     workspace_handle,
                     vec![
@@ -139,7 +133,7 @@ async fn test_show_attach_modal_and_select_process(
                 )
             });
 
-            multi.active_modal::<AttachModal>(cx).unwrap()
+            workspace.active_modal::<AttachModal>(cx).unwrap()
         })
         .unwrap();
 
@@ -214,26 +208,24 @@ async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &m
 
     let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
     workspace
-        .update(cx, |multi, window, cx| {
-            multi.workspace().update(cx, |workspace, cx| {
-                workspace.start_debug_session(
-                    DebugTaskDefinition {
-                        adapter: FakeAdapter::ADAPTER_NAME.into(),
-                        label: "attach with picker".into(),
-                        config: json!({
-                            "request": "attach",
-                            "process_id": pick_pid_placeholder,
-                        }),
-                        tcp_connection: None,
-                    }
-                    .to_scenario(),
-                    SharedTaskContext::default(),
-                    None,
-                    None,
-                    window,
-                    cx,
-                );
-            })
+        .update(cx, |workspace, window, cx| {
+            workspace.start_debug_session(
+                DebugTaskDefinition {
+                    adapter: FakeAdapter::ADAPTER_NAME.into(),
+                    label: "attach with picker".into(),
+                    config: json!({
+                        "request": "attach",
+                        "process_id": pick_pid_placeholder,
+                    }),
+                    tcp_connection: None,
+                }
+                .to_scenario(),
+                SharedTaskContext::default(),
+                None,
+                None,
+                window,
+                cx,
+            )
         })
         .unwrap();
 

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

@@ -145,17 +145,15 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
         };
 
         workspace
-            .update(cx, |multi, window, cx| {
-                multi.workspace().update(cx, |workspace, cx| {
-                    workspace.start_debug_session(
-                        scenario,
-                        task_context.clone(),
-                        None,
-                        None,
-                        window,
-                        cx,
-                    );
-                })
+            .update(cx, |workspace, window, cx| {
+                workspace.start_debug_session(
+                    scenario,
+                    task_context.clone(),
+                    None,
+                    None,
+                    window,
+                    cx,
+                )
             })
             .unwrap();
 
@@ -184,10 +182,8 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     workspace
-        .update(cx, |multi, window, cx| {
-            multi.workspace().update(cx, |workspace, cx| {
-                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
-            });
+        .update(cx, |workspace, window, cx| {
+            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
         })
         .unwrap();
 
@@ -328,10 +324,8 @@ async fn test_debug_modal_subtitles_with_multiple_worktrees(
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
 
     workspace
-        .update(cx, |multi, window, cx| {
-            multi.workspace().update(cx, |workspace, cx| {
-                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
-            });
+        .update(cx, |workspace, window, cx| {
+            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
         })
         .unwrap();
 

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

@@ -1113,8 +1113,8 @@ async fn test_stack_frame_filter_persistence(
     let workspace = init_test_workspace(&project, cx).await;
     let cx = &mut VisualTestContext::from_window(*workspace, cx);
     workspace
-        .update(cx, |workspace, _, cx| {
-            workspace.set_random_database_id(cx);
+        .update(cx, |workspace, _, _| {
+            workspace.set_random_database_id();
         })
         .unwrap();
 
@@ -1211,7 +1211,7 @@ 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.database_id())
         .ok()
         .flatten()
         .expect("workspace id has to be some for this test to work properly");

crates/dev_container/Cargo.toml 🔗

@@ -23,12 +23,7 @@ util.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]
-fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
-project = { workspace = true, features = ["test-support"] }
-settings = { workspace = true, features = ["test-support"] }
-theme.workspace = true
-workspace = { workspace = true, features = ["test-support"] }
 
 [lints]
 workspace = true

crates/dev_container/src/devcontainer_api.rs 🔗

@@ -2,16 +2,18 @@ use std::{
     collections::{HashMap, HashSet},
     fmt::Display,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 
+use gpui::AsyncWindowContext;
 use node_runtime::NodeRuntime;
 use serde::Deserialize;
-use settings::DevContainerConnection;
+use settings::{DevContainerConnection, Settings as _};
 use smol::{fs, process::Command};
 use util::rel_path::RelPath;
 use workspace::Workspace;
 
-use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate};
+use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
 
 /// Represents a discovered devcontainer configuration
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -57,31 +59,6 @@ pub(crate) struct DevContainerConfigurationOutput {
     configuration: DevContainerConfiguration,
 }
 
-pub(crate) struct DevContainerCli {
-    pub path: PathBuf,
-    node_runtime_path: Option<PathBuf>,
-}
-
-impl DevContainerCli {
-    fn command(&self, use_podman: bool) -> Command {
-        let mut command = if let Some(node_runtime_path) = &self.node_runtime_path {
-            let mut command = util::command::new_smol_command(
-                node_runtime_path.as_os_str().display().to_string(),
-            );
-            command.arg(self.path.display().to_string());
-            command
-        } else {
-            util::command::new_smol_command(self.path.display().to_string())
-        };
-
-        if use_podman {
-            command.arg("--docker-path");
-            command.arg("podman");
-        }
-        command
-    }
-}
-
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum DevContainerError {
     DockerNotAvailable,
@@ -122,6 +99,58 @@ impl Display for DevContainerError {
     }
 }
 
+pub(crate) async fn read_devcontainer_configuration_for_project(
+    cx: &mut AsyncWindowContext,
+    node_runtime: &NodeRuntime,
+) -> Result<DevContainerConfigurationOutput, DevContainerError> {
+    let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
+
+    let Some(directory) = project_directory(cx) else {
+        return Err(DevContainerError::NotInValidProject);
+    };
+
+    devcontainer_read_configuration(
+        &path_to_devcontainer_cli,
+        found_in_path,
+        node_runtime,
+        &directory,
+        None,
+        use_podman(cx),
+    )
+    .await
+}
+
+pub(crate) async fn apply_dev_container_template(
+    template: &DevContainerTemplate,
+    options_selected: &HashMap<String, String>,
+    features_selected: &HashSet<DevContainerFeature>,
+    cx: &mut AsyncWindowContext,
+    node_runtime: &NodeRuntime,
+) -> Result<DevContainerApply, DevContainerError> {
+    let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
+
+    let Some(directory) = project_directory(cx) else {
+        return Err(DevContainerError::NotInValidProject);
+    };
+
+    devcontainer_template_apply(
+        template,
+        options_selected,
+        features_selected,
+        &path_to_devcontainer_cli,
+        found_in_path,
+        node_runtime,
+        &directory,
+        false, // devcontainer template apply does not use --docker-path option
+    )
+    .await
+}
+
+fn use_podman(cx: &mut AsyncWindowContext) -> bool {
+    cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman)
+        .unwrap_or(false)
+}
+
 /// Finds all available devcontainer configurations in the project.
 ///
 /// This function scans for:
@@ -129,124 +158,160 @@ impl Display for DevContainerError {
 /// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)
 ///
 /// Returns a list of found configurations, or an empty list if none are found.
-pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<DevContainerConfig> {
-    let project = workspace.project().read(cx);
-
-    let worktree = project
-        .visible_worktrees(cx)
-        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
-
-    let Some(worktree) = worktree else {
-        log::debug!("find_devcontainer_configs: No worktree found");
+pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> {
+    let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+        log::debug!("find_devcontainer_configs: No workspace found");
         return Vec::new();
     };
 
-    let worktree = worktree.read(cx);
-    let mut configs = Vec::new();
+    let Ok(configs) = workspace.update(cx, |workspace, _, cx| {
+        let project = workspace.project().read(cx);
 
-    let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
+        let worktree = project
+            .visible_worktrees(cx)
+            .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
 
-    let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
-        log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
-        return Vec::new();
-    };
+        let Some(worktree) = worktree else {
+            log::debug!("find_devcontainer_configs: No worktree found");
+            return Vec::new();
+        };
 
-    if !devcontainer_entry.is_dir() {
-        log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
-        return Vec::new();
-    }
+        let worktree = worktree.read(cx);
+        let mut configs = Vec::new();
 
-    log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
-    let devcontainer_json_path =
-        RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
-    for entry in worktree.child_entries(devcontainer_path) {
-        log::debug!(
-            "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
-            entry.path.as_unix_str(),
-            entry.is_file(),
-            entry.is_dir()
-        );
+        let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
 
-        if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
-            log::debug!("find_devcontainer_configs: Found default devcontainer.json");
-            configs.push(DevContainerConfig::default_config());
-        } else if entry.is_dir() {
-            let subfolder_name = entry
-                .path
-                .file_name()
-                .map(|n| n.to_string())
-                .unwrap_or_default();
-
-            let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
-            if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
-                if worktree.entry_for_path(rel_config_path).is_some() {
-                    log::debug!(
-                        "find_devcontainer_configs: Found config in subfolder: {}",
-                        subfolder_name
-                    );
-                    configs.push(DevContainerConfig {
-                        name: subfolder_name,
-                        config_path: PathBuf::from(&config_json_path),
-                    });
-                } else {
-                    log::debug!(
-                        "find_devcontainer_configs: Subfolder {} has no devcontainer.json",
-                        subfolder_name
-                    );
+        let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
+            log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
+            return Vec::new();
+        };
+
+        if !devcontainer_entry.is_dir() {
+            log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
+            return Vec::new();
+        }
+
+        log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
+        let devcontainer_json_path =
+            RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
+        for entry in worktree.child_entries(devcontainer_path) {
+            log::debug!(
+                "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
+                entry.path.as_unix_str(),
+                entry.is_file(),
+                entry.is_dir()
+            );
+
+            if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
+                log::debug!("find_devcontainer_configs: Found default devcontainer.json");
+                configs.push(DevContainerConfig::default_config());
+            } else if entry.is_dir() {
+                let subfolder_name = entry
+                    .path
+                    .file_name()
+                    .map(|n| n.to_string())
+                    .unwrap_or_default();
+
+                let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
+                if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
+                    if worktree.entry_for_path(rel_config_path).is_some() {
+                        log::debug!(
+                            "find_devcontainer_configs: Found config in subfolder: {}",
+                            subfolder_name
+                        );
+                        configs.push(DevContainerConfig {
+                            name: subfolder_name,
+                            config_path: PathBuf::from(&config_json_path),
+                        });
+                    } else {
+                        log::debug!(
+                            "find_devcontainer_configs: Subfolder {} has no devcontainer.json",
+                            subfolder_name
+                        );
+                    }
                 }
             }
         }
-    }
 
-    log::info!(
-        "find_devcontainer_configs: Found {} configurations",
-        configs.len()
-    );
+        log::info!(
+            "find_devcontainer_configs: Found {} configurations",
+            configs.len()
+        );
 
-    configs.sort_by(|a, b| {
-        if a.name == "default" {
-            std::cmp::Ordering::Less
-        } else if b.name == "default" {
-            std::cmp::Ordering::Greater
-        } else {
-            a.name.cmp(&b.name)
-        }
-    });
+        configs.sort_by(|a, b| {
+            if a.name == "default" {
+                std::cmp::Ordering::Less
+            } else if b.name == "default" {
+                std::cmp::Ordering::Greater
+            } else {
+                a.name.cmp(&b.name)
+            }
+        });
+
+        configs
+    }) else {
+        log::debug!("find_devcontainer_configs: Failed to update workspace");
+        return Vec::new();
+    };
 
     configs
 }
 
 pub async fn start_dev_container_with_config(
-    context: DevContainerContext,
+    cx: &mut AsyncWindowContext,
+    node_runtime: NodeRuntime,
     config: Option<DevContainerConfig>,
 ) -> Result<(DevContainerConnection, String), DevContainerError> {
-    check_for_docker(context.use_podman).await?;
-    let cli = ensure_devcontainer_cli(&context.node_runtime).await?;
-    let config_path = config.map(|c| context.project_directory.join(&c.config_path));
+    let use_podman = use_podman(cx);
+    check_for_docker(use_podman).await?;
 
-    match devcontainer_up(&context, &cli, config_path.as_deref()).await {
+    let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
+
+    let Some(directory) = project_directory(cx) else {
+        return Err(DevContainerError::NotInValidProject);
+    };
+
+    let config_path = config.map(|c| directory.join(&c.config_path));
+
+    match devcontainer_up(
+        &path_to_devcontainer_cli,
+        found_in_path,
+        &node_runtime,
+        directory.clone(),
+        config_path.clone(),
+        use_podman,
+    )
+    .await
+    {
         Ok(DevContainerUp {
             container_id,
             remote_workspace_folder,
             remote_user,
             ..
         }) => {
-            let project_name =
-                match read_devcontainer_configuration(&context, &cli, config_path.as_deref()).await
-                {
-                    Ok(DevContainerConfigurationOutput {
-                        configuration:
-                            DevContainerConfiguration {
-                                name: Some(project_name),
-                            },
-                    }) => project_name,
-                    _ => get_backup_project_name(&remote_workspace_folder, &container_id),
-                };
+            let project_name = match devcontainer_read_configuration(
+                &path_to_devcontainer_cli,
+                found_in_path,
+                &node_runtime,
+                &directory,
+                config_path.as_ref(),
+                use_podman,
+            )
+            .await
+            {
+                Ok(DevContainerConfigurationOutput {
+                    configuration:
+                        DevContainerConfiguration {
+                            name: Some(project_name),
+                        },
+                }) => project_name,
+                _ => get_backup_project_name(&remote_workspace_folder, &container_id),
+            };
 
             let connection = DevContainerConnection {
                 name: project_name,
-                container_id,
-                use_podman: context.use_podman,
+                container_id: container_id,
+                use_podman,
                 remote_user,
             };
 
@@ -290,9 +355,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
     }
 }
 
-pub(crate) async fn ensure_devcontainer_cli(
+async fn ensure_devcontainer_cli(
     node_runtime: &NodeRuntime,
-) -> Result<DevContainerCli, DevContainerError> {
+) -> Result<(PathBuf, bool), DevContainerError> {
     let mut command = util::command::new_smol_command(&dev_container_cli());
     command.arg("--version");
 
@@ -330,10 +395,7 @@ pub(crate) async fn ensure_devcontainer_cli(
             Ok(output) => {
                 if output.status.success() {
                     log::info!("Found devcontainer CLI in Data dir");
-                    return Ok(DevContainerCli {
-                        path: datadir_cli_path.clone(),
-                        node_runtime_path: Some(node_runtime_path.clone()),
-                    });
+                    return Ok((datadir_cli_path.clone(), false));
                 } else {
                     log::error!(
                         "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
@@ -373,29 +435,32 @@ pub(crate) async fn ensure_devcontainer_cli(
             );
             Err(DevContainerError::DevContainerCliNotAvailable)
         } else {
-            Ok(DevContainerCli {
-                path: datadir_cli_path,
-                node_runtime_path: Some(node_runtime_path),
-            })
+            Ok((datadir_cli_path, false))
         }
     } else {
         log::info!("Found devcontainer cli on $PATH, using it");
-        Ok(DevContainerCli {
-            path: PathBuf::from(&dev_container_cli()),
-            node_runtime_path: None,
-        })
+        Ok((PathBuf::from(&dev_container_cli()), true))
     }
 }
 
 async fn devcontainer_up(
-    context: &DevContainerContext,
-    cli: &DevContainerCli,
-    config_path: Option<&Path>,
+    path_to_cli: &PathBuf,
+    found_in_path: bool,
+    node_runtime: &NodeRuntime,
+    path: Arc<Path>,
+    config_path: Option<PathBuf>,
+    use_podman: bool,
 ) -> Result<DevContainerUp, DevContainerError> {
-    let mut command = cli.command(context.use_podman);
+    let Ok(node_runtime_path) = node_runtime.binary_path().await else {
+        log::error!("Unable to find node runtime path");
+        return Err(DevContainerError::NodeRuntimeNotAvailable);
+    };
+
+    let mut command =
+        devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
     command.arg("up");
     command.arg("--workspace-folder");
-    command.arg(context.project_directory.display().to_string());
+    command.arg(path.display().to_string());
 
     if let Some(config) = config_path {
         command.arg("--config");
@@ -428,15 +493,24 @@ async fn devcontainer_up(
     }
 }
 
-pub(crate) async fn read_devcontainer_configuration(
-    context: &DevContainerContext,
-    cli: &DevContainerCli,
-    config_path: Option<&Path>,
+async fn devcontainer_read_configuration(
+    path_to_cli: &PathBuf,
+    found_in_path: bool,
+    node_runtime: &NodeRuntime,
+    path: &Arc<Path>,
+    config_path: Option<&PathBuf>,
+    use_podman: bool,
 ) -> Result<DevContainerConfigurationOutput, DevContainerError> {
-    let mut command = cli.command(context.use_podman);
+    let Ok(node_runtime_path) = node_runtime.binary_path().await else {
+        log::error!("Unable to find node runtime path");
+        return Err(DevContainerError::NodeRuntimeNotAvailable);
+    };
+
+    let mut command =
+        devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
     command.arg("read-configuration");
     command.arg("--workspace-folder");
-    command.arg(context.project_directory.display().to_string());
+    command.arg(path.display().to_string());
 
     if let Some(config) = config_path {
         command.arg("--config");
@@ -466,14 +540,23 @@ pub(crate) async fn read_devcontainer_configuration(
     }
 }
 
-pub(crate) async fn apply_dev_container_template(
+async fn devcontainer_template_apply(
     template: &DevContainerTemplate,
     template_options: &HashMap<String, String>,
     features_selected: &HashSet<DevContainerFeature>,
-    context: &DevContainerContext,
-    cli: &DevContainerCli,
+    path_to_cli: &PathBuf,
+    found_in_path: bool,
+    node_runtime: &NodeRuntime,
+    path: &Arc<Path>,
+    use_podman: bool,
 ) -> Result<DevContainerApply, DevContainerError> {
-    let mut command = cli.command(context.use_podman);
+    let Ok(node_runtime_path) = node_runtime.binary_path().await else {
+        log::error!("Unable to find node runtime path");
+        return Err(DevContainerError::NodeRuntimeNotAvailable);
+    };
+
+    let mut command =
+        devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
 
     let Ok(serialized_options) = serde_json::to_string(template_options) else {
         log::error!("Unable to serialize options for {:?}", template_options);
@@ -483,7 +566,7 @@ pub(crate) async fn apply_dev_container_template(
     command.arg("templates");
     command.arg("apply");
     command.arg("--workspace-folder");
-    command.arg(context.project_directory.display().to_string());
+    command.arg(path.display().to_string());
     command.arg("--template-id");
     command.arg(format!(
         "{}/{}",
@@ -547,6 +630,28 @@ fn parse_json_from_cli<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T, D
         })
 }
 
+fn devcontainer_cli_command(
+    path_to_cli: &PathBuf,
+    found_in_path: bool,
+    node_runtime_path: &PathBuf,
+    use_podman: bool,
+) -> Command {
+    let mut command = if found_in_path {
+        util::command::new_smol_command(path_to_cli.display().to_string())
+    } else {
+        let mut command =
+            util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
+        command.arg(path_to_cli.display().to_string());
+        command
+    };
+
+    if use_podman {
+        command.arg("--docker-path");
+        command.arg("podman");
+    }
+    command
+}
+
 fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String {
     Path::new(remote_workspace_folder)
         .file_name()
@@ -555,6 +660,22 @@ fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) ->
         .unwrap_or_else(|| container_id.to_string())
 }
 
+fn project_directory(cx: &mut AsyncWindowContext) -> Option<Arc<Path>> {
+    let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+        return None;
+    };
+
+    match workspace.update(cx, |workspace, _, cx| {
+        workspace.project().read(cx).active_project_directory(cx)
+    }) {
+        Ok(dir) => dir,
+        Err(e) => {
+            log::error!("Error getting project directory from workspace: {:?}", e);
+            None
+        }
+    }
+}
+
 fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -> String {
     let features_map = features_selected
         .iter()
@@ -580,160 +701,7 @@ fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -
 
 #[cfg(test)]
 mod tests {
-    use crate::devcontainer_api::{DevContainerUp, find_devcontainer_configs, parse_json_from_cli};
-    use fs::FakeFs;
-    use gpui::TestAppContext;
-    use project::Project;
-    use serde_json::json;
-    use settings::SettingsStore;
-    use workspace::Workspace;
-
-    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);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_find_devcontainer_configs_no_devcontainer_dir(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                "src": { "main.rs": "fn main() {}" }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/project".as_ref()], cx).await;
-        let (workspace, cx) =
-            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
-
-        let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
-        assert!(
-            configs.is_empty(),
-            "Expected no configs when .devcontainer dir is absent, got: {configs:?}"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_find_devcontainer_configs_single_default(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                ".devcontainer": {
-                    "devcontainer.json": r#"{"image": "ubuntu"}"#
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/project".as_ref()], cx).await;
-        let (workspace, cx) =
-            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
-
-        let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
-        assert_eq!(
-            configs.len(),
-            1,
-            "Expected exactly one config, got: {configs:?}"
-        );
-        assert_eq!(configs[0].name, "default");
-        assert_eq!(
-            configs[0].config_path.to_str().unwrap(),
-            ".devcontainer/devcontainer.json"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_find_devcontainer_configs_multiple_subfolders(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                ".devcontainer": {
-                    "python": { "devcontainer.json": r#"{}"# },
-                    "node": { "devcontainer.json": r#"{}"# }
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/project".as_ref()], cx).await;
-        let (workspace, cx) =
-            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
-
-        let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
-        assert_eq!(configs.len(), 2, "Expected two configs, got: {configs:?}");
-        assert_eq!(configs[0].name, "node");
-        assert_eq!(configs[1].name, "python");
-    }
-
-    #[gpui::test]
-    async fn test_find_devcontainer_configs_default_plus_subfolders(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                ".devcontainer": {
-                    "devcontainer.json": r#"{}"#,
-                    "python": { "devcontainer.json": r#"{}"# },
-                    "node": { "devcontainer.json": r#"{}"# }
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/project".as_ref()], cx).await;
-        let (workspace, cx) =
-            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
-
-        let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
-        assert_eq!(configs.len(), 3, "Expected three configs, got: {configs:?}");
-        assert_eq!(
-            configs[0].name, "default",
-            "Default config should be sorted first"
-        );
-        assert_eq!(configs[1].name, "node");
-        assert_eq!(configs[2].name, "python");
-    }
-
-    #[gpui::test]
-    async fn test_find_devcontainer_configs_subfolder_without_json(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                ".devcontainer": {
-                    "devcontainer.json": r#"{}"#,
-                    "has_config": { "devcontainer.json": r#"{}"# },
-                    "no_config": { "README.md": "not a devcontainer" }
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/project".as_ref()], cx).await;
-        let (workspace, cx) =
-            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
-
-        let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
-        assert_eq!(
-            configs.len(),
-            2,
-            "Subfolder without devcontainer.json should be skipped, got: {configs:?}"
-        );
-        assert_eq!(configs[0].name, "default");
-        assert_eq!(configs[1].name, "has_config");
-    }
+    use crate::devcontainer_api::{DevContainerUp, parse_json_from_cli};
 
     #[test]
     fn should_parse_from_devcontainer_json() {

crates/dev_container/src/lib.rs 🔗

@@ -1,5 +1,3 @@
-use std::path::Path;
-
 use gpui::AppContext;
 use gpui::Entity;
 use gpui::Task;
@@ -43,8 +41,7 @@ use http_client::{AsyncBody, HttpClient};
 
 mod devcontainer_api;
 
-use devcontainer_api::ensure_devcontainer_cli;
-use devcontainer_api::read_devcontainer_configuration;
+use devcontainer_api::read_devcontainer_configuration_for_project;
 
 use crate::devcontainer_api::DevContainerError;
 use crate::devcontainer_api::apply_dev_container_template;
@@ -53,34 +50,11 @@ pub use devcontainer_api::{
     DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
 };
 
-pub struct DevContainerContext {
-    pub project_directory: Arc<Path>,
-    pub use_podman: bool,
-    pub node_runtime: node_runtime::NodeRuntime,
-}
-
-impl DevContainerContext {
-    pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option<Self> {
-        let project_directory = workspace.project().read(cx).active_project_directory(cx)?;
-        let use_podman = DevContainerSettings::get_global(cx).use_podman;
-        let node_runtime = workspace.app_state().node_runtime.clone();
-        Some(Self {
-            project_directory,
-            use_podman,
-            node_runtime,
-        })
-    }
-}
-
 #[derive(RegisterSetting)]
 struct DevContainerSettings {
     use_podman: bool,
 }
 
-pub fn use_podman(cx: &App) -> bool {
-    DevContainerSettings::get_global(cx).use_podman
-}
-
 impl Settings for DevContainerSettings {
     fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
@@ -1445,41 +1419,22 @@ fn dispatch_apply_templates(
     cx: &mut Context<DevContainerModal>,
 ) {
     cx.spawn_in(window, async move |this, cx| {
-        let Some((tree_id, context)) = workspace.update(cx, |workspace, cx| {
-            let worktree = workspace
-                .project()
-                .read(cx)
-                .visible_worktrees(cx)
-                .find_map(|tree| {
-                    tree.read(cx)
-                        .root_entry()?
-                        .is_dir()
-                        .then_some(tree.read(cx))
-                });
-            let tree_id = worktree.map(|w| w.id())?;
-            let context = DevContainerContext::from_workspace(workspace, cx)?;
-            Some((tree_id, context))
-        }) else {
-            return;
-        };
-
-        let Ok(cli) = ensure_devcontainer_cli(&context.node_runtime).await else {
-            this.update_in(cx, |this, window, cx| {
-                this.accept_message(
-                    DevContainerMessage::FailedToWriteTemplate(
-                        DevContainerError::DevContainerCliNotAvailable,
-                    ),
-                    window,
-                    cx,
-                );
-            })
-            .log_err();
-            return;
-        };
+        if let Some(tree_id) = workspace.update(cx, |workspace, cx| {
+            let project = workspace.project().clone();
+            let worktree = project.read(cx).visible_worktrees(cx).find_map(|tree| {
+                tree.read(cx)
+                    .root_entry()?
+                    .is_dir()
+                    .then_some(tree.read(cx))
+            });
+            worktree.map(|w| w.id())
+        }) {
+            let node_runtime = workspace.read_with(cx, |workspace, _| {
+                workspace.app_state().node_runtime.clone()
+            });
 
-        {
             if check_for_existing
-                && read_devcontainer_configuration(&context, &cli, None)
+                && read_devcontainer_configuration_for_project(cx, &node_runtime)
                     .await
                     .is_ok()
             {
@@ -1498,8 +1453,8 @@ fn dispatch_apply_templates(
                 &template_entry.template,
                 &template_entry.options_selected,
                 &template_entry.features_selected,
-                &context,
-                &cli,
+                cx,
+                &node_runtime,
             )
             .await
             {
@@ -1541,6 +1496,8 @@ fn dispatch_apply_templates(
                 this.dismiss(&menu::Cancel, window, cx);
             })
             .ok();
+        } else {
+            return;
         }
     })
     .detach();

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -904,7 +904,7 @@ impl Render for BufferDiagnosticsEditor {
                                 .style(ButtonStyle::Transparent)
                                 .tooltip(Tooltip::text("Open File"))
                                 .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
-                                    if let Some(workspace) = Workspace::for_window(window, cx) {
+                                    if let Some(workspace) = window.root::<Workspace>().flatten() {
                                         workspace.update(cx, |workspace, cx| {
                                             workspace
                                                 .open_path(

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -119,7 +119,7 @@ impl Render for EditPredictionButton {
                         IconButton::new("copilot-error", icon)
                             .icon_size(IconSize::Small)
                             .on_click(cx.listener(move |_, _, window, cx| {
-                                if let Some(workspace) = Workspace::for_window(window, cx) {
+                                if let Some(workspace) = window.root::<Workspace>().flatten() {
                                     workspace.update(cx, |workspace, cx| {
                                         let copilot = copilot.clone();
                                         workspace.show_toast(

crates/editor/src/editor.rs 🔗

@@ -3105,24 +3105,6 @@ impl Editor {
         self.workspace.as_ref()?.0.upgrade()
     }
 
-    /// Detaches a task and shows an error notification in the workspace if available,
-    /// otherwise just logs the error.
-    pub fn detach_and_notify_err<R, E>(
-        &self,
-        task: Task<Result<R, E>>,
-        window: &mut Window,
-        cx: &mut App,
-    ) where
-        E: std::fmt::Debug + std::fmt::Display + 'static,
-        R: 'static,
-    {
-        if let Some(workspace) = self.workspace() {
-            task.detach_and_notify_err(workspace.downgrade(), window, cx);
-        } else {
-            task.detach_and_log_err(cx);
-        }
-    }
-
     /// Returns the workspace serialization ID if this editor should be serialized.
     fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> {
         self.workspace
@@ -11479,8 +11461,8 @@ impl Editor {
         let Some(project) = self.project.clone() else {
             return;
         };
-        let task = self.reload(project, window, cx);
-        self.detach_and_notify_err(task, window, cx);
+        self.reload(project, window, cx)
+            .detach_and_notify_err(window, cx);
     }
 
     pub fn restore_file(

crates/editor/src/element.rs 🔗

@@ -99,6 +99,7 @@ use workspace::{
     CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel,
     Workspace,
     item::{BreadcrumbText, Item, ItemBufferKind},
+    notifications::NotifyTaskExt,
 };
 
 /// Determines what kinds of highlights should be applied to a lines background.
@@ -540,21 +541,21 @@ impl EditorElement {
 
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.format(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.format_selections(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.organize_imports(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
@@ -564,49 +565,49 @@ impl EditorElement {
         register_action(editor, window, Editor::show_character_palette);
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.confirm_completion(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.compose_completion(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.confirm_code_action(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.rename(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }
         });
         register_action(editor, window, |editor, action, window, cx| {
             if let Some(task) = editor.confirm_rename(action, window, cx) {
-                editor.detach_and_notify_err(task, window, cx);
+                task.detach_and_notify_err(window, cx);
             } else {
                 cx.propagate();
             }

crates/editor/src/hover_popover.rs 🔗

@@ -719,7 +719,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
     if let Ok(uri) = Url::parse(&link)
         && uri.scheme() == "file"
-        && let Some(workspace) = Workspace::for_window(window, cx)
+        && let Some(workspace) = window.root::<Workspace>().flatten()
     {
         workspace.update(cx, |workspace, cx| {
             let task = workspace.open_abs_path(

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -22,7 +22,7 @@ use language::{
 use lsp::{notification, request};
 use project::Project;
 use smol::stream::StreamExt;
-use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle};
+use workspace::{AppState, Workspace, WorkspaceHandle};
 
 use super::editor_test_context::{AssertionContextManager, EditorTestContext};
 
@@ -95,8 +95,7 @@ impl EditorLspTestContext {
             )
             .await;
 
-        let window =
-            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let workspace = window.root(cx).unwrap();
 
@@ -107,20 +106,12 @@ impl EditorLspTestContext {
             })
             .await
             .unwrap();
-        cx.read(|cx| {
-            workspace
-                .read(cx)
-                .workspace()
-                .read(cx)
-                .worktree_scans_complete(cx)
-        })
-        .await;
-        let file = cx.read(|cx| workspace.read(cx).workspace().file_project_paths(cx)[0].clone());
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
+        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
         let item = workspace
             .update_in(&mut cx, |workspace, window, cx| {
-                workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.open_path(file, None, true, window, cx)
-                })
+                workspace.open_path(file, None, true, window, cx)
             })
             .await
             .expect("Could not open test file");
@@ -130,8 +121,6 @@ impl EditorLspTestContext {
         });
         editor.update_in(&mut cx, |editor, window, cx| {
             let nav_history = workspace
-                .read(cx)
-                .workspace()
                 .read(cx)
                 .active_pane()
                 .read(cx)
@@ -145,8 +134,6 @@ impl EditorLspTestContext {
         // Ensure the language server is fully registered with the buffer
         cx.executor().run_until_parked();
 
-        let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
-
         Self {
             cx: EditorTestContext {
                 cx,

crates/feature_flags/src/flags.rs 🔗

@@ -16,10 +16,6 @@ pub struct AgentV2FeatureFlag;
 
 impl FeatureFlag for AgentV2FeatureFlag {
     const NAME: &'static str = "agent-v2";
-
-    fn enabled_for_staff() -> bool {
-        true
-    }
 }
 
 pub struct AcpBetaFeatureFlag;

crates/file_finder/src/file_finder.rs 🔗

@@ -1566,12 +1566,9 @@ impl PickerDelegate for FileFinderDelegate {
                 .unwrap_or(0)
                 .saturating_sub(1);
             let finder = self.file_finder.clone();
-            let workspace = self.workspace.clone();
 
-            cx.spawn_in(window, async move |_, mut cx| {
-                let item = open_task
-                    .await
-                    .notify_workspace_async_err(workspace, &mut cx)?;
+            cx.spawn_in(window, async move |_, cx| {
+                let item = open_task.await.notify_async_err(cx)?;
                 if let Some(row) = row
                     && let Some(active_editor) = item.downcast::<Editor>()
                 {

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -9,9 +9,7 @@ use project::{FS_WATCH_LATENCY, RemoveOptions};
 use serde_json::json;
 use settings::SettingsStore;
 use util::{path, rel_path::rel_path};
-use workspace::{
-    AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths,
-};
+use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
 
 #[ctor::ctor]
 fn init_logger() {
@@ -2536,14 +2534,8 @@ async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui
         .await;
 
     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-    let window = cx.add_window({
-        let project = project.clone();
-        |window, cx| MultiWorkspace::test_new(project, window, cx)
-    });
-    let cx = VisualTestContext::from_window(*window, cx).into_mut();
-    let workspace = window
-        .read_with(cx, |mw, _| mw.workspace().clone())
-        .unwrap();
+    let (workspace, cx) =
+        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
     cx.update(|_, cx| {
         open_paths(

crates/git_ui/src/commit_view.rs 🔗

@@ -774,7 +774,7 @@ impl CommitView {
                 callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
                 anyhow::Ok(())
             })
-            .detach_and_notify_err(workspace.weak_handle(), window, cx);
+            .detach_and_notify_err(window, cx);
     }
 
     async fn close_commit_view(

crates/git_ui/src/file_diff_view.rs 🔗

@@ -6,7 +6,7 @@ use editor::{Editor, EditorEvent, MultiBuffer};
 use futures::{FutureExt, select_biased};
 use gpui::{
     AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
-    Focusable, IntoElement, Render, Task, WeakEntity, Window,
+    Focusable, IntoElement, Render, Task, Window,
 };
 use language::{Buffer, LanguageRegistry};
 use project::Project;
@@ -39,10 +39,11 @@ impl FileDiffView {
     pub fn open(
         old_path: PathBuf,
         new_path: PathBuf,
-        workspace: WeakEntity<Workspace>,
+        workspace: &Workspace,
         window: &mut Window,
         cx: &mut App,
     ) -> Task<Result<Entity<Self>>> {
+        let workspace = workspace.weak_handle();
         window.spawn(cx, async move |cx| {
             let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
             let old_buffer = project
@@ -405,7 +406,7 @@ mod tests {
                 FileDiffView::open(
                     path!("/test/old_file.txt").into(),
                     path!("/test/new_file.txt").into(),
-                    workspace.weak_handle(),
+                    workspace,
                     window,
                     cx,
                 )
@@ -539,7 +540,7 @@ mod tests {
                 FileDiffView::open(
                     PathBuf::from(path!("/test/old_file.txt")),
                     PathBuf::from(path!("/test/new_file.txt")),
-                    workspace.weak_handle(),
+                    workspace,
                     window,
                     cx,
                 )

crates/git_ui/src/git_panel.rs 🔗

@@ -1274,11 +1274,10 @@ impl GitPanel {
                 })
                 .ok()?;
 
-            let workspace = self.workspace.clone();
             cx.spawn_in(window, async move |_, mut cx| {
                 let item = open_task
                     .await
-                    .notify_workspace_async_err(workspace, &mut cx)
+                    .notify_async_err(&mut cx)
                     .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
                 if let Some(active_editor) = item.downcast::<Editor>() {
                     if let Some(diff_task) =

crates/git_ui/src/project_diff.rs 🔗

@@ -124,7 +124,6 @@ impl ProjectDiff {
             return;
         }
         let workspace = cx.entity();
-        let workspace_weak = workspace.downgrade();
         window
             .spawn(cx, async move |cx| {
                 let this = cx
@@ -139,7 +138,7 @@ impl ProjectDiff {
                     .ok();
                 anyhow::Ok(())
             })
-            .detach_and_notify_err(workspace_weak, window, cx);
+            .detach_and_notify_err(window, cx);
     }
 
     pub fn deploy_at(

crates/git_ui/src/worktree_picker.rs 🔗

@@ -4,8 +4,8 @@ use fuzzy::StringMatchCandidate;
 
 use git::repository::Worktree as GitWorktree;
 use gpui::{
-    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
-    Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
+    Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
     PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window,
     actions, rems,
 };
@@ -20,7 +20,7 @@ use remote_connection::{RemoteConnectionModal, connect};
 use std::{path::PathBuf, sync::Arc};
 use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
 use util::ResultExt;
-use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
+use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
 
 actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]);
 
@@ -289,6 +289,7 @@ impl WorktreeListDelegate {
         };
 
         let branch = worktree_branch.to_string();
+        let window_handle = window.window_handle();
         let workspace = self.workspace.clone();
         cx.spawn_in(window, async move |_, cx| {
             let Some(paths) = worktree_path.await? else {
@@ -354,7 +355,7 @@ impl WorktreeListDelegate {
                     connection_options,
                     vec![new_worktree_path],
                     app_state,
-                    workspace.clone(),
+                    window_handle,
                     replace_current_window,
                     cx,
                 )
@@ -406,12 +407,13 @@ impl WorktreeListDelegate {
                 |e, _, _| Some(e.to_string()),
             );
         } else if let Some(connection_options) = connection_options {
+            let window_handle = window.window_handle();
             cx.spawn_in(window, async move |_, cx| {
                 open_remote_worktree(
                     connection_options,
                     vec![path],
                     app_state,
-                    workspace,
+                    window_handle,
                     replace_current_window,
                     cx,
                 )
@@ -439,16 +441,15 @@ async fn open_remote_worktree(
     connection_options: RemoteConnectionOptions,
     paths: Vec<PathBuf>,
     app_state: Arc<workspace::AppState>,
-    workspace: WeakEntity<Workspace>,
+    window: gpui::AnyWindowHandle,
     replace_current_window: bool,
-    cx: &mut AsyncWindowContext,
+    cx: &mut AsyncApp,
 ) -> anyhow::Result<()> {
-    let workspace_window = cx
-        .window_handle()
-        .downcast::<MultiWorkspace>()
+    let workspace_window = window
+        .downcast::<Workspace>()
         .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
 
-    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
+    let connect_task = workspace_window.update(cx, |workspace, window, cx| {
         workspace.toggle_modal(window, cx, |window, cx| {
             RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
         });
@@ -472,19 +473,17 @@ async fn open_remote_worktree(
 
     let session = connect_task.await;
 
-    workspace
-        .update_in(cx, |workspace, _window, cx| {
-            if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
-                prompt.update(cx, |prompt, cx| prompt.finished(cx))
-            }
-        })
-        .ok();
+    workspace_window.update(cx, |workspace, _window, cx| {
+        if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
+            prompt.update(cx, |prompt, cx| prompt.finished(cx))
+        }
+    })?;
 
     let Some(Some(session)) = session else {
         return Ok(());
     };
 
-    let new_project: Entity<project::Project> = cx.update(|_, cx| {
+    let new_project: Entity<project::Project> = cx.update(|cx| {
         project::Project::remote(
             session,
             app_state.client.clone(),
@@ -495,30 +494,29 @@ async fn open_remote_worktree(
             true,
             cx,
         )
-    })?;
+    });
 
     let window_to_use = if replace_current_window {
         workspace_window
     } else {
         let workspace_position = cx
-            .update(|_, cx| {
+            .update(|cx| {
                 workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
-            })?
+            })
             .await
             .context("fetching workspace position from db")?;
 
         let mut options =
-            cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
+            cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
         options.window_bounds = workspace_position.window_bounds;
 
         cx.open_window(options, |window, cx| {
-            let workspace = cx.new(|cx| {
+            cx.new(|cx| {
                 let mut workspace =
                     Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
                 workspace.centered_layout = workspace_position.centered_layout;
                 workspace
-            });
-            cx.new(|cx| MultiWorkspace::new(workspace, cx))
+            })
         })?
     };
 

crates/icons/src/icons.rs 🔗

@@ -265,8 +265,6 @@ pub enum IconName {
     UserRoundPen,
     Warning,
     WholeWord,
-    WorkspaceNavClosed,
-    WorkspaceNavOpen,
     XCircle,
     XCircleFilled,
     ZedAgent,

crates/inspector_ui/Cargo.toml 🔗

@@ -18,6 +18,7 @@ editor.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 language.workspace = true
+platform_title_bar.workspace = true
 project.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true

crates/inspector_ui/src/inspector.rs 🔗

@@ -1,7 +1,8 @@
 use anyhow::{Context as _, anyhow};
 use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
+use platform_title_bar::PlatformTitleBar;
 use std::{cell::OnceCell, path::Path, sync::Arc};
-use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height};
+use ui::{Label, Tooltip, prelude::*};
 use util::{ResultExt as _, command::new_smol_command};
 use workspace::AppState;
 
@@ -60,7 +61,7 @@ fn render_inspector(
     let ui_font = theme::setup_ui_font(window, cx);
     let colors = cx.theme().colors();
     let inspector_id = inspector.active_element_id();
-    let toolbar_height = platform_title_bar_height(window);
+    let toolbar_height = PlatformTitleBar::height(window);
 
     v_flex()
         .size_full()

crates/journal/src/journal.rs 🔗

@@ -118,20 +118,17 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
                     })?
                     .await?;
                 new_workspace
-                    .update(cx, |multi_workspace, window, cx| {
-                        let workspace = multi_workspace.workspace().clone();
-                        workspace.update(cx, |workspace, cx| {
-                            workspace.open_paths(
-                                vec![entry_path],
-                                workspace::OpenOptions {
-                                    visible: Some(OpenVisible::All),
-                                    ..Default::default()
-                                },
-                                None,
-                                window,
-                                cx,
-                            )
-                        })
+                    .update(cx, |workspace, window, cx| {
+                        workspace.open_paths(
+                            vec![entry_path],
+                            workspace::OpenOptions {
+                                visible: Some(OpenVisible::All),
+                                ..Default::default()
+                            },
+                            None,
+                            window,
+                            cx,
+                        )
                     })?
                     .await
             } else {

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -1319,7 +1319,7 @@ impl KeymapEditor {
         cx.spawn(async move |_, _| {
             remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
         })
-        .detach_and_notify_err(self.workspace.clone(), window, cx);
+        .detach_and_notify_err(window, cx);
     }
 
     fn copy_context_to_clipboard(

crates/miniprofiler_ui/src/miniprofiler_ui.rs 🔗

@@ -8,7 +8,7 @@ use std::{
 use gpui::{
     App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
     ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement,
-    Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
+    Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle,
     WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
 };
 use util::ResultExt;
@@ -22,10 +22,13 @@ use workspace::{
 use zed_actions::OpenPerformanceProfiler;
 
 pub fn init(startup_time: Instant, cx: &mut App) {
-    cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
-        let workspace_handle = cx.entity().downgrade();
-        workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
-            open_performance_profiler(startup_time, workspace_handle.clone(), window, cx);
+    cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| {
+        workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| {
+            let window_handle = window
+                .window_handle()
+                .downcast::<Workspace>()
+                .expect("Workspaces are root Windows");
+            open_performance_profiler(startup_time, workspace, window_handle, cx);
         });
     })
     .detach();
@@ -33,8 +36,8 @@ pub fn init(startup_time: Instant, cx: &mut App) {
 
 fn open_performance_profiler(
     startup_time: Instant,
-    workspace_handle: WeakEntity<Workspace>,
-    _window: &mut gpui::Window,
+    _workspace: &mut workspace::Workspace,
+    workspace_handle: WindowHandle<Workspace>,
     cx: &mut App,
 ) {
     let existing_window = cx
@@ -45,7 +48,7 @@ fn open_performance_profiler(
     if let Some(existing_window) = existing_window {
         existing_window
             .update(cx, |profiler_window, window, _cx| {
-                profiler_window.workspace = Some(workspace_handle.clone());
+                profiler_window.workspace = Some(workspace_handle);
                 window.activate_window();
             })
             .log_err();
@@ -94,14 +97,14 @@ pub struct ProfilerWindow {
     include_self_timings: ToggleState,
     autoscroll: bool,
     scroll_handle: UniformListScrollHandle,
-    workspace: Option<WeakEntity<Workspace>>,
+    workspace: Option<WindowHandle<Workspace>>,
     _refresh: Option<Task<()>>,
 }
 
 impl ProfilerWindow {
     pub fn new(
         startup_time: Instant,
-        workspace_handle: Option<WeakEntity<Workspace>>,
+        workspace_handle: Option<WindowHandle<Workspace>>,
         cx: &mut App,
     ) -> Entity<Self> {
         let entity = cx.new(|cx| ProfilerWindow {
@@ -277,7 +280,7 @@ impl Render for ProfilerWindow {
                                 Button::new("export-data", "Save")
                                     .style(ButtonStyle::Filled)
                                     .on_click(cx.listener(|this, _, _window, cx| {
-                                        let Some(workspace) = this.workspace.as_ref() else {
+                                        let Some(workspace) = this.workspace else {
                                             return;
                                         };
 
@@ -294,7 +297,7 @@ impl Render for ProfilerWindow {
                                             .log_err()
                                             .flatten()
                                             .and_then(|p| p.parent().map(|p| p.to_owned()))
-                                            .unwrap_or_else(PathBuf::default);
+                                            .unwrap_or_else(|| PathBuf::default());
 
                                         let path = cx.prompt_for_new_path(
                                             &active_path,

crates/onboarding/src/onboarding.rs 🔗

@@ -238,16 +238,15 @@ impl Onboarding {
         go_to_welcome_page(cx);
     }
 
-    fn handle_sign_in(&mut self, _: &SignIn, window: &mut Window, cx: &mut Context<Self>) {
+    fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) {
         let client = Client::global(cx);
-        let workspace = self.workspace.clone();
 
         window
-            .spawn(cx, async move |mut cx| {
+            .spawn(cx, async move |cx| {
                 client
-                    .sign_in_with_optional_connect(true, &cx)
+                    .sign_in_with_optional_connect(true, cx)
                     .await
-                    .notify_workspace_async_err(workspace, &mut cx);
+                    .notify_async_err(cx);
             })
             .detach();
     }
@@ -275,7 +274,7 @@ impl Render for Onboarding {
             .size_full()
             .bg(cx.theme().colors().editor_background)
             .on_action(Self::on_finish)
-            .on_action(cx.listener(Self::handle_sign_in))
+            .on_action(Self::handle_sign_in)
             .on_action(Self::handle_open_account)
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
                 window.focus_next(cx);

crates/outline/src/outline.rs 🔗

@@ -41,15 +41,15 @@ pub fn toggle(
     window: &mut Window,
     cx: &mut App,
 ) {
-    if let Some((workspace, outline)) = Workspace::for_window(window, cx).and_then(|workspace| {
-        editor
-            .read(cx)
-            .buffer()
-            .read(cx)
-            .snapshot(cx)
-            .outline(Some(cx.theme().syntax()))
-            .map(|outline| (workspace, outline))
-    }) {
+    let outline = editor
+        .read(cx)
+        .buffer()
+        .read(cx)
+        .snapshot(cx)
+        .outline(Some(cx.theme().syntax()));
+
+    let workspace = window.root::<Workspace>().flatten();
+    if let Some((workspace, outline)) = workspace.zip(outline) {
         workspace.update(cx, |workspace, cx| {
             workspace.toggle_modal(window, cx, |window, cx| {
                 OutlineView::new(outline, editor, window, cx)
@@ -396,7 +396,7 @@ mod tests {
     use project::{FakeFs, Project};
     use serde_json::json;
     use util::{path, rel_path::rel_path};
-    use workspace::{AppState, MultiWorkspace, Workspace};
+    use workspace::{AppState, Workspace};
 
     #[gpui::test]
     async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
@@ -424,9 +424,7 @@ mod tests {
         });
 
         let (workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-
-        let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().update(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()

crates/platform_title_bar/Cargo.toml 🔗

@@ -13,7 +13,6 @@ path = "src/platform_title_bar.rs"
 doctest = false
 
 [dependencies]
-feature_flags.workspace = true
 gpui.workspace = true
 settings.workspace = true
 smallvec.workspace = true

crates/platform_title_bar/src/platform_title_bar.rs 🔗

@@ -1,21 +1,16 @@
 mod platforms;
 mod system_window_tabs;
 
-use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
 use gpui::{
-    AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
-    MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div,
-    px,
+    AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton,
+    ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
 };
 use smallvec::SmallVec;
 use std::mem;
-use ui::{
-    prelude::*,
-    utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
-};
+use ui::prelude::*;
 
 use crate::{
-    platforms::{platform_linux, platform_windows},
+    platforms::{platform_linux, platform_mac, platform_windows},
     system_window_tabs::SystemWindowTabs,
 };
 
@@ -29,8 +24,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 {
@@ -44,11 +37,20 @@ impl PlatformTitleBar {
             children: SmallVec::new(),
             should_move: false,
             system_window_tabs,
-            workspace_sidebar_open: false,
-            sidebar_has_notifications: false,
         }
     }
 
+    #[cfg(not(target_os = "windows"))]
+    pub fn height(window: &mut Window) -> Pixels {
+        (1.75 * window.rem_size()).max(px(34.))
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn height(_window: &mut Window) -> Pixels {
+        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
+        px(32.)
+    }
+
     pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if window.is_window_active() && !self.should_move {
@@ -71,46 +73,17 @@ impl PlatformTitleBar {
     pub fn init(cx: &mut App) {
         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>()
-    }
 }
 
 impl Render for PlatformTitleBar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let supported_controls = window.window_controls();
         let decorations = window.window_decorations();
-        let height = platform_title_bar_height(window);
+        let height = Self::height(window);
         let titlebar_color = self.title_bar_color(window, cx);
         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()
@@ -159,10 +132,8 @@ impl Render for PlatformTitleBar {
             .map(|this| {
                 if window.is_fullscreen() {
                     this.pl_2()
-                } else if self.platform_style == PlatformStyle::Mac
-                    && !is_multiworkspace_sidebar_open
-                {
-                    this.pl(px(TRAFFIC_LIGHT_PADDING))
+                } else if self.platform_style == PlatformStyle::Mac {
+                    this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
                 } else {
                     this.pl_2()
                 }

crates/platform_title_bar/src/platforms/platform_mac.rs 🔗

@@ -0,0 +1,10 @@
+// Use pixels here instead of a rem-based size because the macOS traffic
+// lights are a static size, and don't scale with the rest of the UI.
+//
+// Magic number: There is one extra pixel of padding on the left side due to
+// the 1px border around the window on macOS apps.
+#[cfg(macos_sdk_26)]
+pub const TRAFFIC_LIGHT_PADDING: f32 = 78.;
+
+#[cfg(not(macos_sdk_26))]
+pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;

crates/project_panel/src/project_panel.rs 🔗

@@ -772,11 +772,7 @@ impl ProjectPanel {
                         {
                             match project_panel.confirm_edit(false, window, cx) {
                                 Some(task) => {
-                                    task.detach_and_notify_err(
-                                        project_panel.workspace.clone(),
-                                        window,
-                                        cx,
-                                    );
+                                    task.detach_and_notify_err(window, cx);
                                 }
                                 None => {
                                     project_panel.discard_edit_state(window, cx);
@@ -1652,7 +1648,7 @@ impl ProjectPanel {
 
     fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(task) = self.confirm_edit(true, window, cx) {
-            task.detach_and_notify_err(self.workspace.clone(), window, cx);
+            task.detach_and_notify_err(window, cx);
         }
     }
 
@@ -3037,25 +3033,20 @@ impl ProjectPanel {
             }
 
             let item_count = paste_tasks.len();
-            let workspace = self.workspace.clone();
 
-            cx.spawn_in(window, async move |project_panel, mut cx| {
+            cx.spawn_in(window, async move |project_panel, cx| {
                 let mut last_succeed = None;
                 for task in paste_tasks {
                     match task {
                         PasteTask::Rename(task) => {
-                            if let Some(CreatedEntry::Included(entry)) = task
-                                .await
-                                .notify_workspace_async_err(workspace.clone(), &mut cx)
+                            if let Some(CreatedEntry::Included(entry)) =
+                                task.await.notify_async_err(cx)
                             {
                                 last_succeed = Some(entry);
                             }
                         }
                         PasteTask::Copy(task) => {
-                            if let Some(Some(entry)) = task
-                                .await
-                                .notify_workspace_async_err(workspace.clone(), &mut cx)
-                            {
+                            if let Some(Some(entry)) = task.await.notify_async_err(cx) {
                                 last_succeed = Some(entry);
                             }
                         }
@@ -3397,7 +3388,7 @@ impl ProjectPanel {
         if let Some((file_path1, file_path2)) = selected_files {
             self.workspace
                 .update(cx, |workspace, cx| {
-                    FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
+                    FileDiffView::open(file_path1, file_path2, workspace, window, cx)
                         .detach_and_log_err(cx);
                 })
                 .ok();

crates/recent_projects/Cargo.toml 🔗

@@ -23,7 +23,6 @@ db.workspace = true
 dev_container.workspace = true
 editor.workspace = true
 extension_host.workspace = true
-fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
@@ -67,7 +66,6 @@ language = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
 release_channel.workspace = true
 remote = { workspace = true, features = ["test-support"] }
-remote_connection = { workspace = true, features = ["test-support"] }
 remote_server.workspace = true
 serde_json.workspace = true
 settings = { workspace = true, features = ["test-support"] }

crates/recent_projects/src/disconnected_overlay.rs 🔗

@@ -7,9 +7,7 @@ use ui::{
     HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
     ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems,
 };
-use workspace::{
-    ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
-};
+use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr};
 
 use crate::open_remote_project;
 
@@ -111,7 +109,7 @@ impl DisconnectedOverlay {
             return;
         };
 
-        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
+        let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
             return;
         };
 

crates/recent_projects/src/recent_projects.rs 🔗

@@ -4,9 +4,7 @@ mod remote_connections;
 mod remote_servers;
 mod ssh_config;
 
-use std::{path::PathBuf, sync::Arc};
-
-use fs::Fs;
+use std::path::PathBuf;
 
 #[cfg(target_os = "windows")]
 mod wsl_picker;
@@ -29,11 +27,11 @@ use picker::{
 pub use remote_connections::RemoteSettings;
 pub use remote_servers::RemoteServerProjects;
 use settings::Settings;
-use std::path::Path;
+use std::{path::Path, sync::Arc};
 use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
 use util::{ResultExt, paths::PathExt};
 use workspace::{
-    HistoryManager, ModalView, MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation,
+    CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation,
     WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr,
     with_active_or_new_workspace,
 };
@@ -50,10 +48,9 @@ pub struct RecentProjectEntry {
 pub async fn get_recent_projects(
     current_workspace_id: Option<WorkspaceId>,
     limit: Option<usize>,
-    fs: Arc<dyn fs::Fs>,
 ) -> Vec<RecentProjectEntry> {
     let workspaces = WORKSPACE_DB
-        .recent_workspaces_on_disk(fs.as_ref())
+        .recent_workspaces_on_disk()
         .await
         .unwrap_or_default();
 
@@ -179,7 +176,7 @@ pub fn init(cx: &mut App) {
             let fs = workspace.project().read(cx).fs().clone();
             add_wsl_distro(fs, &open_wsl.distro, cx);
             let open_options = OpenOptions {
-                replace_window: window.window_handle().downcast::<MultiWorkspace>(),
+                replace_window: window.window_handle().downcast::<Workspace>(),
                 ..Default::default()
             };
 
@@ -235,8 +232,10 @@ pub fn init(cx: &mut App) {
 
     cx.on_action(|_: &OpenDevContainer, cx| {
         with_active_or_new_workspace(cx, move |workspace, window, cx| {
-            if !workspace.project().read(cx).is_local() {
-                cx.spawn_in(window, async move |_, cx| {
+            let is_local = workspace.project().read(cx).is_local();
+
+            cx.spawn_in(window, async move |_, cx| {
+                if !is_local {
                     cx.prompt(
                         gpui::PromptLevel::Critical,
                         "Cannot open Dev Container from remote project",
@@ -245,16 +244,21 @@ pub fn init(cx: &mut App) {
                     )
                     .await
                     .ok();
-                })
-                .detach();
-                return;
-            }
+                    return;
+                }
 
-            let fs = workspace.project().read(cx).fs().clone();
-            let handle = cx.entity().downgrade();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                RemoteServerProjects::new_dev_container(fs, window, handle, cx)
-            });
+                cx.update(|_, cx| {
+                    with_active_or_new_workspace(cx, move |workspace, window, cx| {
+                        let fs = workspace.project().read(cx).fs().clone();
+                        let handle = cx.entity().downgrade();
+                        workspace.toggle_modal(window, cx, |window, cx| {
+                            RemoteServerProjects::new_dev_container(fs, window, handle, cx)
+                        });
+                    });
+                })
+                .log_err();
+            })
+            .detach();
         });
     });
 
@@ -330,7 +334,6 @@ impl ModalView for RecentProjects {}
 impl RecentProjects {
     fn new(
         delegate: RecentProjectsDelegate,
-        fs: Option<Arc<dyn Fs>>,
         rem_width: f32,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -347,9 +350,8 @@ impl RecentProjects {
         // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap
         // out workspace locations once the future runs to completion.
         cx.spawn_in(window, async move |this, cx| {
-            let Some(fs) = fs else { return };
             let workspaces = WORKSPACE_DB
-                .recent_workspaces_on_disk(fs.as_ref())
+                .recent_workspaces_on_disk()
                 .await
                 .log_err()
                 .unwrap_or_default();
@@ -359,7 +361,7 @@ impl RecentProjects {
                     picker.update_matches(picker.query(cx), window, cx)
                 })
             })
-            .ok();
+            .ok()
         })
         .detach();
         Self {
@@ -377,11 +379,10 @@ impl RecentProjects {
         cx: &mut Context<Workspace>,
     ) {
         let weak = cx.entity().downgrade();
-        let fs = Some(workspace.app_state().fs.clone());
         workspace.toggle_modal(window, cx, |window, cx| {
             let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle);
 
-            Self::new(delegate, fs, 34., window, cx)
+            Self::new(delegate, 34., window, cx)
         })
     }
 
@@ -392,13 +393,10 @@ impl RecentProjects {
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
-        let fs = workspace
-            .upgrade()
-            .map(|ws| ws.read(cx).app_state().fs.clone());
         cx.new(|cx| {
             let delegate =
                 RecentProjectsDelegate::new(workspace, create_new_window, true, focus_handle);
-            let list = Self::new(delegate, fs, 34., window, cx);
+            let list = Self::new(delegate, 34., window, cx);
             list.picker.focus_handle(cx).focus(window, cx);
             list
         })
@@ -582,21 +580,27 @@ impl PickerDelegate for RecentProjectsDelegate {
                     SerializedWorkspaceLocation::Local => {
                         let paths = candidate_workspace_paths.paths().to_vec();
                         if replace_current_window {
-                            if let Some(handle) =
-                                window.window_handle().downcast::<MultiWorkspace>()
-                            {
-                                cx.defer(move |cx| {
-                                    if let Some(task) = handle
-                                        .update(cx, |multi_workspace, window, cx| {
-                                            multi_workspace.open_project(paths, window, cx)
-                                        })
-                                        .log_err()
-                                    {
-                                        task.detach_and_log_err(cx);
-                                    }
-                                });
-                            }
-                            return;
+                            cx.spawn_in(window, async move |workspace, cx| {
+                                let continue_replacing = workspace
+                                    .update_in(cx, |workspace, window, cx| {
+                                        workspace.prepare_to_close(
+                                            CloseIntent::ReplaceWindow,
+                                            window,
+                                            cx,
+                                        )
+                                    })?
+                                    .await?;
+                                if continue_replacing {
+                                    workspace
+                                        .update_in(cx, |workspace, window, cx| {
+                                            workspace
+                                                .open_workspace_for_paths(true, paths, window, cx)
+                                        })?
+                                        .await
+                                } else {
+                                    Ok(())
+                                }
+                            })
                         } else {
                             workspace.open_workspace_for_paths(false, paths, window, cx)
                         }
@@ -605,7 +609,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                         let app_state = workspace.app_state().clone();
 
                         let replace_window = if replace_current_window {
-                            window.window_handle().downcast::<MultiWorkspace>()
+                            window.window_handle().downcast::<Workspace>()
                         } else {
                             None
                         };
@@ -880,18 +884,10 @@ impl RecentProjectsDelegate {
     ) {
         if let Some(selected_match) = self.matches.get(ix) {
             let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id];
-            let fs = self
-                .workspace
-                .upgrade()
-                .map(|ws| ws.read(cx).app_state().fs.clone());
             cx.spawn_in(window, async move |this, cx| {
-                WORKSPACE_DB
-                    .delete_workspace_by_id(workspace_id)
-                    .await
-                    .log_err();
-                let Some(fs) = fs else { return };
+                let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await;
                 let workspaces = WORKSPACE_DB
-                    .recent_workspaces_on_disk(fs.as_ref())
+                    .recent_workspaces_on_disk()
                     .await
                     .unwrap_or_default();
                 this.update_in(cx, move |picker, window, cx| {
@@ -908,7 +904,6 @@ impl RecentProjectsDelegate {
                             .update(cx, |this, cx| this.delete_history(workspace_id, cx));
                     }
                 })
-                .ok();
             })
             .detach();
         }
@@ -956,7 +951,7 @@ mod tests {
     use super::*;
 
     #[gpui::test]
-    async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) {
+    async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
 
         cx.update(|cx| {
@@ -980,11 +975,6 @@ mod tests {
                 }),
             )
             .await;
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(path!("/test/path"), json!({}))
-            .await;
         cx.update(|cx| {
             open_paths(
                 &[PathBuf::from(path!("/dir/main.ts"))],
@@ -997,40 +987,31 @@ mod tests {
         .unwrap();
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
 
-        let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                assert!(!multi_workspace.workspace().read(cx).is_edited())
-            })
+        let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+        workspace
+            .update(cx, |workspace, _, _| assert!(!workspace.is_edited()))
             .unwrap();
 
-        let editor = multi_workspace
-            .read_with(cx, |multi_workspace, cx| {
-                multi_workspace
-                    .workspace()
-                    .read(cx)
+        let editor = workspace
+            .read_with(cx, |workspace, cx| {
+                workspace
                     .active_item(cx)
                     .unwrap()
                     .downcast::<Editor>()
                     .unwrap()
             })
             .unwrap();
-        multi_workspace
+        workspace
             .update(cx, |_, window, cx| {
                 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
             })
             .unwrap();
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                assert!(
-                    multi_workspace.workspace().read(cx).is_edited(),
-                    "After inserting more text into the editor without saving, we should have a dirty project"
-                )
-            })
+        workspace
+            .update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project"))
             .unwrap();
 
-        let recent_projects_picker = open_recent_projects(&multi_workspace, cx);
-        multi_workspace
+        let recent_projects_picker = open_recent_projects(&workspace, cx);
+        workspace
             .update(cx, |_, _, cx| {
                 recent_projects_picker.update(cx, |picker, cx| {
                     assert_eq!(picker.query(cx), "");
@@ -1054,64 +1035,47 @@ mod tests {
             !cx.has_pending_prompt(),
             "Should have no pending prompt on dirty project before opening the new recent project"
         );
-        let dirty_workspace = multi_workspace
-            .read_with(cx, |multi_workspace, _cx| {
-                multi_workspace.workspace().clone()
-            })
-            .unwrap();
-
-        cx.dispatch_action(*multi_workspace, menu::Confirm);
-        cx.run_until_parked();
-
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
+        cx.dispatch_action(*workspace, menu::Confirm);
+        workspace
+            .update(cx, |workspace, _, cx| {
                 assert!(
-                    multi_workspace
-                        .workspace()
-                        .read(cx)
-                        .active_modal::<RecentProjects>(cx)
-                        .is_none(),
+                    workspace.active_modal::<RecentProjects>(cx).is_none(),
                     "Should remove the modal after selecting new recent project"
-                );
-
-                assert!(
-                    multi_workspace.workspaces().len() >= 2,
-                    "Should have at least 2 workspaces: the dirty one and the newly opened one"
-                );
-
-                assert!(
-                    multi_workspace.workspaces().contains(&dirty_workspace),
-                    "The original dirty workspace should still be present"
-                );
-
-                assert!(
-                    dirty_workspace.read(cx).is_edited(),
-                    "The original workspace should still be dirty"
-                );
+                )
             })
             .unwrap();
-
+        assert!(
+            cx.has_pending_prompt(),
+            "Dirty workspace should prompt before opening the new recent project"
+        );
+        cx.simulate_prompt_answer("Cancel");
         assert!(
             !cx.has_pending_prompt(),
-            "No save prompt in multi-workspace mode — dirty workspace survives in background"
+            "Should have no pending prompt after cancelling"
         );
+        workspace
+            .update(cx, |workspace, _, _| {
+                assert!(
+                    workspace.is_edited(),
+                    "Should be in the same dirty project after cancelling"
+                )
+            })
+            .unwrap();
     }
 
     fn open_recent_projects(
-        multi_workspace: &WindowHandle<MultiWorkspace>,
+        workspace: &WindowHandle<Workspace>,
         cx: &mut TestAppContext,
     ) -> Entity<Picker<RecentProjectsDelegate>> {
         cx.dispatch_action(
-            (*multi_workspace).into(),
+            (*workspace).into(),
             OpenRecent {
                 create_new_window: false,
             },
         );
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace
-                    .workspace()
-                    .read(cx)
+        workspace
+            .update(cx, |workspace, _, cx| {
+                workspace
                     .active_modal::<RecentProjects>(cx)
                     .unwrap()
                     .read(cx)

crates/recent_projects/src/remote_connections.rs 🔗

@@ -19,7 +19,7 @@ use remote::{
 pub use settings::SshConnection;
 use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
 use util::paths::PathWithPosition;
-use workspace::{AppState, MultiWorkspace, Workspace};
+use workspace::{AppState, Workspace};
 
 pub use remote_connection::{
     RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
@@ -131,11 +131,8 @@ pub async fn open_remote_project(
     cx: &mut AsyncApp,
 ) -> Result<()> {
     let created_new_window = open_options.replace_window.is_none();
-    let (window, initial_workspace) = if let Some(window) = open_options.replace_window {
-        let workspace = window.update(cx, |multi_workspace, _, _| {
-            multi_workspace.workspace().clone()
-        })?;
-        (window, workspace)
+    let window = if let Some(window) = open_options.replace_window {
+        window
     } else {
         let workspace_position = cx
             .update(|cx| {
@@ -148,7 +145,7 @@ pub async fn open_remote_project(
             cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
         options.window_bounds = workspace_position.window_bounds;
 
-        let window = cx.open_window(options, |window, cx| {
+        cx.open_window(options, |window, cx| {
             let project = project::Project::local(
                 app_state.client.clone(),
                 app_state.node_runtime.clone(),
@@ -162,17 +159,12 @@ pub async fn open_remote_project(
                 },
                 cx,
             );
-            let workspace = cx.new(|cx| {
+            cx.new(|cx| {
                 let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
                 workspace.centered_layout = workspace_position.centered_layout;
                 workspace
-            });
-            cx.new(|cx| MultiWorkspace::new(workspace, cx))
-        })?;
-        let workspace = window.update(cx, |multi_workspace, _, _cx| {
-            multi_workspace.workspace().clone()
-        })?;
-        (window, workspace)
+            })
+        })?
     };
 
     loop {
@@ -180,38 +172,35 @@ pub async fn open_remote_project(
         let delegate = window.update(cx, {
             let paths = paths.clone();
             let connection_options = connection_options.clone();
-            let initial_workspace = initial_workspace.clone();
-            move |_multi_workspace: &mut MultiWorkspace, window, cx| {
+            move |workspace, window, cx| {
                 window.activate_window();
-                initial_workspace.update(cx, |workspace, cx| {
-                    workspace.hide_modal(window, cx);
-                    workspace.toggle_modal(window, cx, |window, cx| {
-                        RemoteConnectionModal::new(&connection_options, paths, window, cx)
-                    });
-
-                    let ui = workspace
-                        .active_modal::<RemoteConnectionModal>(cx)?
-                        .read(cx)
-                        .prompt
-                        .clone();
-
-                    ui.update(cx, |ui, _cx| {
-                        ui.set_cancellation_tx(cancel_tx);
-                    });
-
-                    Some(Arc::new(RemoteClientDelegate::new(
-                        window.window_handle(),
-                        ui.downgrade(),
-                        if let RemoteConnectionOptions::Ssh(options) = &connection_options {
-                            options
-                                .password
-                                .as_deref()
-                                .and_then(|pw| EncryptedPassword::try_from(pw).ok())
-                        } else {
-                            None
-                        },
-                    )))
-                })
+                workspace.hide_modal(window, cx);
+                workspace.toggle_modal(window, cx, |window, cx| {
+                    RemoteConnectionModal::new(&connection_options, paths, window, cx)
+                });
+
+                let ui = workspace
+                    .active_modal::<RemoteConnectionModal>(cx)?
+                    .read(cx)
+                    .prompt
+                    .clone();
+
+                ui.update(cx, |ui, _cx| {
+                    ui.set_cancellation_tx(cancel_tx);
+                });
+
+                Some(Arc::new(RemoteClientDelegate::new(
+                    window.window_handle(),
+                    ui.downgrade(),
+                    if let RemoteConnectionOptions::Ssh(options) = &connection_options {
+                        options
+                            .password
+                            .as_deref()
+                            .and_then(|pw| EncryptedPassword::try_from(pw).ok())
+                    } else {
+                        None
+                    },
+                )))
             }
         })?;
 
@@ -220,11 +209,13 @@ pub async fn open_remote_project(
         let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);
         let connection = select! {
             _ = cancel_rx => {
-                initial_workspace.update(cx, |workspace, cx| {
-                    if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
-                        ui.update(cx, |modal, cx| modal.finished(cx))
-                    }
-                });
+                window
+                    .update(cx, |workspace, _, cx| {
+                        if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
+                            ui.update(cx, |modal, cx| modal.finished(cx))
+                        }
+                    })
+                    .ok();
 
                 break;
             },
@@ -233,11 +224,13 @@ pub async fn open_remote_project(
         let remote_connection = match connection {
             Ok(connection) => connection,
             Err(e) => {
-                initial_workspace.update(cx, |workspace, cx| {
-                    if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
-                        ui.update(cx, |modal, cx| modal.finished(cx))
-                    }
-                });
+                window
+                    .update(cx, |workspace, _, cx| {
+                        if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
+                            ui.update(cx, |modal, cx| modal.finished(cx))
+                        }
+                    })
+                    .ok();
                 log::error!("Failed to open project: {e:#}");
                 let response = window
                     .update(cx, |_, window, cx| {
@@ -291,11 +284,13 @@ pub async fn open_remote_project(
             })
             .await;
 
-        initial_workspace.update(cx, |workspace, cx| {
-            if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
-                ui.update(cx, |modal, cx| modal.finished(cx))
-            }
-        });
+        window
+            .update(cx, |workspace, _, cx| {
+                if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
+                    ui.update(cx, |modal, cx| modal.finished(cx))
+                }
+            })
+            .ok();
 
         match opened_items {
             Err(e) => {
@@ -325,20 +320,20 @@ pub async fn open_remote_project(
                     continue;
                 }
 
-                if created_new_window {
-                    window
-                        .update(cx, |_, window, _| window.remove_window())
-                        .ok();
-                }
-                initial_workspace.update(cx, |workspace, cx| {
-                    trusted_worktrees::track_worktree_trust(
-                        workspace.project().read(cx).worktree_store(),
-                        None,
-                        None,
-                        None,
-                        cx,
-                    );
-                });
+                window
+                    .update(cx, |workspace, window, cx| {
+                        if created_new_window {
+                            window.remove_window();
+                        }
+                        trusted_worktrees::track_worktree_trust(
+                            workspace.project().read(cx).worktree_store(),
+                            None,
+                            None,
+                            None,
+                            cx,
+                        );
+                    })
+                    .ok();
             }
 
             Ok(items) => {
@@ -371,20 +366,14 @@ pub async fn open_remote_project(
         break;
     }
 
-    // Register the remote client with extensions. We use `multi_workspace.workspace()` here
-    // (not `initial_workspace`) because `open_remote_project_inner` activated the new remote
-    // workspace, so the active workspace is now the one with the remote project.
     window
-        .update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {
-            let workspace = multi_workspace.workspace().clone();
-            workspace.update(cx, |workspace, cx| {
-                if let Some(client) = workspace.project().read(cx).remote_client() {
-                    if let Some(extension_store) = ExtensionStore::try_global(cx) {
-                        extension_store
-                            .update(cx, |store, cx| store.register_remote_client(client, cx));
-                    }
+        .update(cx, |workspace, _, cx| {
+            if let Some(client) = workspace.project().read(cx).remote_client() {
+                if let Some(extension_store) = ExtensionStore::try_global(cx) {
+                    extension_store
+                        .update(cx, |store, cx| store.register_remote_client(client, cx));
                 }
-            });
+            }
         })
         .ok();
     Ok(())
@@ -511,16 +500,12 @@ mod tests {
         let windows = cx.update(|cx| cx.windows().len());
         assert_eq!(windows, 1, "Should have opened a window");
 
-        let multi_workspace_handle =
-            cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+        let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
 
-        multi_workspace_handle
-            .update(cx, |multi_workspace, _, cx| {
-                let workspace = multi_workspace.workspace().clone();
-                workspace.update(cx, |workspace, cx| {
-                    let project = workspace.project().read(cx);
-                    assert!(project.is_remote(), "Project should be a remote project");
-                });
+        workspace_handle
+            .update(cx, |workspace, _, cx| {
+                let project = workspace.project().read(cx);
+                assert!(project.is_remote(), "Project should be a remote project");
             })
             .unwrap();
     }

crates/recent_projects/src/remote_servers.rs 🔗

@@ -6,8 +6,7 @@ use crate::{
     ssh_config::parse_ssh_config_hosts,
 };
 use dev_container::{
-    DevContainerConfig, DevContainerContext, find_devcontainer_configs,
-    start_dev_container_with_config,
+    DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
 };
 use editor::Editor;
 
@@ -52,7 +51,7 @@ use util::{
     rel_path::RelPath,
 };
 use workspace::{
-    ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
+    ModalView, OpenLog, OpenOptions, Toast, Workspace,
     notifications::{DetachAndPromptErr, NotificationId},
     open_remote_project_with_existing_connection,
 };
@@ -479,11 +478,10 @@ impl ProjectPicker {
                         .log_err()?;
                     let window = cx
                         .open_window(options, |window, cx| {
-                            let workspace = cx.new(|cx| {
+                            cx.new(|cx| {
                                 telemetry::event!("SSH Project Created");
                                 Workspace::new(None, project.clone(), app_state.clone(), window, cx)
-                            });
-                            cx.new(|cx| MultiWorkspace::new(workspace, cx))
+                            })
                         })
                         .log_err()?;
 
@@ -810,18 +808,11 @@ impl RemoteServerProjects {
         workspace: WeakEntity<Workspace>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let configs = workspace
-            .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
-            .unwrap_or_default();
-
-        let initial_mode = if configs.len() > 1 {
-            DevContainerCreationProgress::SelectingConfig
-        } else {
-            DevContainerCreationProgress::Creating
-        };
-
-        let mut this = Self::new_inner(
-            Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(initial_mode, cx)),
+        let this = Self::new_inner(
+            Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
+                DevContainerCreationProgress::Creating,
+                cx,
+            )),
             false,
             fs,
             window,
@@ -829,15 +820,35 @@ impl RemoteServerProjects {
             cx,
         );
 
-        if configs.len() > 1 {
-            let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
-            this.dev_container_picker =
-                Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
-        } else {
-            let config = configs.into_iter().next();
-            this.open_dev_container(config, window, cx);
-            this.view_in_progress_dev_container(window, cx);
-        }
+        // Spawn a task to scan for configs and then start the container
+        cx.spawn_in(window, async move |entity, cx| {
+            let configs = find_devcontainer_configs(cx);
+
+            entity
+                .update_in(cx, |this, window, cx| {
+                    if configs.len() > 1 {
+                        // Multiple configs found - show selection UI
+                        let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
+                        this.dev_container_picker = Some(
+                            cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)),
+                        );
+
+                        let state = CreateRemoteDevContainer::new(
+                            DevContainerCreationProgress::SelectingConfig,
+                            cx,
+                        );
+                        this.mode = Mode::CreateRemoteDevContainer(state);
+                        cx.notify();
+                    } else {
+                        // Single or no config - proceed with opening
+                        let config = configs.into_iter().next();
+                        this.open_dev_container(config, window, cx);
+                        this.view_in_progress_dev_container(window, cx);
+                    }
+                })
+                .log_err();
+        })
+        .detach();
 
         this
     }
@@ -1540,9 +1551,7 @@ impl RemoteServerProjects {
 
                 let replace_window = match (create_new_window, secondary_confirm) {
                     (true, false) | (false, true) => None,
-                    (true, true) | (false, false) => {
-                        window.window_handle().downcast::<MultiWorkspace>()
-                    }
+                    (true, true) | (false, false) => window.window_handle().downcast::<Workspace>(),
                 };
 
                 cx.spawn_in(window, async move |_, cx| {
@@ -1794,25 +1803,25 @@ impl RemoteServerProjects {
     }
 
     fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let configs = self
-            .workspace
-            .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
-            .unwrap_or_default();
+        cx.spawn_in(window, async move |entity, cx| {
+            let configs = find_devcontainer_configs(cx);
 
-        if configs.len() > 1 {
-            let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
-            self.dev_container_picker =
-                Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
+            entity
+                .update_in(cx, |this, window, cx| {
+                    let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
+                    this.dev_container_picker =
+                        Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
 
-            let state =
-                CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx);
-            self.mode = Mode::CreateRemoteDevContainer(state);
-            cx.notify();
-        } else {
-            let config = configs.into_iter().next();
-            self.open_dev_container(config, window, cx);
-            self.view_in_progress_dev_container(window, cx);
-        }
+                    let state = CreateRemoteDevContainer::new(
+                        DevContainerCreationProgress::SelectingConfig,
+                        cx,
+                    );
+                    this.mode = Mode::CreateRemoteDevContainer(state);
+                    cx.notify();
+                })
+                .log_err();
+        })
+        .detach();
     }
 
     fn open_dev_container(
@@ -1821,25 +1830,21 @@ impl RemoteServerProjects {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some((app_state, context)) = self
+        let Some(app_state) = self
             .workspace
-            .read_with(cx, |workspace, cx| {
-                let app_state = workspace.app_state().clone();
-                let context = DevContainerContext::from_workspace(workspace, cx)?;
-                Some((app_state, context))
-            })
+            .read_with(cx, |workspace, _| workspace.app_state().clone())
             .log_err()
-            .flatten()
         else {
-            log::error!("No active project directory for Dev Container");
             return;
         };
 
-        let replace_window = window.window_handle().downcast::<MultiWorkspace>();
+        let replace_window = window.window_handle().downcast::<Workspace>();
 
         cx.spawn_in(window, async move |entity, cx| {
             let (connection, starting_dir) =
-                match start_dev_container_with_config(context, config).await {
+                match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config)
+                    .await
+                {
                     Ok((c, s)) => (Connection::DevContainer(c), s),
                     Err(e) => {
                         log::error!("Failed to start dev container: {:?}", e);

crates/recent_projects/src/wsl_picker.rs 🔗

@@ -8,7 +8,7 @@ use ui::{
     Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex,
 };
 use util::ResultExt as _;
-use workspace::{ModalView, MultiWorkspace};
+use workspace::{ModalView, Workspace};
 
 use crate::open_remote_project;
 
@@ -249,7 +249,7 @@ impl WslOpenModal {
             false => !secondary,
         };
         let replace_window = match replace_current_window {
-            true => window.window_handle().downcast::<MultiWorkspace>(),
+            true => window.window_handle().downcast::<Workspace>(),
             false => None,
         };
 

crates/repl/src/repl_sessions_ui.rs 🔗

@@ -78,8 +78,9 @@ pub fn init(cx: &mut App) {
                 return;
             }
 
-            cx.defer_in(window, |editor, _window, cx| {
-                let project = editor.project().cloned();
+            cx.defer_in(window, |editor, window, cx| {
+                let workspace = Workspace::for_window(window, cx);
+                let project = workspace.map(|workspace| workspace.read(cx).project().clone());
 
                 let is_local_project = project
                     .as_ref()

crates/rules_library/src/rules_library.rs 🔗

@@ -24,7 +24,7 @@ use theme::ThemeSettings;
 use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
 use ui_input::ErasedEditor;
 use util::{ResultExt, TryFutureExt};
-use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations};
+use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
 use zed_actions::assistant::InlineAssist;
 
 use prompt_store::*;
@@ -968,14 +968,12 @@ impl RulesLibrary {
                 .assist(rule_editor, initial_prompt, window, cx);
         } else {
             for window in cx.windows() {
-                if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
-                    let panel = multi_workspace
-                        .update(cx, |multi_workspace, window, cx| {
+                if let Some(workspace) = window.downcast::<Workspace>() {
+                    let panel = workspace
+                        .update(cx, |workspace, window, cx| {
                             window.activate_window();
-                            multi_workspace.workspace().update(cx, |workspace, cx| {
-                                self.inline_assist_delegate
-                                    .focus_agent_panel(workspace, window, cx)
-                            })
+                            self.inline_assist_delegate
+                                .focus_agent_panel(workspace, window, cx)
                         })
                         .ok();
                     if panel == Some(true) {

crates/search/src/search.rs 🔗

@@ -191,7 +191,7 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
         struct NotifType();
         let notification_id = NotificationId::unique::<NotifType>();
 
-        let Some(workspace) = Workspace::for_window(window, cx) else {
+        let Some(workspace) = window.root::<Workspace>().flatten() else {
             return;
         };
         workspace.update(cx, |workspace, cx| {

crates/session/src/session.rs 🔗

@@ -47,15 +47,6 @@ impl Session {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test_with_old_session(old_session_id: String) -> Self {
-        Self {
-            session_id: uuid::Uuid::new_v4().to_string(),
-            old_session_id: Some(old_session_id),
-            old_window_ids: None,
-        }
-    }
-
     pub fn id(&self) -> &str {
         &self.session_id
     }
@@ -118,11 +109,6 @@ impl AppSession {
         self.session.old_session_id.as_deref()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn replace_session_for_test(&mut self, session: Session) {
-        self.session = session;
-    }
-
     pub fn last_session_window_stack(&self) -> Option<Vec<WindowId>> {
         self.session.old_window_ids.clone()
     }

crates/settings_profile_selector/src/settings_profile_selector.rs 🔗

@@ -287,7 +287,7 @@ mod tests {
     use serde_json::json;
     use settings::Settings;
     use theme::{self, ThemeSettings};
-    use workspace::{self, AppState, MultiWorkspace};
+    use workspace::{self, AppState};
     use zed_actions::settings_profile_selector;
 
     async fn init_test(
@@ -320,11 +320,8 @@ mod tests {
 
         let fs = FakeFs::new(cx.executor());
         let project = Project::test(fs, ["/test".as_ref()], cx).await;
-        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
-        let cx = VisualTestContext::from_window(*window, cx).into_mut();
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         cx.update(|_, cx| {
             assert!(!cx.has_global::<ActiveSettingsProfileName>());

crates/settings_ui/src/settings_ui.rs 🔗

@@ -40,9 +40,7 @@ use ui::{
 };
 
 use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
-use workspace::{
-    AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, client_side_decorations,
-};
+use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
 use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
 
 use crate::components::{
@@ -396,7 +394,7 @@ pub fn init(cx: &mut App) {
                 |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
                     let window_handle = window
                         .window_handle()
-                        .downcast::<MultiWorkspace>()
+                        .downcast::<Workspace>()
                         .expect("Workspaces are root Windows");
                     open_settings_editor(workspace, Some(&path), false, window_handle, cx);
                 },
@@ -404,14 +402,14 @@ pub fn init(cx: &mut App) {
             .register_action(|workspace, _: &OpenSettings, window, cx| {
                 let window_handle = window
                     .window_handle()
-                    .downcast::<MultiWorkspace>()
+                    .downcast::<Workspace>()
                     .expect("Workspaces are root Windows");
                 open_settings_editor(workspace, None, false, window_handle, cx);
             })
             .register_action(|workspace, _: &OpenProjectSettings, window, cx| {
                 let window_handle = window
                     .window_handle()
-                    .downcast::<MultiWorkspace>()
+                    .downcast::<Workspace>()
                     .expect("Workspaces are root Windows");
                 open_settings_editor(workspace, None, true, window_handle, cx);
             });
@@ -549,7 +547,7 @@ pub fn open_settings_editor(
     _workspace: &mut Workspace,
     path: Option<&str>,
     open_project_settings: bool,
-    workspace_handle: WindowHandle<MultiWorkspace>,
+    workspace_handle: WindowHandle<Workspace>,
     cx: &mut App,
 ) {
     telemetry::event!("Settings Viewed");
@@ -717,7 +715,7 @@ fn active_language_mut() -> Option<std::sync::RwLockWriteGuard<'static, Option<S
 
 pub struct SettingsWindow {
     title_bar: Option<Entity<PlatformTitleBar>>,
-    original_window: Option<WindowHandle<MultiWorkspace>>,
+    original_window: Option<WindowHandle<Workspace>>,
     files: Vec<(SettingsUiFile, FocusHandle)>,
     worktree_root_dirs: HashMap<WorktreeId, String>,
     current_file: SettingsUiFile,
@@ -1449,7 +1447,7 @@ impl SettingsUiFile {
 
 impl SettingsWindow {
     fn new(
-        original_window: Option<WindowHandle<MultiWorkspace>>,
+        original_window: Option<WindowHandle<Workspace>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -1520,21 +1518,34 @@ impl SettingsWindow {
         .detach();
 
         if let Some(app_state) = AppState::global(cx).upgrade() {
-            let workspaces: Vec<Entity<Workspace>> = app_state
+            for project in app_state
                 .workspace_store
                 .read(cx)
                 .workspaces()
-                .filter_map(|weak| weak.upgrade())
-                .collect();
-
-            for workspace in workspaces {
-                let project = workspace.read(cx).project().clone();
+                .iter()
+                .filter_map(|space| {
+                    space
+                        .read(cx)
+                        .ok()
+                        .map(|workspace| workspace.project().clone())
+                })
+                .collect::<Vec<_>>()
+            {
                 cx.observe_release_in(&project, window, |this, _, window, cx| {
                     this.fetch_files(window, cx)
                 })
                 .detach();
                 cx.subscribe_in(&project, window, Self::handle_project_event)
                     .detach();
+            }
+
+            for workspace in app_state
+                .workspace_store
+                .read(cx)
+                .workspaces()
+                .iter()
+                .filter_map(|space| space.entity(cx).ok())
+            {
                 cx.observe_release_in(&workspace, window, |this, _, window, cx| {
                     this.fetch_files(window, cx)
                 })
@@ -3309,19 +3320,56 @@ impl SettingsWindow {
                     return;
                 };
                 original_window
-                    .update(cx, |multi_workspace, window, cx| {
-                        multi_workspace
-                            .workspace()
-                            .clone()
-                            .update(cx, |workspace, cx| {
-                                workspace
-                                    .with_local_or_wsl_workspace(
-                                        window,
-                                        cx,
-                                        open_user_settings_in_workspace,
-                                    )
-                                    .detach();
-                            });
+                    .update(cx, |workspace, window, cx| {
+                        workspace
+                            .with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
+                                let project = workspace.project().clone();
+
+                                cx.spawn_in(window, async move |workspace, cx| {
+                                    let (config_dir, settings_file) =
+                                        project.update(cx, |project, cx| {
+                                            (
+                                                project.try_windows_path_to_wsl(
+                                                    paths::config_dir().as_path(),
+                                                    cx,
+                                                ),
+                                                project.try_windows_path_to_wsl(
+                                                    paths::settings_file().as_path(),
+                                                    cx,
+                                                ),
+                                            )
+                                        });
+                                    let config_dir = config_dir.await?;
+                                    let settings_file = settings_file.await?;
+                                    project
+                                        .update(cx, |project, cx| {
+                                            project.find_or_create_worktree(&config_dir, false, cx)
+                                        })
+                                        .await
+                                        .ok();
+                                    workspace
+                                        .update_in(cx, |workspace, window, cx| {
+                                            workspace.open_paths(
+                                                vec![settings_file],
+                                                OpenOptions {
+                                                    visible: Some(OpenVisible::None),
+                                                    ..Default::default()
+                                                },
+                                                None,
+                                                window,
+                                                cx,
+                                            )
+                                        })?
+                                        .await;
+
+                                    workspace.update_in(cx, |_, window, cx| {
+                                        window.activate_window();
+                                        cx.notify();
+                                    })
+                                })
+                                .detach();
+                            })
+                            .detach();
                     })
                     .ok();
 
@@ -3333,22 +3381,22 @@ impl SettingsWindow {
                     return;
                 };
 
-                let Some((workspace_window, worktree, corresponding_workspace)) = app_state
+                let Some((worktree, corresponding_workspace)) = app_state
                     .workspace_store
                     .read(cx)
-                    .workspaces_with_windows()
-                    .filter_map(|(window_handle, weak)| {
-                        let workspace = weak.upgrade()?;
-                        let window = window_handle.downcast::<MultiWorkspace>()?;
-                        Some((window, workspace))
-                    })
-                    .find_map(|(window, workspace): (_, Entity<Workspace>)| {
+                    .workspaces()
+                    .iter()
+                    .find_map(|workspace| {
                         workspace
-                            .read(cx)
-                            .project()
-                            .read(cx)
-                            .worktree_for_id(*worktree_id, cx)
-                            .map(|worktree| (window, worktree, workspace))
+                            .read_with(cx, |workspace, cx| {
+                                workspace
+                                    .project()
+                                    .read(cx)
+                                    .worktree_for_id(*worktree_id, cx)
+                            })
+                            .ok()
+                            .flatten()
+                            .zip(Some(*workspace))
                     })
                 else {
                     log::error!(
@@ -3376,15 +3424,14 @@ impl SettingsWindow {
 
                 // TODO: move zed::open_local_file() APIs to this crate, and
                 // re-implement the "initial_contents" behavior
-                let workspace_weak = corresponding_workspace.downgrade();
-                workspace_window
+                corresponding_workspace
                     .update(cx, |_, window, cx| {
-                        cx.spawn_in(window, async move |_, cx| {
+                        cx.spawn_in(window, async move |workspace, cx| {
                             if let Some(create_task) = create_task {
                                 create_task.await.ok()?;
                             };
 
-                            workspace_weak
+                            workspace
                                 .update_in(cx, |workspace, window, cx| {
                                     workspace.open_path(
                                         (worktree_id, settings_path.clone()),
@@ -3398,7 +3445,7 @@ impl SettingsWindow {
                                 .await
                                 .log_err()?;
 
-                            workspace_weak
+                            workspace
                                 .update_in(cx, |_, window, cx| {
                                     window.activate_window();
                                     cx.notify();
@@ -3705,7 +3752,7 @@ impl Render for SettingsWindow {
 }
 
 fn all_projects(
-    window: Option<&WindowHandle<MultiWorkspace>>,
+    window: Option<&WindowHandle<Workspace>>,
     cx: &App,
 ) -> impl Iterator<Item = Entity<Project>> {
     let mut seen_project_ids = std::collections::HashSet::new();
@@ -3716,19 +3763,10 @@ fn all_projects(
                 .workspace_store
                 .read(cx)
                 .workspaces()
-                .filter_map(|weak| weak.upgrade())
-                .map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
+                .iter()
+                .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone()))
                 .chain(
-                    window
-                        .and_then(|handle| handle.read(cx).ok())
-                        .into_iter()
-                        .flat_map(|multi_workspace| {
-                            multi_workspace
-                                .workspaces()
-                                .iter()
-                                .map(|workspace| workspace.read(cx).project().clone())
-                                .collect::<Vec<_>>()
-                        }),
+                    window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())),
                 )
                 .filter(move |project| seen_project_ids.insert(project.entity_id()))
         })
@@ -3736,51 +3774,6 @@ fn all_projects(
         .flatten()
 }
 
-fn open_user_settings_in_workspace(
-    workspace: &mut Workspace,
-    window: &mut Window,
-    cx: &mut Context<Workspace>,
-) {
-    let project = workspace.project().clone();
-
-    cx.spawn_in(window, async move |workspace, cx| {
-        let (config_dir, settings_file) = project.update(cx, |project, cx| {
-            (
-                project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx),
-                project.try_windows_path_to_wsl(paths::settings_file().as_path(), cx),
-            )
-        });
-        let config_dir = config_dir.await?;
-        let settings_file = settings_file.await?;
-        project
-            .update(cx, |project, cx| {
-                project.find_or_create_worktree(&config_dir, false, cx)
-            })
-            .await
-            .ok();
-        workspace
-            .update_in(cx, |workspace, window, cx| {
-                workspace.open_paths(
-                    vec![settings_file],
-                    OpenOptions {
-                        visible: Some(OpenVisible::None),
-                        ..Default::default()
-                    },
-                    None,
-                    window,
-                    cx,
-                )
-            })?
-            .await;
-
-        workspace.update_in(cx, |_, window, cx| {
-            window.activate_window();
-            cx.notify();
-        })
-    })
-    .detach();
-}
-
 fn update_settings_file(
     file: SettingsUiFile,
     file_name: Option<&'static str>,
@@ -4761,33 +4754,29 @@ pub mod test {
             .await
             .expect("Failed to create worktree_c");
 
-        let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
-            let workspace = cx.new(|cx| {
-                Workspace::new(
-                    Default::default(),
-                    project1.clone(),
-                    app_state.clone(),
-                    window,
-                    cx,
-                )
-            });
-            MultiWorkspace::new(workspace, cx)
+        let (_workspace1, cx) = cx.add_window_view(|window, cx| {
+            Workspace::new(
+                Default::default(),
+                project1.clone(),
+                app_state.clone(),
+                window,
+                cx,
+            )
         });
 
-        let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
-            let workspace = cx.new(|cx| {
-                Workspace::new(
-                    Default::default(),
-                    project2.clone(),
-                    app_state.clone(),
-                    window,
-                    cx,
-                )
-            });
-            MultiWorkspace::new(workspace, cx)
+        let _workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap();
+
+        let (_workspace2, cx) = cx.add_window_view(|window, cx| {
+            Workspace::new(
+                Default::default(),
+                project2.clone(),
+                app_state.clone(),
+                window,
+                cx,
+            )
         });
 
-        let workspace2_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
+        let workspace2_handle = cx.window_handle().downcast::<Workspace>().unwrap();
 
         cx.run_until_parked();
 
@@ -4906,20 +4895,17 @@ pub mod test {
             .await
             .expect("Failed to create worktree_a");
 
-        let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
-            let workspace = cx.new(|cx| {
-                Workspace::new(
-                    Default::default(),
-                    project1.clone(),
-                    app_state.clone(),
-                    window,
-                    cx,
-                )
-            });
-            MultiWorkspace::new(workspace, cx)
+        let (_workspace1, cx) = cx.add_window_view(|window, cx| {
+            Workspace::new(
+                Default::default(),
+                project1.clone(),
+                app_state.clone(),
+                window,
+                cx,
+            )
         });
 
-        let workspace1_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
+        let workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap();
 
         cx.run_until_parked();
 
@@ -4956,17 +4942,14 @@ pub mod test {
             .await
             .expect("Failed to create worktree_b");
 
-        let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
-            let workspace = cx.new(|cx| {
-                Workspace::new(
-                    Default::default(),
-                    project2.clone(),
-                    app_state.clone(),
-                    window,
-                    cx,
-                )
-            });
-            MultiWorkspace::new(workspace, cx)
+        let (_workspace2, cx) = cx.add_window_view(|window, cx| {
+            Workspace::new(
+                Default::default(),
+                project2.clone(),
+                app_state.clone(),
+                window,
+                cx,
+            )
         });
 
         cx.run_until_parked();

crates/sidebar/Cargo.toml 🔗

@@ -1,43 +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 = []
-test-support = []
-
-[dependencies]
-acp_thread.workspace = true
-agent_ui.workspace = true
-db.workspace = true
-fs.workspace = true
-fuzzy.workspace = true
-serde_json.workspace = true
-gpui.workspace = true
-picker.workspace = true
-project.workspace = true
-recent_projects.workspace = true
-theme.workspace = true
-ui.workspace = true
-ui_input.workspace = true
-util.workspace = true
-workspace.workspace = true
-
-[dev-dependencies]
-editor.workspace = true
-feature_flags.workspace = true
-fs = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-project = { workspace = true, features = ["test-support"] }
-recent_projects = { workspace = true, features = ["test-support"] }
-settings = { workspace = true, features = ["test-support"] }
-workspace = { workspace = true, features = ["test-support"] }

crates/sidebar/src/sidebar.rs 🔗

@@ -1,1304 +0,0 @@
-use acp_thread::ThreadStatus;
-use agent_ui::{AgentPanel, AgentPanelEvent};
-use db::kvp::KEY_VALUE_STORE;
-use fs::Fs;
-use fuzzy::StringMatchCandidate;
-use gpui::{
-    App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, SharedString,
-    Subscription, Task, Window, px,
-};
-use picker::{Picker, PickerDelegate};
-use project::Event as ProjectEvent;
-use recent_projects::{RecentProjectEntry, get_recent_projects};
-
-use std::collections::{HashMap, HashSet};
-
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-use theme::ActiveTheme;
-use ui::utils::TRAFFIC_LIGHT_PADDING;
-use ui::{CommonAnimationExt, Divider, HighlightedLabel, ListItem, Tab, Tooltip, prelude::*};
-use ui_input::ErasedEditor;
-use util::ResultExt as _;
-use workspace::{
-    MultiWorkspace, NewWorkspaceInWindow, Sidebar as WorkspaceSidebar, SidebarEvent,
-    ToggleWorkspaceSidebar, Workspace,
-};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum AgentThreadStatus {
-    Running,
-    Completed,
-}
-
-#[derive(Clone, Debug)]
-struct AgentThreadInfo {
-    title: SharedString,
-    status: AgentThreadStatus,
-}
-
-const LAST_THREAD_TITLES_KEY: &str = "sidebar-last-thread-titles";
-
-const DEFAULT_WIDTH: Pixels = px(320.0);
-const MIN_WIDTH: Pixels = px(200.0);
-const MAX_WIDTH: Pixels = px(800.0);
-const MAX_MATCHES: usize = 100;
-
-#[derive(Clone)]
-struct WorkspaceThreadEntry {
-    index: usize,
-    worktree_label: SharedString,
-    full_path: SharedString,
-    thread_info: Option<AgentThreadInfo>,
-}
-
-impl WorkspaceThreadEntry {
-    fn new(
-        index: usize,
-        workspace: &Entity<Workspace>,
-        persisted_titles: &HashMap<String, String>,
-        cx: &App,
-    ) -> Self {
-        let workspace_ref = workspace.read(cx);
-
-        let worktrees: Vec<_> = workspace_ref
-            .worktrees(cx)
-            .map(|worktree| worktree.read(cx).abs_path())
-            .collect();
-
-        let worktree_names: Vec<String> = worktrees
-            .iter()
-            .filter_map(|path| {
-                path.file_name()
-                    .map(|name| name.to_string_lossy().to_string())
-            })
-            .collect();
-
-        let worktree_label: SharedString = if worktree_names.is_empty() {
-            format!("Workspace {}", index + 1).into()
-        } else {
-            worktree_names.join(", ").into()
-        };
-
-        let full_path: SharedString = worktrees
-            .iter()
-            .map(|path| path.to_string_lossy().to_string())
-            .collect::<Vec<_>>()
-            .join("\n")
-            .into();
-
-        let thread_info = Self::thread_info(workspace, cx).or_else(|| {
-            if worktrees.is_empty() {
-                return None;
-            }
-            let path_key = sorted_paths_key(&worktrees);
-            let title = persisted_titles.get(&path_key)?;
-            Some(AgentThreadInfo {
-                title: SharedString::from(title.clone()),
-                status: AgentThreadStatus::Completed,
-            })
-        });
-
-        Self {
-            index,
-            worktree_label,
-            full_path,
-            thread_info,
-        }
-    }
-
-    fn thread_info(workspace: &Entity<Workspace>, cx: &App) -> Option<AgentThreadInfo> {
-        let agent_panel = workspace.read(cx).panel::<AgentPanel>(cx)?;
-        let thread = agent_panel.read(cx).active_agent_thread(cx)?;
-        let thread_ref = thread.read(cx);
-        let title = thread_ref.title();
-        let status = match thread_ref.status() {
-            ThreadStatus::Generating => AgentThreadStatus::Running,
-            ThreadStatus::Idle => AgentThreadStatus::Completed,
-        };
-        Some(AgentThreadInfo { title, status })
-    }
-}
-
-#[derive(Clone)]
-enum SidebarEntry {
-    Separator(SharedString),
-    WorkspaceThread(WorkspaceThreadEntry),
-    RecentProject(RecentProjectEntry),
-}
-
-impl SidebarEntry {
-    fn searchable_text(&self) -> &str {
-        match self {
-            SidebarEntry::Separator(_) => "",
-            SidebarEntry::WorkspaceThread(entry) => entry.worktree_label.as_ref(),
-            SidebarEntry::RecentProject(entry) => entry.name.as_ref(),
-        }
-    }
-}
-
-#[derive(Clone)]
-struct SidebarMatch {
-    entry: SidebarEntry,
-    positions: Vec<usize>,
-}
-
-struct WorkspacePickerDelegate {
-    multi_workspace: Entity<MultiWorkspace>,
-    entries: Vec<SidebarEntry>,
-    active_workspace_index: usize,
-    workspace_thread_count: usize,
-    /// All recent projects including what's filtered out of entries
-    /// used to add unopened projects to entries on rebuild
-    recent_projects: Vec<RecentProjectEntry>,
-    recent_project_thread_titles: HashMap<SharedString, SharedString>,
-    matches: Vec<SidebarMatch>,
-    selected_index: usize,
-    query: String,
-    notified_workspaces: HashSet<usize>,
-}
-
-impl WorkspacePickerDelegate {
-    fn new(multi_workspace: Entity<MultiWorkspace>) -> Self {
-        Self {
-            multi_workspace,
-            entries: Vec::new(),
-            active_workspace_index: 0,
-            workspace_thread_count: 0,
-            recent_projects: Vec::new(),
-            recent_project_thread_titles: HashMap::new(),
-            matches: Vec::new(),
-            selected_index: 0,
-            query: String::new(),
-            notified_workspaces: HashSet::new(),
-        }
-    }
-
-    fn set_entries(
-        &mut self,
-        workspace_threads: Vec<WorkspaceThreadEntry>,
-        active_workspace_index: usize,
-        cx: &App,
-    ) {
-        let old_statuses: HashMap<usize, AgentThreadStatus> = self
-            .entries
-            .iter()
-            .filter_map(|entry| match entry {
-                SidebarEntry::WorkspaceThread(thread) => thread
-                    .thread_info
-                    .as_ref()
-                    .map(|info| (thread.index, info.status.clone())),
-                _ => None,
-            })
-            .collect();
-
-        for thread in &workspace_threads {
-            if let Some(info) = &thread.thread_info {
-                if info.status == AgentThreadStatus::Completed
-                    && thread.index != active_workspace_index
-                {
-                    if old_statuses.get(&thread.index) == Some(&AgentThreadStatus::Running) {
-                        self.notified_workspaces.insert(thread.index);
-                    }
-                }
-            }
-        }
-
-        if self.active_workspace_index != active_workspace_index {
-            self.notified_workspaces.remove(&active_workspace_index);
-        }
-        self.active_workspace_index = active_workspace_index;
-        self.workspace_thread_count = workspace_threads.len();
-        self.rebuild_entries(workspace_threads, cx);
-    }
-
-    fn set_recent_projects(&mut self, recent_projects: Vec<RecentProjectEntry>, cx: &App) {
-        self.recent_project_thread_titles.clear();
-        if let Some(map) = read_thread_title_map() {
-            for entry in &recent_projects {
-                let path_key = sorted_paths_key(&entry.paths);
-                if let Some(title) = map.get(&path_key) {
-                    self.recent_project_thread_titles
-                        .insert(entry.full_path.clone(), title.clone().into());
-                }
-            }
-        }
-
-        self.recent_projects = recent_projects;
-
-        let workspace_threads: Vec<WorkspaceThreadEntry> = self
-            .entries
-            .iter()
-            .filter_map(|entry| match entry {
-                SidebarEntry::WorkspaceThread(thread) => Some(thread.clone()),
-                _ => None,
-            })
-            .collect();
-        self.rebuild_entries(workspace_threads, cx);
-    }
-
-    fn open_workspace_path_sets(&self, cx: &App) -> Vec<Vec<Arc<Path>>> {
-        self.multi_workspace
-            .read(cx)
-            .workspaces()
-            .iter()
-            .map(|workspace| {
-                let mut paths = workspace.read(cx).root_paths(cx);
-                paths.sort();
-                paths
-            })
-            .collect()
-    }
-
-    fn rebuild_entries(&mut self, workspace_threads: Vec<WorkspaceThreadEntry>, cx: &App) {
-        let open_path_sets = self.open_workspace_path_sets(cx);
-
-        self.entries.clear();
-
-        if !workspace_threads.is_empty() {
-            self.entries
-                .push(SidebarEntry::Separator("Active Workspaces".into()));
-            for thread in workspace_threads {
-                self.entries.push(SidebarEntry::WorkspaceThread(thread));
-            }
-        }
-
-        let recent: Vec<_> = self
-            .recent_projects
-            .iter()
-            .filter(|project| {
-                let mut project_paths: Vec<&Path> =
-                    project.paths.iter().map(|p| p.as_path()).collect();
-                project_paths.sort();
-                !open_path_sets.iter().any(|open_paths| {
-                    open_paths.len() == project_paths.len()
-                        && open_paths
-                            .iter()
-                            .zip(&project_paths)
-                            .all(|(a, b)| a.as_ref() == *b)
-                })
-            })
-            .cloned()
-            .collect();
-
-        if !recent.is_empty() {
-            self.entries
-                .push(SidebarEntry::Separator("Recent Projects".into()));
-            for project in recent {
-                self.entries.push(SidebarEntry::RecentProject(project));
-            }
-        }
-    }
-
-    fn open_recent_project(paths: Vec<PathBuf>, window: &mut Window, cx: &mut App) {
-        let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
-            return;
-        };
-
-        cx.defer(move |cx| {
-            if let Some(task) = handle
-                .update(cx, |multi_workspace, window, cx| {
-                    multi_workspace.open_project(paths, window, cx)
-                })
-                .log_err()
-            {
-                task.detach_and_log_err(cx);
-            }
-        });
-    }
-}
-
-impl PickerDelegate for WorkspacePickerDelegate {
-    type ListItem = AnyElement;
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn can_select(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) -> bool {
-        match self.matches.get(ix) {
-            Some(SidebarMatch {
-                entry: SidebarEntry::Separator(_),
-                ..
-            }) => false,
-            _ => true,
-        }
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search…".into()
-    }
-
-    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
-        if self.query.is_empty() {
-            None
-        } else {
-            Some("No threads match your search.".into())
-        }
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        self.query = query.clone();
-        let entries = self.entries.clone();
-
-        if query.is_empty() {
-            self.matches = entries
-                .into_iter()
-                .map(|entry| SidebarMatch {
-                    entry,
-                    positions: Vec::new(),
-                })
-                .collect();
-
-            let separator_offset = if self.workspace_thread_count > 0 {
-                1
-            } else {
-                0
-            };
-            self.selected_index = (self.active_workspace_index + separator_offset)
-                .min(self.matches.len().saturating_sub(1));
-            return Task::ready(());
-        }
-
-        let executor = cx.background_executor().clone();
-        cx.spawn_in(window, async move |picker, cx| {
-            let matches = cx
-                .background_spawn(async move {
-                    let data_entries: Vec<(usize, &SidebarEntry)> = entries
-                        .iter()
-                        .enumerate()
-                        .filter(|(_, entry)| !matches!(entry, SidebarEntry::Separator(_)))
-                        .collect();
-
-                    let candidates: Vec<StringMatchCandidate> = data_entries
-                        .iter()
-                        .enumerate()
-                        .map(|(candidate_index, (_, entry))| {
-                            StringMatchCandidate::new(candidate_index, entry.searchable_text())
-                        })
-                        .collect();
-
-                    let search_matches = fuzzy::match_strings(
-                        &candidates,
-                        &query,
-                        false,
-                        true,
-                        MAX_MATCHES,
-                        &Default::default(),
-                        executor,
-                    )
-                    .await;
-
-                    let mut workspace_matches = Vec::new();
-                    let mut project_matches = Vec::new();
-
-                    for search_match in search_matches {
-                        let (original_index, _) = data_entries[search_match.candidate_id];
-                        let entry = entries[original_index].clone();
-                        let sidebar_match = SidebarMatch {
-                            positions: search_match.positions,
-                            entry: entry.clone(),
-                        };
-                        match entry {
-                            SidebarEntry::WorkspaceThread(_) => {
-                                workspace_matches.push(sidebar_match)
-                            }
-                            SidebarEntry::RecentProject(_) => project_matches.push(sidebar_match),
-                            SidebarEntry::Separator(_) => {}
-                        }
-                    }
-
-                    let mut result = Vec::new();
-                    if !workspace_matches.is_empty() {
-                        result.push(SidebarMatch {
-                            entry: SidebarEntry::Separator("Active Workspaces".into()),
-                            positions: Vec::new(),
-                        });
-                        result.extend(workspace_matches);
-                    }
-                    if !project_matches.is_empty() {
-                        result.push(SidebarMatch {
-                            entry: SidebarEntry::Separator("Recent Projects".into()),
-                            positions: Vec::new(),
-                        });
-                        result.extend(project_matches);
-                    }
-                    result
-                })
-                .await;
-
-            picker
-                .update_in(cx, |picker, _window, _cx| {
-                    picker.delegate.matches = matches;
-                    if picker.delegate.matches.is_empty() {
-                        picker.delegate.selected_index = 0;
-                    } else {
-                        let first_selectable = picker
-                            .delegate
-                            .matches
-                            .iter()
-                            .position(|m| !matches!(m.entry, SidebarEntry::Separator(_)))
-                            .unwrap_or(0);
-                        picker.delegate.selected_index = first_selectable;
-                    }
-                })
-                .log_err();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(selected_match) = self.matches.get(self.selected_index) else {
-            return;
-        };
-
-        match &selected_match.entry {
-            SidebarEntry::Separator(_) => {}
-            SidebarEntry::WorkspaceThread(thread_entry) => {
-                let target_index = thread_entry.index;
-                self.multi_workspace.update(cx, |multi_workspace, cx| {
-                    multi_workspace.activate_index(target_index, window, cx);
-                });
-            }
-            SidebarEntry::RecentProject(project_entry) => {
-                let paths = project_entry.paths.clone();
-                Self::open_recent_project(paths, window, cx);
-            }
-        }
-    }
-
-    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
-
-    fn render_match(
-        &self,
-        index: usize,
-        selected: bool,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let match_entry = self.matches.get(index)?;
-        let SidebarMatch { entry, positions } = match_entry;
-
-        fn render_title(text: SharedString, positions: &[usize]) -> AnyElement {
-            if positions.is_empty() {
-                div()
-                    .p_0p5()
-                    .child(Label::new(text).truncate())
-                    .into_any_element()
-            } else {
-                div()
-                    .p_0p5()
-                    .child(HighlightedLabel::new(text, positions.to_vec()).truncate())
-                    .into_any_element()
-            }
-        }
-
-        fn render_thread_status_icon(
-            workspace_index: usize,
-            status: &AgentThreadStatus,
-            has_notification: bool,
-        ) -> AnyElement {
-            match status {
-                AgentThreadStatus::Running => Icon::new(IconName::LoadCircle)
-                    .size(IconSize::XSmall)
-                    .color(Color::Muted)
-                    .with_keyed_rotate_animation(
-                        SharedString::from(format!("workspace-{}-spinner", workspace_index)),
-                        3,
-                    )
-                    .into_any_element(),
-                AgentThreadStatus::Completed => {
-                    let color = if has_notification {
-                        Color::Accent
-                    } else {
-                        Color::Muted
-                    };
-                    Icon::new(IconName::Check)
-                        .size(IconSize::XSmall)
-                        .color(color)
-                        .into_any_element()
-                }
-            }
-        }
-
-        fn render_project_row(
-            title: AnyElement,
-            thread_subtitle: Option<SharedString>,
-            status_icon: Option<AnyElement>,
-            cx: &App,
-        ) -> Div {
-            h_flex()
-                .items_start()
-                .gap(DynamicSpacing::Base06.rems(cx))
-                .child(
-                    div().pt(px(4.0)).child(
-                        Icon::new(IconName::Folder)
-                            .color(Color::Muted)
-                            .size(IconSize::XSmall),
-                    ),
-                )
-                .child(v_flex().overflow_hidden().child(title).when_some(
-                    thread_subtitle,
-                    |this, subtitle| {
-                        this.child(
-                            h_flex()
-                                .gap_1()
-                                .items_center()
-                                .px_0p5()
-                                .when_some(status_icon, |this, icon| this.child(icon))
-                                .child(
-                                    Label::new(subtitle)
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted)
-                                        .truncate(),
-                                ),
-                        )
-                    },
-                ))
-        }
-
-        match entry {
-            SidebarEntry::Separator(title) => Some(
-                div()
-                    .px_0p5()
-                    .when(index > 0, |this| this.mt_1().child(Divider::horizontal()))
-                    .child(
-                        ListItem::new("section_header").selectable(false).child(
-                            Label::new(title.clone())
-                                .size(LabelSize::XSmall)
-                                .color(Color::Muted)
-                                .when(index > 0, |this| this.mt_1p5())
-                                .mb_1(),
-                        ),
-                    )
-                    .into_any_element(),
-            ),
-            SidebarEntry::WorkspaceThread(thread_entry) => {
-                let worktree_label = thread_entry.worktree_label.clone();
-                let full_path = thread_entry.full_path.clone();
-                let title = render_title(worktree_label.clone(), positions);
-                let thread_info = thread_entry.thread_info.clone();
-                let workspace_index = thread_entry.index;
-                let multi_workspace = self.multi_workspace.clone();
-                let workspace_count = self.multi_workspace.read(_cx).workspaces().len();
-
-                let close_button = if workspace_count > 1 {
-                    Some(
-                        IconButton::new(
-                            SharedString::from(format!("close-workspace-{}", workspace_index)),
-                            IconName::Close,
-                        )
-                        .icon_size(IconSize::XSmall)
-                        .icon_color(Color::Muted)
-                        .tooltip(Tooltip::text("Close Workspace"))
-                        .on_click({
-                            let multi_workspace = multi_workspace;
-                            move |_, window, cx| {
-                                multi_workspace.update(cx, |mw, cx| {
-                                    mw.remove_workspace(workspace_index, window, cx);
-                                });
-                            }
-                        }),
-                    )
-                } else {
-                    None
-                };
-
-                let has_notification = self.notified_workspaces.contains(&workspace_index);
-                let (thread_subtitle, status_icon) = match thread_info {
-                    Some(info) => (
-                        Some(info.title),
-                        Some(render_thread_status_icon(
-                            workspace_index,
-                            &info.status,
-                            has_notification,
-                        )),
-                    ),
-                    None => (None, None),
-                };
-
-                Some(
-                    ListItem::new(("workspace-item", thread_entry.index))
-                        .toggle_state(selected)
-                        .when_some(close_button, |item, button| item.end_hover_slot(button))
-                        .child(render_project_row(title, thread_subtitle, status_icon, _cx))
-                        .when(!full_path.is_empty(), |item| {
-                            item.tooltip(move |_, cx| {
-                                Tooltip::with_meta(
-                                    worktree_label.clone(),
-                                    None,
-                                    full_path.clone(),
-                                    cx,
-                                )
-                            })
-                        })
-                        .into_any_element(),
-                )
-            }
-            SidebarEntry::RecentProject(project_entry) => {
-                let name = project_entry.name.clone();
-                let full_path = project_entry.full_path.clone();
-                let title = render_title(name.clone(), positions);
-                let item_id: SharedString =
-                    format!("recent-project-{:?}", project_entry.workspace_id).into();
-                let thread_title = self
-                    .recent_project_thread_titles
-                    .get(&project_entry.full_path)
-                    .cloned();
-
-                Some(
-                    ListItem::new(item_id)
-                        .toggle_state(selected)
-                        .child(render_project_row(title, thread_title, None, _cx))
-                        .tooltip(move |_, cx| {
-                            Tooltip::with_meta(name.clone(), None, full_path.clone(), cx)
-                        })
-                        .into_any_element(),
-                )
-            }
-        }
-    }
-
-    fn render_editor(
-        &self,
-        editor: &Arc<dyn ErasedEditor>,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Div {
-        h_flex()
-            .h(Tab::container_height(cx))
-            .w_full()
-            .px_2()
-            .gap_2()
-            .justify_between()
-            .border_b_1()
-            .border_color(cx.theme().colors().border)
-            .child(
-                Icon::new(IconName::MagnifyingGlass)
-                    .color(Color::Muted)
-                    .size(IconSize::Small),
-            )
-            .child(editor.render(window, cx))
-    }
-}
-
-pub struct Sidebar {
-    multi_workspace: Entity<MultiWorkspace>,
-    width: Pixels,
-    picker: Entity<Picker<WorkspacePickerDelegate>>,
-    _subscription: Subscription,
-    _project_subscriptions: Vec<Subscription>,
-    _agent_panel_subscriptions: Vec<Subscription>,
-    _thread_subscriptions: Vec<Subscription>,
-    #[cfg(any(test, feature = "test-support"))]
-    test_thread_infos: HashMap<usize, AgentThreadInfo>,
-    #[cfg(any(test, feature = "test-support"))]
-    test_recent_project_thread_titles: HashMap<SharedString, SharedString>,
-    _fetch_recent_projects: Task<()>,
-}
-
-impl EventEmitter<SidebarEvent> for Sidebar {}
-
-impl Sidebar {
-    pub fn new(
-        multi_workspace: Entity<MultiWorkspace>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = WorkspacePickerDelegate::new(multi_workspace.clone());
-        let picker = cx.new(|cx| {
-            Picker::list(delegate, window, cx)
-                .max_height(None)
-                .show_scrollbar(true)
-                .modal(false)
-        });
-
-        let subscription = cx.observe_in(
-            &multi_workspace,
-            window,
-            |this, multi_workspace, window, cx| {
-                this.queue_refresh(multi_workspace, window, cx);
-            },
-        );
-
-        let fetch_recent_projects = {
-            let picker = picker.downgrade();
-            let fs = <dyn Fs>::global(cx);
-            cx.spawn_in(window, async move |_this, cx| {
-                let projects = get_recent_projects(None, None, fs).await;
-
-                cx.update(|window, cx| {
-                    if let Some(picker) = picker.upgrade() {
-                        picker.update(cx, |picker, cx| {
-                            picker.delegate.set_recent_projects(projects, cx);
-                            let query = picker.query(cx);
-                            picker.update_matches(query, window, cx);
-                        });
-                    }
-                })
-                .log_err();
-            })
-        };
-
-        let mut this = Self {
-            multi_workspace,
-            width: DEFAULT_WIDTH,
-            picker,
-            _subscription: subscription,
-            _project_subscriptions: Vec::new(),
-            _agent_panel_subscriptions: Vec::new(),
-            _thread_subscriptions: Vec::new(),
-            #[cfg(any(test, feature = "test-support"))]
-            test_thread_infos: HashMap::new(),
-            #[cfg(any(test, feature = "test-support"))]
-            test_recent_project_thread_titles: HashMap::new(),
-            _fetch_recent_projects: fetch_recent_projects,
-        };
-        this.queue_refresh(this.multi_workspace.clone(), window, cx);
-        this
-    }
-
-    fn subscribe_to_projects(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Vec<Subscription> {
-        let projects: Vec<_> = self
-            .multi_workspace
-            .read(cx)
-            .workspaces()
-            .iter()
-            .map(|w| w.read(cx).project().clone())
-            .collect();
-
-        projects
-            .iter()
-            .map(|project| {
-                cx.subscribe_in(
-                    project,
-                    window,
-                    |this, _project, event, window, cx| match event {
-                        ProjectEvent::WorktreeAdded(_)
-                        | ProjectEvent::WorktreeRemoved(_)
-                        | ProjectEvent::WorktreeOrderChanged => {
-                            this.queue_refresh(this.multi_workspace.clone(), window, cx);
-                        }
-                        _ => {}
-                    },
-                )
-            })
-            .collect()
-    }
-
-    fn build_workspace_thread_entries(
-        &self,
-        multi_workspace: &MultiWorkspace,
-        cx: &App,
-    ) -> (Vec<WorkspaceThreadEntry>, usize) {
-        let persisted_titles = read_thread_title_map().unwrap_or_default();
-
-        #[allow(unused_mut)]
-        let mut entries: Vec<WorkspaceThreadEntry> = multi_workspace
-            .workspaces()
-            .iter()
-            .enumerate()
-            .map(|(index, workspace)| {
-                WorkspaceThreadEntry::new(index, workspace, &persisted_titles, cx)
-            })
-            .collect();
-
-        #[cfg(any(test, feature = "test-support"))]
-        for (index, info) in &self.test_thread_infos {
-            if let Some(entry) = entries.get_mut(*index) {
-                entry.thread_info = Some(info.clone());
-            }
-        }
-
-        (entries, multi_workspace.active_workspace_index())
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_test_recent_projects(
-        &self,
-        projects: Vec<RecentProjectEntry>,
-        cx: &mut Context<Self>,
-    ) {
-        self.picker.update(cx, |picker, _cx| {
-            picker.delegate.recent_projects = projects;
-        });
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_test_thread_info(
-        &mut self,
-        index: usize,
-        title: SharedString,
-        status: AgentThreadStatus,
-    ) {
-        self.test_thread_infos
-            .insert(index, AgentThreadInfo { title, status });
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_test_recent_project_thread_title(
-        &mut self,
-        full_path: SharedString,
-        title: SharedString,
-        cx: &mut Context<Self>,
-    ) {
-        self.test_recent_project_thread_titles
-            .insert(full_path.clone(), title.clone());
-        self.picker.update(cx, |picker, _cx| {
-            picker
-                .delegate
-                .recent_project_thread_titles
-                .insert(full_path, title);
-        });
-    }
-
-    fn subscribe_to_agent_panels(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Vec<Subscription> {
-        let workspaces: Vec<_> = self.multi_workspace.read(cx).workspaces().to_vec();
-
-        workspaces
-            .iter()
-            .map(|workspace| {
-                if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
-                    cx.subscribe_in(
-                        &agent_panel,
-                        window,
-                        |this, _, _event: &AgentPanelEvent, window, cx| {
-                            this.queue_refresh(this.multi_workspace.clone(), window, cx);
-                        },
-                    )
-                } else {
-                    // Panel hasn't loaded yet — observe the workspace so we
-                    // re-subscribe once the panel appears on its dock.
-                    cx.observe_in(workspace, window, |this, _, window, cx| {
-                        this.queue_refresh(this.multi_workspace.clone(), window, cx);
-                    })
-                }
-            })
-            .collect()
-    }
-
-    fn subscribe_to_threads(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Vec<Subscription> {
-        let workspaces: Vec<_> = self.multi_workspace.read(cx).workspaces().to_vec();
-
-        workspaces
-            .iter()
-            .filter_map(|workspace| {
-                let agent_panel = workspace.read(cx).panel::<AgentPanel>(cx)?;
-                let thread = agent_panel.read(cx).active_agent_thread(cx)?;
-                Some(cx.observe_in(&thread, window, |this, _, window, cx| {
-                    this.queue_refresh(this.multi_workspace.clone(), window, cx);
-                }))
-            })
-            .collect()
-    }
-
-    fn persist_thread_titles(
-        &self,
-        entries: &[WorkspaceThreadEntry],
-        multi_workspace: &Entity<MultiWorkspace>,
-        cx: &mut Context<Self>,
-    ) {
-        let mut map = read_thread_title_map().unwrap_or_default();
-        let workspaces = multi_workspace.read(cx).workspaces().to_vec();
-        let mut changed = false;
-
-        for (workspace, entry) in workspaces.iter().zip(entries.iter()) {
-            if let Some(ref info) = entry.thread_info {
-                let paths: Vec<_> = workspace
-                    .read(cx)
-                    .worktrees(cx)
-                    .map(|wt| wt.read(cx).abs_path())
-                    .collect();
-                if paths.is_empty() {
-                    continue;
-                }
-                let path_key = sorted_paths_key(&paths);
-                let title = info.title.to_string();
-                if map.get(&path_key) != Some(&title) {
-                    map.insert(path_key, title);
-                    changed = true;
-                }
-            }
-        }
-
-        if changed {
-            if let Some(json) = serde_json::to_string(&map).log_err() {
-                cx.background_spawn(async move {
-                    KEY_VALUE_STORE
-                        .write_kvp(LAST_THREAD_TITLES_KEY.into(), json)
-                        .await
-                        .log_err();
-                })
-                .detach();
-            }
-        }
-    }
-
-    fn queue_refresh(
-        &mut self,
-        multi_workspace: Entity<MultiWorkspace>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        cx.defer_in(window, move |this, window, cx| {
-            this._project_subscriptions = this.subscribe_to_projects(window, cx);
-            this._agent_panel_subscriptions = this.subscribe_to_agent_panels(window, cx);
-            this._thread_subscriptions = this.subscribe_to_threads(window, cx);
-            let (entries, active_index) = multi_workspace.read_with(cx, |multi_workspace, cx| {
-                this.build_workspace_thread_entries(multi_workspace, cx)
-            });
-
-            this.persist_thread_titles(&entries, &multi_workspace, cx);
-
-            let had_notifications = !this.picker.read(cx).delegate.notified_workspaces.is_empty();
-            this.picker.update(cx, |picker, cx| {
-                picker.delegate.set_entries(entries, active_index, cx);
-                let query = picker.query(cx);
-                picker.update_matches(query, window, cx);
-            });
-            let has_notifications = !this.picker.read(cx).delegate.notified_workspaces.is_empty();
-            if had_notifications != has_notifications {
-                multi_workspace.update(cx, |_, cx| cx.notify());
-            }
-        });
-    }
-}
-
-impl WorkspaceSidebar for Sidebar {
-    fn width(&self, _cx: &App) -> Pixels {
-        self.width
-    }
-
-    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 has_notifications(&self, cx: &App) -> bool {
-        !self.picker.read(cx).delegate.notified_workspaces.is_empty()
-    }
-}
-
-impl Focusable for Sidebar {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.read(cx).focus_handle(cx)
-    }
-}
-
-fn sorted_paths_key<P: AsRef<Path>>(paths: &[P]) -> String {
-    let mut sorted: Vec<String> = paths
-        .iter()
-        .map(|p| p.as_ref().to_string_lossy().to_string())
-        .collect();
-    sorted.sort();
-    sorted.join("\n")
-}
-
-fn read_thread_title_map() -> Option<HashMap<String, String>> {
-    let json = KEY_VALUE_STORE
-        .read_kvp(LAST_THREAD_TITLES_KEY)
-        .log_err()
-        .flatten()?;
-    serde_json::from_str(&json).log_err()
-}
-
-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);
-
-        v_flex()
-            .id("workspace-sidebar")
-            .key_context("WorkspaceSidebar")
-            .font(ui_font)
-            .h_full()
-            .w(self.width)
-            .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_2()
-                    .when(cfg!(target_os = "macos"), |this| {
-                        this.pl(px(TRAFFIC_LIGHT_PADDING))
-                    })
-                    .justify_between()
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(
-                        IconButton::new("close-sidebar", IconName::WorkspaceNavOpen)
-                            .icon_size(IconSize::Small)
-                            .tooltip(|_window, cx| {
-                                Tooltip::for_action("Close Sidebar", &ToggleWorkspaceSidebar, cx)
-                            })
-                            .on_click(cx.listener(|_this, _, _window, cx| {
-                                cx.emit(SidebarEvent::Close);
-                            })),
-                    )
-                    .child(
-                        IconButton::new("new-workspace", IconName::Plus)
-                            .icon_size(IconSize::Small)
-                            .tooltip(|_window, cx| {
-                                Tooltip::for_action("New Workspace", &NewWorkspaceInWindow, cx)
-                            })
-                            .on_click(cx.listener(|this, _, window, cx| {
-                                this.multi_workspace.update(cx, |multi_workspace, cx| {
-                                    multi_workspace.create_workspace(window, cx);
-                                });
-                            })),
-                    ),
-            )
-            .child(self.picker.clone())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use feature_flags::FeatureFlagAppExt as _;
-    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);
-            editor::init(cx);
-            cx.update_flags(false, vec!["agent-v2".into()]);
-        });
-    }
-
-    fn set_thread_info_and_refresh(
-        sidebar: &Entity<Sidebar>,
-        multi_workspace: &Entity<MultiWorkspace>,
-        index: usize,
-        title: &str,
-        status: AgentThreadStatus,
-        cx: &mut gpui::VisualTestContext,
-    ) {
-        sidebar.update_in(cx, |s, _window, _cx| {
-            s.set_test_thread_info(index, SharedString::from(title.to_string()), status.clone());
-        });
-        multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
-        cx.run_until_parked();
-    }
-
-    fn has_notifications(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) -> bool {
-        sidebar.read_with(cx, |s, cx| s.has_notifications(cx))
-    }
-
-    #[gpui::test]
-    async fn test_notification_on_running_to_completed_transition(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
-        let project = project::Project::test(fs, [], cx).await;
-
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
-
-        let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| {
-            let mw_handle = cx.entity();
-            cx.new(|cx| Sidebar::new(mw_handle, window, cx))
-        });
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.register_sidebar(sidebar.clone(), window, cx);
-        });
-        cx.run_until_parked();
-
-        // Create a second workspace and switch to it so workspace 0 is background.
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.create_workspace(window, cx);
-        });
-        cx.run_until_parked();
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate_index(1, window, cx);
-        });
-        cx.run_until_parked();
-
-        assert!(
-            !has_notifications(&sidebar, cx),
-            "should have no notifications initially"
-        );
-
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Running,
-            cx,
-        );
-
-        assert!(
-            !has_notifications(&sidebar, cx),
-            "Running status alone should not create a notification"
-        );
-
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Completed,
-            cx,
-        );
-
-        assert!(
-            has_notifications(&sidebar, cx),
-            "Running → Completed transition should create a notification"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_no_notification_for_active_workspace(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
-        let project = project::Project::test(fs, [], cx).await;
-
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
-
-        let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| {
-            let mw_handle = cx.entity();
-            cx.new(|cx| Sidebar::new(mw_handle, window, cx))
-        });
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.register_sidebar(sidebar.clone(), window, cx);
-        });
-        cx.run_until_parked();
-
-        // Workspace 0 is the active workspace — thread completes while
-        // the user is already looking at it.
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Running,
-            cx,
-        );
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Completed,
-            cx,
-        );
-
-        assert!(
-            !has_notifications(&sidebar, cx),
-            "should not notify for the workspace the user is already looking at"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_notification_cleared_on_workspace_activation(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
-        let project = project::Project::test(fs, [], cx).await;
-
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
-
-        let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| {
-            let mw_handle = cx.entity();
-            cx.new(|cx| Sidebar::new(mw_handle, window, cx))
-        });
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.register_sidebar(sidebar.clone(), window, cx);
-        });
-        cx.run_until_parked();
-
-        // Create a second workspace so we can switch away and back.
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.create_workspace(window, cx);
-        });
-        cx.run_until_parked();
-
-        // Switch to workspace 1 so workspace 0 becomes a background workspace.
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate_index(1, window, cx);
-        });
-        cx.run_until_parked();
-
-        // Thread on workspace 0 transitions Running → Completed while
-        // the user is looking at workspace 1.
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Running,
-            cx,
-        );
-        set_thread_info_and_refresh(
-            &sidebar,
-            &multi_workspace,
-            0,
-            "Test Thread",
-            AgentThreadStatus::Completed,
-            cx,
-        );
-
-        assert!(
-            has_notifications(&sidebar, cx),
-            "background workspace completion should create a notification"
-        );
-
-        // Switching back to workspace 0 should clear the notification.
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.activate_index(0, window, cx);
-        });
-        cx.run_until_parked();
-
-        assert!(
-            !has_notifications(&sidebar, cx),
-            "notification should be cleared when workspace becomes active"
-        );
-    }
-}

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"] }
 menu.workspace = true

crates/title_bar/src/project_dropdown.rs 🔗

@@ -11,8 +11,7 @@ use project::{Project, Worktree, git_store::Repository};
 use recent_projects::{RecentProjectEntry, delete_recent_project, get_recent_projects};
 use settings::WorktreeId;
 use ui::{ContextMenu, DocumentationAside, DocumentationSide, Tooltip, prelude::*};
-use util::ResultExt as _;
-use workspace::{MultiWorkspace, Workspace};
+use workspace::{CloseIntent, Workspace};
 
 actions!(project_dropdown, [RemoveSelectedFolder]);
 
@@ -67,12 +66,8 @@ impl ProjectDropdown {
         let recent_projects_for_fetch = recent_projects.clone();
         let menu_shell_for_fetch = menu_shell.clone();
         let workspace_for_fetch = workspace.clone();
-        let fs = workspace
-            .upgrade()
-            .map(|ws| ws.read(cx).app_state().fs.clone());
 
         cx.spawn_in(window, async move |_this, cx| {
-            let Some(fs) = fs else { return };
             let current_workspace_id = cx
                 .update(|_, cx| {
                     workspace_for_fetch
@@ -82,7 +77,7 @@ impl ProjectDropdown {
                 .ok()
                 .flatten();
 
-            let projects = get_recent_projects(current_workspace_id, None, fs).await;
+            let projects = get_recent_projects(current_workspace_id, None).await;
 
             cx.update(|window, cx| {
                 *recent_projects_for_fetch.borrow_mut() = projects;
@@ -93,7 +88,7 @@ impl ProjectDropdown {
                     });
                 }
             })
-            .ok();
+            .ok()
         })
         .detach();
 
@@ -401,31 +396,36 @@ impl ProjectDropdown {
         window: &mut Window,
         cx: &mut App,
     ) {
-        if create_new_window {
-            let Some(workspace) = workspace.upgrade() else {
-                return;
-            };
-            workspace.update(cx, |workspace, cx| {
-                workspace
-                    .open_workspace_for_paths(false, paths, window, cx)
-                    .detach_and_log_err(cx);
-            });
-        } else {
-            let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
-                return;
-            };
+        let Some(workspace) = workspace.upgrade() else {
+            return;
+        };
 
-            cx.defer(move |cx| {
-                if let Some(task) = handle
-                    .update(cx, |multi_workspace, window, cx| {
-                        multi_workspace.open_project(paths, window, cx)
-                    })
-                    .log_err()
-                {
-                    task.detach_and_log_err(cx);
-                }
-            });
-        }
+        workspace.update(cx, |workspace, cx| {
+            if create_new_window {
+                workspace.open_workspace_for_paths(false, paths, window, cx)
+            } else {
+                cx.spawn_in(window, {
+                    let paths = paths.clone();
+                    async move |workspace, cx| {
+                        let continue_replacing = workspace
+                            .update_in(cx, |workspace, window, cx| {
+                                workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
+                            })?
+                            .await?;
+                        if continue_replacing {
+                            workspace
+                                .update_in(cx, |workspace, window, cx| {
+                                    workspace.open_workspace_for_paths(true, paths, window, cx)
+                                })?
+                                .await
+                        } else {
+                            Ok(())
+                        }
+                    }
+                })
+            }
+            .detach_and_log_err(cx);
+        });
     }
 
     /// Get all projects sorted alphabetically with their branch info.

crates/title_bar/src/title_bar.rs 🔗

@@ -22,7 +22,6 @@ 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, Entity, FocusHandle, Focusable,
     InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
@@ -39,13 +38,10 @@ use theme::ActiveTheme;
 use title_bar_settings::TitleBarSettings;
 use ui::{
     Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu,
-    PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
+    PopoverMenuHandle, TintColor, Tooltip, prelude::*,
 };
 use util::ResultExt;
-use workspace::{
-    MultiWorkspace, SwitchProject, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace,
-    notifications::NotifyResultExt,
-};
+use workspace::{SwitchProject, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt};
 use zed_actions::OpenRemote;
 
 pub use onboarding_banner::restore_banner;
@@ -84,10 +80,8 @@ pub fn init(cx: &mut App) {
                 .titlebar_item()
                 .and_then(|item| item.downcast::<TitleBar>().ok())
             {
-                window.defer(cx, move |window, cx| {
-                    titlebar.update(cx, |titlebar, cx| {
-                        titlebar.show_project_dropdown(window, cx);
-                    })
+                titlebar.update(cx, |titlebar, cx| {
+                    titlebar.show_project_dropdown(window, cx);
                 });
             }
         });
@@ -164,7 +158,7 @@ impl Render for TitleBar {
 
         children.push(
             h_flex()
-                .gap_0p5()
+                .gap_1()
                 .map(|title_bar| {
                     let mut render_project_items = title_bar_settings.show_branch_name
                         || title_bar_settings.show_project_items;
@@ -177,7 +171,6 @@ impl Render for TitleBar {
                                 title_bar.child(menu)
                             },
                         )
-                        .children(self.render_workspace_sidebar_toggle(window, cx))
                         .children(self.render_restricted_mode(cx))
                         .when(render_project_items, |title_bar| {
                             title_bar
@@ -239,7 +232,7 @@ impl Render for TitleBar {
                 );
             });
 
-            let height = platform_title_bar_height(window);
+            let height = PlatformTitleBar::height(window);
             let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
                 platform_titlebar.title_bar_color(window, cx)
             });
@@ -347,48 +340,6 @@ impl TitleBar {
 
         let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
 
-        // 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>()
-                else {
-                    return;
-                };
-
-                let _ = cx.update(|cx| {
-                    let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else {
-                        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);
-                        });
-                    }
-                });
-            })
-            .detach();
-        }
-
         Self {
             platform_titlebar,
             application_menu,
@@ -676,41 +627,6 @@ impl TitleBar {
         )
     }
 
-    fn render_workspace_sidebar_toggle(
-        &self,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<AnyElement> {
-        if !cx.has_flag::<AgentV2FeatureFlag>() {
-            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 Workspace Sidebar", &ToggleWorkspaceSidebar, cx)
-                })
-                .on_click(|_, window, cx| {
-                    window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
-                })
-                .into_any_element(),
-        )
-    }
-
     pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
         let workspace = self.workspace.clone();
 
@@ -995,18 +911,16 @@ impl TitleBar {
 
     pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
         let client = self.client.clone();
-        let workspace = self.workspace.clone();
         Button::new("sign_in", "Sign In")
             .label_size(LabelSize::Small)
             .on_click(move |_, window, cx| {
                 let client = client.clone();
-                let workspace = workspace.clone();
                 window
-                    .spawn(cx, async move |mut cx| {
+                    .spawn(cx, async move |cx| {
                         client
                             .sign_in_with_optional_connect(true, cx)
                             .await
-                            .notify_workspace_async_err(workspace, &mut cx);
+                            .notify_async_err(cx);
                     })
                     .detach();
             })

crates/ui/src/components/thread_item.rs 🔗

@@ -1,6 +1,5 @@
 use crate::{
-    DecoratedIcon, DiffStat, HighlightedLabel, IconDecoration, IconDecorationKind, SpinnerLabel,
-    prelude::*,
+    Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*,
 };
 use gpui::{ClickEvent, SharedString};
 
@@ -9,7 +8,6 @@ pub struct ThreadItem {
     id: ElementId,
     icon: IconName,
     title: SharedString,
-    highlight_positions: Vec<usize>,
     timestamp: SharedString,
     running: bool,
     generation_done: bool,
@@ -26,7 +24,6 @@ impl ThreadItem {
             id: id.into(),
             icon: IconName::ZedAgent,
             title: title.into(),
-            highlight_positions: Vec::new(),
             timestamp: "".into(),
             running: false,
             generation_done: false,
@@ -78,11 +75,6 @@ impl ThreadItem {
         self
     }
 
-    pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
-        self.highlight_positions = positions;
-        self
-    }
-
     pub fn on_click(
         mut self,
         handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -120,17 +112,7 @@ impl RenderOnce for ThreadItem {
             agent_icon.into_any_element()
         };
 
-        // let has_no_changes = self.added.is_none() && self.removed.is_none();
-
-        let title = self.title;
-        let highlight_positions = self.highlight_positions;
-        let title_label = if highlight_positions.is_empty() {
-            Label::new(title).truncate().into_any_element()
-        } else {
-            HighlightedLabel::new(title, highlight_positions)
-                .truncate()
-                .into_any_element()
-        };
+        let has_no_changes = self.added.is_none() && self.removed.is_none();
 
         v_flex()
             .id(self.id.clone())
@@ -145,7 +127,7 @@ impl RenderOnce for ThreadItem {
                     .w_full()
                     .gap_1p5()
                     .child(icon)
-                    .child(title_label)
+                    .child(Label::new(self.title).truncate())
                     .when(self.running, |this| {
                         this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent)))
                     }),
@@ -155,32 +137,26 @@ impl RenderOnce for ThreadItem {
                     .gap_1p5()
                     .child(icon_container()) // Icon Spacing
                     .when_some(self.worktree, |this, name| {
-                        this.child(Label::new(name).size(LabelSize::Small).color(Color::Muted))
+                        this.child(Chip::new(name).label_size(LabelSize::XSmall))
                     })
                     .child(
-                        Label::new("•")
+                        Label::new(self.timestamp)
                             .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .alpha(0.5),
+                            .color(Color::Muted),
                     )
                     .child(
-                        Label::new(self.timestamp)
+                        Label::new("•")
                             .size(LabelSize::Small)
-                            .color(Color::Muted),
+                            .color(Color::Muted)
+                            .alpha(0.5),
                     )
-                    // .child(
-                    //     Label::new("•")
-                    //         .size(LabelSize::Small)
-                    //         .color(Color::Muted)
-                    //         .alpha(0.5),
-                    // )
-                    // .when(has_no_changes, |this| {
-                    //     this.child(
-                    //         Label::new("No Changes")
-                    //             .size(LabelSize::Small)
-                    //             .color(Color::Muted),
-                    //     )
-                    // })
+                    .when(has_no_changes, |this| {
+                        this.child(
+                            Label::new("No Changes")
+                                .size(LabelSize::Small)
+                                .color(Color::Muted),
+                        )
+                    })
                     .when(self.added.is_some() || self.removed.is_some(), |this| {
                         this.child(DiffStat::new(
                             self.id,

crates/ui/src/utils.rs 🔗

@@ -5,7 +5,6 @@ use theme::ActiveTheme;
 
 mod apca_contrast;
 mod color_contrast;
-mod constants;
 mod corner_solver;
 mod format_distance;
 mod search_input;
@@ -13,7 +12,6 @@ mod with_rem_size;
 
 pub use apca_contrast::*;
 pub use color_contrast::*;
-pub use constants::*;
 pub use corner_solver::{CornerSolver, inner_corner_radius};
 pub use format_distance::*;
 pub use search_input::*;

crates/ui/src/utils/constants.rs 🔗

@@ -1,27 +0,0 @@
-use gpui::{Pixels, Window, px};
-
-// Use pixels here instead of a rem-based size because the macOS traffic
-// lights are a static size, and don't scale with the rest of the UI.
-//
-// Magic number: There is one extra pixel of padding on the left side due to
-// the 1px border around the window on macOS apps.
-#[cfg(macos_sdk_26)]
-pub const TRAFFIC_LIGHT_PADDING: f32 = 78.;
-
-#[cfg(not(macos_sdk_26))]
-pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;
-
-/// Returns the platform-appropriate title bar height.
-///
-/// On Windows, this returns a fixed height of 32px.
-/// On other platforms, it scales with the window's rem size (1.75x) with a minimum of 34px.
-#[cfg(not(target_os = "windows"))]
-pub fn platform_title_bar_height(window: &Window) -> Pixels {
-    (1.75 * window.rem_size()).max(px(34.))
-}
-
-#[cfg(target_os = "windows")]
-pub fn platform_title_bar_height(_window: &Window) -> Pixels {
-    // todo(windows) instead of hard coded size report the actual size to the Windows platform API
-    px(32.)
-}

crates/vim/src/command.rs 🔗

@@ -318,7 +318,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         }
     });
     Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
         workspace.update(cx, |workspace, cx| {
@@ -327,7 +327,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
 
     Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
         workspace.update(cx, |workspace, cx| {
@@ -346,7 +346,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
 
     Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
         workspace.update(cx, |workspace, cx| {
@@ -398,7 +398,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 
                 if action.filename.is_empty() {
                     if whole_buffer {
-                        if let Some(workspace) = vim.workspace(window, cx) {
+                        if let Some(workspace) = vim.workspace(window) {
                             workspace.update(cx, |workspace, cx| {
                                 workspace
                                     .save_active_item(
@@ -472,7 +472,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             return;
         }
         if action.filename.is_empty() {
-            if let Some(workspace) = vim.workspace(window, cx) {
+            if let Some(workspace) = vim.workspace(window) {
                 workspace.update(cx, |workspace, cx| {
                     workspace
                         .save_active_item(
@@ -549,7 +549,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
 
     Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
 
@@ -647,7 +647,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 
     Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
         vim.update_editor(cx, |vim, editor, cx| {
-            let Some(workspace) = vim.workspace(window, cx) else {
+            let Some(workspace) = vim.workspace(window) else {
                 return;
             };
             let Some(project) = editor.project().cloned() else {
@@ -814,7 +814,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             }
         };
 
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
         let task = workspace.update(cx, |workspace, cx| {
@@ -855,7 +855,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
 
     Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
         let count = Vim::take_count(cx).unwrap_or(1);
@@ -888,7 +888,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
             anyhow::Ok(())
         });
         if let Some(e @ Err(_)) = result {
-            let Some(workspace) = vim.workspace(window, cx) else {
+            let Some(workspace) = vim.workspace(window) else {
                 return;
             };
             workspace.update(cx, |workspace, cx| {
@@ -932,7 +932,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         let range = match result {
             None => return,
             Some(e @ Err(_)) => {
-                let Some(workspace) = vim.workspace(window, cx) else {
+                let Some(workspace) = vim.workspace(window) else {
                     return;
                 };
                 workspace.update(cx, |workspace, cx| {
@@ -2132,7 +2132,7 @@ impl OnMatchingLines {
         let range = match result {
             None => return,
             Some(e @ Err(_)) => {
-                let Some(workspace) = vim.workspace(window, cx) else {
+                let Some(workspace) = vim.workspace(window) else {
                     return;
                 };
                 workspace.update(cx, |workspace, cx| {
@@ -2149,7 +2149,7 @@ impl OnMatchingLines {
         let mut regexes = match Regex::new(&self.search) {
             Ok(regex) => vec![(regex, !self.invert)],
             e @ Err(_) => {
-                let Some(workspace) = vim.workspace(window, cx) else {
+                let Some(workspace) = vim.workspace(window) else {
                     return;
                 };
                 workspace.update(cx, |workspace, cx| {
@@ -2347,7 +2347,7 @@ impl Vim {
         cx: &mut Context<Vim>,
     ) {
         self.stop_recording(cx);
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         let command = self.update_editor(cx, |_, editor, cx| {
@@ -2396,7 +2396,7 @@ impl Vim {
         cx: &mut Context<Vim>,
     ) {
         self.stop_recording(cx);
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         let command = self.update_editor(cx, |_, editor, cx| {
@@ -2448,7 +2448,7 @@ impl ShellExec {
     }
 
     pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
-        let Some(workspace) = vim.workspace(window, cx) else {
+        let Some(workspace) = vim.workspace(window) else {
             return;
         };
 

crates/vim/src/normal/mark.rs 🔗

@@ -81,7 +81,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         workspace.update(cx, |workspace, cx| {
@@ -133,7 +133,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         let task = workspace.update(cx, |workspace, cx| {
@@ -272,7 +272,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut App,
     ) {
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         if name == "`" {
@@ -324,7 +324,7 @@ impl Vim {
             return Some(Mark::Local(anchors));
         }
         VimGlobals::update_global(cx, |globals, cx| {
-            let workspace_id = self.workspace(window, cx)?.entity_id();
+            let workspace_id = self.workspace(window)?.entity_id();
             globals
                 .marks
                 .get_mut(&workspace_id)?
@@ -339,7 +339,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut App,
     ) {
-        let Some(workspace) = self.workspace(window, cx) else {
+        let Some(workspace) = self.workspace(window) else {
             return;
         };
         if name == "`" || name == "'" {

crates/vim/src/normal/repeat.rs 🔗

@@ -112,7 +112,7 @@ impl Replayer {
         let this = self.clone();
         window.defer(cx, move |window, cx| {
             this.next(window, cx);
-            let Some(workspace) = Workspace::for_window(window, cx) else {
+            let Some(Some(workspace)) = window.root::<Workspace>() else {
                 return;
             };
             let Some(editor) = workspace
@@ -165,7 +165,7 @@ impl Replayer {
                 text,
                 utf16_range_to_replace,
             } => {
-                let Some(workspace) = Workspace::for_window(window, cx) else {
+                let Some(Some(workspace)) = window.root::<Workspace>() else {
                     return;
                 };
                 let Some(editor) = workspace

crates/vim/src/normal/search.rs 🔗

@@ -555,7 +555,7 @@ impl Vim {
         let replacement = action.replacement.clone();
         let Some(((pane, workspace), editor)) = self
             .pane(window, cx)
-            .zip(self.workspace(window, cx))
+            .zip(self.workspace(window))
             .zip(self.editor())
         else {
             return;

crates/vim/src/state.rs 🔗

@@ -36,7 +36,7 @@ use ui::{
 use util::ResultExt;
 use util::rel_path::RelPath;
 use workspace::searchable::Direction;
-use workspace::{MultiWorkspace, Workspace, WorkspaceDb, WorkspaceId};
+use workspace::{Workspace, WorkspaceDb, WorkspaceId};
 
 #[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)]
 pub enum Mode {
@@ -731,16 +731,12 @@ impl VimGlobals {
                 });
                 GlobalCommandPaletteInterceptor::set(cx, command_interceptor);
                 for window in cx.windows() {
-                    if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
-                        multi_workspace
-                            .update(cx, |multi_workspace, _, cx| {
-                                for workspace in multi_workspace.workspaces() {
-                                    workspace.update(cx, |workspace, cx| {
-                                        Vim::update_globals(cx, |globals, cx| {
-                                            globals.register_workspace(workspace, cx)
-                                        });
-                                    });
-                                }
+                    if let Some(workspace) = window.downcast::<Workspace>() {
+                        workspace
+                            .update(cx, |workspace, _, cx| {
+                                Vim::update_globals(cx, |globals, cx| {
+                                    globals.register_workspace(workspace, cx)
+                                });
                             })
                             .ok();
                     }

crates/vim/src/vim.rs 🔗

@@ -1003,12 +1003,12 @@ impl Vim {
         self.editor.upgrade()
     }
 
-    pub fn workspace(&self, window: &Window, cx: &App) -> Option<Entity<Workspace>> {
-        Workspace::for_window(window, cx)
+    pub fn workspace(&self, window: &mut Window) -> Option<Entity<Workspace>> {
+        window.root::<Workspace>().flatten()
     }
 
-    pub fn pane(&self, window: &Window, cx: &Context<Self>) -> Option<Entity<Pane>> {
-        self.workspace(window, cx)
+    pub fn pane(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Entity<Pane>> {
+        self.workspace(window)
             .map(|workspace| workspace.read(cx).focused_pane(window, cx))
     }
 

crates/workspace/Cargo.toml 🔗

@@ -79,7 +79,6 @@ db = { workspace = true, features = ["test-support"] }
 fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
-remote = { workspace = true, features = ["test-support"] }
 session = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 http_client = { workspace = true, features = ["test-support"] }

crates/workspace/src/history_manager.rs 🔗

@@ -1,6 +1,5 @@
-use std::{path::PathBuf, sync::Arc};
+use std::path::PathBuf;
 
-use fs::Fs;
 use gpui::{AppContext, Entity, Global, MenuItem};
 use smallvec::SmallVec;
 use ui::{App, Context};
@@ -10,10 +9,10 @@ use crate::{
     NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList,
 };
 
-pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
+pub fn init(cx: &mut App) {
     let manager = cx.new(|_| HistoryManager::new());
     HistoryManager::set_global(manager.clone(), cx);
-    HistoryManager::init(manager, fs, cx);
+    HistoryManager::init(manager, cx);
 }
 
 pub struct HistoryManager {
@@ -39,10 +38,10 @@ impl HistoryManager {
         }
     }
 
-    fn init(this: Entity<HistoryManager>, fs: Arc<dyn Fs>, cx: &App) {
+    fn init(this: Entity<HistoryManager>, cx: &App) {
         cx.spawn(async move |cx| {
             let recent_folders = WORKSPACE_DB
-                .recent_workspaces_on_disk(fs.as_ref())
+                .recent_workspaces_on_disk()
                 .await
                 .unwrap_or_default()
                 .into_iter()

crates/workspace/src/multi_workspace.rs 🔗

@@ -1,513 +0,0 @@
-use anyhow::Result;
-use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
-use gpui::{
-    AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, Focusable, ManagedView,
-    MouseButton, Pixels, Render, Subscription, Task, Window, actions, deferred, px,
-};
-use project::Project;
-use std::path::PathBuf;
-use ui::prelude::*;
-
-const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
-
-use crate::{
-    DockPosition, Item, ModalView, Panel, Workspace, WorkspaceId, client_side_decorations,
-};
-
-actions!(
-    multi_workspace,
-    [
-        /// Creates a new workspace within the current window.
-        NewWorkspaceInWindow,
-        /// Switches to the next workspace within the current window.
-        NextWorkspaceInWindow,
-        /// Switches to the previous workspace within the current window.
-        PreviousWorkspaceInWindow,
-        /// Toggles the workspace switcher sidebar.
-        ToggleWorkspaceSidebar,
-    ]
-);
-
-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;
-}
-
-pub trait SidebarHandle: 'static + Send + Sync {
-    fn width(&self, cx: &App) -> Pixels;
-    fn set_width(&self, width: Option<Pixels>, cx: &mut App);
-    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;
-}
-
-#[derive(Clone)]
-pub struct DraggedSidebar;
-
-impl Render for DraggedSidebar {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        gpui::Empty
-    }
-}
-
-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(&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)
-    }
-}
-
-pub struct MultiWorkspace {
-    workspaces: Vec<Entity<Workspace>>,
-    active_workspace_index: usize,
-    sidebar: Option<Box<dyn SidebarHandle>>,
-    sidebar_open: bool,
-    _sidebar_subscription: Option<Subscription>,
-}
-
-impl MultiWorkspace {
-    pub fn new(workspace: Entity<Workspace>, _cx: &mut Context<Self>) -> Self {
-        Self {
-            workspaces: vec![workspace],
-            active_workspace_index: 0,
-            sidebar: None,
-            sidebar_open: false,
-            _sidebar_subscription: None,
-        }
-    }
-
-    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(crate) fn multi_workspace_enabled(&self, cx: &App) -> bool {
-        cx.has_flag::<AgentV2FeatureFlag>()
-    }
-
-    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);
-            let pane = self.workspace().read(cx).active_pane().clone();
-            window.focus(&pane.read(cx).focus_handle(cx), cx);
-        } else {
-            self.open_sidebar(window, cx);
-            if let Some(sidebar) = &self.sidebar {
-                sidebar.focus(window, cx);
-            }
-        }
-    }
-
-    pub fn open_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.sidebar_open = true;
-        self.serialize(window, cx);
-        cx.notify();
-    }
-
-    fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.sidebar_open = false;
-        self.serialize(window, cx);
-        cx.notify();
-    }
-
-    pub fn is_sidebar_open(&self) -> bool {
-        self.sidebar_open
-    }
-
-    pub fn workspace(&self) -> &Entity<Workspace> {
-        &self.workspaces[self.active_workspace_index]
-    }
-
-    pub fn workspaces(&self) -> &[Entity<Workspace>] {
-        &self.workspaces
-    }
-
-    pub fn active_workspace_index(&self) -> usize {
-        self.active_workspace_index
-    }
-
-    pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
-            self.workspaces[0] = workspace;
-            self.active_workspace_index = 0;
-            cx.notify();
-            return;
-        }
-
-        let index = self.add_workspace(workspace, cx);
-        if self.active_workspace_index != index {
-            self.active_workspace_index = index;
-            cx.notify();
-        }
-    }
-
-    /// Adds a workspace to this window without changing which workspace is active.
-    /// Returns the index of the workspace (existing or newly inserted).
-    pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
-        if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
-            index
-        } else {
-            self.workspaces.push(workspace);
-            cx.notify();
-            self.workspaces.len() - 1
-        }
-    }
-
-    pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
-        debug_assert!(
-            index < self.workspaces.len(),
-            "workspace index out of bounds"
-        );
-        self.active_workspace_index = index;
-        self.serialize(window, cx);
-        self.focus_active_workspace(window, cx);
-        cx.notify();
-    }
-
-    pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self.workspaces.len() > 1 {
-            let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
-            self.activate_index(next_index, window, cx);
-        }
-    }
-
-    pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self.workspaces.len() > 1 {
-            let prev_index = if self.active_workspace_index == 0 {
-                self.workspaces.len() - 1
-            } else {
-                self.active_workspace_index - 1
-            };
-            self.activate_index(prev_index, window, cx);
-        }
-    }
-
-    fn serialize(&self, window: &mut Window, cx: &mut App) {
-        let window_id = window.window_handle().window_id();
-        let state = crate::persistence::model::MultiWorkspaceState {
-            active_workspace_id: self.workspace().read(cx).database_id(),
-            sidebar_open: self.sidebar_open,
-        };
-        cx.background_spawn(async move {
-            crate::persistence::write_multi_workspace_state(window_id, state).await;
-        })
-        .detach();
-    }
-
-    fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
-        let pane = self.workspace().read(cx).active_pane().clone();
-        let focus_handle = pane.read(cx).focus_handle(cx);
-        window.focus(&focus_handle, cx);
-    }
-
-    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
-        self.workspace().read(cx).panel::<T>(cx)
-    }
-
-    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
-        self.workspace().read(cx).active_modal::<V>(cx)
-    }
-
-    pub fn add_panel<T: Panel>(
-        &mut self,
-        panel: Entity<T>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.workspace().update(cx, |workspace, cx| {
-            workspace.add_panel(panel, window, cx);
-        });
-    }
-
-    pub fn focus_panel<T: Panel>(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Entity<T>> {
-        self.workspace()
-            .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
-    }
-
-    pub fn toggle_modal<V: ModalView, B>(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-        build: B,
-    ) where
-        B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
-    {
-        self.workspace().update(cx, |workspace, cx| {
-            workspace.toggle_modal(window, cx, build);
-        });
-    }
-
-    pub fn toggle_dock(
-        &mut self,
-        dock_side: DockPosition,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.workspace().update(cx, |workspace, cx| {
-            workspace.toggle_dock(dock_side, window, cx);
-        });
-    }
-
-    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
-        self.workspace().read(cx).active_item_as::<I>(cx)
-    }
-
-    pub fn items_of_type<'a, T: Item>(
-        &'a self,
-        cx: &'a App,
-    ) -> impl 'a + Iterator<Item = Entity<T>> {
-        self.workspace().read(cx).items_of_type::<T>(cx)
-    }
-
-    pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
-        self.workspace().read(cx).database_id()
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
-        self.workspace().update(cx, |workspace, _cx| {
-            workspace.set_random_database_id();
-        });
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
-        Self::new(workspace, cx)
-    }
-
-    pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.multi_workspace_enabled(cx) {
-            return;
-        }
-        let app_state = self.workspace().read(cx).app_state().clone();
-        let project = Project::local(
-            app_state.client.clone(),
-            app_state.node_runtime.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
-            None,
-            project::LocalProjectFlags::default(),
-            cx,
-        );
-        let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
-        self.activate(new_workspace, cx);
-        self.focus_active_workspace(window, cx);
-    }
-
-    pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
-        if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
-            return;
-        }
-
-        self.workspaces.remove(index);
-
-        if self.active_workspace_index >= self.workspaces.len() {
-            self.active_workspace_index = self.workspaces.len() - 1;
-        } else if self.active_workspace_index > index {
-            self.active_workspace_index -= 1;
-        }
-
-        self.focus_active_workspace(window, cx);
-        cx.notify();
-    }
-
-    pub fn open_project(
-        &mut self,
-        paths: Vec<PathBuf>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let workspace = self.workspace().clone();
-
-        if self.multi_workspace_enabled(cx) {
-            workspace.update(cx, |workspace, cx| {
-                workspace.open_workspace_for_paths(true, paths, window, cx)
-            })
-        } else {
-            cx.spawn_in(window, async move |_this, cx| {
-                let should_continue = workspace
-                    .update_in(cx, |workspace, window, cx| {
-                        workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
-                    })?
-                    .await?;
-                if should_continue {
-                    workspace
-                        .update_in(cx, |workspace, window, cx| {
-                            workspace.open_workspace_for_paths(true, paths, window, cx)
-                        })?
-                        .await
-                } else {
-                    Ok(())
-                }
-            })
-        }
-    }
-}
-
-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
-        };
-
-        client_side_decorations(
-            h_flex()
-                .key_context("Workspace")
-                .size_full()
-                .on_action(
-                    cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
-                        this.create_workspace(window, cx);
-                    }),
-                )
-                .on_action(
-                    cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
-                        this.activate_next_workspace(window, cx);
-                    }),
-                )
-                .on_action(cx.listener(
-                    |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
-                        this.activate_previous_workspace(window, cx);
-                    },
-                ))
-                .on_action(cx.listener(
-                    |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
-                        this.toggle_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()
-                        .flex_1()
-                        .size_full()
-                        .overflow_hidden()
-                        .child(self.workspace().clone()),
-                ),
-            window,
-            cx,
-        )
-    }
-}

crates/workspace/src/notifications.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{MultiWorkspace, SuppressNotification, Toast, Workspace};
+use crate::{SuppressNotification, Toast, Workspace};
 use anyhow::Context as _;
 use gpui::{
-    AnyEntity, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, ClickEvent, Context,
+    AnyEntity, AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, Context,
     DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle,
-    Task, TextStyleRefinement, UnderlineStyle, WeakEntity, svg,
+    Task, TextStyleRefinement, UnderlineStyle, svg,
 };
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use parking_lot::Mutex;
@@ -1037,18 +1037,14 @@ pub fn show_app_notification<V: Notification + 'static>(
             .insert(id.clone(), build_notification.clone());
 
         for window in cx.windows() {
-            if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
-                multi_workspace
-                    .update(cx, |multi_workspace, _window, cx| {
-                        for workspace in multi_workspace.workspaces() {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.show_notification_without_handling_dismiss_events(
-                                    &id,
-                                    cx,
-                                    |cx| build_notification(cx),
-                                );
-                            });
-                        }
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
+                workspace_window
+                    .update(cx, |workspace, _window, cx| {
+                        workspace.show_notification_without_handling_dismiss_events(
+                            &id,
+                            cx,
+                            |cx| build_notification(cx),
+                        );
                     })
                     .ok(); // Doesn't matter if the windows are dropped
             }
@@ -1062,15 +1058,11 @@ pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
     cx.defer(move |cx| {
         GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
         for window in cx.windows() {
-            if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
                 let id = id.clone();
-                multi_workspace
-                    .update(cx, |multi_workspace, _window, cx| {
-                        for workspace in multi_workspace.workspaces() {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.dismiss_notification(&id, cx)
-                            });
-                        }
+                workspace_window
+                    .update(cx, |workspace, _window, cx| {
+                        workspace.dismiss_notification(&id, cx)
                     })
                     .ok();
             }
@@ -1084,11 +1076,7 @@ pub trait NotifyResultExt {
     fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
     -> Option<Self::Ok>;
 
-    fn notify_workspace_async_err(
-        self,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut AsyncApp,
-    ) -> Option<Self::Ok>;
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
 
     /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
     fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
@@ -1111,18 +1099,17 @@ where
         }
     }
 
-    fn notify_workspace_async_err(
-        self,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut AsyncApp,
-    ) -> Option<T> {
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
         match self {
             Ok(value) => Some(value),
             Err(err) => {
                 log::error!("{err:?}");
-                workspace
-                    .update(cx, |workspace, cx| workspace.show_error(&err, cx))
-                    .ok();
+                cx.update_root(|view, _, cx| {
+                    if let Ok(workspace) = view.downcast::<Workspace>() {
+                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+                    }
+                })
+                .ok();
                 None
             }
         }
@@ -1150,12 +1137,7 @@ where
 }
 
 pub trait NotifyTaskExt {
-    fn detach_and_notify_err(
-        self,
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut App,
-    );
+    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
 }
 
 impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
@@ -1163,16 +1145,9 @@ where
     E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
     R: 'static,
 {
-    fn detach_and_notify_err(
-        self,
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut App,
-    ) {
+    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
         window
-            .spawn(cx, async move |mut cx| {
-                self.await.notify_workspace_async_err(workspace, &mut cx)
-            })
+            .spawn(cx, async move |cx| self.await.notify_async_err(cx))
             .detach();
     }
 }

crates/workspace/src/pane.rs 🔗

@@ -3881,10 +3881,9 @@ impl Pane {
                         .path_for_entry(project_entry_id, cx)
                     {
                         let load_path_task = workspace.load_path(project_path.clone(), window, cx);
-                        cx.spawn_in(window, async move |workspace, mut cx| {
-                            if let Some((project_entry_id, build_item)) = load_path_task
-                                .await
-                                .notify_workspace_async_err(workspace.clone(), &mut cx)
+                        cx.spawn_in(window, async move |workspace, cx| {
+                            if let Some((project_entry_id, build_item)) =
+                                load_path_task.await.notify_async_err(cx)
                             {
                                 let (to_pane, new_item_handle) = workspace
                                     .update_in(cx, |workspace, window, cx| {

crates/workspace/src/persistence.rs 🔗

@@ -8,8 +8,6 @@ use std::{
     sync::Arc,
 };
 
-use fs::Fs;
-
 use anyhow::{Context as _, Result, bail};
 use collections::{HashMap, HashSet, IndexSet};
 use db::{
@@ -50,7 +48,7 @@ use model::{
     SerializedPaneGroup, SerializedWorkspace,
 };
 
-use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace};
+use self::model::{DockStructure, SerializedWorkspaceLocation};
 
 // https://www.sqlite.org/limits.html
 // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
@@ -283,64 +281,6 @@ impl From<WindowBoundsJson> for WindowBounds {
     }
 }
 
-fn multi_workspace_states() -> db::kvp::ScopedKeyValueStore<'static> {
-    KEY_VALUE_STORE.scoped("multi_workspace_state")
-}
-
-fn read_multi_workspace_state(window_id: WindowId) -> model::MultiWorkspaceState {
-    multi_workspace_states()
-        .read(&window_id.as_u64().to_string())
-        .log_err()
-        .flatten()
-        .and_then(|json| serde_json::from_str(&json).ok())
-        .unwrap_or_default()
-}
-
-pub async fn write_multi_workspace_state(window_id: WindowId, state: model::MultiWorkspaceState) {
-    if let Ok(json_str) = serde_json::to_string(&state) {
-        multi_workspace_states()
-            .write(window_id.as_u64().to_string(), json_str)
-            .await
-            .log_err();
-    }
-}
-
-pub fn read_serialized_multi_workspaces(
-    session_workspaces: Vec<model::SessionWorkspace>,
-) -> Vec<model::SerializedMultiWorkspace> {
-    let mut window_groups: Vec<Vec<model::SessionWorkspace>> = Vec::new();
-    let mut window_id_to_group: HashMap<WindowId, usize> = HashMap::default();
-
-    for session_workspace in session_workspaces {
-        match session_workspace.window_id {
-            Some(window_id) => {
-                let group_index = *window_id_to_group.entry(window_id).or_insert_with(|| {
-                    window_groups.push(Vec::new());
-                    window_groups.len() - 1
-                });
-                window_groups[group_index].push(session_workspace);
-            }
-            None => {
-                window_groups.push(vec![session_workspace]);
-            }
-        }
-    }
-
-    window_groups
-        .into_iter()
-        .map(|group| {
-            let window_id = group.first().and_then(|sw| sw.window_id);
-            let state = window_id
-                .map(read_multi_workspace_state)
-                .unwrap_or_default();
-            model::SerializedMultiWorkspace {
-                workspaces: group,
-                state,
-            }
-        })
-        .collect()
-}
-
 const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
 
 pub fn read_default_dock_state() -> Option<DockStructure> {
@@ -1768,26 +1708,10 @@ impl WorkspaceDb {
         }
     }
 
-    async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool {
-        let mut any_dir = false;
-        for path in paths {
-            match fs.metadata(path).await.ok().flatten() {
-                None => return false,
-                Some(meta) => {
-                    if meta.is_dir {
-                        any_dir = true;
-                    }
-                }
-            }
-        }
-        any_dir
-    }
-
     // Returns the recent locations which are still valid on disk and deletes ones which no longer
     // exist.
     pub async fn recent_workspaces_on_disk(
         &self,
-        fs: &dyn Fs,
     ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
         let mut result = Vec::new();
         let mut delete_tasks = Vec::new();
@@ -1820,8 +1744,11 @@ impl WorkspaceDb {
             // If a local workspace points to WSL, this check will cause us to wait for the
             // WSL VM and file server to boot up. This can block for many seconds.
             // Supported scenarios use remote workspaces.
-            if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
-                result.push((id, SerializedWorkspaceLocation::Local, paths));
+            if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
+                // Only show directories in recent projects
+                if paths.paths().iter().any(|path| path.is_dir()) {
+                    result.push((id, SerializedWorkspaceLocation::Local, paths));
+                }
             } else {
                 delete_tasks.push(self.delete_workspace_by_id(id));
             }
@@ -1833,67 +1760,65 @@ impl WorkspaceDb {
 
     pub async fn last_workspace(
         &self,
-        fs: &dyn Fs,
     ) -> Result<Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
-        Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next())
+        Ok(self.recent_workspaces_on_disk().await?.into_iter().next())
     }
 
     // Returns the locations of the workspaces that were still opened when the last
     // session was closed (i.e. when Zed was quit).
     // If `last_session_window_order` is provided, the returned locations are ordered
     // according to that.
-    pub async fn last_session_workspace_locations(
+    pub fn last_session_workspace_locations(
         &self,
         last_session_id: &str,
         last_session_window_stack: Option<Vec<WindowId>>,
-        fs: &dyn Fs,
-    ) -> Result<Vec<SessionWorkspace>> {
+    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
         let mut workspaces = Vec::new();
 
         for (workspace_id, paths, window_id, remote_connection_id) in
             self.session_workspaces(last_session_id.to_owned())?
         {
-            let window_id = window_id.map(WindowId::from);
-
             if let Some(remote_connection_id) = remote_connection_id {
-                workspaces.push(SessionWorkspace {
+                workspaces.push((
                     workspace_id,
-                    location: SerializedWorkspaceLocation::Remote(
+                    SerializedWorkspaceLocation::Remote(
                         self.remote_connection(remote_connection_id)?,
                     ),
                     paths,
-                    window_id,
-                });
+                    window_id.map(WindowId::from),
+                ));
             } else if paths.is_empty() {
                 // Empty workspace with items (drafts, files) - include for restoration
-                workspaces.push(SessionWorkspace {
+                workspaces.push((
                     workspace_id,
-                    location: SerializedWorkspaceLocation::Local,
+                    SerializedWorkspaceLocation::Local,
                     paths,
-                    window_id,
-                });
-            } else {
-                if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
-                    workspaces.push(SessionWorkspace {
-                        workspace_id,
-                        location: SerializedWorkspaceLocation::Local,
-                        paths,
-                        window_id,
-                    });
-                }
+                    window_id.map(WindowId::from),
+                ));
+            } else if paths.paths().iter().all(|path| path.exists())
+                && paths.paths().iter().any(|path| path.is_dir())
+            {
+                workspaces.push((
+                    workspace_id,
+                    SerializedWorkspaceLocation::Local,
+                    paths,
+                    window_id.map(WindowId::from),
+                ));
             }
         }
 
         if let Some(stack) = last_session_window_stack {
-            workspaces.sort_by_key(|workspace| {
-                workspace
-                    .window_id
+            workspaces.sort_by_key(|(_, _, _, window_id)| {
+                window_id
                     .and_then(|id| stack.iter().position(|&order_id| order_id == id))
                     .unwrap_or(usize::MAX)
             });
         }
 
-        Ok(workspaces)
+        Ok(workspaces
+            .into_iter()
+            .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths))
+            .collect::<Vec<_>>())
     }
 
     fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
@@ -2347,12 +2272,11 @@ pub fn delete_unloaded_items(
 mod tests {
     use super::*;
     use crate::persistence::model::{
-        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace,
+        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
     };
     use gpui;
     use pretty_assertions::assert_eq;
     use remote::SshConnectionOptions;
-    use serde_json::json;
     use std::{thread, time::Duration};
 
     #[gpui::test]
@@ -3116,18 +3040,12 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
+    async fn test_last_session_workspace_locations() {
         let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
         let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
         let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
         let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
 
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(dir1.path(), json!({})).await;
-        fs.insert_tree(dir2.path(), json!({})).await;
-        fs.insert_tree(dir3.path(), json!({})).await;
-        fs.insert_tree(dir4.path(), json!({})).await;
-
         let db =
             WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
 
@@ -3170,55 +3088,47 @@ mod tests {
         ]));
 
         let locations = db
-            .last_session_workspace_locations("one-session", stack, fs.as_ref())
-            .await
+            .last_session_workspace_locations("one-session", stack)
             .unwrap();
         assert_eq!(
             locations,
             [
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(4),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir4.path()]),
-                    window_id: Some(WindowId::from(2u64)),
-                },
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(3),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir3.path()]),
-                    window_id: Some(WindowId::from(8u64)),
-                },
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(2),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir2.path()]),
-                    window_id: Some(WindowId::from(5u64)),
-                },
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(1),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir1.path()]),
-                    window_id: Some(WindowId::from(9u64)),
-                },
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(5),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
-                    window_id: Some(WindowId::from(3u64)),
-                },
-                SessionWorkspace {
-                    workspace_id: WorkspaceId(6),
-                    location: SerializedWorkspaceLocation::Local,
-                    paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
-                    window_id: Some(WindowId::from(4u64)),
-                },
+                (
+                    WorkspaceId(4),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir4.path()])
+                ),
+                (
+                    WorkspaceId(3),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir3.path()])
+                ),
+                (
+                    WorkspaceId(2),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir2.path()])
+                ),
+                (
+                    WorkspaceId(1),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir1.path()])
+                ),
+                (
+                    WorkspaceId(5),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
+                ),
+                (
+                    WorkspaceId(6),
+                    SerializedWorkspaceLocation::Local,
+                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
+                ),
             ]
         );
     }
 
     #[gpui::test]
-    async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
-        let fs = fs::FakeFs::new(cx.executor());
+    async fn test_last_session_workspace_locations_remote() {
         let db =
             WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
                 .await;
@@ -3280,45 +3190,40 @@ mod tests {
         ]));
 
         let have = db
-            .last_session_workspace_locations("one-session", stack, fs.as_ref())
-            .await
+            .last_session_workspace_locations("one-session", stack)
             .unwrap();
         assert_eq!(have.len(), 4);
         assert_eq!(
             have[0],
-            SessionWorkspace {
-                workspace_id: WorkspaceId(4),
-                location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
-                paths: PathList::default(),
-                window_id: Some(WindowId::from(2u64)),
-            }
+            (
+                WorkspaceId(4),
+                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
+                PathList::default()
+            )
         );
         assert_eq!(
             have[1],
-            SessionWorkspace {
-                workspace_id: WorkspaceId(3),
-                location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
-                paths: PathList::default(),
-                window_id: Some(WindowId::from(8u64)),
-            }
+            (
+                WorkspaceId(3),
+                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
+                PathList::default()
+            )
         );
         assert_eq!(
             have[2],
-            SessionWorkspace {
-                workspace_id: WorkspaceId(2),
-                location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
-                paths: PathList::default(),
-                window_id: Some(WindowId::from(5u64)),
-            }
+            (
+                WorkspaceId(2),
+                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
+                PathList::default()
+            )
         );
         assert_eq!(
             have[3],
-            SessionWorkspace {
-                workspace_id: WorkspaceId(1),
-                location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
-                paths: PathList::default(),
-                window_id: Some(WindowId::from(9u64)),
-            }
+            (
+                WorkspaceId(1),
+                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
+                PathList::default()
+            )
         );
     }
 
@@ -3650,192 +3555,4 @@ mod tests {
         assert!(retrieved.display.is_some());
         assert_eq!(retrieved.display.unwrap(), display_uuid);
     }
-
-    #[gpui::test]
-    async fn test_last_session_workspace_locations_groups_by_window_id(
-        cx: &mut gpui::TestAppContext,
-    ) {
-        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
-        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
-        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
-        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
-        let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(dir1.path(), json!({})).await;
-        fs.insert_tree(dir2.path(), json!({})).await;
-        fs.insert_tree(dir3.path(), json!({})).await;
-        fs.insert_tree(dir4.path(), json!({})).await;
-        fs.insert_tree(dir5.path(), json!({})).await;
-
-        let db =
-            WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
-                .await;
-
-        // Simulate two MultiWorkspace windows each containing two workspaces,
-        // plus one single-workspace window:
-        //   Window 10: workspace 1, workspace 2
-        //   Window 20: workspace 3, workspace 4
-        //   Window 30: workspace 5 (only one)
-        //
-        // On session restore, the caller should be able to group these by
-        // window_id to reconstruct the MultiWorkspace windows.
-        let workspaces_data: Vec<(i64, &Path, u64)> = vec![
-            (1, dir1.path(), 10),
-            (2, dir2.path(), 10),
-            (3, dir3.path(), 20),
-            (4, dir4.path(), 20),
-            (5, dir5.path(), 30),
-        ];
-
-        for (id, dir, window_id) in &workspaces_data {
-            db.save_workspace(SerializedWorkspace {
-                id: WorkspaceId(*id),
-                paths: PathList::new(&[*dir]),
-                location: SerializedWorkspaceLocation::Local,
-                center_group: Default::default(),
-                window_bounds: Default::default(),
-                display: Default::default(),
-                docks: Default::default(),
-                centered_layout: false,
-                session_id: Some("test-session".to_owned()),
-                breakpoints: Default::default(),
-                window_id: Some(*window_id),
-                user_toolchains: Default::default(),
-            })
-            .await;
-        }
-
-        let locations = db
-            .last_session_workspace_locations("test-session", None, fs.as_ref())
-            .await
-            .unwrap();
-
-        // All 5 workspaces should be returned with their window_ids.
-        assert_eq!(locations.len(), 5);
-
-        // Every entry should have a window_id so the caller can group them.
-        for session_workspace in &locations {
-            assert!(
-                session_workspace.window_id.is_some(),
-                "workspace {:?} missing window_id",
-                session_workspace.workspace_id
-            );
-        }
-
-        // Group by window_id, simulating what the restoration code should do.
-        let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
-        for session_workspace in &locations {
-            if let Some(window_id) = session_workspace.window_id {
-                by_window
-                    .entry(window_id)
-                    .or_default()
-                    .push(session_workspace.workspace_id);
-            }
-        }
-
-        // Should produce 3 windows, not 5.
-        assert_eq!(
-            by_window.len(),
-            3,
-            "Expected 3 window groups, got {}: {:?}",
-            by_window.len(),
-            by_window
-        );
-
-        // Window 10 should contain workspaces 1 and 2.
-        let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
-        assert_eq!(window_10.len(), 2);
-        assert!(window_10.contains(&WorkspaceId(1)));
-        assert!(window_10.contains(&WorkspaceId(2)));
-
-        // Window 20 should contain workspaces 3 and 4.
-        let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
-        assert_eq!(window_20.len(), 2);
-        assert!(window_20.contains(&WorkspaceId(3)));
-        assert!(window_20.contains(&WorkspaceId(4)));
-
-        // Window 30 should contain only workspace 5.
-        let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
-        assert_eq!(window_30.len(), 1);
-        assert!(window_30.contains(&WorkspaceId(5)));
-    }
-
-    #[gpui::test]
-    async fn test_read_serialized_multi_workspaces_with_state() {
-        use crate::persistence::model::MultiWorkspaceState;
-
-        // Write multi-workspace state for two windows via the scoped KVP.
-        let window_10 = WindowId::from(10u64);
-        let window_20 = WindowId::from(20u64);
-
-        write_multi_workspace_state(
-            window_10,
-            MultiWorkspaceState {
-                active_workspace_id: Some(WorkspaceId(2)),
-                sidebar_open: true,
-            },
-        )
-        .await;
-
-        write_multi_workspace_state(
-            window_20,
-            MultiWorkspaceState {
-                active_workspace_id: Some(WorkspaceId(3)),
-                sidebar_open: false,
-            },
-        )
-        .await;
-
-        // Build session workspaces: two in window 10, one in window 20, one with no window.
-        let session_workspaces = vec![
-            SessionWorkspace {
-                workspace_id: WorkspaceId(1),
-                location: SerializedWorkspaceLocation::Local,
-                paths: PathList::new(&["/a"]),
-                window_id: Some(window_10),
-            },
-            SessionWorkspace {
-                workspace_id: WorkspaceId(2),
-                location: SerializedWorkspaceLocation::Local,
-                paths: PathList::new(&["/b"]),
-                window_id: Some(window_10),
-            },
-            SessionWorkspace {
-                workspace_id: WorkspaceId(3),
-                location: SerializedWorkspaceLocation::Local,
-                paths: PathList::new(&["/c"]),
-                window_id: Some(window_20),
-            },
-            SessionWorkspace {
-                workspace_id: WorkspaceId(4),
-                location: SerializedWorkspaceLocation::Local,
-                paths: PathList::new(&["/d"]),
-                window_id: None,
-            },
-        ];
-
-        let results = read_serialized_multi_workspaces(session_workspaces);
-
-        // 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.
-        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.
-        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);
-    }
 }

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

@@ -10,7 +10,7 @@ use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
-use gpui::{AsyncWindowContext, Entity, WeakEntity, WindowId};
+use gpui::{AsyncWindowContext, Entity, WeakEntity};
 
 use language::{Toolchain, ToolchainScope};
 use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
@@ -49,32 +49,6 @@ impl SerializedWorkspaceLocation {
     }
 }
 
-/// A workspace entry from a previous session, containing all the info needed
-/// to restore it including which window it belonged to (for MultiWorkspace grouping).
-#[derive(Debug, PartialEq, Clone)]
-pub struct SessionWorkspace {
-    pub workspace_id: WorkspaceId,
-    pub location: SerializedWorkspaceLocation,
-    pub paths: PathList,
-    pub window_id: Option<WindowId>,
-}
-
-/// Per-window state for a MultiWorkspace, persisted to KVP.
-#[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.
-#[derive(Debug, Clone)]
-pub struct SerializedMultiWorkspace {
-    pub workspaces: Vec<SessionWorkspace>,
-    pub state: MultiWorkspaceState,
-}
-
 #[derive(Debug, PartialEq, Clone)]
 pub(crate) struct SerializedWorkspace {
     pub(crate) id: WorkspaceId,

crates/workspace/src/welcome.rs 🔗

@@ -114,9 +114,7 @@ impl RenderOnce for SectionButton {
                             .size(rems_from_px(12.)),
                     ),
             )
-            .on_click(move |_, window, cx| {
-                self.focus_handle.dispatch_action(&*self.action, window, cx)
-            })
+            .on_click(move |_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
     }
 }
 
@@ -227,13 +225,9 @@ impl WelcomePage {
             .detach();
 
         if fallback_to_recent_projects {
-            let fs = workspace
-                .upgrade()
-                .map(|ws| ws.read(cx).app_state().fs.clone());
             cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
-                let Some(fs) = fs else { return };
                 let workspaces = WORKSPACE_DB
-                    .recent_workspaces_on_disk(fs.as_ref())
+                    .recent_workspaces_on_disk()
                     .await
                     .log_err()
                     .unwrap_or_default();
@@ -273,18 +267,21 @@ impl WelcomePage {
     ) {
         if let Some(recent_workspaces) = &self.recent_workspaces {
             if let Some((_workspace_id, location, paths)) = recent_workspaces.get(action.index) {
+                let paths = paths.clone();
+                let location = location.clone();
                 let is_local = matches!(location, SerializedWorkspaceLocation::Local);
+                let workspace = self.workspace.clone();
 
                 if is_local {
-                    let paths = paths.clone();
                     let paths = paths.paths().to_vec();
-                    self.workspace
-                        .update(cx, |workspace, cx| {
+                    cx.spawn_in(window, async move |_, cx| {
+                        let _ = workspace.update_in(cx, |workspace, window, cx| {
                             workspace
                                 .open_workspace_for_paths(true, paths, window, cx)
-                                .detach_and_log_err(cx);
-                        })
-                        .log_err();
+                                .detach();
+                        });
+                    })
+                    .detach();
                 } else {
                     use zed_actions::OpenRecent;
                     window.dispatch_action(OpenRecent::default().boxed_clone(), cx);

crates/workspace/src/workspace.rs 🔗

@@ -3,7 +3,6 @@ pub mod history_manager;
 pub mod invalid_item_view;
 pub mod item;
 mod modal_layer;
-mod multi_workspace;
 pub mod notifications;
 pub mod pane;
 pub mod pane_group;
@@ -23,10 +22,6 @@ mod workspace_settings;
 
 pub use crate::notifications::NotificationFrame;
 pub use dock::Panel;
-pub use multi_workspace::{
-    DraggedSidebar, MultiWorkspace, NewWorkspaceInWindow, NextWorkspaceInWindow,
-    PreviousWorkspaceInWindow, Sidebar, SidebarEvent, SidebarHandle, ToggleWorkspaceSidebar,
-};
 pub use path_list::PathList;
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
 
@@ -76,8 +71,7 @@ pub use pane_group::{
 use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
 pub use persistence::{
     DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
-    model::{ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace},
-    read_serialized_multi_workspaces,
+    model::{ItemId, SerializedWorkspaceLocation},
 };
 use postage::stream::Stream;
 use project::{
@@ -568,27 +562,9 @@ pub struct OpenTerminal {
     pub local: bool,
 }
 
-#[derive(
-    Clone,
-    Copy,
-    Debug,
-    Default,
-    Hash,
-    PartialEq,
-    Eq,
-    PartialOrd,
-    Ord,
-    serde::Serialize,
-    serde::Deserialize,
-)]
+#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
 pub struct WorkspaceId(i64);
 
-impl WorkspaceId {
-    pub fn from_i64(value: i64) -> Self {
-        Self(value)
-    }
-}
-
 impl StaticColumnCount for WorkspaceId {}
 impl Bind for WorkspaceId {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
@@ -623,14 +599,11 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
                 cx.update(|cx| {
                     if let Some(workspace_window) = cx
                         .active_window()
-                        .and_then(|window| window.downcast::<MultiWorkspace>())
+                        .and_then(|window| window.downcast::<Workspace>())
                     {
                         workspace_window
-                            .update(cx, |multi_workspace, _, cx| {
-                                let workspace = multi_workspace.workspace().clone();
-                                workspace.update(cx, |workspace, cx| {
-                                    workspace.show_portal_error(err.to_string(), cx);
-                                });
+                            .update(cx, |workspace, _, cx| {
+                                workspace.show_portal_error(err.to_string(), cx);
                             })
                             .ok();
                     }
@@ -645,7 +618,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
     component::init();
     theme_preview::init(cx);
     toast_layer::init(cx);
-    history_manager::init(app_state.fs.clone(), cx);
+    history_manager::init(cx);
 
     cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
         .on_action(|_: &Reload, cx| reload(cx))
@@ -996,7 +969,7 @@ struct GlobalAppState(Weak<AppState>);
 impl Global for GlobalAppState {}
 
 pub struct WorkspaceStore {
-    workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
+    workspaces: HashSet<WindowHandle<Workspace>>,
     client: Arc<Client>,
     _subscriptions: Vec<client::Subscription>,
 }
@@ -1482,11 +1455,9 @@ impl Workspace {
 
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
-        let any_window_handle = window.window_handle();
+        let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
         app_state.workspace_store.update(cx, |store, _| {
-            store
-                .workspaces
-                .insert((any_window_handle, weak_handle.clone()));
+            store.workspaces.insert(window_handle);
         });
 
         let mut current_user = app_state.user_store.read(cx).watch_current_user();
@@ -1611,13 +1582,10 @@ impl Workspace {
                 GlobalTheme::reload_theme(cx);
                 GlobalTheme::reload_icon_theme(cx);
             }),
-            cx.on_release({
-                let weak_handle = weak_handle.clone();
-                move |this, cx| {
-                    this.app_state.workspace_store.update(cx, move |store, _| {
-                        store.workspaces.retain(|(_, weak)| weak != &weak_handle);
-                    })
-                }
+            cx.on_release(move |this, cx| {
+                this.app_state.workspace_store.update(cx, move |store, _| {
+                    store.workspaces.remove(&window_handle);
+                })
             }),
         ];
 
@@ -1691,13 +1659,13 @@ impl Workspace {
     pub fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
-        requesting_window: Option<WindowHandle<MultiWorkspace>>,
+        requesting_window: Option<WindowHandle<Workspace>>,
         env: Option<HashMap<String, String>>,
         init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
         cx: &mut App,
     ) -> Task<
         anyhow::Result<(
-            WindowHandle<MultiWorkspace>,
+            WindowHandle<Workspace>,
             Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
         )>,
     > {
@@ -1795,23 +1763,71 @@ impl Workspace {
                 });
             }
 
-            let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
-                if let Some(window) = requesting_window {
-                    let centered_layout = serialized_workspace
-                        .as_ref()
-                        .map(|w| w.centered_layout)
-                        .unwrap_or(false);
+            let window = if let Some(window) = requesting_window {
+                let centered_layout = serialized_workspace
+                    .as_ref()
+                    .map(|w| w.centered_layout)
+                    .unwrap_or(false);
+
+                cx.update_window(window.into(), |_, window, cx| {
+                    window.replace_root(cx, |window, cx| {
+                        let mut workspace = Workspace::new(
+                            Some(workspace_id),
+                            project_handle.clone(),
+                            app_state.clone(),
+                            window,
+                            cx,
+                        );
+
+                        workspace.centered_layout = centered_layout;
+
+                        // Call init callback to add items before window renders
+                        if let Some(init) = init {
+                            init(&mut workspace, window, cx);
+                        }
+
+                        workspace
+                    });
+                })?;
+                window
+            } else {
+                let window_bounds_override = window_bounds_env_override();
+
+                let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
+                    (Some(WindowBounds::Windowed(bounds)), None)
+                } else if let Some(workspace) = serialized_workspace.as_ref()
+                    && let Some(display) = workspace.display
+                    && let Some(bounds) = workspace.window_bounds.as_ref()
+                {
+                    // Reopening an existing workspace - restore its saved bounds
+                    (Some(bounds.0), Some(display))
+                } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
+                    // New or empty workspace - use the last known window bounds
+                    (Some(bounds), Some(display))
+                } else {
+                    // New window - let GPUI's default_bounds() handle cascading
+                    (None, None)
+                };
 
-                    let workspace = window.update(cx, |multi_workspace, window, cx| {
-                        let workspace = cx.new(|cx| {
+                // Use the serialized workspace to construct the new window
+                let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
+                options.window_bounds = window_bounds;
+                let centered_layout = serialized_workspace
+                    .as_ref()
+                    .map(|w| w.centered_layout)
+                    .unwrap_or(false);
+                cx.open_window(options, {
+                    let app_state = app_state.clone();
+                    let project_handle = project_handle.clone();
+                    move |window, cx| {
+                        cx.new(|cx| {
                             let mut workspace = Workspace::new(
                                 Some(workspace_id),
-                                project_handle.clone(),
-                                app_state.clone(),
+                                project_handle,
+                                app_state,
                                 window,
                                 cx,
                             );
-
                             workspace.centered_layout = centered_layout;
 
                             // Call init callback to add items before window renders
@@ -1820,69 +1836,10 @@ impl Workspace {
                             }
 
                             workspace
-                        });
-                        multi_workspace.activate(workspace.clone(), cx);
-                        workspace
-                    })?;
-                    (window, workspace)
-                } else {
-                    let window_bounds_override = window_bounds_env_override();
-
-                    let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
-                        (Some(WindowBounds::Windowed(bounds)), None)
-                    } else if let Some(workspace) = serialized_workspace.as_ref()
-                        && let Some(display) = workspace.display
-                        && let Some(bounds) = workspace.window_bounds.as_ref()
-                    {
-                        // Reopening an existing workspace - restore its saved bounds
-                        (Some(bounds.0), Some(display))
-                    } else if let Some((display, bounds)) =
-                        persistence::read_default_window_bounds()
-                    {
-                        // New or empty workspace - use the last known window bounds
-                        (Some(bounds), Some(display))
-                    } else {
-                        // New window - let GPUI's default_bounds() handle cascading
-                        (None, None)
-                    };
-
-                    // Use the serialized workspace to construct the new window
-                    let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
-                    options.window_bounds = window_bounds;
-                    let centered_layout = serialized_workspace
-                        .as_ref()
-                        .map(|w| w.centered_layout)
-                        .unwrap_or(false);
-                    let window = cx.open_window(options, {
-                        let app_state = app_state.clone();
-                        let project_handle = project_handle.clone();
-                        move |window, cx| {
-                            let workspace = cx.new(|cx| {
-                                let mut workspace = Workspace::new(
-                                    Some(workspace_id),
-                                    project_handle,
-                                    app_state,
-                                    window,
-                                    cx,
-                                );
-                                workspace.centered_layout = centered_layout;
-
-                                // Call init callback to add items before window renders
-                                if let Some(init) = init {
-                                    init(&mut workspace, window, cx);
-                                }
-
-                                workspace
-                            });
-                            cx.new(|cx| MultiWorkspace::new(workspace, cx))
-                        }
-                    })?;
-                    let workspace =
-                        window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
-                            multi_workspace.workspace().clone()
-                        })?;
-                    (window, workspace)
-                };
+                        })
+                    }
+                })?
+            };
 
             notify_if_database_failed(window, cx);
             // Check if this is an empty workspace (no paths to open)
@@ -1895,10 +1852,8 @@ impl Workspace {
                 .unwrap_or(false);
 
             let opened_items = window
-                .update(cx, |_, window, cx| {
-                    workspace.update(cx, |_workspace: &mut Workspace, cx| {
-                        open_items(serialized_workspace, project_paths, window, cx)
-                    })
+                .update(cx, |_workspace, window, cx| {
+                    open_items(serialized_workspace, project_paths, window, cx)
                 })?
                 .await
                 .unwrap_or_default();
@@ -1910,30 +1865,29 @@ impl Workspace {
             if is_empty_workspace && !serialized_workspace_has_paths {
                 if let Some(default_docks) = persistence::read_default_dock_state() {
                     window
-                        .update(cx, |_, window, cx| {
-                            workspace.update(cx, |workspace, cx| {
-                                for (dock, serialized_dock) in [
-                                    (&workspace.right_dock, &default_docks.right),
-                                    (&workspace.left_dock, &default_docks.left),
-                                    (&workspace.bottom_dock, &default_docks.bottom),
-                                ] {
-                                    dock.update(cx, |dock, cx| {
-                                        dock.serialized_dock = Some(serialized_dock.clone());
-                                        dock.restore_state(window, cx);
-                                    });
-                                }
-                                cx.notify();
-                            });
+                        .update(cx, |workspace, window, cx| {
+                            for (dock, serialized_dock) in [
+                                (&mut workspace.right_dock, default_docks.right),
+                                (&mut workspace.left_dock, default_docks.left),
+                                (&mut workspace.bottom_dock, default_docks.bottom),
+                            ]
+                            .iter_mut()
+                            {
+                                dock.update(cx, |dock, cx| {
+                                    dock.serialized_dock = Some(serialized_dock.clone());
+                                    dock.restore_state(window, cx);
+                                });
+                            }
+                            cx.notify();
                         })
                         .log_err();
                 }
             }
 
             window
-                .update(cx, |_, _window, cx| {
-                    workspace.update(cx, |this: &mut Workspace, cx| {
-                        this.update_history(cx);
-                    });
+                .update(cx, |workspace, window, cx| {
+                    window.activate_window();
+                    workspace.update_history(cx);
                 })
                 .log_err();
             Ok((window, opened_items))
@@ -2539,11 +2493,8 @@ impl Workspace {
             let env = self.project.read(cx).cli_environment(cx);
             let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
             cx.spawn_in(window, async move |_vh, cx| {
-                let (multi_workspace_window, _) = task.await?;
-                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
-                    let workspace = multi_workspace.workspace().clone();
-                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
-                })
+                let (workspace, _) = task.await?;
+                workspace.update(cx, callback)
             })
         }
     }
@@ -2569,11 +2520,8 @@ impl Workspace {
             let env = self.project.read(cx).cli_environment(cx);
             let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
             cx.spawn_in(window, async move |_vh, cx| {
-                let (multi_workspace_window, _) = task.await?;
-                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
-                    let workspace = multi_workspace.workspace().clone();
-                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
-                })
+                let (workspace, _) = task.await?;
+                workspace.update(cx, callback)
             })
         }
     }
@@ -2675,7 +2623,7 @@ impl Workspace {
             let workspace_count = cx.update(|_window, cx| {
                 cx.windows()
                     .iter()
-                    .filter(|window| window.downcast::<MultiWorkspace>().is_some())
+                    .filter(|window| window.downcast::<Workspace>().is_some())
                     .count()
             })?;
 
@@ -2688,12 +2636,10 @@ impl Workspace {
                 let remaining_workspaces = cx.update(|_window, cx| {
                     cx.windows()
                         .iter()
-                        .filter_map(|window| window.downcast::<MultiWorkspace>())
-                        .filter_map(|multi_workspace| {
-                            multi_workspace
-                                .update(cx, |multi_workspace, _, cx| {
-                                    multi_workspace.workspace().read(cx).removing
-                                })
+                        .filter_map(|window| window.downcast::<Workspace>())
+                        .filter_map(|workspace| {
+                            workspace
+                                .update(cx, |workspace, _, _| workspace.removing)
                                 .ok()
                         })
                         .filter(|removing| !removing)
@@ -2729,18 +2675,13 @@ impl Workspace {
                 }
                 if close_intent == CloseIntent::ReplaceWindow {
                     _ = active_call.update(cx, |this, cx| {
-                        let multi_workspace = cx
+                        let workspace = cx
                             .windows()
                             .iter()
-                            .filter_map(|window| window.downcast::<MultiWorkspace>())
+                            .filter_map(|window| window.downcast::<Workspace>())
                             .next()
                             .unwrap();
-                        let project = multi_workspace
-                            .read(cx)?
-                            .workspace()
-                            .read(cx)
-                            .project
-                            .clone();
+                        let project = workspace.read(cx)?.project.clone();
                         if project.read(cx).is_shared() {
                             this.unshare_project(project, cx)?;
                         }
@@ -2948,7 +2889,7 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        let window_handle = window.window_handle().downcast::<MultiWorkspace>();
+        let window_handle = window.window_handle().downcast::<Self>();
         let is_remote = self.project.read(cx).is_via_collab();
         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
@@ -5133,27 +5074,21 @@ impl Workspace {
             self.update_window_edited(window, cx);
             return;
         }
-
-        let workspace = self.weak_handle();
-        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
-            return;
-        };
-        let on_release_callback = Box::new(move |cx: &mut App| {
-            window_handle
-                .update(cx, |_, window, cx| {
-                    workspace
-                        .update(cx, |workspace, cx| {
-                            workspace.dirty_items.remove(&item_id);
-                            workspace.update_window_edited(window, cx)
+        if let Some(window_handle) = window.window_handle().downcast::<Self>() {
+            let s = item.on_release(
+                cx,
+                Box::new(move |cx| {
+                    window_handle
+                        .update(cx, |this, window, cx| {
+                            this.dirty_items.remove(&item_id);
+                            this.update_window_edited(window, cx)
                         })
                         .ok();
-                })
-                .ok();
-        });
-
-        let s = item.on_release(cx, on_release_callback);
-        self.dirty_items.insert(item_id, s);
-        self.update_window_edited(window, cx);
+                }),
+            );
+            self.dirty_items.insert(item_id, s);
+            self.update_window_edited(window, cx);
+        }
     }
 
     fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
@@ -6731,11 +6666,8 @@ impl Workspace {
         )
     }
 
-    pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
-        window
-            .root::<MultiWorkspace>()
-            .flatten()
-            .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
+    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
+        window.root().flatten()
     }
 
     pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
@@ -7110,30 +7042,27 @@ enum ActivateInDirectionTarget {
     Dock(Entity<Dock>),
 }
 
-fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
-    window
-        .update(cx, |multi_workspace, _, cx| {
-            let workspace = multi_workspace.workspace().clone();
-            workspace.update(cx, |workspace, cx| {
-                if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
-                    struct DatabaseFailedNotification;
-
-                    workspace.show_notification(
-                        NotificationId::unique::<DatabaseFailedNotification>(),
-                        cx,
-                        |cx| {
-                            cx.new(|cx| {
-                                MessageNotification::new("Failed to load the database file.", cx)
-                                    .primary_message("File an Issue")
-                                    .primary_icon(IconName::Plus)
-                                    .primary_on_click(|window, cx| {
-                                        window.dispatch_action(Box::new(FileBugReport), cx)
-                                    })
-                            })
-                        },
-                    );
-                }
-            });
+fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
+    workspace
+        .update(cx, |workspace, _, cx| {
+            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
+                struct DatabaseFailedNotification;
+
+                workspace.show_notification(
+                    NotificationId::unique::<DatabaseFailedNotification>(),
+                    cx,
+                    |cx| {
+                        cx.new(|cx| {
+                            MessageNotification::new("Failed to load the database file.", cx)
+                                .primary_message("File an Issue")
+                                .primary_icon(IconName::Plus)
+                                .primary_on_click(|window, cx| {
+                                    window.dispatch_action(Box::new(FileBugReport), cx)
+                                })
+                        })
+                    },
+                );
+            }
         })
         .log_err();
 }
@@ -7287,14 +7216,15 @@ impl Render for Workspace {
             .collect::<Vec<_>>();
         let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
 
-        self.actions(div(), window, cx)
-            .key_context(context)
-            .relative()
-            .size_full()
-            .flex()
-            .flex_col()
-            .font(ui_font)
-            .gap_0()
+        client_side_decorations(
+            self.actions(div(), window, cx)
+                .key_context(context)
+                .relative()
+                .size_full()
+                .flex()
+                .flex_col()
+                .font(ui_font)
+                .gap_0()
                 .justify_start()
                 .items_start()
                 .text_color(colors.text)
@@ -7777,7 +7707,10 @@ impl Render for Workspace {
                         })
                         .child(self.modal_layer.clone())
                         .child(self.toast_layer.clone()),
-                )
+                ),
+            window,
+            cx,
+        )
     }
 }
 
@@ -7822,22 +7755,16 @@ impl WorkspaceStore {
             };
 
             let mut response = proto::FollowResponse::default();
-
-            this.workspaces.retain(|(window_handle, weak_workspace)| {
-                let Some(workspace) = weak_workspace.upgrade() else {
-                    return false;
-                };
-                window_handle
-                    .update(cx, |_, window, cx| {
-                        workspace.update(cx, |workspace, cx| {
-                            let handler_response =
-                                workspace.handle_follow(follower.project_id, window, cx);
-                            if let Some(active_view) = handler_response.active_view
-                                && workspace.project.read(cx).remote_id() == follower.project_id
-                            {
-                                response.active_view = Some(active_view)
-                            }
-                        });
+            this.workspaces.retain(|workspace| {
+                workspace
+                    .update(cx, |workspace, window, cx| {
+                        let handler_response =
+                            workspace.handle_follow(follower.project_id, window, cx);
+                        if let Some(active_view) = handler_response.active_view
+                            && workspace.project.read(cx).remote_id() == follower.project_id
+                        {
+                            response.active_view = Some(active_view)
+                        }
                     })
                     .is_ok()
             });
@@ -7855,24 +7782,14 @@ impl WorkspaceStore {
         let update = envelope.payload;
 
         this.update(&mut cx, |this, cx| {
-            this.workspaces.retain(|(window_handle, weak_workspace)| {
-                let Some(workspace) = weak_workspace.upgrade() else {
-                    return false;
-                };
-                window_handle
-                    .update(cx, |_, window, cx| {
-                        workspace.update(cx, |workspace, cx| {
-                            let project_id = workspace.project.read(cx).remote_id();
-                            if update.project_id != project_id && update.project_id.is_some() {
-                                return;
-                            }
-                            workspace.handle_update_followers(
-                                leader_id,
-                                update.clone(),
-                                window,
-                                cx,
-                            );
-                        });
+            this.workspaces.retain(|workspace| {
+                workspace
+                    .update(cx, |workspace, window, cx| {
+                        let project_id = workspace.project.read(cx).remote_id();
+                        if update.project_id != project_id && update.project_id.is_some() {
+                            return;
+                        }
+                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
                     })
                     .is_ok()
             });
@@ -7880,14 +7797,8 @@ impl WorkspaceStore {
         })
     }
 
-    pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
-        self.workspaces.iter().map(|(_, weak)| weak)
-    }
-
-    pub fn workspaces_with_windows(
-        &self,
-    ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
-        self.workspaces.iter().map(|(window, weak)| (*window, weak))
+    pub fn workspaces(&self) -> &HashSet<WindowHandle<Workspace>> {
+        &self.workspaces
     }
 }
 
@@ -7939,119 +7850,19 @@ impl WorkspaceHandle for Entity<Workspace> {
     }
 }
 
-pub async fn last_opened_workspace_location(
-    fs: &dyn fs::Fs,
-) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
-    DB.last_workspace(fs).await.log_err().flatten()
+pub async fn last_opened_workspace_location()
+-> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
+    DB.last_workspace().await.log_err().flatten()
 }
 
-pub async fn last_session_workspace_locations(
+pub fn last_session_workspace_locations(
     last_session_id: &str,
     last_session_window_stack: Option<Vec<WindowId>>,
-    fs: &dyn fs::Fs,
-) -> Option<Vec<SessionWorkspace>> {
-    DB.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
-        .await
+) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
+    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
         .log_err()
 }
 
-pub async fn restore_multiworkspace(
-    multi_workspace: SerializedMultiWorkspace,
-    app_state: Arc<AppState>,
-    cx: &mut AsyncApp,
-) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
-    let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
-    let mut group_iter = workspaces.into_iter();
-    let first = group_iter
-        .next()
-        .context("window group must not be empty")?;
-
-    let window_handle = if first.paths.is_empty() {
-        cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
-            .await?
-    } else {
-        let (window, _items) = cx
-            .update(|cx| {
-                Workspace::new_local(
-                    first.paths.paths().to_vec(),
-                    app_state.clone(),
-                    None,
-                    None,
-                    None,
-                    cx,
-                )
-            })
-            .await?;
-        window
-    };
-
-    for session_workspace in group_iter {
-        if session_workspace.paths.is_empty() {
-            cx.update(|cx| {
-                open_workspace_by_id(
-                    session_workspace.workspace_id,
-                    app_state.clone(),
-                    Some(window_handle),
-                    cx,
-                )
-            })
-            .await?;
-        } else {
-            cx.update(|cx| {
-                Workspace::new_local(
-                    session_workspace.paths.paths().to_vec(),
-                    app_state.clone(),
-                    Some(window_handle),
-                    None,
-                    None,
-                    cx,
-                )
-            })
-            .await?;
-        }
-    }
-
-    if let Some(target_id) = state.active_workspace_id {
-        window_handle
-            .update(cx, |multi_workspace, window, cx| {
-                let target_index = multi_workspace
-                    .workspaces()
-                    .iter()
-                    .position(|ws| ws.read(cx).database_id() == Some(target_id));
-                if let Some(index) = target_index {
-                    multi_workspace.activate_index(index, window, cx);
-                } else if !multi_workspace.workspaces().is_empty() {
-                    multi_workspace.activate_index(0, window, cx);
-                }
-            })
-            .ok();
-    } else {
-        window_handle
-            .update(cx, |multi_workspace, window, cx| {
-                if !multi_workspace.workspaces().is_empty() {
-                    multi_workspace.activate_index(0, window, cx);
-                }
-            })
-            .ok();
-    }
-
-    if state.sidebar_open {
-        window_handle
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.open_sidebar(window, cx);
-            })
-            .ok();
-    }
-
-    window_handle
-        .update(cx, |_, window, _cx| {
-            window.activate_window();
-        })
-        .ok();
-
-    Ok(window_handle)
-}
-
 actions!(
     collab,
     [
@@ -8091,8 +7902,7 @@ actions!(
 async fn join_channel_internal(
     channel_id: ChannelId,
     app_state: &Arc<AppState>,
-    requesting_window: Option<WindowHandle<MultiWorkspace>>,
-    requesting_workspace: Option<WeakEntity<Workspace>>,
+    requesting_window: Option<WindowHandle<Workspace>>,
     active_call: &Entity<ActiveCall>,
     cx: &mut AsyncApp,
 ) -> Result<bool> {
@@ -8128,8 +7938,8 @@ async fn join_channel_internal(
     }
 
     if should_prompt {
-        if let Some(multi_workspace) = requesting_window {
-            let answer = multi_workspace
+        if let Some(workspace) = requesting_window {
+            let answer = workspace
                 .update(cx, |_, window, cx| {
                     window.prompt(
                         PromptLevel::Warning,
@@ -8198,9 +8008,9 @@ async fn join_channel_internal(
         // If you are the first to join a channel, see if you should share your project.
         if room.remote_participants().is_empty()
             && !room.local_participant_is_guest()
-            && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
+            && let Some(workspace) = requesting_window
         {
-            let project = workspace.update(cx, |workspace, cx| {
+            let project = workspace.update(cx, |workspace, _, cx| {
                 let project = workspace.project.read(cx);
 
                 if !CallSettings::get_global(cx).share_on_join {
@@ -8219,7 +8029,7 @@ async fn join_channel_internal(
                     None
                 }
             });
-            if let Some(project) = project {
+            if let Ok(Some(project)) = project {
                 return Some(cx.spawn(async move |room, cx| {
                     room.update(cx, |room, cx| room.share_project(project, cx))?
                         .await?;
@@ -8240,21 +8050,14 @@ async fn join_channel_internal(
 pub fn join_channel(
     channel_id: ChannelId,
     app_state: Arc<AppState>,
-    requesting_window: Option<WindowHandle<MultiWorkspace>>,
-    requesting_workspace: Option<WeakEntity<Workspace>>,
+    requesting_window: Option<WindowHandle<Workspace>>,
     cx: &mut App,
 ) -> Task<Result<()>> {
     let active_call = ActiveCall::global(cx);
     cx.spawn(async move |cx| {
-        let result = join_channel_internal(
-            channel_id,
-            &app_state,
-            requesting_window,
-            requesting_workspace,
-            &active_call,
-            cx,
-        )
-        .await;
+        let result =
+            join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx)
+                .await;
 
         // join channel succeeded, and opened a window
         if matches!(result, Ok(true)) {
@@ -8278,12 +8081,6 @@ pub fn join_channel(
                 })
                 .await?;
 
-            window_handle
-                .update(cx, |_, window, _cx| {
-                    window.activate_window();
-                })
-                .ok();
-
             if result.is_ok() {
                 cx.update(|cx| {
                     cx.dispatch_action(&OpenChannelNotes);
@@ -8338,10 +8135,10 @@ pub fn join_channel(
     })
 }
 
-pub async fn get_any_active_multi_workspace(
+pub async fn get_any_active_workspace(
     app_state: Arc<AppState>,
     mut cx: AsyncApp,
-) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
+) -> anyhow::Result<WindowHandle<Workspace>> {
     // find an existing workspace to focus and show call controls
     let active_window = activate_any_workspace_window(&mut cx);
     if active_window.is_none() {
@@ -8351,17 +8148,17 @@ pub async fn get_any_active_multi_workspace(
     activate_any_workspace_window(&mut cx).context("could not open zed")
 }
 
-fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
+fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
     cx.update(|cx| {
         if let Some(workspace_window) = cx
             .active_window()
-            .and_then(|window| window.downcast::<MultiWorkspace>())
+            .and_then(|window| window.downcast::<Workspace>())
         {
             return Some(workspace_window);
         }
 
         for window in cx.windows() {
-            if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
+            if let Some(workspace_window) = window.downcast::<Workspace>() {
                 workspace_window
                     .update(cx, |_, window, _| window.activate_window())
                     .ok();
@@ -8372,17 +8169,14 @@ fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Multi
     })
 }
 
-pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
+pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
     cx.windows()
         .into_iter()
-        .filter_map(|window| window.downcast::<MultiWorkspace>())
-        .filter(|multi_workspace| {
-            multi_workspace.read(cx).is_ok_and(|multi_workspace| {
-                multi_workspace
-                    .workspaces()
-                    .iter()
-                    .any(|workspace| workspace.read(cx).project.read(cx).is_local())
-            })
+        .filter_map(|window| window.downcast::<Workspace>())
+        .filter(|workspace| {
+            workspace
+                .read(cx)
+                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
         })
         .collect()
 }
@@ -8393,7 +8187,7 @@ pub struct OpenOptions {
     pub focus: Option<bool>,
     pub open_new_workspace: Option<bool>,
     pub prefer_focused_window: bool,
-    pub replace_window: Option<WindowHandle<MultiWorkspace>>,
+    pub replace_window: Option<WindowHandle<Workspace>>,
     pub env: Option<HashMap<String, String>>,
 }
 
@@ -8401,9 +8195,8 @@ pub struct OpenOptions {
 pub fn open_workspace_by_id(
     workspace_id: WorkspaceId,
     app_state: Arc<AppState>,
-    requesting_window: Option<WindowHandle<MultiWorkspace>>,
     cx: &mut App,
-) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
+) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
     let project_handle = Project::local(
         app_state.client.clone(),
         app_state.node_runtime.clone(),
@@ -8423,87 +8216,52 @@ pub fn open_workspace_by_id(
             .workspace_for_id(workspace_id)
             .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
 
-        let centered_layout = serialized_workspace.centered_layout;
+        let window_bounds_override = window_bounds_env_override();
 
-        let (window, workspace) = if let Some(window) = requesting_window {
-            let workspace = window.update(cx, |multi_workspace, window, cx| {
-                let workspace = cx.new(|cx| {
-                    let mut workspace = Workspace::new(
-                        Some(workspace_id),
-                        project_handle.clone(),
-                        app_state.clone(),
-                        window,
-                        cx,
-                    );
-                    workspace.centered_layout = centered_layout;
-                    workspace
-                });
-                multi_workspace.add_workspace(workspace.clone(), cx);
-                workspace
-            })?;
-            (window, workspace)
+        let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
+            (Some(WindowBounds::Windowed(bounds)), None)
+        } else if let Some(display) = serialized_workspace.display
+            && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
+        {
+            (Some(bounds.0), Some(display))
+        } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
+            (Some(bounds), Some(display))
         } else {
-            let window_bounds_override = window_bounds_env_override();
-
-            let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
-                (Some(WindowBounds::Windowed(bounds)), None)
-            } else if let Some(display) = serialized_workspace.display
-                && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
-            {
-                (Some(bounds.0), Some(display))
-            } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
-                (Some(bounds), Some(display))
-            } else {
-                (None, None)
-            };
-
-            let options = cx.update(|cx| {
-                let mut options = (app_state.build_window_options)(display, cx);
-                options.window_bounds = window_bounds;
-                options
-            });
-
-            let window = cx.open_window(options, {
-                let app_state = app_state.clone();
-                let project_handle = project_handle.clone();
-                move |window, cx| {
-                    let workspace = cx.new(|cx| {
-                        let mut workspace = Workspace::new(
-                            Some(workspace_id),
-                            project_handle,
-                            app_state,
-                            window,
-                            cx,
-                        );
-                        workspace.centered_layout = centered_layout;
-                        workspace
-                    });
-                    cx.new(|cx| MultiWorkspace::new(workspace, cx))
-                }
-            })?;
+            (None, None)
+        };
 
-            let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
-                multi_workspace.workspace().clone()
-            })?;
+        let options = cx.update(|cx| {
+            let mut options = (app_state.build_window_options)(display, cx);
+            options.window_bounds = window_bounds;
+            options
+        });
+        let centered_layout = serialized_workspace.centered_layout;
 
-            (window, workspace)
-        };
+        let window = cx.open_window(options, {
+            let app_state = app_state.clone();
+            let project_handle = project_handle.clone();
+            move |window, cx| {
+                cx.new(|cx| {
+                    let mut workspace =
+                        Workspace::new(Some(workspace_id), project_handle, app_state, window, cx);
+                    workspace.centered_layout = centered_layout;
+                    workspace
+                })
+            }
+        })?;
 
         notify_if_database_failed(window, cx);
 
         // Restore items from the serialized workspace
         window
-            .update(cx, |_, window, cx| {
-                workspace.update(cx, |_workspace, cx| {
-                    open_items(Some(serialized_workspace), vec![], window, cx)
-                })
+            .update(cx, |_workspace, window, cx| {
+                open_items(Some(serialized_workspace), vec![], window, cx)
             })?
             .await?;
 
-        window.update(cx, |_, window, cx| {
-            workspace.update(cx, |workspace, cx| {
-                workspace.serialize_workspace(window, cx);
-            });
+        window.update(cx, |workspace, window, cx| {
+            window.activate_window();
+            workspace.serialize_workspace(window, cx);
         })?;
 
         Ok(window)

crates/zed/Cargo.toml 🔗

@@ -49,7 +49,6 @@ visual-tests = [
     "language_model/test-support",
     "fs/test-support",
     "recent_projects/test-support",
-    "sidebar/test-support",
     "title_bar/test-support",
 ]
 
@@ -188,7 +187,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/main.rs 🔗

@@ -54,8 +54,8 @@ use theme::{ActiveTheme, GlobalTheme, ThemeRegistry};
 use util::{ResultExt, TryFutureExt, maybe};
 use uuid::Uuid;
 use workspace::{
-    AppState, MultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace, Toast,
-    WorkspaceSettings, WorkspaceStore, notifications::NotificationId, restore_multiworkspace,
+    AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceId,
+    WorkspaceSettings, WorkspaceStore, notifications::NotificationId,
 };
 use zed::{
     OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options,
@@ -511,13 +511,15 @@ fn main() {
                 let workspace_store = workspace_store.clone();
                 Arc::new(move |cx: &mut App| {
                     workspace_store.update(cx, |workspace_store, cx| {
-                        Ok(workspace_store
+                        workspace_store
                             .workspaces()
-                            .filter_map(|weak| weak.upgrade())
-                            .map(|workspace: gpui::Entity<workspace::Workspace>| {
-                                workspace.read(cx).project().read(cx).lsp_store()
+                            .iter()
+                            .map(|workspace| {
+                                workspace.update(cx, |workspace, _, cx| {
+                                    workspace.project().read(cx).lsp_store()
+                                })
                             })
-                            .collect())
+                            .collect()
                     })
                 })
             }),
@@ -847,7 +849,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
             OpenRequestKind::Extension { extension_id } => {
                 cx.spawn(async move |cx| {
                     let workspace =
-                        workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
+                        workspace::get_any_active_workspace(app_state, cx.clone()).await?;
                     workspace.update(cx, |_, window, cx| {
                         window.dispatch_action(
                             Box::new(zed_actions::Extensions {
@@ -862,40 +864,31 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
             }
             OpenRequestKind::AgentPanel { initial_prompt } => {
                 cx.spawn(async move |cx| {
-                    let multi_workspace =
-                        workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
-
-                    multi_workspace.update(cx, |multi_workspace, window, cx| {
-                        multi_workspace.workspace().update(cx, |workspace, cx| {
-                            if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
-                                panel.update(cx, |panel, cx| {
-                                    panel.new_external_thread_with_text(initial_prompt, window, cx);
-                                });
-                            }
-                        });
+                    let workspace =
+                        workspace::get_any_active_workspace(app_state, cx.clone()).await?;
+                    workspace.update(cx, |workspace, window, cx| {
+                        if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel.new_external_thread_with_text(initial_prompt, window, cx);
+                            });
+                        }
                     })
                 })
                 .detach_and_log_err(cx);
             }
             OpenRequestKind::SharedAgentThread { session_id } => {
                 cx.spawn(async move |cx| {
-                    let multi_workspace =
-                        workspace::get_any_active_multi_workspace(app_state.clone(), cx.clone())
-                            .await?;
-
                     let workspace =
-                        multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?;
+                        workspace::get_any_active_workspace(app_state.clone(), cx.clone()).await?;
 
                     let (client, thread_store) =
-                        multi_workspace.update(cx, |_, _window, cx| {
-                            workspace.update(cx, |workspace, cx| {
-                                let client = workspace.project().read(cx).client();
-                                let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
-                                    .panel::<AgentPanel>(cx)
-                                    .map(|panel| panel.read(cx).thread_store().clone());
-                                anyhow::Ok((client, thread_store))
-                            })
-                        })??;
+                        workspace.update(cx, |workspace, _window, cx| {
+                            let client = workspace.project().read(cx).client();
+                            let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
+                                .panel::<AgentPanel>(cx)
+                                .map(|panel| panel.read(cx).thread_store().clone());
+                            (client, thread_store)
+                        })?;
 
                     let Some(thread_store): Option<gpui::Entity<ThreadStore>> = thread_store else {
                         anyhow::bail!("Agent panel not available");
@@ -928,27 +921,25 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                         meta: None,
                     };
 
-                    let sharer_username = response.sharer_username.clone();
-
-                    multi_workspace.update(cx, |_, window, cx| {
-                        workspace.update(cx, |workspace, cx| {
-                            if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                                panel.update(cx, |panel, cx| {
-                                    panel.open_thread(thread_metadata, window, cx);
-                                });
-                                panel.focus_handle(cx).focus(window, cx);
-                            }
+                    workspace.update(cx, |workspace, window, cx| {
+                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel.open_thread(thread_metadata, window, cx);
+                            });
+                            panel.focus_handle(cx).focus(window, cx);
+                        }
+                    })?;
 
-                            struct ImportedThreadToast;
-                            workspace.show_toast(
-                                Toast::new(
-                                    NotificationId::unique::<ImportedThreadToast>(),
-                                    format!("Imported shared thread from {}", sharer_username),
-                                )
-                                .autohide(),
-                                cx,
-                            );
-                        });
+                    workspace.update(cx, |workspace, _window, cx| {
+                        struct ImportedThreadToast;
+                        workspace.show_toast(
+                            Toast::new(
+                                NotificationId::unique::<ImportedThreadToast>(),
+                                format!("Imported shared thread from {}", response.sharer_username),
+                            )
+                            .autohide(),
+                            cx,
+                        );
                     })?;
 
                     anyhow::Ok(())
@@ -1023,7 +1014,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                 // [ languages $(language) tab_size]
                 cx.spawn(async move |cx| {
                     let workspace =
-                        workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
+                        workspace::get_any_active_workspace(app_state, cx.clone()).await?;
 
                     workspace.update(cx, |_, window, cx| match setting_path {
                         None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx),
@@ -1085,29 +1076,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                     .await?;
 
                     workspace
-                        .update(cx, |multi_workspace, window, cx| {
-                            multi_workspace
-                                .workspace()
-                                .clone()
-                                .update(cx, |workspace, cx| {
-                                    let Some(repo) =
-                                        workspace.project().read(cx).active_repository(cx)
-                                    else {
-                                        log::error!("no active repository found for commit view");
-                                        return Err(anyhow::anyhow!("no active repository found"));
-                                    };
-
-                                    git_ui::commit_view::CommitView::open(
-                                        sha,
-                                        repo.downgrade(),
-                                        workspace.weak_handle(),
-                                        None,
-                                        None,
-                                        window,
-                                        cx,
-                                    );
-                                    Ok(())
-                                })
+                        .update(cx, |workspace, window, cx| {
+                            let Some(repo) = workspace.project().read(cx).active_repository(cx)
+                            else {
+                                log::error!("no active repository found for commit view");
+                                return Err(anyhow::anyhow!("no active repository found"));
+                            };
+
+                            git_ui::commit_view::CommitView::open(
+                                sha,
+                                repo.downgrade(),
+                                workspace.weak_handle(),
+                                None,
+                                None,
+                                window,
+                                cx,
+                            );
+                            Ok(())
                         })
                         .log_err();
 
@@ -1177,7 +1162,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                             client::ChannelId(channel_id),
                             app_state.clone(),
                             None,
-                            None,
                             cx,
                         )
                     })
@@ -1185,9 +1169,8 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                 }
 
                 let workspace_window =
-                    workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
-
-                let workspace = workspace_window.read_with(cx, |mw, _| mw.workspace().clone())?;
+                    workspace::get_any_active_workspace(app_state, cx.clone()).await?;
+                let workspace = workspace_window.entity(cx)?;
 
                 let mut promises = Vec::new();
                 for (channel_id, heading) in request.open_channel_notes {
@@ -1277,53 +1260,78 @@ async fn installation_id() -> Result<IdType> {
     Ok(IdType::New(installation_id))
 }
 
-pub(crate) async fn restore_or_create_workspace(
-    app_state: Arc<AppState>,
-    cx: &mut AsyncApp,
-) -> Result<()> {
-    if let Some((multi_workspaces, remote_workspaces)) = restorable_workspaces(cx, &app_state).await
-    {
+async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp) -> Result<()> {
+    if let Some(locations) = restorable_workspace_locations(cx, &app_state).await {
+        let use_system_window_tabs =
+            cx.update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs);
         let mut results: Vec<Result<(), Error>> = Vec::new();
         let mut tasks = Vec::new();
 
-        let mut local_results = Vec::new();
-        for multi_workspace in multi_workspaces {
-            local_results
-                .push(restore_multiworkspace(multi_workspace, app_state.clone(), cx).await);
-        }
-
-        for result in local_results {
-            results.push(result.map(|_| ()));
-        }
+        for (index, (workspace_id, location, paths)) in locations.into_iter().enumerate() {
+            match location {
+                SerializedWorkspaceLocation::Local if paths.is_empty() => {
+                    // Restore empty workspace by ID (has items like drafts but no folders)
+                    let app_state = app_state.clone();
+                    let task = cx.spawn(async move |cx| {
+                        let open_task = cx.update(|cx| {
+                            workspace::open_workspace_by_id(workspace_id, app_state, cx)
+                        });
+                        open_task.await.map(|_| ())
+                    });
 
-        for session_workspace in remote_workspaces {
-            let app_state = app_state.clone();
-            let SerializedWorkspaceLocation::Remote(mut connection_options) =
-                session_workspace.location
-            else {
-                continue;
-            };
-            let paths = session_workspace.paths;
-            if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
-                cx.update(|cx| {
-                    RemoteSettings::get_global(cx).fill_connection_options_from_settings(options)
-                });
+                    if use_system_window_tabs && index == 0 {
+                        results.push(task.await);
+                    } else {
+                        tasks.push(task);
+                    }
+                }
+                SerializedWorkspaceLocation::Local => {
+                    let app_state = app_state.clone();
+                    let task = cx.spawn(async move |cx| {
+                        let open_task = cx.update(|cx| {
+                            workspace::open_paths(
+                                &paths.paths(),
+                                app_state,
+                                workspace::OpenOptions::default(),
+                                cx,
+                            )
+                        });
+                        open_task.await.map(|_| ())
+                    });
+
+                    // If we're using system window tabs and this is the first workspace,
+                    // wait for it to finish so that the other windows can be added as tabs.
+                    if use_system_window_tabs && index == 0 {
+                        results.push(task.await);
+                    } else {
+                        tasks.push(task);
+                    }
+                }
+                SerializedWorkspaceLocation::Remote(mut connection_options) => {
+                    let app_state = app_state.clone();
+                    if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
+                        cx.update(|cx| {
+                            RemoteSettings::get_global(cx)
+                                .fill_connection_options_from_settings(options)
+                        });
+                    }
+                    let task = cx.spawn(async move |cx| {
+                        recent_projects::open_remote_project(
+                            connection_options,
+                            paths.paths().into_iter().map(PathBuf::from).collect(),
+                            app_state,
+                            workspace::OpenOptions::default(),
+                            cx,
+                        )
+                        .await
+                        .map_err(|e| anyhow::anyhow!(e))
+                    });
+                    tasks.push(task);
+                }
             }
-            let task = cx.spawn(async move |cx| {
-                recent_projects::open_remote_project(
-                    connection_options,
-                    paths.paths().iter().map(PathBuf::from).collect(),
-                    app_state,
-                    workspace::OpenOptions::default(),
-                    cx,
-                )
-                .await
-                .map_err(|e| anyhow::anyhow!(e))
-            });
-            tasks.push(task);
         }
 
-        // Wait for all window groups and remote workspaces to open concurrently
+        // Wait for all workspaces to open concurrently
         results.extend(future::join_all(tasks).await);
 
         // Show notifications for any errors that occurred
@@ -1348,16 +1356,12 @@ pub(crate) async fn restore_or_create_workspace(
             // Try to find an active workspace to show the toast
             let toast_shown = cx.update(|cx| {
                 if let Some(window) = cx.active_window()
-                    && let Some(multi_workspace) = window.downcast::<MultiWorkspace>()
+                    && let Some(workspace) = window.downcast::<Workspace>()
                 {
-                    multi_workspace
-                        .update(cx, |multi_workspace, _, cx| {
-                            multi_workspace.workspace().update(cx, |workspace, cx| {
-                                workspace.show_toast(
-                                    Toast::new(NotificationId::unique::<()>(), message),
-                                    cx,
-                                )
-                            });
+                    workspace
+                        .update(cx, |workspace, _, cx| {
+                            workspace
+                                .show_toast(Toast::new(NotificationId::unique::<()>(), message), cx)
                         })
                         .ok();
                     return true;
@@ -1398,25 +1402,10 @@ pub(crate) async fn restore_or_create_workspace(
     Ok(())
 }
 
-async fn restorable_workspaces(
-    cx: &mut AsyncApp,
-    app_state: &Arc<AppState>,
-) -> Option<(
-    Vec<workspace::SerializedMultiWorkspace>,
-    Vec<SessionWorkspace>,
-)> {
-    let locations = restorable_workspace_locations(cx, app_state).await?;
-    let (remote_workspaces, local_workspaces) = locations
-        .into_iter()
-        .partition(|sw| matches!(sw.location, SerializedWorkspaceLocation::Remote(_)));
-    let multi_workspaces = workspace::read_serialized_multi_workspaces(local_workspaces);
-    Some((multi_workspaces, remote_workspaces))
-}
-
 pub(crate) async fn restorable_workspace_locations(
     cx: &mut AsyncApp,
     app_state: &Arc<AppState>,
-) -> Option<Vec<SessionWorkspace>> {
+) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
     let mut restore_behavior = cx.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup);
 
     let session_handle = app_state.session.clone();
@@ -1440,16 +1429,9 @@ pub(crate) async fn restorable_workspace_locations(
 
     match restore_behavior {
         workspace::RestoreOnStartupBehavior::LastWorkspace => {
-            workspace::last_opened_workspace_location(app_state.fs.as_ref())
+            workspace::last_opened_workspace_location()
                 .await
-                .map(|(workspace_id, location, paths)| {
-                    vec![SessionWorkspace {
-                        workspace_id,
-                        location,
-                        paths,
-                        window_id: None,
-                    }]
-                })
+                .map(|location| vec![location])
         }
         workspace::RestoreOnStartupBehavior::LastSession => {
             if let Some(last_session_id) = last_session_id {
@@ -1458,9 +1440,7 @@ pub(crate) async fn restorable_workspace_locations(
                 let mut locations = workspace::last_session_workspace_locations(
                     &last_session_id,
                     last_session_window_stack,
-                    app_state.fs.as_ref(),
                 )
-                .await
                 .filter(|locations| !locations.is_empty());
 
                 // Since last_session_window_order returns the windows ordered front-to-back

crates/zed/src/visual_test_runner.rs 🔗

@@ -59,7 +59,6 @@ use {
     },
     image::RgbaImage,
     project_panel::ProjectPanel,
-    recent_projects::RecentProjectEntry,
     settings::{NotifyWhenAgentWaiting, Settings as _},
     settings_ui::SettingsWindow,
     std::{
@@ -71,7 +70,7 @@ use {
     },
     util::ResultExt as _,
     watch,
-    workspace::{AppState, MultiWorkspace, Workspace, WorkspaceId},
+    workspace::{AppState, Workspace},
     zed_actions::OpenSettingsAt,
 };
 
@@ -436,24 +435,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
         }
     }
 
-    // Run Test 3: Multi-workspace sidebar visual tests
-    println!("\n--- Test 3: multi_workspace_sidebar ---");
-    match run_multi_workspace_sidebar_visual_tests(app_state.clone(), &mut cx, update_baseline) {
-        Ok(TestResult::Passed) => {
-            println!("✓ multi_workspace_sidebar: PASSED");
-            passed += 1;
-        }
-        Ok(TestResult::BaselineUpdated(_)) => {
-            println!("✓ multi_workspace_sidebar: Baselines updated");
-            updated += 1;
-        }
-        Err(e) => {
-            eprintln!("✗ multi_workspace_sidebar: FAILED - {}", e);
-            failed += 1;
-        }
-    }
-
-    // Run Test 4: Agent Thread View tests
+    // Run Test 3: Agent Thread View tests
     #[cfg(feature = "visual-tests")]
     {
         println!("\n--- Test 3: agent_thread_with_image (collapsed + expanded) ---");
@@ -2799,300 +2781,3 @@ fn run_tool_permissions_visual_tests(
     // Return success - we're just capturing screenshots, not comparing baselines
     Ok(TestResult::Passed)
 }
-
-#[cfg(target_os = "macos")]
-fn run_multi_workspace_sidebar_visual_tests(
-    app_state: Arc<AppState>,
-    cx: &mut VisualTestAppContext,
-    update_baseline: bool,
-) -> Result<TestResult> {
-    // Create temporary directories to act as worktrees for active workspaces
-    let temp_dir = tempfile::tempdir()?;
-    let temp_path = temp_dir.keep();
-    let canonical_temp = temp_path.canonicalize()?;
-
-    let workspace1_dir = canonical_temp.join("private-test-remote");
-    let workspace2_dir = canonical_temp.join("zed");
-    std::fs::create_dir_all(&workspace1_dir)?;
-    std::fs::create_dir_all(&workspace2_dir)?;
-
-    // Create directories for recent projects (they must exist on disk for display)
-    let recent1_dir = canonical_temp.join("tiny-project");
-    let recent2_dir = canonical_temp.join("font-kit");
-    let recent3_dir = canonical_temp.join("ideas");
-    let recent4_dir = canonical_temp.join("tmp");
-    std::fs::create_dir_all(&recent1_dir)?;
-    std::fs::create_dir_all(&recent2_dir)?;
-    std::fs::create_dir_all(&recent3_dir)?;
-    std::fs::create_dir_all(&recent4_dir)?;
-
-    // Enable the agent-v2 feature flag so multi-workspace is active
-    cx.update(|cx| {
-        cx.update_flags(true, vec!["agent-v2".to_string()]);
-    });
-
-    // Create both projects upfront so we can build both workspaces during
-    // window creation, before the MultiWorkspace entity exists.
-    // This avoids a re-entrant read panic that occurs when Workspace::new
-    // tries to access the window root (MultiWorkspace) while it's being updated.
-    let project1 = cx.update(|cx| {
-        project::Project::local(
-            app_state.client.clone(),
-            app_state.node_runtime.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
-            None,
-            project::LocalProjectFlags {
-                init_worktree_trust: false,
-                ..Default::default()
-            },
-            cx,
-        )
-    });
-
-    let project2 = cx.update(|cx| {
-        project::Project::local(
-            app_state.client.clone(),
-            app_state.node_runtime.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
-            None,
-            project::LocalProjectFlags {
-                init_worktree_trust: false,
-                ..Default::default()
-            },
-            cx,
-        )
-    });
-
-    let window_size = size(px(1280.0), px(800.0));
-    let bounds = Bounds {
-        origin: point(px(0.0), px(0.0)),
-        size: window_size,
-    };
-
-    // Open a MultiWorkspace window with both workspaces created at construction time
-    let multi_workspace_window: WindowHandle<MultiWorkspace> = cx
-        .update(|cx| {
-            cx.open_window(
-                WindowOptions {
-                    window_bounds: Some(WindowBounds::Windowed(bounds)),
-                    focus: false,
-                    show: false,
-                    ..Default::default()
-                },
-                |window, cx| {
-                    let workspace1 = cx.new(|cx| {
-                        Workspace::new(None, project1.clone(), app_state.clone(), window, cx)
-                    });
-                    let workspace2 = cx.new(|cx| {
-                        Workspace::new(None, project2.clone(), app_state.clone(), window, cx)
-                    });
-                    cx.new(|cx| {
-                        let mut multi_workspace = MultiWorkspace::new(workspace1, cx);
-                        multi_workspace.activate(workspace2, cx);
-                        multi_workspace
-                    })
-                },
-            )
-        })
-        .context("Failed to open MultiWorkspace window")?;
-
-    cx.run_until_parked();
-
-    // Add worktree to workspace 1 (index 0) so it shows as "private-test-remote"
-    let add_worktree1_task = multi_workspace_window
-        .update(cx, |multi_workspace, _window, cx| {
-            let workspace1 = &multi_workspace.workspaces()[0];
-            let project = workspace1.read(cx).project().clone();
-            project.update(cx, |project, cx| {
-                project.find_or_create_worktree(&workspace1_dir, true, cx)
-            })
-        })
-        .context("Failed to start adding worktree 1")?;
-
-    cx.background_executor.allow_parking();
-    cx.foreground_executor
-        .block_test(add_worktree1_task)
-        .context("Failed to add worktree 1")?;
-    cx.background_executor.forbid_parking();
-
-    cx.run_until_parked();
-
-    // Add worktree to workspace 2 (index 1) so it shows as "zed"
-    let add_worktree2_task = multi_workspace_window
-        .update(cx, |multi_workspace, _window, cx| {
-            let workspace2 = &multi_workspace.workspaces()[1];
-            let project = workspace2.read(cx).project().clone();
-            project.update(cx, |project, cx| {
-                project.find_or_create_worktree(&workspace2_dir, true, cx)
-            })
-        })
-        .context("Failed to start adding worktree 2")?;
-
-    cx.background_executor.allow_parking();
-    cx.foreground_executor
-        .block_test(add_worktree2_task)
-        .context("Failed to add worktree 2")?;
-    cx.background_executor.forbid_parking();
-
-    cx.run_until_parked();
-
-    // Switch to workspace 1 so it's highlighted as active (index 0)
-    multi_workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.activate_index(0, window, cx);
-        })
-        .context("Failed to activate workspace 1")?;
-
-    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();
-
-    // Inject recent project entries into the sidebar.
-    // We update the sidebar entity directly (not through the MultiWorkspace window update)
-    // to avoid a re-entrant read panic: rebuild_entries reads MultiWorkspace, so we can't
-    // be inside a MultiWorkspace update when that happens.
-    cx.update(|cx| {
-        sidebar.update(cx, |sidebar, cx| {
-            let recent_projects = vec![
-                RecentProjectEntry {
-                    name: "tiny-project".into(),
-                    full_path: recent1_dir.to_string_lossy().to_string().into(),
-                    paths: vec![recent1_dir.clone()],
-                    workspace_id: WorkspaceId::default(),
-                },
-                RecentProjectEntry {
-                    name: "font-kit".into(),
-                    full_path: recent2_dir.to_string_lossy().to_string().into(),
-                    paths: vec![recent2_dir.clone()],
-                    workspace_id: WorkspaceId::default(),
-                },
-                RecentProjectEntry {
-                    name: "ideas".into(),
-                    full_path: recent3_dir.to_string_lossy().to_string().into(),
-                    paths: vec![recent3_dir.clone()],
-                    workspace_id: WorkspaceId::default(),
-                },
-                RecentProjectEntry {
-                    name: "tmp".into(),
-                    full_path: recent4_dir.to_string_lossy().to_string().into(),
-                    paths: vec![recent4_dir.clone()],
-                    workspace_id: WorkspaceId::default(),
-                },
-            ];
-            sidebar.set_test_recent_projects(recent_projects, cx);
-        });
-    });
-
-    // Set thread info directly on the sidebar for visual testing
-    cx.update(|cx| {
-        sidebar.update(cx, |sidebar, _cx| {
-            sidebar.set_test_thread_info(
-                0,
-                "Refine thread view scrolling behavior".into(),
-                sidebar::AgentThreadStatus::Completed,
-            );
-            sidebar.set_test_thread_info(
-                1,
-                "Add line numbers option to FileEditBlock".into(),
-                sidebar::AgentThreadStatus::Running,
-            );
-        });
-    });
-
-    // Set last-worked-on thread titles on some recent projects for visual testing
-    cx.update(|cx| {
-        sidebar.update(cx, |sidebar, cx| {
-            sidebar.set_test_recent_project_thread_title(
-                recent1_dir.to_string_lossy().to_string().into(),
-                "Fix flaky test in CI pipeline".into(),
-                cx,
-            );
-            sidebar.set_test_recent_project_thread_title(
-                recent2_dir.to_string_lossy().to_string().into(),
-                "Upgrade font rendering engine".into(),
-                cx,
-            );
-        });
-    });
-
-    cx.run_until_parked();
-
-    // Open the sidebar
-    multi_workspace_window
-        .update(cx, |multi_workspace, window, cx| {
-            multi_workspace.toggle_sidebar(window, cx);
-        })
-        .context("Failed to toggle sidebar")?;
-
-    // Let rendering settle
-    for _ in 0..10 {
-        cx.advance_clock(Duration::from_millis(100));
-        cx.run_until_parked();
-    }
-
-    // Refresh the window
-    cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
-        window.refresh();
-    })?;
-
-    cx.run_until_parked();
-
-    // Capture: sidebar open with active workspaces and recent projects
-    let test_result = run_visual_test(
-        "multi_workspace_sidebar_open",
-        multi_workspace_window.into(),
-        cx,
-        update_baseline,
-    )?;
-
-    // Clean up worktrees
-    multi_workspace_window
-        .update(cx, |multi_workspace, _window, cx| {
-            for workspace in multi_workspace.workspaces() {
-                let project = workspace.read(cx).project().clone();
-                project.update(cx, |project, cx| {
-                    let worktree_ids: Vec<_> =
-                        project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
-                    for id in worktree_ids {
-                        project.remove_worktree(id, cx);
-                    }
-                });
-            }
-        })
-        .log_err();
-
-    cx.run_until_parked();
-
-    // Close the window
-    cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
-        window.remove_window();
-    })
-    .log_err();
-
-    cx.run_until_parked();
-
-    for _ in 0..15 {
-        cx.advance_clock(Duration::from_millis(100));
-        cx.run_until_parked();
-    }
-
-    Ok(test_result)
-}

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,
@@ -89,9 +88,9 @@ use workspace::notifications::{
 };
 use workspace::utility_pane::utility_slot_for_dock_position;
 use workspace::{
-    AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace,
-    WorkspaceSettings, create_and_open_local_file,
-    notifications::simple_message_notification::MessageNotification, open_new,
+    AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings,
+    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
+    open_new,
 };
 use workspace::{
     CloseIntent, CloseProject, CloseWindow, NotificationFrame, RestoreBanner,
@@ -371,16 +370,6 @@ pub fn initialize_workspace(
     })
     .detach();
 
-    cx.observe_new(|multi_workspace: &mut MultiWorkspace, window, cx| {
-        let Some(window) = window else {
-            return;
-        };
-        let multi_workspace_handle = cx.entity();
-        let sidebar = cx.new(|cx| Sidebar::new(multi_workspace_handle, window, cx));
-        multi_workspace.register_sidebar(sidebar, window, cx);
-    })
-    .detach();
-
     cx.observe_new(move |workspace: &mut Workspace, window, cx| {
         let Some(window) = window else {
             return;
@@ -1163,7 +1152,7 @@ fn register_actions(
         .register_action({
             let app_state = Arc::downgrade(&app_state);
             move |_, _: &CloseProject, window, cx| {
-                let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
+                let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
                     return;
                 };
                 if let Some(app_state) = app_state.upgrade() {
@@ -1259,7 +1248,6 @@ fn initialize_pane(
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
-    let workspace_handle = cx.weak_entity();
     pane.update(cx, |pane, cx| {
         pane.toolbar().update(cx, |toolbar, cx| {
             let multibuffer_hint = cx.new(|_| MultibufferHint::new());
@@ -1292,12 +1280,11 @@ fn initialize_pane(
             toolbar.add_item(telemetry_log_item, window, cx);
             let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
             toolbar.add_item(syntax_tree_item, window, cx);
-            let migration_banner =
-                cx.new(|inner_cx| MigrationBanner::new(workspace_handle.clone(), inner_cx));
-            toolbar.add_item(migration_banner, window, cx);
             let highlights_tree_item =
                 cx.new(|_| language_tools::HighlightsTreeToolbarItemView::new());
             toolbar.add_item(highlights_tree_item, window, cx);
+            let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
+            toolbar.add_item(migration_banner, window, cx);
             let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
             toolbar.add_item(project_diff_toolbar, window, cx);
             let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
@@ -1372,10 +1359,10 @@ fn quit(_: &Quit, cx: &mut App) {
 
     let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
     cx.spawn(async move |cx| {
-        let mut workspace_windows: Vec<WindowHandle<MultiWorkspace>> = cx.update(|cx| {
+        let mut workspace_windows: Vec<WindowHandle<Workspace>> = cx.update(|cx| {
             cx.windows()
                 .into_iter()
-                .filter_map(|window| window.downcast::<MultiWorkspace>())
+                .filter_map(|window| window.downcast::<Workspace>())
                 .collect::<Vec<_>>()
         });
 
@@ -1385,8 +1372,8 @@ fn quit(_: &Quit, cx: &mut App) {
             workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
         });
 
-        if should_confirm && let Some(multi_workspace) = workspace_windows.first() {
-            let answer = multi_workspace
+        if should_confirm && let Some(workspace) = workspace_windows.first() {
+            let answer = workspace
                 .update(cx, |_, window, cx| {
                     window.prompt(
                         PromptLevel::Info,
@@ -1410,30 +1397,14 @@ fn quit(_: &Quit, cx: &mut App) {
 
         // If the user cancels any save prompt, then keep the app open.
         for window in workspace_windows {
-            let workspaces = window
-                .update(cx, |multi_workspace, _, _| {
-                    multi_workspace.workspaces().to_vec()
+            if let Some(should_close) = window
+                .update(cx, |workspace, window, cx| {
+                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
                 })
-                .log_err();
-
-            let Some(workspaces) = workspaces else {
-                continue;
-            };
-
-            for workspace in workspaces {
-                if let Some(should_close) = window
-                    .update(cx, |multi_workspace, window, cx| {
-                        multi_workspace.activate(workspace.clone(), cx);
-                        window.activate_window();
-                        workspace.update(cx, |workspace, cx| {
-                            workspace.prepare_to_close(CloseIntent::Quit, window, cx)
-                        })
-                    })
-                    .log_err()
-                {
-                    if !should_close.await? {
-                        return Ok(());
-                    }
+                .log_err()
+            {
+                if !should_close.await? {
+                    return Ok(());
                 }
             }
         }
@@ -2385,7 +2356,6 @@ mod tests {
     use settings::{SaturatingBool, SettingsStore, watch_config_file};
     use std::{
         path::{Path, PathBuf},
-        sync::Arc,
         time::Duration,
     };
     use theme::ThemeRegistry;
@@ -2393,7 +2363,6 @@ mod tests {
         path,
         rel_path::{RelPath, rel_path},
     };
-    use workspace::MultiWorkspace;
     use workspace::{
         NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
         WorkspaceHandle,
@@ -2429,12 +2398,10 @@ mod tests {
         .unwrap();
         assert_eq!(cx.read(|cx| cx.windows().len()), 1);
 
-        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert!(workspace.active_item_as::<Editor>(cx).is_some())
-                });
+        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
+        workspace
+            .update(cx, |workspace, _, cx| {
+                assert!(workspace.active_item_as::<Editor>(cx).is_some())
             })
             .unwrap();
     }
@@ -2442,10 +2409,6 @@ mod tests {
     #[gpui::test]
     async fn test_open_paths_action(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
-        cx.update(|cx| {
-            use feature_flags::FeatureFlagAppExt as _;
-            cx.update_flags(false, vec!["agent-v2".to_string()]);
-        });
         app_state
             .fs
             .as_fake()
@@ -2499,23 +2462,21 @@ mod tests {
         .await
         .unwrap();
         assert_eq!(cx.read(|cx| cx.windows().len()), 1);
-        let multi_workspace_1 = cx
-            .read(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
+        let workspace_1 = cx
+            .read(|cx| cx.windows()[0].downcast::<Workspace>())
             .unwrap();
         cx.run_until_parked();
-        multi_workspace_1
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert_eq!(workspace.worktrees(cx).count(), 2);
-                    assert!(workspace.left_dock().read(cx).is_open());
-                    assert!(
-                        workspace
-                            .active_pane()
-                            .read(cx)
-                            .focus_handle(cx)
-                            .is_focused(window)
-                    );
-                });
+        workspace_1
+            .update(cx, |workspace, window, cx| {
+                assert_eq!(workspace.worktrees(cx).count(), 2);
+                assert!(workspace.left_dock().read(cx).is_open());
+                assert!(
+                    workspace
+                        .active_pane()
+                        .read(cx)
+                        .focus_handle(cx)
+                        .is_focused(window)
+                );
             })
             .unwrap();
 
@@ -2533,7 +2494,7 @@ mod tests {
 
         // Replace existing windows
         let window = cx
-            .update(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
+            .update(|cx| cx.windows()[0].downcast::<Workspace>())
             .unwrap();
         cx.update(|cx| {
             open_paths(
@@ -2550,12 +2511,11 @@ mod tests {
         .unwrap();
         cx.background_executor.run_until_parked();
         assert_eq!(cx.read(|cx| cx.windows().len()), 2);
-        let multi_workspace_1 = cx
-            .update(|cx| cx.windows()[0].downcast::<MultiWorkspace>())
+        let workspace_1 = cx
+            .update(|cx| cx.windows()[0].downcast::<Workspace>())
             .unwrap();
-        multi_workspace_1
-            .update(cx, |multi_workspace, window, cx| {
-                let workspace = multi_workspace.workspace().read(cx);
+        workspace_1
+            .update(cx, |workspace, window, cx| {
                 assert_eq!(
                     workspace
                         .worktrees(cx)
@@ -2727,21 +2687,17 @@ mod tests {
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
 
         // When opening the workspace, the window is not in a edited state.
-        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
 
-        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
-            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
+        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
+            cx.update(|cx| window.read(cx).unwrap().is_edited())
         };
         let pane = window
-            .read_with(cx, |multi_workspace, cx| {
-                multi_workspace.workspace().read(cx).active_pane().clone()
-            })
+            .read_with(cx, |workspace, _| workspace.active_pane().clone())
             .unwrap();
         let editor = window
-            .read_with(cx, |multi_workspace, cx| {
-                multi_workspace
-                    .workspace()
-                    .read(cx)
+            .read_with(cx, |workspace, cx| {
+                workspace
                     .active_item(cx)
                     .unwrap()
                     .downcast::<Editor>()
@@ -2814,26 +2770,22 @@ mod tests {
         executor.run_until_parked();
 
         window
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    let editor = workspace
-                        .active_item(cx)
-                        .unwrap()
-                        .downcast::<Editor>()
-                        .unwrap();
+            .update(cx, |workspace, _, cx| {
+                let editor = workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .unwrap();
 
-                    editor.update(cx, |editor, cx| {
-                        assert_eq!(editor.text(cx), "hey");
-                    });
+                editor.update(cx, |editor, cx| {
+                    assert_eq!(editor.text(cx), "hey");
                 });
             })
             .unwrap();
 
         let editor = window
-            .read_with(cx, |multi_workspace, cx| {
-                multi_workspace
-                    .workspace()
-                    .read(cx)
+            .read_with(cx, |workspace, cx| {
+                workspace
                     .active_item(cx)
                     .unwrap()
                     .downcast::<Editor>()
@@ -2886,17 +2838,15 @@ mod tests {
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
 
         // When opening the workspace, the window is not in a edited state.
-        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
 
-        let window_is_edited = |window: WindowHandle<MultiWorkspace>, cx: &mut TestAppContext| {
-            cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited())
+        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
+            cx.update(|cx| window.read(cx).unwrap().is_edited())
         };
 
         let editor = window
-            .read_with(cx, |multi_workspace, cx| {
-                multi_workspace
-                    .workspace()
-                    .read(cx)
+            .read_with(cx, |workspace, cx| {
+                workspace
                     .active_item(cx)
                     .unwrap()
                     .downcast::<Editor>()
@@ -2943,27 +2893,22 @@ mod tests {
         cx.run_until_parked();
 
         // When opening the workspace, the window is not in a edited state.
-        let window = cx.update(|cx| {
-            cx.active_window()
-                .unwrap()
-                .downcast::<MultiWorkspace>()
-                .unwrap()
-        });
+        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
         assert!(window_is_edited(window, cx));
 
         window
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    let editor = workspace
-                        .active_item(cx)
-                        .unwrap()
-                        .downcast::<editor::Editor>()
-                        .unwrap();
-                    editor.update(cx, |editor, cx| {
-                        assert_eq!(editor.text(cx), "EDIThey");
-                        assert!(editor.is_dirty(cx));
-                    });
+            .update(cx, |workspace, _, cx| {
+                let editor = workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<editor::Editor>()
+                    .unwrap();
+                editor.update(cx, |editor, cx| {
+                    assert_eq!(editor.text(cx), "EDIThey");
+                    assert!(editor.is_dirty(cx));
                 });
+
+                editor
             })
             .unwrap();
     }
@@ -2985,40 +2930,36 @@ mod tests {
         .unwrap();
         cx.run_until_parked();
 
-        let multi_workspace = cx
-            .update(|cx| cx.windows().first().unwrap().downcast::<MultiWorkspace>())
+        let workspace = cx
+            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
             .unwrap();
 
-        let editor = multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    let editor = workspace
-                        .active_item(cx)
-                        .unwrap()
-                        .downcast::<editor::Editor>()
-                        .unwrap();
-                    editor.update(cx, |editor, cx| {
-                        assert!(editor.text(cx).is_empty());
-                        assert!(!editor.is_dirty(cx));
-                    });
+        let editor = workspace
+            .update(cx, |workspace, _, cx| {
+                let editor = workspace
+                    .active_item(cx)
+                    .unwrap()
+                    .downcast::<editor::Editor>()
+                    .unwrap();
+                editor.update(cx, |editor, cx| {
+                    assert!(editor.text(cx).is_empty());
+                    assert!(!editor.is_dirty(cx));
+                });
 
-                    editor
-                })
+                editor
             })
             .unwrap();
 
-        let save_task = multi_workspace
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.save_active_item(SaveIntent::Save, window, cx)
-                })
+        let save_task = workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.save_active_item(SaveIntent::Save, window, cx)
             })
             .unwrap();
         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
         cx.background_executor.run_until_parked();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
         save_task.await.unwrap();
-        multi_workspace
+        workspace
             .update(cx, |_, _, cx| {
                 editor.update(cx, |editor, cx| {
                     assert!(!editor.is_dirty(cx));
@@ -3199,10 +3140,8 @@ mod tests {
         .unwrap();
         cx.run_until_parked();
         assert_eq!(cx.update(|cx| cx.windows().len()), 1);
-        let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
+        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
+        let workspace = window.root(cx).unwrap();
 
         #[track_caller]
         fn assert_project_panel_selection(
@@ -3237,19 +3176,17 @@ mod tests {
 
         // Open a file within an existing worktree.
         window
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.open_paths(
-                        vec![path!("/dir1/a.txt").into()],
-                        OpenOptions {
-                            visible: Some(OpenVisible::All),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    )
-                })
+            .update(cx, |workspace, window, cx| {
+                workspace.open_paths(
+                    vec![path!("/dir1/a.txt").into()],
+                    OpenOptions {
+                        visible: Some(OpenVisible::All),
+                        ..Default::default()
+                    },
+                    None,
+                    window,
+                    cx,
+                )
             })
             .unwrap()
             .await;
@@ -3278,19 +3215,17 @@ mod tests {
 
         // Open a file outside of any existing worktree.
         window
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.open_paths(
-                        vec![path!("/dir2/b.txt").into()],
-                        OpenOptions {
-                            visible: Some(OpenVisible::All),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    )
-                })
+            .update(cx, |workspace, window, cx| {
+                workspace.open_paths(
+                    vec![path!("/dir2/b.txt").into()],
+                    OpenOptions {
+                        visible: Some(OpenVisible::All),
+                        ..Default::default()
+                    },
+                    None,
+                    window,
+                    cx,
+                )
             })
             .unwrap()
             .await;
@@ -3330,19 +3265,17 @@ mod tests {
 
         // Ensure opening a directory and one of its children only adds one worktree.
         window
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.open_paths(
-                        vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
-                        OpenOptions {
-                            visible: Some(OpenVisible::All),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    )
-                })
+            .update(cx, |workspace, window, cx| {
+                workspace.open_paths(
+                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
+                    OpenOptions {
+                        visible: Some(OpenVisible::All),
+                        ..Default::default()
+                    },
+                    None,
+                    window,
+                    cx,
+                )
             })
             .unwrap()
             .await;
@@ -3382,19 +3315,17 @@ mod tests {
 
         // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
         window
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    workspace.open_paths(
-                        vec![path!("/d.txt").into()],
-                        OpenOptions {
-                            visible: Some(OpenVisible::None),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    )
-                })
+            .update(cx, |workspace, window, cx| {
+                workspace.open_paths(
+                    vec![path!("/d.txt").into()],
+                    OpenOptions {
+                        visible: Some(OpenVisible::None),
+                        ..Default::default()
+                    },
+                    None,
+                    window,
+                    cx,
+                )
             })
             .unwrap()
             .await;
@@ -3488,13 +3419,8 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
         project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
-        let window = cx.add_window({
-            let project = project.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
+        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
+        let workspace = window.root(cx).unwrap();
 
         let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
         let paths_to_open = [
@@ -3515,9 +3441,7 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            opened_workspace
-                .read_with(cx, |mw, _| mw.workspace().entity_id())
-                .unwrap(),
+            opened_workspace.root(cx).unwrap().entity_id(),
             workspace.entity_id(),
             "Excluded files in subfolders of a workspace root should be opened in the workspace"
         );
@@ -4940,7 +4864,6 @@ mod tests {
                 "lsp_tool",
                 "markdown",
                 "menu",
-                "multi_workspace",
                 "new_process_modal",
                 "notebook",
                 "notification_panel",
@@ -5028,7 +4951,7 @@ mod tests {
         cx.update(init);
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
+        let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
 
         cx.update(|cx| {
             cx.dispatch_action(&OpenDefaultSettings);
@@ -5037,12 +4960,10 @@ mod tests {
 
         assert_eq!(cx.read(|cx| cx.windows().len()), 1);
 
-        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
-        let active_editor = multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace
-                    .workspace()
-                    .update(cx, |workspace, cx| workspace.active_item_as::<Editor>(cx))
+        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
+        let active_editor = workspace
+            .update(cx, |workspace, _, cx| {
+                workspace.active_item_as::<Editor>(cx)
             })
             .unwrap();
         assert!(
@@ -5346,22 +5267,16 @@ mod tests {
             .await;
 
         let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
-        let window_a = cx.add_window({
-            let project = project_a.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
+        let window_a =
+            cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx));
 
         let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
-        let window_b = cx.add_window({
-            let project = project_b.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
+        let window_b =
+            cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx));
 
         let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
-        let window_c = cx.add_window({
-            let project = project_c.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
+        let window_c =
+            cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx));
 
         for window in [window_a, window_b, window_c] {
             let _ = cx.update_window(*window, |_, window, _| {
@@ -5382,8 +5297,8 @@ mod tests {
             cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
                 .unwrap();
 
-            let _ = window.read_with(cx, |multi_workspace, cx| {
-                let pane = multi_workspace.workspace().read(cx).active_pane().read(cx);
+            let _ = window.read_with(cx, |workspace, cx| {
+                let pane = workspace.active_pane().read(cx);
                 let project_path = pane.active_item().unwrap().project_path(cx).unwrap();
 
                 assert_eq!(
@@ -5393,707 +5308,4 @@ mod tests {
             });
         }
     }
-
-    #[gpui::test]
-    async fn test_open_paths_switches_to_best_workspace(cx: &mut TestAppContext) {
-        let app_state = init_test(cx);
-        cx.update(|cx| {
-            use feature_flags::FeatureFlagAppExt as _;
-            cx.update_flags(false, vec!["agent-v2".to_string()]);
-        });
-
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                path!("/"),
-                json!({
-                    "dir1": {
-                        "a.txt": "content a"
-                    },
-                    "dir2": {
-                        "b.txt": "content b"
-                    },
-                    "dir3": {
-                        "c.txt": "content c"
-                    }
-                }),
-            )
-            .await;
-
-        // Create a window with workspace 0 containing /dir1
-        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;
-
-        let window = cx.add_window({
-            let project = project1.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
-
-        cx.run_until_parked();
-        assert_eq!(cx.windows().len(), 1, "Should start with 1 window");
-
-        // Create workspace 2 with /dir2
-        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
-        let workspace2 = window
-            .update(cx, |_, window, cx| {
-                cx.new(|cx| Workspace::test_new(project2.clone(), window, cx))
-            })
-            .unwrap();
-
-        // Create workspace 3 with /dir3
-        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
-        let workspace3 = window
-            .update(cx, |_, window, cx| {
-                cx.new(|cx| Workspace::test_new(project3.clone(), window, cx))
-            })
-            .unwrap();
-
-        let workspace1 = window
-            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
-            .unwrap();
-
-        window
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.activate(workspace2.clone(), cx);
-                multi_workspace.activate(workspace3.clone(), cx);
-                // Switch back to workspace1 for test setup
-                multi_workspace.activate(workspace1, cx);
-                assert_eq!(multi_workspace.active_workspace_index(), 0);
-            })
-            .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify setup: 3 workspaces, workspace 0 active, still 1 window
-        window
-            .read_with(cx, |multi_workspace, _| {
-                assert_eq!(multi_workspace.workspaces().len(), 3);
-                assert_eq!(multi_workspace.active_workspace_index(), 0);
-            })
-            .unwrap();
-        assert_eq!(cx.windows().len(), 1);
-
-        // Open a file in /dir3 - should switch to workspace 3 (not just "the other one")
-        cx.update(|cx| {
-            open_paths(
-                &[PathBuf::from(path!("/dir3/c.txt"))],
-                app_state.clone(),
-                OpenOptions::default(),
-                cx,
-            )
-        })
-        .await
-        .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace 2 is active and file opened there
-        window
-            .read_with(cx, |multi_workspace, cx| {
-                assert_eq!(
-                    multi_workspace.active_workspace_index(),
-                    2,
-                    "Should have switched to workspace 3 which contains /dir3"
-                );
-                let active_item = multi_workspace
-                    .workspace()
-                    .read(cx)
-                    .active_pane()
-                    .read(cx)
-                    .active_item()
-                    .expect("Should have an active item");
-                assert_eq!(active_item.tab_content_text(0, cx), "c.txt");
-            })
-            .unwrap();
-        assert_eq!(cx.windows().len(), 1, "Should reuse existing window");
-
-        // Open a file in /dir2 - should switch to workspace 2
-        cx.update(|cx| {
-            open_paths(
-                &[PathBuf::from(path!("/dir2/b.txt"))],
-                app_state.clone(),
-                OpenOptions::default(),
-                cx,
-            )
-        })
-        .await
-        .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace 1 is active and file opened there
-        window
-            .read_with(cx, |multi_workspace, cx| {
-                assert_eq!(
-                    multi_workspace.active_workspace_index(),
-                    1,
-                    "Should have switched to workspace 2 which contains /dir2"
-                );
-                let active_item = multi_workspace
-                    .workspace()
-                    .read(cx)
-                    .active_pane()
-                    .read(cx)
-                    .active_item()
-                    .expect("Should have an active item");
-                assert_eq!(active_item.tab_content_text(0, cx), "b.txt");
-            })
-            .unwrap();
-
-        // Verify c.txt is still in workspace 3 (file opened in correct workspace, not active one)
-        workspace3.read_with(cx, |workspace, cx| {
-            let active_item = workspace
-                .active_pane()
-                .read(cx)
-                .active_item()
-                .expect("Workspace 2 should have an active item");
-            assert_eq!(
-                active_item.tab_content_text(0, cx),
-                "c.txt",
-                "c.txt should have been opened in workspace 3, not the active workspace"
-            );
-        });
-
-        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");
-
-        // Open a file in /dir1 - should switch back to workspace 0
-        cx.update(|cx| {
-            open_paths(
-                &[PathBuf::from(path!("/dir1/a.txt"))],
-                app_state.clone(),
-                OpenOptions::default(),
-                cx,
-            )
-        })
-        .await
-        .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace 0 is active and file opened there
-        window
-            .read_with(cx, |multi_workspace, cx| {
-                assert_eq!(
-                    multi_workspace.active_workspace_index(),
-                    0,
-                    "Should have switched back to workspace 0 which contains /dir1"
-                );
-                let active_item = multi_workspace
-                    .workspace()
-                    .read(cx)
-                    .active_pane()
-                    .read(cx)
-                    .active_item()
-                    .expect("Should have an active item");
-                assert_eq!(active_item.tab_content_text(0, cx), "a.txt");
-            })
-            .unwrap();
-        assert_eq!(cx.windows().len(), 1, "Should still have only 1 window");
-    }
-
-    #[gpui::test]
-    async fn test_quit_checks_all_workspaces_for_dirty_items(cx: &mut TestAppContext) {
-        let app_state = init_test(cx);
-        cx.update(init);
-        cx.update(|cx| {
-            use feature_flags::FeatureFlagAppExt as _;
-            cx.update_flags(false, vec!["agent-v2".to_string()]);
-        });
-
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                path!("/"),
-                json!({
-                    "dir1": {
-                        "a.txt": "content a"
-                    },
-                    "dir2": {
-                        "b.txt": "content b"
-                    },
-                    "dir3": {
-                        "c.txt": "content c"
-                    }
-                }),
-            )
-            .await;
-
-        // === Setup Window 1 with two workspaces ===
-        let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await;
-        let window1 = cx.add_window({
-            let project = project1.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
-
-        cx.run_until_parked();
-
-        let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await;
-        let workspace1_1 = window1
-            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
-            .unwrap();
-        let workspace1_2 = window1
-            .update(cx, |_, window, cx| {
-                cx.new(|cx| Workspace::test_new(project2.clone(), window, cx))
-            })
-            .unwrap();
-
-        window1
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.activate(workspace1_2.clone(), cx);
-                multi_workspace.activate(workspace1_1.clone(), cx);
-            })
-            .unwrap();
-
-        // === Setup Window 2 with one workspace ===
-        let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await;
-        let window2 = cx.add_window({
-            let project = project3.clone();
-            |window, cx| MultiWorkspace::test_new(project, window, cx)
-        });
-
-        cx.run_until_parked();
-        assert_eq!(cx.windows().len(), 2);
-
-        // === Case 1: Active workspace has dirty item, quit can be cancelled ===
-        let worktree1_id = project1.update(cx, |project, cx| {
-            project.worktrees(cx).next().unwrap().read(cx).id()
-        });
-
-        let editor1 = window1
-            .update(cx, |_, window, cx| {
-                workspace1_1.update(cx, |workspace, cx| {
-                    workspace.open_path((worktree1_id, rel_path("a.txt")), None, true, window, cx)
-                })
-            })
-            .unwrap()
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        window1
-            .update(cx, |_, window, cx| {
-                editor1.update(cx, |editor, cx| {
-                    editor.insert("dirty in active workspace", window, cx);
-                });
-            })
-            .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace1_1 is active
-        window1
-            .read_with(cx, |multi_workspace, _| {
-                assert_eq!(multi_workspace.active_workspace_index(), 0);
-            })
-            .unwrap();
-
-        cx.dispatch_action(*window1, Quit);
-        cx.run_until_parked();
-
-        assert!(
-            cx.has_pending_prompt(),
-            "Case 1: Should prompt to save dirty item in active workspace"
-        );
-
-        cx.simulate_prompt_answer("Cancel");
-        cx.run_until_parked();
-
-        assert_eq!(
-            cx.windows().len(),
-            2,
-            "Case 1: Windows should still exist after cancelling quit"
-        );
-
-        // Clean up Case 1: Close the dirty item without saving
-        let close_task = window1
-            .update(cx, |_, window, cx| {
-                workspace1_1.update(cx, |workspace, cx| {
-                    workspace.active_pane().update(cx, |pane, cx| {
-                        pane.close_active_item(&Default::default(), window, cx)
-                    })
-                })
-            })
-            .unwrap();
-        cx.run_until_parked();
-        cx.simulate_prompt_answer("Don't Save");
-        close_task.await.ok();
-        cx.run_until_parked();
-
-        // === Case 2: Non-active workspace (same window) has dirty item ===
-        let worktree2_id = project2.update(cx, |project, cx| {
-            project.worktrees(cx).next().unwrap().read(cx).id()
-        });
-
-        let editor2 = window1
-            .update(cx, |_, window, cx| {
-                workspace1_2.update(cx, |workspace, cx| {
-                    workspace.open_path((worktree2_id, rel_path("b.txt")), None, true, window, cx)
-                })
-            })
-            .unwrap()
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        window1
-            .update(cx, |_, window, cx| {
-                editor2.update(cx, |editor, cx| {
-                    editor.insert("dirty in non-active workspace", window, cx);
-                });
-            })
-            .unwrap();
-
-        cx.run_until_parked();
-
-        // Verify workspace1_1 is still active (not workspace1_2 with dirty item)
-        window1
-            .read_with(cx, |multi_workspace, _| {
-                assert_eq!(multi_workspace.active_workspace_index(), 0);
-            })
-            .unwrap();
-
-        cx.dispatch_action(*window1, Quit);
-        cx.run_until_parked();
-
-        // Verify the non-active workspace got activated to show the dirty item
-        window1
-            .read_with(cx, |multi_workspace, _| {
-                assert_eq!(
-                    multi_workspace.active_workspace_index(),
-                    1,
-                    "Case 2: Non-active workspace should be activated when it has dirty item"
-                );
-            })
-            .unwrap();
-
-        assert!(
-            cx.has_pending_prompt(),
-            "Case 2: Should prompt to save dirty item in non-active workspace"
-        );
-
-        cx.simulate_prompt_answer("Cancel");
-        cx.run_until_parked();
-
-        assert_eq!(
-            cx.windows().len(),
-            2,
-            "Case 2: Windows should still exist after cancelling quit"
-        );
-
-        // Clean up Case 2: Close the dirty item without saving
-        let close_task = window1
-            .update(cx, |_, window, cx| {
-                workspace1_2.update(cx, |workspace, cx| {
-                    workspace.active_pane().update(cx, |pane, cx| {
-                        pane.close_active_item(&Default::default(), window, cx)
-                    })
-                })
-            })
-            .unwrap();
-        cx.run_until_parked();
-        cx.simulate_prompt_answer("Don't Save");
-        close_task.await.ok();
-        cx.run_until_parked();
-
-        // === Case 3: Non-active window has dirty item ===
-        let workspace3 = window2
-            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
-            .unwrap();
-
-        let worktree3_id = project3.update(cx, |project, cx| {
-            project.worktrees(cx).next().unwrap().read(cx).id()
-        });
-
-        let editor3 = window2
-            .update(cx, |_, window, cx| {
-                workspace3.update(cx, |workspace, cx| {
-                    workspace.open_path((worktree3_id, rel_path("c.txt")), None, true, window, cx)
-                })
-            })
-            .unwrap()
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        window2
-            .update(cx, |_, window, cx| {
-                editor3.update(cx, |editor, cx| {
-                    editor.insert("dirty in other window", window, cx);
-                });
-            })
-            .unwrap();
-
-        cx.run_until_parked();
-
-        // Activate window1 explicitly (editing in window2 may have activated it)
-        window1
-            .update(cx, |_, window, _| window.activate_window())
-            .unwrap();
-        cx.run_until_parked();
-
-        // Verify window2 is not active (window1 should still be active)
-        assert_eq!(
-            cx.update(|cx| window2.is_active(cx)),
-            Some(false),
-            "Case 3: window2 should not be active before quit"
-        );
-
-        // Dispatch quit from window1 (window2 has the dirty item)
-        cx.dispatch_action(*window1, Quit);
-        cx.run_until_parked();
-
-        // Verify window2 is now active (quit handler activated it to show dirty item)
-        assert_eq!(
-            cx.update(|cx| window2.is_active(cx)),
-            Some(true),
-            "Case 3: window2 should be activated when it has dirty item"
-        );
-
-        assert!(
-            cx.has_pending_prompt(),
-            "Case 3: Should prompt to save dirty item in non-active window"
-        );
-
-        cx.simulate_prompt_answer("Cancel");
-        cx.run_until_parked();
-
-        assert_eq!(
-            cx.windows().len(),
-            2,
-            "Case 3: Windows should still exist after cancelling quit"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_multi_workspace_session_restore(cx: &mut TestAppContext) {
-        use collections::HashMap;
-        use session::Session;
-        use workspace::{Workspace, WorkspaceId};
-
-        let app_state = init_test(cx);
-
-        cx.update(|cx| {
-            use feature_flags::FeatureFlagAppExt as _;
-            cx.update_flags(false, vec!["agent-v2".to_string()]);
-        });
-
-        let dir1 = path!("/dir1");
-        let dir2 = path!("/dir2");
-        let dir3 = path!("/dir3");
-
-        let fs = app_state.fs.clone();
-        let fake_fs = fs.as_fake();
-        fake_fs.insert_tree(dir1, json!({})).await;
-        fake_fs.insert_tree(dir2, json!({})).await;
-        fake_fs.insert_tree(dir3, json!({})).await;
-
-        let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned());
-
-        // --- Create 3 workspaces in 2 windows ---
-        //
-        //   Window A: workspace for dir1, workspace for dir2
-        //   Window B: workspace for dir3
-        let (window_a, _) = cx
-            .update(|cx| {
-                Workspace::new_local(vec![dir1.into()], app_state.clone(), None, None, None, cx)
-            })
-            .await
-            .expect("failed to open first workspace");
-
-        window_a
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.open_project(vec![dir2.into()], window, cx)
-            })
-            .unwrap()
-            .await
-            .expect("failed to open second workspace into window A");
-        cx.run_until_parked();
-
-        let (window_b, _) = cx
-            .update(|cx| {
-                Workspace::new_local(vec![dir3.into()], app_state.clone(), None, None, None, cx)
-            })
-            .await
-            .expect("failed to open third workspace");
-
-        // Currently dir2 is active because it was added last.
-        // So, switch window_a's active workspace to dir1 (index 0).
-        // This sets up a non-trivial assertion: after restore, dir1 should
-        // still be active rather than whichever workspace happened to restore last.
-        window_a
-            .update(cx, |multi_workspace, window, cx| {
-                multi_workspace.activate_index(0, window, cx);
-            })
-            .unwrap();
-
-        // --- Flush serialization ---
-        cx.executor().advance_clock(SERIALIZATION_THROTTLE_TIME);
-        cx.run_until_parked();
-
-        // Verify all workspaces retained their session_ids.
-        let locations = workspace::last_session_workspace_locations(&session_id, None, fs.as_ref())
-            .await
-            .expect("expected session workspace locations");
-        assert_eq!(
-            locations.len(),
-            3,
-            "all 3 workspaces should have session_ids in the DB"
-        );
-
-        // Close the original windows.
-        window_a
-            .update(cx, |_, window, _| window.remove_window())
-            .unwrap();
-        window_b
-            .update(cx, |_, window, _| window.remove_window())
-            .unwrap();
-        cx.run_until_parked();
-
-        // Simulate a new session launch: replace the session so that
-        // `last_session_id()` returns the ID used during workspace creation.
-        // `restore_on_startup` defaults to `LastSession`, which is what we need.
-        cx.update(|cx| {
-            app_state.session.update(cx, |app_session, _cx| {
-                app_session
-                    .replace_session_for_test(Session::test_with_old_session(session_id.clone()));
-            });
-        });
-
-        // --- Read back from DB and verify grouping ---
-        let locations = workspace::last_session_workspace_locations(&session_id, None, fs.as_ref())
-            .await
-            .expect("expected session workspace locations");
-
-        assert_eq!(locations.len(), 3, "expected 3 session workspaces");
-
-        let mut groups_by_window: HashMap<gpui::WindowId, Vec<WorkspaceId>> = HashMap::default();
-        for session_workspace in &locations {
-            if let Some(window_id) = session_workspace.window_id {
-                groups_by_window
-                    .entry(window_id)
-                    .or_default()
-                    .push(session_workspace.workspace_id);
-            }
-        }
-        assert_eq!(
-            groups_by_window.len(),
-            2,
-            "expected 2 window groups, got {groups_by_window:?}"
-        );
-        assert!(
-            groups_by_window.values().any(|g| g.len() == 2),
-            "expected one group with 2 workspaces"
-        );
-        assert!(
-            groups_by_window.values().any(|g| g.len() == 1),
-            "expected one group with 1 workspace"
-        );
-
-        let mut async_cx = cx.to_async();
-        crate::restore_or_create_workspace(app_state.clone(), &mut async_cx)
-            .await
-            .expect("failed to restore workspaces");
-        cx.run_until_parked();
-
-        // --- Verify the restored windows ---
-        let restored_windows: Vec<WindowHandle<MultiWorkspace>> = cx.read(|cx| {
-            cx.windows()
-                .into_iter()
-                .filter_map(|window| window.downcast::<MultiWorkspace>())
-                .collect()
-        });
-
-        assert_eq!(
-            restored_windows.len(),
-            2,
-            "expected 2 restored windows, got {}",
-            restored_windows.len()
-        );
-
-        let workspace_counts: Vec<usize> = restored_windows
-            .iter()
-            .map(|window| {
-                window
-                    .read_with(cx, |multi_workspace, _| multi_workspace.workspaces().len())
-                    .unwrap()
-            })
-            .collect();
-        let mut sorted_counts = workspace_counts.clone();
-        sorted_counts.sort();
-        assert_eq!(
-            sorted_counts,
-            vec![1, 2],
-            "expected one window with 1 workspace and one with 2, got {workspace_counts:?}"
-        );
-
-        let dir1_path: Arc<Path> = Path::new(dir1).into();
-        let dir2_path: Arc<Path> = Path::new(dir2).into();
-        let dir3_path: Arc<Path> = Path::new(dir3).into();
-
-        let all_restored_paths: Vec<Vec<Vec<Arc<Path>>>> = restored_windows
-            .iter()
-            .map(|window| {
-                window
-                    .read_with(cx, |multi_workspace, cx| {
-                        multi_workspace
-                            .workspaces()
-                            .iter()
-                            .map(|ws| ws.read(cx).root_paths(cx))
-                            .collect()
-                    })
-                    .unwrap()
-            })
-            .collect();
-
-        let two_ws_window = all_restored_paths
-            .iter()
-            .find(|paths| paths.len() == 2)
-            .expect("expected a window with 2 workspaces");
-        assert!(
-            two_ws_window.iter().any(|p| p.contains(&dir1_path)),
-            "2-workspace window should contain dir1, got {two_ws_window:?}"
-        );
-        assert!(
-            two_ws_window.iter().any(|p| p.contains(&dir2_path)),
-            "2-workspace window should contain dir2, got {two_ws_window:?}"
-        );
-
-        let one_ws_window = all_restored_paths
-            .iter()
-            .find(|paths| paths.len() == 1)
-            .expect("expected a window with 1 workspace");
-        assert!(
-            one_ws_window[0].contains(&dir3_path),
-            "1-workspace window should contain dir3, got {one_ws_window:?}"
-        );
-
-        // --- Verify the active workspace is preserved ---
-        for window in &restored_windows {
-            let (active_paths, workspace_count) = window
-                .read_with(cx, |multi_workspace, cx| {
-                    let active = multi_workspace.workspace();
-                    (
-                        active.read(cx).root_paths(cx),
-                        multi_workspace.workspaces().len(),
-                    )
-                })
-                .unwrap();
-
-            if workspace_count == 2 {
-                assert!(
-                    active_paths.contains(&dir1_path),
-                    "2-workspace window should have dir1 active, got {active_paths:?}"
-                );
-            } else {
-                assert!(
-                    active_paths.contains(&dir3_path),
-                    "1-workspace window should have dir3 active, got {active_paths:?}"
-                );
-            }
-        }
-    }
 }

crates/zed/src/zed/migrate.rs 🔗

@@ -1,7 +1,6 @@
 use anyhow::{Context as _, Result};
 use editor::Editor;
 use fs::Fs;
-use gpui::WeakEntity;
 use migrator::{migrate_keymap, migrate_settings};
 use settings::{KeymapFile, Settings, SettingsStore};
 use util::ResultExt;
@@ -23,7 +22,6 @@ pub enum MigrationType {
 }
 
 pub struct MigrationBanner {
-    workspace: WeakEntity<Workspace>,
     migration_type: Option<MigrationType>,
     should_migrate_task: Option<Task<()>>,
     markdown: Option<Entity<Markdown>>,
@@ -56,7 +54,7 @@ struct GlobalMigrationNotification(Entity<MigrationNotification>);
 impl Global for GlobalMigrationNotification {}
 
 impl MigrationBanner {
-    pub fn new(workspace: WeakEntity<Workspace>, cx: &mut Context<Self>) -> Self {
+    pub fn new(_: &Workspace, cx: &mut Context<Self>) -> Self {
         if let Some(notifier) = MigrationNotification::try_global(cx) {
             cx.subscribe(
                 &notifier,
@@ -67,7 +65,6 @@ impl MigrationBanner {
             .detach();
         }
         Self {
-            workspace,
             migration_type: None,
             should_migrate_task: None,
             markdown: None,
@@ -238,22 +235,22 @@ impl Render for MigrationBanner {
                     ),
             )
             .child(
-                Button::new("backup-and-migrate", "Backup and Update").on_click({
-                    let workspace = self.workspace.clone();
+                Button::new("backup-and-migrate", "Backup and Update").on_click(
                     move |_, window, cx| {
                         let fs = <dyn Fs>::global(cx);
-                        let task = match migration_type {
+                        match migration_type {
                             Some(MigrationType::Keymap) => {
                                 cx.background_spawn(write_keymap_migration(fs.clone()))
+                                    .detach_and_notify_err(window, cx);
                             }
                             Some(MigrationType::Settings) => {
                                 cx.background_spawn(write_settings_migration(fs.clone()))
+                                    .detach_and_notify_err(window, cx);
                             }
                             None => unreachable!(),
-                        };
-                        task.detach_and_notify_err(workspace.clone(), window, cx);
-                    }
-                }),
+                        }
+                    },
+                ),
             )
             .into_any_element()
     }

crates/zed/src/zed/open_listener.rs 🔗

@@ -1,5 +1,5 @@
 use crate::handle_open_request;
-use crate::restore_or_create_workspace;
+use crate::restorable_workspace_locations;
 use anyhow::{Context as _, Result, anyhow};
 use cli::{CliRequest, CliResponse, ipc::IpcSender};
 use cli::{IpcHandshake, ipc};
@@ -30,7 +30,7 @@ use util::ResultExt;
 use util::paths::PathWithPosition;
 use workspace::PathList;
 use workspace::item::ItemHandle;
-use workspace::{AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation};
+use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace};
 
 #[derive(Default, Debug)]
 pub struct OpenRequest {
@@ -337,7 +337,7 @@ pub async fn open_paths_with_positions(
     open_options: workspace::OpenOptions,
     cx: &mut AsyncApp,
 ) -> Result<(
-    WindowHandle<MultiWorkspace>,
+    WindowHandle<Workspace>,
     Vec<Option<Result<Box<dyn ItemHandle>>>>,
 )> {
     let mut caret_positions = HashMap::default();
@@ -357,29 +357,24 @@ pub async fn open_paths_with_positions(
         })
         .collect::<Vec<_>>();
 
-    let (multi_workspace, mut items) = cx
+    let (workspace, mut items) = cx
         .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))
         .await?;
 
     if diff_all && !diff_paths.is_empty() {
-        if let Ok(diff_view) = multi_workspace.update(cx, |multi_workspace, window, cx| {
-            multi_workspace.workspace().update(cx, |workspace, cx| {
-                MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx)
-            })
+        if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| {
+            MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx)
         }) {
             if let Some(diff_view) = diff_view.await.log_err() {
                 items.push(Some(Ok(Box::new(diff_view))));
             }
         }
     } else {
-        let workspace_weak = multi_workspace.read_with(cx, |multi_workspace, _cx| {
-            multi_workspace.workspace().downgrade()
-        })?;
         for diff_pair in diff_paths {
             let old_path = Path::new(&diff_pair[0]).canonicalize()?;
             let new_path = Path::new(&diff_pair[1]).canonicalize()?;
-            if let Ok(diff_view) = multi_workspace.update(cx, |_multi_workspace, window, cx| {
-                FileDiffView::open(old_path, new_path, workspace_weak.clone(), window, cx)
+            if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| {
+                FileDiffView::open(old_path, new_path, workspace, window, cx)
             }) {
                 if let Some(diff_view) = diff_view.await.log_err() {
                     items.push(Some(Ok(Box::new(diff_view))))
@@ -400,7 +395,7 @@ pub async fn open_paths_with_positions(
             continue;
         };
         if let Some(active_editor) = item.downcast::<Editor>() {
-            multi_workspace
+            workspace
                 .update(cx, |_, window, cx| {
                     active_editor.update(cx, |editor, cx| {
                         editor.go_to_singleton_buffer_point(point, window, cx);
@@ -410,7 +405,7 @@ pub async fn open_paths_with_positions(
         }
     }
 
-    Ok((multi_workspace, items))
+    Ok((workspace, items))
 }
 
 pub async fn handle_cli_connection(
@@ -493,13 +488,20 @@ async fn open_workspaces(
     env: Option<collections::HashMap<String, String>>,
     cx: &mut AsyncApp,
 ) -> Result<()> {
-    if paths.is_empty() && diff_paths.is_empty() && open_new_workspace != Some(true) {
-        return restore_or_create_workspace(app_state, cx).await;
-    }
-
     let grouped_locations: Vec<(SerializedWorkspaceLocation, PathList)> =
         if paths.is_empty() && diff_paths.is_empty() {
-            Vec::new()
+            if open_new_workspace == Some(true) {
+                Vec::new()
+            } else {
+                // The workspace_id from the database is not used;
+                // open_paths will assign a new WorkspaceId when opening the workspace.
+                restorable_workspace_locations(cx, &app_state)
+                    .await
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|(_workspace_id, location, paths)| (location, paths))
+                    .collect()
+            }
         } else {
             vec![(
                 SerializedWorkspaceLocation::Local,
@@ -753,7 +755,7 @@ mod tests {
     use serde_json::json;
     use std::{sync::Arc, task::Poll};
     use util::path;
-    use workspace::{AppState, MultiWorkspace};
+    use workspace::{AppState, Workspace};
 
     #[gpui::test]
     fn test_parse_ssh_url(cx: &mut TestAppContext) {
@@ -889,12 +891,10 @@ mod tests {
         open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await;
 
         assert_eq!(cx.windows().len(), 1);
-        let multi_workspace = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert!(workspace.active_item_as::<Editor>(cx).is_none())
-                });
+        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
+        workspace
+            .update(cx, |workspace, _, cx| {
+                assert!(workspace.active_item_as::<Editor>(cx).is_none())
             })
             .unwrap();
 
@@ -902,11 +902,9 @@ mod tests {
         open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await;
 
         assert_eq!(cx.windows().len(), 1);
-        multi_workspace
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert!(workspace.active_item_as::<Editor>(cx).is_some());
-                });
+        workspace
+            .update(cx, |workspace, _, cx| {
+                assert!(workspace.active_item_as::<Editor>(cx).is_some());
             })
             .unwrap();
 
@@ -921,14 +919,12 @@ mod tests {
 
         assert_eq!(cx.windows().len(), 2);
 
-        let multi_workspace_2 = cx.windows()[1].downcast::<MultiWorkspace>().unwrap();
-        multi_workspace_2
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert!(workspace.active_item_as::<Editor>(cx).is_some());
-                    let items = workspace.items(cx).collect::<Vec<_>>();
-                    assert_eq!(items.len(), 1, "Workspace should have two items");
-                });
+        let workspace_2 = cx.windows()[1].downcast::<Workspace>().unwrap();
+        workspace_2
+            .update(cx, |workspace, _, cx| {
+                assert!(workspace.active_item_as::<Editor>(cx).is_some());
+                let items = workspace.items(cx).collect::<Vec<_>>();
+                assert_eq!(items.len(), 1, "Workspace should have two items");
             })
             .unwrap();
     }
@@ -1004,12 +1000,10 @@ mod tests {
         open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await;
 
         assert_eq!(cx.windows().len(), 1);
-        let multi_workspace_1 = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
-        multi_workspace_1
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    assert!(workspace.active_item_as::<Editor>(cx).is_some())
-                });
+        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap();
+        workspace_1
+            .update(cx, |workspace, _, cx| {
+                assert!(workspace.active_item_as::<Editor>(cx).is_some())
             })
             .unwrap();
 
@@ -1018,12 +1012,10 @@ mod tests {
         open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await;
 
         assert_eq!(cx.windows().len(), 1);
-        multi_workspace_1
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    let items = workspace.items(cx).collect::<Vec<_>>();
-                    assert_eq!(items.len(), 2, "Workspace should have two items");
-                });
+        workspace_1
+            .update(cx, |workspace, _, cx| {
+                let items = workspace.items(cx).collect::<Vec<_>>();
+                assert_eq!(items.len(), 2, "Workspace should have two items");
             })
             .unwrap();
 
@@ -1032,13 +1024,11 @@ mod tests {
         open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await;
 
         assert_eq!(cx.windows().len(), 2);
-        let multi_workspace_2 = cx.windows()[1].downcast::<MultiWorkspace>().unwrap();
-        multi_workspace_2
-            .update(cx, |multi_workspace, _, cx| {
-                multi_workspace.workspace().update(cx, |workspace, cx| {
-                    let items = workspace.items(cx).collect::<Vec<_>>();
-                    assert_eq!(items.len(), 1, "Workspace should have two items");
-                });
+        let workspace_2 = cx.windows()[1].downcast::<Workspace>().unwrap();
+        workspace_2
+            .update(cx, |workspace, _, cx| {
+                let items = workspace.items(cx).collect::<Vec<_>>();
+                assert_eq!(items.len(), 1, "Workspace should have two items");
             })
             .unwrap();
     }