Repository navigation
fix(quit): enforce Warn Before Quit when Cmd+Q arrives via app switcher - #2186
Conversation
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>
|
@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughModified Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR fixes a gap in the "Warn Before Quit" feature: when Cmd+Q arrives via the Cmd+Tab app-switcher, macOS calls Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "fix(quit): enforce Warn Before Quit when..." | Re-trigger Greptile |
| 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) |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| 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) | ||
| } |
There was a problem hiding this comment.
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.
|
Thank you for the contribution! |
…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>
Summary
applicationShouldTerminatewas unconditionally returning.terminateNow, bypassing theQuitWarningSettingscheck entirelyapplicationShouldTerminatedirectly without going throughhandleCustomShortcut, so the existing dialog inhandleQuitShortcutWarningwas never shownQuitWarningSettings.isEnabled()inapplicationShouldTerminate; if enabled, show the same confirmation alert (with suppression button) and reply asynchronously viaNSApp.reply(toApplicationShouldTerminate:)isQuitWarningConfirmedflag to prevent a double dialog when the normal Cmd+Q path (handleQuitShortcutWarning) has already obtained user confirmation before callingNSApp.terminateFixes #2139
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit