Skip to content

fix(quit): enforce Warn Before Quit when Cmd+Q arrives via app switcher - #2186

Merged
lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
anthhub:fix/cmd-q-warn-before-quit
Mar 26, 2026
Merged

lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
anthhub:fix/cmd-q-warn-before-quit

Conversation

@anthhub

@anthhub anthhub commented Mar 26, 2026 •

Copy link
Copy Markdown

Summary

  • applicationShouldTerminate was unconditionally returning .terminateNow, bypassing the QuitWarningSettings check entirely
  • When Cmd+Q is pressed in the Cmd+Tab app switcher, macOS calls applicationShouldTerminate directly without going through handleCustomShortcut, so the existing dialog in handleQuitShortcutWarning was never shown
  • Fix: check QuitWarningSettings.isEnabled() in applicationShouldTerminate; if enabled, show the same confirmation alert (with suppression button) and reply asynchronously via NSApp.reply(toApplicationShouldTerminate:)
  • Add isQuitWarningConfirmed flag to prevent a double dialog when the normal Cmd+Q path (handleQuitShortcutWarning) has already obtained user confirmation before calling NSApp.terminate

Fixes #2139

Test plan

  • Enable Settings → Warn Before Quit
  • Press Cmd+Tab to open the app switcher, then press Cmd+Q while cmux is highlighted — confirm that the "Quit cmux?" alert appears and cancelling keeps the app running
  • With warning enabled, press Cmd+Q normally (not via app switcher) — confirm only one alert appears (no double dialog)
  • Tick "Don't warn again for Cmd+Q" in the app-switcher alert — confirm subsequent quits (both paths) skip the dialog
  • Disable Warn Before Quit in Settings — confirm Cmd+Q via app switcher quits immediately without any alert

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed quit confirmation dialog appearing multiple times during app termination.
    • Improved stability when dismissing the quit warning prompt.

applicationShouldTerminate was returning .terminateNow unconditionally,
bypassing QuitWarningSettings. Now it shows the same confirmation alert
used by handleQuitShortcutWarning, with an isQuitWarningConfirmed flag
to prevent a double dialog when the Cmd+Q shortcut path already confirmed.

Fixes manaflow-ai#2139

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 26, 2026

Copy link
Copy Markdown

@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Modified AppDelegate.swift to fix the quit confirmation dialog not appearing when quitting via Cmd+Q from the macOS Cmd+Tab switcher. Added state tracking and asynchronous handling using .terminateLater to ensure the dialog displays regardless of app focus state.

Changes

Cohort / File(s) Summary
App Termination State & Flow
Sources/AppDelegate.swift
Added isQuitWarningConfirmed state to prevent re-entrant quit dialogs. Updated applicationShouldTerminate to use asynchronous alert presentation with .terminateLater, ensuring the quit warning displays even when triggered from app switcher. Modified handleQuitShortcutWarning to set confirmation flag before terminating.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A quit that wouldn't warn before,
Now whispers soft when Cmd+Q you pour,
From switchers high to focus low,
The dialog dances—now you'll know!
✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: enforcing the Warn Before Quit dialog when Cmd+Q arrives via the app switcher, which is the primary objective of this PR.
Description check ✅ Passed The description covers the problem, fix, and test plan, meeting most template requirements. However, it lacks explicit mention of testing completion, any demo video, and bot review requests.
Linked Issues check ✅ Passed The PR successfully addresses issue #2139 by implementing the check for QuitWarningSettings in applicationShouldTerminate and showing the confirmation dialog from the app switcher path.
Out of Scope Changes check ✅ Passed All changes in AppDelegate.swift directly address the linked issue; no out-of-scope modifications detected. The isQuitWarningConfirmed flag and updated termination logic are essential to the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap in the "Warn Before Quit" feature: when Cmd+Q arrives via the Cmd+Tab app-switcher, macOS calls applicationShouldTerminate directly, completely bypassing handleCustomShortcut / handleQuitShortcutWarning. The fix correctly gates applicationShouldTerminate on QuitWarningSettings.isEnabled(), shows the same confirmation alert asynchronously, and uses NSApp.reply(toApplicationShouldTerminate:) to answer the pending termination request. A new isQuitWarningConfirmed flag prevents a double-dialog on the normal Cmd+Q path.\n\nKey observations:\n- Logic is correct for all described scenarios (warning on/off, both Cmd+Q paths, "Don't warn again" suppression).\n- isQuitWarningConfirmed consume-and-reset: the flag is read in applicationShouldTerminate but never reset to false after serving its purpose. In the extremely unlikely event that the app survives after returning .terminateNow, the flag stays true and would silently bypass the dialog on every subsequent quit via app switcher.\n- Duplicated alert construction: the full NSAlert setup block is copy-pasted verbatim between applicationShouldTerminate (lines 2723–2730) and handleQuitShortcutWarning (lines 9013–9020). A private showQuitConfirmationAlert() helper would eliminate the drift risk.

Confidence Score: 4/5

Safe to merge; the core fix is correct and all described test scenarios are handled properly, with two non-blocking P2 items remaining.

