Dismiss stale remote connection modal when switching back to local workspace (#53575)

Eric Holk created

When the sidebar opens a remote SSH project, it shows a
`RemoteConnectionModal` on the currently active (local) workspace. After
the connection succeeds and a new remote workspace is created and
activated, the modal on the local workspace was never dismissed.
Switching back to the local workspace (e.g. by activating a thread)
would re-render the local workspace's modal layer, revealing the stale
"Starting proxy..." modal.

Other code paths that show this modal (`recent_projects`, `git_ui`)
already call `modal.finished(cx)` after the connection completes. The
sidebar and agent panel paths were missing this cleanup.

## Changes

- **`remote_connection`**: Added `dismiss_connection_modal()`, a public
utility that finds and dismisses any active `RemoteConnectionModal` on a
given workspace.
- **`sidebar`**: Fixed two call sites (`open_workspace_for_group` and
`open_workspace_and_activate_thread`) to dismiss the modal after the
connection task completes, regardless of success or failure.
- **`agent_ui`**: Fixed `open_worktree_workspace_and_start_thread` to
dismiss the modal after workspace creation completes.

Release Notes:

- (Preview only) Fixed a spurious "Starting proxy..." modal appearing
and hanging when switching back to a local project after opening a
remote SSH project in a multi-project workspace.

Change summary

crates/agent_ui/src/agent_panel.rs                | 47 +++++++++-------
crates/remote_connection/src/remote_connection.rs | 17 ++++++
crates/sidebar/src/sidebar.rs                     | 35 ++++++++----
3 files changed, 66 insertions(+), 33 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -3190,28 +3190,33 @@ impl AgentPanel {
         let window_handle = window_handle
             .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
 
-        let workspace_task = window_handle.update(cx, |multi_workspace, window, cx| {
-            let path_list = PathList::new(&all_paths);
-            let active_workspace = multi_workspace.workspace().clone();
-
-            multi_workspace.find_or_create_workspace(
-                path_list,
-                remote_connection_options,
-                None,
-                move |connection_options, window, cx| {
-                    remote_connection::connect_with_modal(
-                        &active_workspace,
-                        connection_options,
-                        window,
-                        cx,
-                    )
-                },
-                window,
-                cx,
-            )
-        })?;
+        let (workspace_task, modal_workspace) =
+            window_handle.update(cx, |multi_workspace, window, cx| {
+                let path_list = PathList::new(&all_paths);
+                let active_workspace = multi_workspace.workspace().clone();
+                let modal_workspace = active_workspace.clone();
+
+                let task = multi_workspace.find_or_create_workspace(
+                    path_list,
+                    remote_connection_options,
+                    None,
+                    move |connection_options, window, cx| {
+                        remote_connection::connect_with_modal(
+                            &active_workspace,
+                            connection_options,
+                            window,
+                            cx,
+                        )
+                    },
+                    window,
+                    cx,
+                );
+                (task, modal_workspace)
+            })?;
 
-        let new_workspace = workspace_task.await?;
+        let result = workspace_task.await;
+        remote_connection::dismiss_connection_modal(&modal_workspace, cx);
+        let new_workspace = result?;
 
         let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
 

crates/remote_connection/src/remote_connection.rs 🔗

@@ -574,6 +574,23 @@ pub fn connect_with_modal(
     })
 }
 
+/// Dismisses any active [`RemoteConnectionModal`] on the given workspace.
+///
+/// This should be called after a remote connection attempt completes
+/// (success or failure) when the modal was shown on a workspace that may
+/// outlive the connection flow — for example, when the modal is shown
+/// on a local workspace before switching to a newly-created remote
+/// workspace.
+pub fn dismiss_connection_modal(workspace: &Entity<Workspace>, cx: &mut gpui::AsyncWindowContext) {
+    workspace
+        .update_in(cx, |workspace, _window, cx| {
+            if let Some(modal) = workspace.active_modal::<RemoteConnectionModal>(cx) {
+                modal.update(cx, |modal, cx| modal.finished(cx));
+            }
+        })
+        .ok();
+}
+
 /// Creates a [`RemoteClient`] by reusing an existing connection from the
 /// global pool. No interactive UI is shown. This should only be called
 /// when [`remote::has_active_connection`] returns `true`.

crates/sidebar/src/sidebar.rs 🔗

@@ -753,19 +753,26 @@ impl Sidebar {
         let host = project_group_key.host();
         let provisional_key = Some(project_group_key.clone());
         let active_workspace = multi_workspace.read(cx).workspace().clone();
+        let modal_workspace = active_workspace.clone();
 
-        multi_workspace
-            .update(cx, |this, cx| {
-                this.find_or_create_workspace(
-                    path_list,
-                    host,
-                    provisional_key,
-                    |options, window, cx| connect_remote(active_workspace, options, window, cx),
-                    window,
-                    cx,
-                )
-            })
-            .detach_and_log_err(cx);
+        let task = multi_workspace.update(cx, |this, cx| {
+            this.find_or_create_workspace(
+                path_list,
+                host,
+                provisional_key,
+                |options, window, cx| connect_remote(active_workspace, options, window, cx),
+                window,
+                cx,
+            )
+        });
+
+        cx.spawn_in(window, async move |_this, cx| {
+            let result = task.await;
+            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
+            result?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
     }
 
     /// Rebuilds the sidebar contents from current workspace and thread state.
@@ -2292,6 +2299,7 @@ impl Sidebar {
         let host = project_group_key.host();
         let provisional_key = Some(project_group_key.clone());
         let active_workspace = multi_workspace.read(cx).workspace().clone();
+        let modal_workspace = active_workspace.clone();
 
         let open_task = multi_workspace.update(cx, |this, cx| {
             this.find_or_create_workspace(
@@ -2306,6 +2314,9 @@ impl Sidebar {
 
         cx.spawn_in(window, async move |this, cx| {
             let result = open_task.await;
+            // Dismiss the modal as soon as the open attempt completes so
+            // failures or cancellations do not leave a stale connection modal behind.
+            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
 
             if result.is_err() || is_remote {
                 this.update(cx, |this, _cx| {