fix(quit): enforce Warn Before Quit when Cmd+Q arrives via app switcher (#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 #2139

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Achieve 2026-03-27 06:12:32 +08:00 committed by GitHub
parent 46589f531c
commit 6a41c9f42f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2212,6 +2212,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private var lastTypingActivityAt: TimeInterval = 0
private var didHandleExplicitOpenIntentAtStartup = false
private var isTerminatingApp = false
// Set to true when the user has already confirmed quit via the warning dialog,
// so applicationShouldTerminate does not show a second alert.
private var isQuitWarningConfirmed = false
private var didInstallLifecycleSnapshotObservers = false
private var didDisableSuddenTermination = false
private var commandPaletteVisibilityByWindowId: [UUID: Bool] = [:]
@ -2701,7 +2704,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
isTerminatingApp = true
_ = saveSessionSnapshot(includeScrollback: true, removeWhenEmpty: false)
return .terminateNow
// If the user already confirmed via the Cmd+Q shortcut warning dialog
// (handleQuitShortcutWarning), skip the check to avoid a second alert.
if isQuitWarningConfirmed {
return .terminateNow
}
// Respect the "Warn Before Quit" setting even when Cmd+Q arrives via
// the Cmd+Tab app switcher, bypassing handleCustomShortcut.
guard QuitWarningSettings.isEnabled() else {
return .terminateNow
}
// Show the same confirmation dialog used by the Cmd+Q shortcut path,
// then reply asynchronously so we can return .terminateLater now.
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)
}
return .terminateLater
}
func applicationWillTerminate(_ notification: Notification) {
@ -8983,6 +9025,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
if response == .alertFirstButtonReturn {
// Mark as confirmed so applicationShouldTerminate does not show a
// second alert when NSApp.terminate re-enters the delegate callback.
isQuitWarningConfirmed = true
NSApp.terminate(nil)
}
return true