The fix correctly addresses the root cause (app-switcher bypassing handleCustomShortcut), the isQuitWarningConfirmed double-dialog guard works for both quit paths, and suppression-button persistence is handled consistently. The two remaining issues — not resetting isQuitWarningConfirmed after consumption, and duplicated alert-construction code — are style/defensive-coding concerns that don't affect normal user flows.

Sources/AppDelegate.swift — the duplicated alert block and the unconsumed isQuitWarningConfirmed flag after .terminateNow.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Adds isQuitWarningConfirmed flag and async quit-dialog in applicationShouldTerminate to cover the Cmd+Tab app-switcher path; two style-level concerns: flag not reset after consumption, and duplicated alert-construction logic.

Sequence Diagram

sequenceDiagram
    participant User
    participant macOS
    participant AppDelegate

    Note over User,AppDelegate: Normal Cmd+Q path (handleCustomShortcut)
    User->>AppDelegate: Cmd+Q key event
    AppDelegate->>AppDelegate: handleQuitShortcutWarning()
    AppDelegate->>AppDelegate: QuitWarningSettings.isEnabled()?
    alt warning disabled
        AppDelegate->>macOS: NSApp.terminate(nil)
    else warning enabled
        AppDelegate->>User: Show "Quit cmux?" alert (runModal)
        User-->>AppDelegate: Confirms quit
        AppDelegate->>AppDelegate: isQuitWarningConfirmed = true
        AppDelegate->>macOS: NSApp.terminate(nil)
        macOS->>AppDelegate: applicationShouldTerminate(_:)
        AppDelegate->>AppDelegate: isQuitWarningConfirmed == true → .terminateNow
        macOS->>AppDelegate: applicationWillTerminate(_:)
    end

    Note over User,AppDelegate: Cmd+Tab app-switcher path (NEW)
    User->>macOS: Cmd+Q in app switcher
    macOS->>AppDelegate: applicationShouldTerminate(_:)
    AppDelegate->>AppDelegate: isTerminatingApp = true, saveSessionSnapshot
    AppDelegate->>AppDelegate: isQuitWarningConfirmed? → false
    AppDelegate->>AppDelegate: QuitWarningSettings.isEnabled()?
    alt warning disabled
        AppDelegate-->>macOS: .terminateNow
    else warning enabled
        AppDelegate->>AppDelegate: DispatchQueue.main.async { show alert }
        AppDelegate-->>macOS: .terminateLater
        AppDelegate->>User: Show "Quit cmux?" alert (runModal)
        alt User confirms
            AppDelegate->>AppDelegate: isQuitWarningConfirmed = true
            AppDelegate->>macOS: NSApp.reply(toApplicationShouldTerminate: true)
            macOS->>AppDelegate: applicationWillTerminate(_:)
        else User cancels
            AppDelegate->>AppDelegate: isTerminatingApp = false
            AppDelegate->>macOS: NSApp.reply(toApplicationShouldTerminate: false)
        end
    end
Loading

