Preserve panel zoom state across workspace switches (#49069)

Richard Feldman created

When the agent panel (or any dock panel) is open and
fullscreened/zoomed, switching to a different workspace in the sidebar
and then switching back caused the panel to close. It should remain both
open and zoomed.

The root cause was in `MultiWorkspace::focus_active_workspace()` — it
always focused the center pane of the active workspace. This triggered
`dismiss_zoomed_items_to_reveal(None)`, which closed any zoomed dock
panel (the same behavior as when a user intentionally clicks away from a
zoomed panel).

The fix checks if any dock has a zoomed panel before deciding what to
focus. If a zoomed panel exists, it focuses that panel instead of the
center pane, preventing the dismiss logic from firing.

Closes AI-22

Release Notes:

- Fixed panels losing their fullscreen state when switching between
workspaces.

Change summary

crates/workspace/src/multi_workspace.rs | 24 ++++++
crates/workspace/src/workspace.rs       | 95 +++++++++++++++++++++++++++
2 files changed, 117 insertions(+), 2 deletions(-)

Detailed changes

crates/workspace/src/multi_workspace.rs 🔗

@@ -302,8 +302,28 @@ impl MultiWorkspace {
     }
 
     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);
+        // If a dock panel is zoomed, focus it instead of the center pane.
+        // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
+        // which closes the zoomed dock.
+        let focus_handle = {
+            let workspace = self.workspace().read(cx);
+            let mut target = None;
+            for dock in workspace.all_docks() {
+                let dock = dock.read(cx);
+                if dock.is_open() {
+                    if let Some(panel) = dock.active_panel() {
+                        if panel.is_zoomed(window, cx) {
+                            target = Some(panel.panel_focus_handle(cx));
+                            break;
+                        }
+                    }
+                }
+            }
+            target.unwrap_or_else(|| {
+                let pane = workspace.active_pane().clone();
+                pane.read(cx).focus_handle(cx)
+            })
+        };
         window.focus(&focus_handle, cx);
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -12724,6 +12724,101 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
+        init_test(cx);
+        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_handle =
+            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+
+        let workspace_a = multi_workspace_handle
+            .read_with(cx, |mw, _| mw.workspace().clone())
+            .unwrap();
+
+        let _workspace_b = multi_workspace_handle
+            .update(cx, |mw, window, cx| {
+                mw.test_add_workspace(project_b, window, cx)
+            })
+            .unwrap();
+
+        // Switch to workspace A
+        multi_workspace_handle
+            .update(cx, |mw, window, cx| {
+                mw.activate_index(0, window, cx);
+            })
+            .unwrap();
+
+        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
+
+        // Add a panel to workspace A's right dock and open the dock
+        let panel = workspace_a.update_in(cx, |workspace, window, cx| {
+            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
+            workspace.add_panel(panel.clone(), window, cx);
+            workspace
+                .right_dock()
+                .update(cx, |dock, cx| dock.set_open(true, window, cx));
+            panel
+        });
+
+        // Focus the panel through the workspace (matching existing test pattern)
+        workspace_a.update_in(cx, |workspace, window, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(window, cx);
+        });
+
+        // Zoom the panel
+        panel.update_in(cx, |panel, window, cx| {
+            panel.set_zoomed(true, window, cx);
+        });
+
+        // Verify the panel is zoomed and the dock is open
+        workspace_a.update_in(cx, |workspace, window, cx| {
+            assert!(
+                workspace.right_dock().read(cx).is_open(),
+                "dock should be open before switch"
+            );
+            assert!(
+                panel.is_zoomed(window, cx),
+                "panel should be zoomed before switch"
+            );
+            assert!(
+                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
+                "panel should be focused before switch"
+            );
+        });
+
+        // Switch to workspace B
+        multi_workspace_handle
+            .update(cx, |mw, window, cx| {
+                mw.activate_index(1, window, cx);
+            })
+            .unwrap();
+        cx.run_until_parked();
+
+        // Switch back to workspace A
+        multi_workspace_handle
+            .update(cx, |mw, window, cx| {
+                mw.activate_index(0, window, cx);
+            })
+            .unwrap();
+        cx.run_until_parked();
+
+        // Verify the panel is still zoomed and the dock is still open
+        workspace_a.update_in(cx, |workspace, window, cx| {
+            assert!(
+                workspace.right_dock().read(cx).is_open(),
+                "dock should still be open after switching back"
+            );
+            assert!(
+                panel.is_zoomed(window, cx),
+                "panel should still be zoomed after switching back"
+            );
+        });
+    }
+
     fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
         pane.read(cx)
             .items()