Skip to content

Route the macOS Quit Apple Event through the ⌘Q path - #1036

Open
zcy22606 wants to merge 1 commit into
hardbeat920:mainfrom
zcy22606:fix/route-quit-apple-event
Open

zcy22606 wants to merge 1 commit into
hardbeat920:mainfrom
zcy22606:fix/route-quit-apple-event

Conversation

@zcy22606

@zcy22606 zcy22606 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What changed

On macOS, quitting from the Dock (right-click > Quit), from AppleScript (osascript -e 'quit app "MonoCode"'), or at logout / restart now goes through the same quit flow as ⌘Q: the busy-turn prompt when agents are running, and the workspace save before exit.

  • src-tauri/src/macos.rs: new route_quit_event(). It installs a handler for the core Quit Apple Event ('aevt' / 'quit') with NSAppleEventManager; the handler calls window::request_quit. The handler object is kept alive in a thread_local, the same way DockMenuTarget is, because the event manager does not retain it.
  • src-tauri/src/lib.rs: call it from RunEvent::Ready, next to request_badge_authorization.

Why

⌘Q, the tray's Quit and app.exit() all reach window::request_quit: the app menu's Quit is a custom item (menu.rs, "quit" => crate::window::request_quit(app)), and RunEvent::ExitRequested with an exit code calls it too. That function polls every window for running turns, asks once if anything is busy, waits for each window to persist its workspace (quit_commit / quit_ready), and only then calls confirm_quit.

The Quit Apple Event never reaches any of that. The Dock, AppleScript and loginwindow all send kAEQuitApplication, and AppKit's default handler for it calls -[NSApplication terminate:] directly. tao's app delegate does not implement applicationShouldTerminate:, so nothing can say no: AppKit goes straight to applicationWillTerminate:, which tao turns into LoopDestroyed → RunEvent::Exit. ExitRequested is never emitted, so prevent_exit() and request_quit never run. RunEvent::Exit reaps the harness and PTY children, so running agents are killed without the prompt, and the windows never get quit_commit, so whatever the ⌘Q path would have saved first is lost.

Neither tauri nor tao exposes a hook for this (no applicationShouldTerminate:, no Apple Event callback), so the handler is installed directly. Two details:

  • Timing. AppKit installs its own Quit handler during finishLaunching, and setEventHandler: replaces whatever was registered before, so a handler set in setup would be overwritten. RunEvent::Ready is emitted from tao's applicationDidFinishLaunching:, which is after that.
  • Thread. Apple Events are dispatched on the main run loop, so the handler runs on the main thread, like the menu handler that already calls request_quit.

Behaviour at logout / restart: the handler returns right away, and the app quits once the normal flow finishes. With nothing running that is the poll (≤ 2 s) plus the commit (≤ 10 s, COMMIT_TIMEOUT), then confirm_quit exits. With a turn running the app shows the same "agents are still running" dialog as ⌘Q and stays open until it is answered; if it is never answered, macOS cancels the logout and names the app, as it does for any app with an unsaved-changes prompt. The app does not hang: CONFIRM_TIMEOUT abandons the unanswered quit and a later Quit starts a fresh one.

Related: #1012 also deals with state lost at macOS restart (it saves window geometry on move/resize). It touches a different arm of the RunEvent match and does not conflict; with this change, logout also reaches confirm_quit → app.exit, so the window-state plugin's exit save runs too.

UI

No visual change. Dock > Quit and osascript … quit now show the same quit confirmation as ⌘Q when agents are running.

Validation

In src-tauri:

  • cargo fmt --check: clean
  • cargo clippy --workspace --all-targets -- -D warnings: clean
  • cargo test: 655 passed, 1 ignored. (On one earlier run harness::tests::omp_accepts_rpc_capable_binary_and_rejects_other_names failed; it passes on its own on unmodified main and on the rerun, and this change does not touch harness.rs.)

No unit test: the change is Objective-C glue that only does something when AppKit dispatches a real Apple Event, and request_quit's state machine already has its own tests in window.rs.

Checked by hand on macOS in a downstream fork that carries the same code: Dock > Quit and osascript -e 'quit app "…"' both go through the same flow as ⌘Q (the busy-turn prompt when a turn is running, quitting without a prompt when idle), and the live transcripts are saved. I did not test logout / restart by hand; the behaviour described above comes from reading the code.

Checklist

  • I ran npm run check (check:rust; no web files changed)
  • This PR is small and focused
  • I did not mix unrelated changes

Summary by CodeRabbit

  • Bug Fixes
    • macOS now routes system-initiated quit events through the app’s standard quit flow. Quit requests from the operating system are therefore recognized and handled using the app’s existing behavior. This update applies to the macOS app; no changes to quit handling on other platforms are included.

Dock > Quit, AppleScript quit and logout send kAEQuitApplication, which
AppKit answers with terminate: on the spot. tao has no
applicationShouldTerminate:, so ExitRequested never fires: running agents
are killed without the busy-turn prompt and nothing the ⌘Q path saves
first is saved. Install a handler for the event after launch that calls
window::request_quit instead.
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 02f0de24-5894-4c09-a36a-3a60960e2d44
📥 Commits

Reviewing files that changed from the base of the PR and between e49a5ae and 0674a73.

📒 Files selected for processing: 2
  • src-tauri/src/lib.rs
  • src-tauri/src/macos.rs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

📝 Walkthrough

The macOS Ready handler now registers an Apple Event quit handler. On the main thread, the handler retains a target for aevt/quit, which forwards quit events to crate::window::request_quit.

Changes: macOS Quit Event Routing

Layer / File(s) Summary
Register and route quit events
src-tauri/src/macos.rs, src-tauri/src/lib.rs
route_quit_event registers and retains a target for aevt/quit on the main thread. The macOS Ready handler calls this function. The target forwards quit events to crate::window::request_quit.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ReadyHandler
  participant route_quit_event
  participant AppleEventSystem
  participant QuitEventTarget
  participant request_quit
  ReadyHandler->>route_quit_event: pass app handle
  route_quit_event->>AppleEventSystem: register target for aevt/quit
  AppleEventSystem->>QuitEventTarget: deliver quit event
  QuitEventTarget->>request_quit: forward quit event
Loading

Suggested reviewers: hardbeat920

Merge Risk: 🔵 Low · up to 0674a

AppleScript or another reply-requesting sender may be told the app quit even when the user cancels. This is a bounded integration risk, not an established blocker to merging.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely states the main change: routing the macOS Quit Apple Event through the existing ⌘Q quit flow.
Description check Passed The description includes all required sections, explains the change and motivation, documents UI impact, reports validation, and completes the checklist.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches: 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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