Reviews (1): Last reviewed commit: "fix(quit): enforce Warn Before Quit when..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift
Comment on lines +2737 to +2744
let shouldQuit = response == .alertFirstButtonReturn
if shouldQuit {
self.isQuitWarningConfirmed = true
} else {
// Reset so that the next quit attempt can show the dialog again.
self.isTerminatingApp = false
}
NSApp.reply(toApplicationShouldTerminate: shouldQuit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 isQuitWarningConfirmed not reset after confirmed-but-surviving termination

In the shouldQuit == true branch, isQuitWarningConfirmed is set to true before NSApp.reply(toApplicationShouldTerminate: true). In the normal flow this is fine because the app terminates immediately and the flag is irrelevant.

However, the same flag is also set true in handleQuitShortcutWarning (line 9030) before NSApp.terminate(nil). That call triggers applicationShouldTerminate, which early-returns .terminateNow — consuming the flag — but never resets it to false afterward. If, in some edge case, a second applicationShouldTerminate invocation arrives while the flag is still true (e.g., the app was prevented from terminating by some other mechanism), the warning dialog would be silently bypassed.

A minimal defensive fix is to clear the flag immediately after it has served its purpose:

if isQuitWarningConfirmed {
    isQuitWarningConfirmed = false   // consume-and-reset; one-shot guard
    return .terminateNow
}

This makes the guard idempotent and harmless on repeated calls.

Comment thread Sources/AppDelegate.swift
Comment on lines +2722 to +2744
DispatchQueue.main.async {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = String(localized: "dialog.quitCmux.title", defaultValue: "Quit cmux?")
alert.informativeText = String(localized: "dialog.quitCmux.message", defaultValue: "This will close all windows and workspaces.")
alert.addButton(withTitle: String(localized: "dialog.quitCmux.quit", defaultValue: "Quit"))
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
alert.showsSuppressionButton = true
alert.suppressionButton?.title = String(localized: "dialog.dontWarnCmdQ", defaultValue: "Don't warn again for Cmd+Q")

let response = alert.runModal()
if alert.suppressionButton?.state == .on {
QuitWarningSettings.setEnabled(false)
}

let shouldQuit = response == .alertFirstButtonReturn
if shouldQuit {
self.isQuitWarningConfirmed = true
} else {
// Reset so that the next quit attempt can show the dialog again.
self.isTerminatingApp = false
}
NSApp.reply(toApplicationShouldTerminate: shouldQuit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicated alert-construction logic between two code paths

The full alert setup block (message text, informative text, buttons, suppression button title, runModal, suppression-state check) is an exact copy of the equivalent block in handleQuitShortcutWarning (lines 9013–9025). Any future change to the dialog — wording, a third button, accessibility label, etc. — must be applied in both places or the two paths will silently diverge.

Consider extracting a private helper:

/// Shows the "Quit cmux?" confirmation alert and returns true if the user confirmed.
/// Also handles disabling QuitWarningSettings when the suppression button is checked.
@discardableResult
private func showQuitConfirmationAlert() -> Bool {
    let alert = NSAlert()
    alert.alertStyle = .warning
    alert.messageText = String(localized: "dialog.quitCmux.title", defaultValue: "Quit cmux?")
    alert.informativeText = String(localized: "dialog.quitCmux.message", defaultValue: "This will close all windows and workspaces.")
    alert.addButton(withTitle: String(localized: "dialog.quitCmux.quit", defaultValue: "Quit"))
    alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
    alert.showsSuppressionButton = true
    alert.suppressionButton?.title = String(localized: "dialog.dontWarnCmdQ", defaultValue: "Don't warn again for Cmd+Q")
    let response = alert.runModal()
    if alert.suppressionButton?.state == .on {
        QuitWarningSettings.setEnabled(false)
    }
    return response == .alertFirstButtonReturn
}

Both applicationShouldTerminate and handleQuitShortcutWarning would then call showQuitConfirmationAlert() instead of rebuilding the alert inline.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 2733-2745: The suppression choice may not be flushed to disk
before the app exits; ensure the preference write is durably persisted before
calling NSApp.reply(toApplicationShouldTerminate:). Either update
QuitWarningSettings.setEnabled(_:) to perform a synchronous persistence (e.g.,
set the value on UserDefaults and call a durable flush such as
UserDefaults.standard.synchronize() or CFPreferencesAppSynchronize) or, after
calling QuitWarningSettings.setEnabled(false), explicitly flush the UserDefaults
on the main thread before calling NSApp.reply(toApplicationShouldTerminate:
shouldQuit); keep the existing isQuitWarningConfirmed/isTerminatingApp logic
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df974888-3e88-4e5a-b9d8-9fb7aa3c3b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 8a37815 and d1da61a.

📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

Comment thread Sources/AppDelegate.swift
Comment on lines +2733 to +2745
if alert.suppressionButton?.state == .on {
QuitWarningSettings.setEnabled(false)
}

let shouldQuit = response == .alertFirstButtonReturn
if shouldQuit {
self.isQuitWarningConfirmed = true
} else {
// Reset so that the next quit attempt can show the dialog again.
self.isTerminatingApp = false
}
NSApp.reply(toApplicationShouldTerminate: shouldQuit)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist the suppression choice before replying true.

QuitWarningSettings.setEnabled(false) only updates UserDefaults, so NSApp.reply(toApplicationShouldTerminate: true) can let the app exit before “Don’t warn again” is flushed. That makes the new app-switcher path flaky against the PR’s suppression-persistence goal. Please make this write synchronous, or defer the terminate reply until the preference is durably persisted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 2733 - 2745, The suppression choice
may not be flushed to disk before the app exits; ensure the preference write is
durably persisted before calling NSApp.reply(toApplicationShouldTerminate:).
Either update QuitWarningSettings.setEnabled(_:) to perform a synchronous
persistence (e.g., set the value on UserDefaults and call a durable flush such
as UserDefaults.standard.synchronize() or CFPreferencesAppSynchronize) or, after
calling QuitWarningSettings.setEnabled(false), explicitly flush the UserDefaults
on the main thread before calling NSApp.reply(toApplicationShouldTerminate:
shouldQuit); keep the existing isQuitWarningConfirmed/isTerminatingApp logic
unchanged.

@lawrencecchen
lawrencecchen merged commit 6a41c9f into manaflow-ai:main Mar 26, 2026
4 of 5 checks passed
@lawrencecchen

Copy link
Copy Markdown
Contributor

Thank you for the contribution!

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…er (manaflow-ai#2186)

applicationShouldTerminate was returning .terminateNow unconditionally,
bypassing QuitWarningSettings. Now it shows the same confirmation alert
used by handleQuitShortcutWarning, with an isQuitWarningConfirmed flag
to prevent a double dialog when the Cmd+Q shortcut path already confirmed.

Fixes manaflow-ai#2139

Co-authored-by: Claude Sonnet 4.6 <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.

"Warn Before Quit" dialog not shown when quitting via Cmd+Q in Cmd+Tab app switcher

2 participants