fix(macos): answer the Dock's reopen so a retired tty7 can come back

Closing the last window with the tray icon on retires tty7 to the tray:
process alive, Dock icon up, nothing on screen. That state had no way back
through the icon. macOS relaunching an already-running app arrives as
`applicationShouldHandleReopen:hasVisibleWindows:`, gpui's delegate forwards
it to a callback registered with `Application::on_reopen`, and tty7
registered none — so the click was a no-op, and the only ways back in were
`⌘N`, the tray's "Show tty7", or quitting and relaunching.

`windows::reopen` takes that callback, in the two shapes the state has: a
window still registered is activated rather than doubled, and no window at
all goes through the pathless-launch restore (`restore_target` + `open_at` +
`announce_detached_at_launch`) — the same path the tray's windowless branch
takes, so the workspace that retired is the one that returns and not a blank
one beside it. `reopen_with` is the seam the tests drive, so a reopen that
opens a second window beside the one on screen cannot pass.

`Application::on_reopen` is registered beside `on_open_urls` in `main`,
because it has to exist before `run` — `keymap::init` runs inside the loop —
and the callback defers to the loop with `cx.spawn` rather than opening
windows on AppKit's delegate stack, the shape `on_open_urls` already uses.
`activate_window` is `makeKeyAndOrderFront:` on macOS.

Reported from a macOS machine where a lid close and wake left the process
frontmost with no window: `launchservicesd SETFRONT` at 23:51:40 with the
process still reported `running-active-NotVisible`, and the layout only back
after a quit and relaunch. The window itself being lost across display
sleep → wake is not explained by this change and carries no guess-fix here:
nothing in tty7 or in the pinned gpui hangs off display sleep or wake.

cargo fmt --check; cargo check --locked -p tty7 --tests; cargo test --locked
-p tty7 --bin tty7-app -- 1888 passed, 0 failed.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
Silas Su
2026-09-14 00:18:07 +08:00
co-authored by CommandCodeBot
parent 66c42e48b7
commit 0499890f76
2 changed files with 101 additions and 0 deletions
+18
View File
@@ -638,6 +638,24 @@ fn main() {
application.on_open_urls(move |urls| {
let _ = external_open_tx.try_send(urls);
});
// macOS relaunching an app that is already running — the Dock icon, a
// double-click on the bundle, `open -a tty7` — only reaches this process
// as `applicationShouldHandleReopen:`, and gpui's delegate does nothing
// with it unless a handler is registered. That is the one entrance a
// tray-resident tty7 has: with `show_tray_icon` on, closing the last
// window keeps the process and its Dock icon alive with nothing on
// screen, and without this the icon's click was a no-op (the only ways
// back in were ⌘N, the tray's "Show tty7", or quitting and relaunching).
// Like `on_open_urls`, the hook lives on `Application`, so it cannot go
// beside the other app-level handlers in `keymap::init`; and like that
// path it defers to the loop rather than opening windows on AppKit's
// delegate stack, which is also how Zed's own reopen handler runs.
application.on_reopen(|cx| {
cx.spawn(async move |cx| {
let _ = cx.update(crate::ui::windows::reopen);
})
.detach();
});
application.run(move |cx| {
// gpui invokes this callback without an `App` context. Bridge it
// back onto the application loop instead of touching UI state on
+83
View File
@@ -480,6 +480,30 @@ fn open_missing_cli_window_with(
announce_detached_at_launch(cx, restore);
}
/// Brings the UI back when macOS relaunches a tty7 that is already running.
///
/// AppKit only asks when it found no visible window, and that is a state
/// tty7 lives in on purpose: closing the last window with the tray icon on
/// retires to the tray, process alive and Dock icon up. A window that is
/// still there is activated rather than doubled — `activate_window` is
/// `makeKeyAndOrderFront:`, which is what orders a window AppKit left behind
/// back to the front. None at all gets the last layout back the way a
/// pathless launch and the tray's windowless path do, so the workspace that
/// retired is the one that returns and not a blank one beside it.
pub fn reopen(cx: &mut App) {
reopen_with(cx, open_at);
}
fn reopen_with(
cx: &mut App,
open: impl FnOnce(&mut App, Option<WorkspaceId>, Option<std::path::PathBuf>),
) {
match WindowRegistry::most_recent(cx) {
Some(workspace) => activate(cx, workspace),
None => open_missing_cli_window_with(cx, None, open),
}
}
pub fn refresh_menu(cx: &mut App) {
crate::ui::theme::set_menus(cx);
}
@@ -988,6 +1012,65 @@ mod tests {
assert_eq!(opened, Some((None, None)));
}
#[gpui::test]
fn a_reopen_with_no_window_up_restores_the_workspace_that_retired(
cx: &mut gpui::TestAppContext,
) {
// The Dock icon after the last window retired to the tray: the
// process is alive with nothing on screen, and the click has to bring
// back the layout that was there, not mint a blank workspace beside it.
let view = WindowView::default();
let restored = view.id;
let mut opened = None;
cx.update(|cx| {
WindowRegistry::init(cx);
WorkspaceStore::install_for_test(
cx,
WindowViews {
views: vec![view],
active: Some(restored),
},
);
reopen_with(cx, |_, workspace, path| {
opened = Some((workspace, path));
});
});
assert_eq!(opened, Some((Some(restored), None)));
}
#[gpui::test]
fn a_reopen_with_a_window_up_activates_it_instead_of_opening_another(
cx: &mut gpui::TestAppContext,
) {
// AppKit also asks while a window is still there but off screen. That
// window is the thing to activate; a second one would take the
// restore away from it.
use gpui::VisualContext as _;
let (app, mut vcx) = crate::ui::app::test_window::harness(cx);
let handle = vcx.window_handle();
app.update_in(&mut vcx, |_, _, cx| {
WindowRegistry::init(cx);
let view = WindowView::default();
let open = view.id;
WorkspaceStore::install_for_test(
cx,
WindowViews {
views: vec![view],
active: Some(open),
},
);
WindowRegistry::register(cx, open, handle, app.downgrade());
reopen_with(cx, |_, workspace, _| {
panic!("a reopen with a window up opened another for {workspace:?}");
});
assert_eq!(WindowRegistry::count(cx), 1);
});
}
#[gpui::test]
fn a_request_carrying_a_path_restores_the_layout_and_brings_the_path_along(
cx: &mut gpui::TestAppContext,