Prevent dev container modal dismissal during creation (#52506)

Toni Alatalo and Claude Opus 4.6 created

## Context

When the dev container creation modal is showing "Creating Dev
Container", clicking anywhere on the workspace backdrop dismisses the
dialog. The container creation continues in the background, but the user
loses visual feedback and the subsequent `open_remote_project` call may
fail because the modal entity is gone.

This adds an `allow_dismissal` flag to `RemoteServerProjects` that
blocks accidental dismissal (backdrop clicks, focus loss) while a dev
container is being created, but allows explicit dismissal on success or
error.

## How to Review

Small PR — two files changed:

1. **`remote_servers.rs`** (the fix): `allow_dismissal` bool field
added, set to `false` when entering Creating state, set to `true` before
emitting `DismissEvent` on success/error. `on_before_dismiss` override
checks the flag.
2. **`recent_projects.rs`** (the test): Regression test that opens a dev
container modal, simulates a backdrop click, and asserts the modal stays
open.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed dev container creation modal being dismissed when clicking
outside it

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

Change summary

crates/recent_projects/src/recent_projects.rs | 67 ++++++++++++++++++++
crates/recent_projects/src/remote_servers.rs  | 20 +++++
2 files changed, 83 insertions(+), 4 deletions(-)

Detailed changes

crates/recent_projects/src/recent_projects.rs 🔗

@@ -2003,7 +2003,7 @@ mod tests {
     use std::path::PathBuf;
 
     use editor::Editor;
-    use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
+    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle};
 
     use serde_json::json;
     use settings::SettingsStore;
@@ -2242,6 +2242,71 @@ mod tests {
             .unwrap();
     }
 
+    #[gpui::test]
+    async fn test_dev_container_modal_not_dismissed_on_backdrop_click(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                path!("/project"),
+                json!({
+                    ".devcontainer": {
+                        "devcontainer.json": "{}"
+                    },
+                    "src": {
+                        "main.rs": "fn main() {}"
+                    }
+                }),
+            )
+            .await;
+
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from(path!("/project"))],
+                app_state,
+                workspace::OpenOptions::default(),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+        let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+
+        cx.run_until_parked();
+
+        cx.dispatch_action(*multi_workspace, OpenDevContainer);
+
+        multi_workspace
+            .update(cx, |multi_workspace, _, cx| {
+                assert!(
+                    multi_workspace
+                        .active_modal::<RemoteServerProjects>(cx)
+                        .is_some(),
+                    "Dev container modal should be open"
+                );
+            })
+            .unwrap();
+
+        // Click outside the modal (on the backdrop) to try to dismiss it
+        let mut vcx = VisualTestContext::from_window(*multi_workspace, cx);
+        vcx.simulate_click(gpui::point(px(1.0), px(1.0)), gpui::Modifiers::default());
+
+        multi_workspace
+            .update(cx, |multi_workspace, _, cx| {
+                assert!(
+                    multi_workspace
+                        .active_modal::<RemoteServerProjects>(cx)
+                        .is_some(),
+                    "Dev container modal should remain open during creation"
+                );
+            })
+            .unwrap();
+    }
+
     #[gpui::test]
     async fn test_open_dev_container_action_with_multiple_configs(cx: &mut TestAppContext) {
         let app_state = init_test(cx);

crates/recent_projects/src/remote_servers.rs 🔗

@@ -54,7 +54,7 @@ use util::{
     rel_path::RelPath,
 };
 use workspace::{
-    AppState, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
+    AppState, DismissDecision, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
     notifications::{DetachAndPromptErr, NotificationId},
     open_remote_project_with_existing_connection,
 };
@@ -69,6 +69,7 @@ pub struct RemoteServerProjects {
     create_new_window: bool,
     dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>,
     _subscription: Subscription,
+    allow_dismissal: bool,
 }
 
 struct CreateRemoteServer {
@@ -920,6 +921,7 @@ impl RemoteServerProjects {
             create_new_window,
             dev_container_picker: None,
             _subscription,
+            allow_dismissal: true,
         }
     }
 
@@ -1140,6 +1142,7 @@ impl RemoteServerProjects {
     }
 
     fn view_in_progress_dev_container(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.allow_dismissal = false;
         self.mode = Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
             DevContainerCreationProgress::Creating,
             cx,
@@ -1309,6 +1312,7 @@ impl RemoteServerProjects {
                 cx.emit(DismissEvent);
             }
             _ => {
+                self.allow_dismissal = true;
                 self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
                 self.focus_handle(cx).focus(window, cx);
                 cx.notify();
@@ -1875,6 +1879,7 @@ impl RemoteServerProjects {
                         .ok();
                         entity
                             .update_in(cx, |remote_server_projects, window, cx| {
+                                remote_server_projects.allow_dismissal = true;
                                 remote_server_projects.mode =
                                     Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
                                         DevContainerCreationProgress::Error(format!("{e}")),
@@ -1897,7 +1902,8 @@ impl RemoteServerProjects {
             .log_err();
 
             entity
-                .update(cx, |_, cx| {
+                .update(cx, |this, cx| {
+                    this.allow_dismissal = true;
                     cx.emit(DismissEvent);
                 })
                 .log_err();
@@ -2948,7 +2954,15 @@ fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
     element.read(cx).text(cx).trim().to_string()
 }
 
-impl ModalView for RemoteServerProjects {}
+impl ModalView for RemoteServerProjects {
+    fn on_before_dismiss(
+        &mut self,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) -> DismissDecision {
+        DismissDecision::Dismiss(self.allow_dismissal)
+    }
+}
 
 impl Focusable for RemoteServerProjects {
     fn focus_handle(&self, cx: &App) -> FocusHandle {