Closes #61346 # Objective At startup, a synchronous `try_recv` decides whether to restore the session. On a macOS cold launch the CLI request arrives via `application:openURLs:`, which AppKit dispatches only *after* the startup closure returns, so the check finds nothing, session restore begins, and the request lands moments later on the post-startup path. Both behaviors are individually correct: a user with `restore_on_startup: "last_session"` wants their session back, and the requested file should open. The defect is that they race, the CLI open runs its existing-window matching before restore has placed any windows, finds nothing to reuse, and opens a redundant window. ## Solution Make the post-startup open loop wait for restore to settle before handling a request, so `find_existing_workspace` sees the restored windows and honors `cli_default_open_behavior`. The wait races full restore completion against the first window being placed, so a slow or hung remote-workspace restore can't block a local open. No timeouts involved, the fix is ordering, not delay :). ## Testing **Steps**: 1. disable & enable the setting `close windows when quiting an application` in macos itself in `desktop & dock` section 2. create a bundled Zed app 3. install cli binary to actually test the change 4. Follow any of the steps inside the before/after video's to test all the cases we have I tested this with a manual prod build using a custom script that bundles Zed as a preview app. So I could actually test the cli baviour since it works different for non-bundled builds. So the easiest way to test this would be to run bundling on this PR and install the cli binary to actually test it with the fix included. ## 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 adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase <details> <summary>**Disabled** Close windows when quiting an application</summary> **before** (disabled apps on close & `add to existing window` cli bahavior) https://github.com/user-attachments/assets/b497130b-3432-4fa8-9fbe-70c6f63d165e **After** (disabled apps on close & `add to existing window` cli bahavior) https://github.com/user-attachments/assets/354657f7-8dfe-4beb-9dd4-2bb615cb912c **After** (disabled apps on close & `open a new window` cli bahavior) https://github.com/user-attachments/assets/07fc3062-89a3-4e2a-811e-296533feaad7 </details> <details> <summary>**Enabled** Close windows when quiting an application (unchanged)</summary> **After** (enabled** apps on close & `add to existing window` cli bahavior (**unchanged**)) https://github.com/user-attachments/assets/445f1d3a-2987-46b1-9bfb-b2d61894ddec **After** (enabled apps on close & `open a new window` cli bahavior (**unchanged**)) https://github.com/user-attachments/assets/c3078b0d-7fb4-42db-9dea-095439e23830 </details> --- Release Notes: - Fixed an extra window opening on macOS when launching Zed from the CLI with session restore enabled
Unified · Split
| @@ -26,7 +26,7 @@ use db::kvp::{GlobalKeyValueStore, KeyValueStore}; · 25 unmodified lines | ||
| 26 | 26 | use editor::Editor; |
| 27 | 27 | use extension::ExtensionHostProxy; |
| 28 | 28 | use fs::{Fs, RealFs}; |
| 29 | −use futures::{StreamExt, channel::oneshot, future}; | |
| 29 | +use futures::{FutureExt, StreamExt, channel::oneshot, future}; | |
| 30 | 30 | use git::GitHostingProviderRegistry; |
| 31 | 31 | use git_ui::clone::clone_and_open; |
| 32 | 32 | use gpui::{ |
| @@ -945,11 +945,22 @@ fn main() { · 912 unmodified lines | ||
| 945 | 945 | }), |
| 946 | 946 | }; |
| 947 | 947 | |
| 948 | + let (first_window_tx, first_window_rx) = oneshot::channel::<()>(); | |
| 949 | + let first_window_tx = Rc::new(RefCell::new(Some(first_window_tx))); | |
| 950 | + let _first_window_subscription = cx.observe_new::<MultiWorkspace>(move |_, _, _| { | |
| 951 | + if let Some(tx) = first_window_tx.borrow_mut().take() { | |
| 952 | + tx.send(()).ok(); | |
| 953 | + } | |
| 954 | + }); | |
| 955 | + | |
| 956 | + let restore_finished = cx.background_spawn(restore_task).shared(); | |
| 957 | + | |
| 948 | 958 | cx.spawn({ |
| 949 | 959 | let db = workspace::WorkspaceDb::global(cx); |
| 950 | 960 | let fs = app_state.fs.clone(); |
| 961 | + let restore_finished = restore_finished.clone(); | |
| 951 | 962 | async move |_cx| { |
| 952 | − restore_task.await; | |
| 963 | + restore_finished.await; | |
| 953 | 964 | db.garbage_collect_workspaces( |
| 954 | 965 | fs.as_ref(), |
| 955 | 966 | ¤t_session_id, |
| @@ -965,7 +976,16 @@ fn main() { · 9 unmodified lines | ||
| 965 | 976 | component_preview::init(app_state.clone(), cx); |
| 966 | 977 | |
| 967 | 978 | cx.spawn(async move |cx| { |
| 979 | + let _first_window_subscription = _first_window_subscription; | |
| 980 | + let first_window_placed = first_window_rx.shared(); | |
| 968 | 981 | while let Some(urls) = open_rx.next().await { |
| 982 | + // On a macOS cold launch, `zed <path>` arrives here after startup already | |
| 983 | + // began restoring the session, so wait for a restored window to exist before | |
| 984 | + // matching. Otherwise this open sees no windows and spawns a redundant one (#61346). | |
| 985 | + futures::select_biased! { | |
| 986 | + _ = restore_finished.clone() => {} | |
| 987 | + _ = first_window_placed.clone() => {} | |
| 988 | + } | |
| 969 | 989 | cx.update(|cx| { |
| 970 | 990 | if let Some(request) = OpenRequest::parse(urls, cx).log_err() { |
| 971 | 991 | handle_open_request(request, app_state.clone(), cx); |