gpui: Persist window bounds and display when detaching a workspace session (#45201)

shibang created

Closes #41246 #45092

Release Notes:

- N/A

**Root Cause**:
Empty local workspaces returned `DetachFromSession` from 
`serialize_workspace_location()`, and the `DetachFromSession` handler
only cleared the session_id **without saving window bounds**.

**Fix Applied**:
Modified the `DetachFromSession` handler to save window bounds via
`set_window_open_status()`:
```rust
WorkspaceLocation::DetachFromSession => {
    let window_bounds = SerializedWindowBounds(window.window_bounds());
    let display = window.display(cx).and_then(|d| d.uuid().ok());
    window.spawn(cx, async move |_| {
        persistence::DB
            .set_window_open_status(database_id, window_bounds, display.unwrap_or_default())
            .await.log_err();
        persistence::DB.set_session_id(database_id, None).await.log_err();
    })
}
```

**Recording**:


https://github.com/user-attachments/assets/2b6564d4-4e1b-40fe-943b-147296340aa7

Change summary

crates/workspace/src/persistence.rs | 49 +++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs   | 24 +++++++++++---
2 files changed, 67 insertions(+), 6 deletions(-)

Detailed changes

crates/workspace/src/persistence.rs 🔗

@@ -3296,4 +3296,53 @@ mod tests {
 
         assert_eq!(workspace.center_group, new_workspace.center_group);
     }
+
+    #[gpui::test]
+    async fn test_empty_workspace_window_bounds() {
+        zlog::init_test();
+
+        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
+        let id = db.next_id().await.unwrap();
+
+        // Create a workspace with empty paths (empty workspace)
+        let empty_paths: &[&str] = &[];
+        let display_uuid = Uuid::new_v4();
+        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
+            origin: point(px(100.0), px(200.0)),
+            size: size(px(800.0), px(600.0)),
+        }));
+
+        let workspace = SerializedWorkspace {
+            id,
+            paths: PathList::new(empty_paths),
+            location: SerializedWorkspaceLocation::Local,
+            center_group: Default::default(),
+            window_bounds: None,
+            display: None,
+            docks: Default::default(),
+            breakpoints: Default::default(),
+            centered_layout: false,
+            session_id: None,
+            window_id: None,
+            user_toolchains: Default::default(),
+        };
+
+        // Save the workspace (this creates the record with empty paths)
+        db.save_workspace(workspace.clone()).await;
+
+        // Save window bounds separately (as the actual code does via set_window_open_status)
+        db.set_window_open_status(id, window_bounds, display_uuid)
+            .await
+            .unwrap();
+
+        // Retrieve it using empty paths
+        let retrieved = db.workspace_for_roots(empty_paths).unwrap();
+
+        // Verify window bounds were persisted
+        assert_eq!(retrieved.id, id);
+        assert!(retrieved.window_bounds.is_some());
+        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
+        assert!(retrieved.display.is_some());
+        assert_eq!(retrieved.display.unwrap(), display_uuid);
+    }
 }

crates/workspace/src/workspace.rs 🔗

@@ -5665,12 +5665,24 @@ impl Workspace {
                     persistence::DB.save_workspace(serialized_workspace).await;
                 })
             }
-            WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
-                persistence::DB
-                    .set_session_id(database_id, None)
-                    .await
-                    .log_err();
-            }),
+            WorkspaceLocation::DetachFromSession => {
+                let window_bounds = SerializedWindowBounds(window.window_bounds());
+                let display = window.display(cx).and_then(|d| d.uuid().ok());
+                window.spawn(cx, async move |_| {
+                    persistence::DB
+                        .set_window_open_status(
+                            database_id,
+                            window_bounds,
+                            display.unwrap_or_default(),
+                        )
+                        .await
+                        .log_err();
+                    persistence::DB
+                        .set_session_id(database_id, None)
+                        .await
+                        .log_err();
+                })
+            }
             WorkspaceLocation::None => Task::ready(()),
         }
     }