Skip to content

fix(main): defer hide() in hideOnClose handler past windowShouldClose: - #22

Open
MaxLeiter wants to merge 1 commit into
loc:mainfrom
MaxLeiter:fix/defer-hide-on-close-macos
Open

fix(main): defer hide() in hideOnClose handler past windowShouldClose:#22
MaxLeiter wants to merge 1 commit into
loc:mainfrom
MaxLeiter:fix/defer-hide-on-close-macos

Conversation

@MaxLeiter

Copy link
Copy Markdown

Bug

On macOS, clicking the red traffic-light on a hideOnClose pool window quits the consumer's entire app, not just the popout. Cmd+W on the same window works correctly.

Reported and root-caused in anthropics/apps#43100 (closed unmerged in favor of fixing it here, per @alicelovescake).

Root cause

WindowInstance's close handler does event.preventDefault() then browserWindow.hide() synchronously. The two close paths diverge at the native layer:

  • Red button → AppKit -[NSWindow performClose:]-windowShouldClose: delegate → emits JS close. The handler's hide()-[NSWindow orderOut:] runs while still inside windowShouldClose:, which trips app termination.
  • Cmd+Wfocused.close()NativeWindowMac::Close() → emits JS close directly (no delegate). Same handler runs, no termination.

Fix

Wrap the hide() in setImmediate(...) so windowShouldClose: returns NO before the window orders out. The preventDefault() and UserCloseRequested event stay synchronous so consumers see no API change. The isDestroyed() guard re-evaluates inside the deferred callback to handle a force-close racing the deferred hide.

The defer is unconditional (not Darwin-gated) — one event-loop tick is harmless on Windows/Linux and keeps the code path uniform.

Observable change

The renderer receives UserCloseRequested while the window is still visible for ~1 tick (previously it was already hidden). The Hidden event still fires when the deferred orderOut: runs, so anything that genuinely depends on visibility should be listening for that instead.

Tests

Two new unit tests in tests/unit/main-process.test.ts:

  • hideOnClose: defers hide() past the close event, preventDefault stays synchronous — asserts hide() is not called inside the close dispatch and is called on the next tick.
  • hideOnClose: deferred hide() is a no-op if the window was destroyed in between — guards the race against a destroy() between the close event and the deferred hide.

yarn test:unit: 174 passed. yarn typecheck, yarn lint, yarn format:check: clean.

Verification

Verified working on macOS by @alicelovescake against an earlier iteration of the same defer in anthropics/apps#43100. The exact patch in that PR (against the built dist/) is byte-equivalent to this change in src/.

Related

🤖 Generated with Claude Code

The hideOnClose close handler does event.preventDefault() then
browserWindow.hide() synchronously. On macOS, the red traffic-light
button fires the JS close event from inside AppKit's windowShouldClose:
delegate; calling hide() (orderOut:) synchronously within that callback
trips app termination — the consumer's whole app quits, not just the
pooled window.

Cmd+W and BrowserWindow.close() route through NativeWindowMac::Close()
(no delegate) and never hit this, but the one-tick defer is harmless
there too, so it's not Darwin-gated.

Reported and verified in anthropics/apps#43100.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants