Skip to content

fix: adopt the native macOS app lifecycle around hide-to-tray - #4876

Closed
ole-tidal wants to merge 2 commits into
block:mainfrom
ole-tidal:macos-native-app-lifecycle
Closed

fix: adopt the native macOS app lifecycle around hide-to-tray#4876
ole-tidal wants to merge 2 commits into
block:mainfrom
ole-tidal:macos-native-app-lifecycle

Conversation

@ole-tidal

@ole-tidal ole-tidal commented Aug 5, 2026

Copy link
Copy Markdown

Why

Buzz on macOS deviated from standard app behavior in ways that compounded into "the app ignores me" moments (originally reported as Rectangle and ⌘W not working). All of them trace back to the hide-to-tray window model missing its native-lifecycle counterparts:

  1. ⌘Tab into Buzz while the window is hidden showed… nothing. Only the menu bar switched. macOS surfaces Dock-icon clicks as applicationShouldHandleReopen: (Tauri's Reopen event — already handled), but ⌘Tab and other app-switcher activations only post NSApplicationDidBecomeActiveNotification, which nothing observed. Every standard macOS app re-presents a window on activation.
  2. After the window hides, Buzz stayed the active app — frontmost with zero windows: a menu bar with nothing under it, a dead ⌘Tab target, and window managers like Rectangle resolve their target from the frontmost app's focused window and find none.
  3. The main window had an empty title ("title": ""), so Mission Control, the Window menu, and AX tools showed a nameless window.
  4. No Settings… (⌘,) or Check for Updates… menu items — standard app-submenu entries users reach for.

What

  • app_activation.rs (new): observes NSApplicationDidBecomeActiveNotification (block2 observer, same pattern and safety posture as mouse_nav.rs) and re-presents the main window when the app becomes active with no visible webview window. Gated on the deliberate first reveal (INITIAL_REVEAL_DONE, set by the first-frame reveal and by any explicit show) so launch-time self-activation cannot preempt the geometry-settled reveal. Minimized-only counts as "nothing visible", matching how ⌘Tab de-miniaturizes in standard apps.
  • lib.rs: after hide-to-tray on CloseRequested, if no other window is visible, app.hide() yields activation to the next app — the same handoff as closing the last window of any Mac app. The two changes compose: ⌘W hands focus to the next app; ⌘Tab back re-presents the window.
  • tauri.conf.json: window title: "Buzz" (still visually hidden via hiddenTitle + overlay title bar).
  • App submenu gains Check for Updates… and Settings… (⌘,) in their HIG positions. Both re-present the window and forward events to the webview: Settings toggles the settings route (mirroring the existing webview ⌘, shortcut, which keeps working on non-mac where there is no native menu), Check for Updates lands on Settings → Updates with the check already running.

Verification on a local build

Tested against a locally built instance (just desktop-standalone), driving the real app pid-exactly over the Accessibility API:

  • Window AX title is Buzz (was empty) — windowTitles=["Buzz"].
  • App submenu shows Check for Updates… and Settings… with cmd char ,, in HIG positions between About and Services.
  • ⌘W (posted to the pid) hides the window to tray; process stays alive; no other window visible afterward.
  • The activation observer fires on NSApplicationDidBecomeActiveNotification with correct gate values — verified via a temporary log line in the running app: did-become-active reveal_done=true any_visible=true (correctly no-oping while a window is visible).
  • show_main_window re-presents the hidden window (tray path, exercised repeatedly).
  • The restore decision (should_restore) is unit-tested for all four gate combinations.

Scope note on Rectangle: during live verification, the reporter's residual "Rectangle can't resize the window" symptom (window visible and focused) turned out to be Rectangle's own stale Accessibility session — restarting Rectangle fixed it, and the window was AX-resizable throughout. This PR's window-manager relevance is limited to the real defect above: the frontmost-with-zero-windows state after close-to-tray, where there is no window for any WM to target.

Verification gap, called out honestly: the composed end-to-end gesture — a real ⌘Tab into hidden-window Buzz auto-re-presenting, and the frontmost-app switch after close — cannot be driven programmatically: macOS 14+/Tahoe refuses every synthetic activation route without real user intent (NSRunningApplication.activate cooperative-refused, activateIgnoringOtherApps a documented no-op, System Events set frontmost refused, HID-synthesized Dock clicks filtered). Every constituent link is verified above; the composition is three unit-tested lines. One human ⌘Tab confirms it.

Also not coverable in standalone mode (no community, so AppShell and its listeners never mount): the webview side of the Settings…/Check for Updates… events. That path is typechecked and mirrors the existing ⌘, handler; the native side (items present, correct accelerator) is verified above.

Builds on the ⌘W PR (#4875) — both touch app_menu.rs. Opened from a fork, so it targets main directly and includes that PR's commit until it merges; once it lands, this diff reduces to the lifecycle changes (the second commit).

🤖 Generated with Claude Code

ole-tidal and others added 2 commits August 5, 2026 12:23
…wns it

Cmd+W is the standard macOS chord for closing the focused window; for a
tray-resident app that means hide-to-tray, matching the red close button.
The item returns as a custom MenuItem (predefined items cannot be toggled)
and Buzz Term disables it for exactly the span it owns the keyboard, so the
accelerator falls through to the webview and the close-tab chord still runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ole Bakstad <obakstad@squareup.com>
Four deviations from standard macOS behavior, all rooted in the
hide-to-tray window model, compounded into "the app ignores me" moments:

- Cmd+Tab into Buzz while the window was hidden showed only the menu
  bar: macOS surfaces Dock clicks as Reopen, but app-switcher
  activations only post NSApplicationDidBecomeActiveNotification, which
  nothing observed. New app_activation module re-presents the main
  window when the app becomes active with nothing visible, gated on the
  deliberate first reveal.
- Closing the last window left Buzz frontmost with zero windows — a
  dead Cmd+Tab target that window managers (Rectangle) can't resolve a
  window from. CloseRequested now yields activation (app.hide()) when
  no other window remains visible.
- The main window carried an empty title; Mission Control, the Window
  menu, and AX tools showed a nameless window. It is now "Buzz",
  still visually hidden by the overlay title bar.
- The app submenu lacked the standard Settings… (Cmd+,) and Check for
  Updates… items. Both are added in HIG positions and forward to the
  webview: Settings toggles the settings route (mirroring the existing
  webview shortcut, which keeps working elsewhere), Check for Updates
  lands on Settings → Updates with the check already running.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ole Bakstad <obakstad@squareup.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01de0c1c55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// and the block is sendable — it captures only a cloned `AppHandle`,
// which is `Send + Sync`. The observer token is deliberately leaked:
// the observer must live for the whole app lifetime.
let token = unsafe {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid adding unsafe notification registration

This new AppKit observer introduces an unsafe block in production desktop code. The repo-level agent rules explicitly disallow unsafe code, so this should be refactored behind an existing safe abstraction or otherwise removed rather than landing a new unsafe call site.

AGENTS.md reference: AGENTS.md:L113-L114

Useful? React with 👍 / 👎.

@ole-tidal
ole-tidal marked this pull request as draft August 5, 2026 10:50
@ole-tidal

Copy link
Copy Markdown
Author

🤖 Closing at the reporter's request: the driving pain point (window-manager control) turned out to be a stale Rectangle Accessibility session on the reporting machine, resolved by restarting Rectangle. Withdrawing the remaining native-behavior changes rather than pursuing them.

@ole-tidal ole-tidal closed this Aug 5, 2026
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.

1 participant