Skip to content

Avoid nested quit confirmation modal loops - #6461

Merged
azooz2003-bit merged 7 commits into
mainfrom
feat-sentry-ui-hang-quit-confirm
Jun 21, 2026
Merged

azooz2003-bit merged 7 commits into
mainfrom
feat-sentry-ui-hang-quit-confirm

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Addresses the high-frequency quit-path Sentry hang signature:

https://manaflow.sentry.io/issues/7391555405/

That issue's representative events stop in NSAlert.runModal under AppDelegate.applicationShouldTerminate, which means the main thread is sitting in a nested AppKit modal loop during quit confirmation.

Changes

  • Replaces quit confirmation runModal() calls with a retained QuitConfirmationAlertPresenter that uses beginSheetModal(for:completionHandler:) when a main window is available.
  • Falls back to a modeless alert window instead of runModal() when no sheet host is available.
  • Keeps .terminateLater semantics and calls reply(toApplicationShouldTerminate:) from the async completion.
  • Moves full session snapshot/history flushing out of the pre-confirmation path so canceling quit does not do heavy termination persistence first.
  • Preserves suppression, cancel, confirmed quit, and remote tmux kill deferral behavior.

Tests

  • xcodebuild test -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-sentryquit -only-testing:cmuxTests/QuitConfirmationModalLoopRegressionTests -only-testing:cmuxTests/QuitConfirmationAlertPresenterTests CODE_SIGNING_ALLOWED=NO

The first commit is test-only and fails against the old runModal() quit-confirmation paths; the second commit makes it pass.

Dogfood

Tagged build: quitcf

The tagged dev app builds and launches. The exact release/stable quit-confirmation dialog path cannot be fully UI-preflighted in a tagged dev build because existing policy intentionally skips quit confirmation for dev builds (QuitConfirmationStore.shouldShowConfirmation(... isDevBuild: true) == false). I verified the presenter behavior with the focused unit test and verified the tagged app launches and responds on its debug socket.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Prevents UI hangs on quit by removing nested AppKit modal loops and unifying quit confirmation into a non-blocking presenter. Avoids duplicate dialogs and returns the right terminate reply for both Cmd+Q and app-initiated quits.

  • Bug Fixes

    • Added a retained QuitConfirmationAlertPresenter that uses beginSheetModal when a host window exists, or a modeless alert with direct button actions otherwise (no runModal()).
    • Handles concurrent quit flows: applicationShouldTerminate uses a pending-reply policy to return .terminateLater only when terminate triggered the dialog; otherwise .terminateCancel. Blocks re-entry while a presenter is active.
    • Defers session snapshot/history flush until after confirmation so cancel stays fast; keeps suppression, cancel/confirm behavior, and remote tmux kill deferral intact.
    • Adds tests for sheet and modeless paths (no nested modal loop) and the pending-terminate reply policy.
  • Refactors

    • Moved quit confirmation helpers out of AppDelegate into a dedicated presenter file and an AppDelegate extension for pending-terminate and dirty-workspace checks.

Written for commit 2f41a34. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Improved quit-confirmation flow with more reliable dialog handling, including session snapshot/history preservation and “don’t warn again” suppression.
    • Added a dedicated quit confirmation presenter to show the warning consistently (sheet when possible, otherwise modal).
  • Bug Fixes
    • Fixed Cmd+Q quit-warning to prevent duplicate/stacked dialogs and correctly apply suppression; termination now defers when needed before cleanup work completes.
  • Tests
    • Added coverage for presenter presentation mode and the resulting termination/confirmation outcomes.

@vercel

vercel Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 21, 2026 3:04am
cmux-staging Building Building Preview, Comment Jun 21, 2026 3:04am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces QuitConfirmationAlertPresenter, a new @MainActor NSObject class that centralizes quit-dialog construction and presentation. Refactors AppDelegate to track an in-flight presenter and use new shared helpers (prepareForConfirmedAppTermination, presentQuitConfirmationAlert, handleApplicationTerminateQuitConfirmationResponse, pendingTerminateReply) in both applicationShouldTerminate(_:) and handleQuitShortcutWarning(), replacing inline NSAlert.runModal() calls. Tests verify sheet-modal vs. standalone-modal behavior and pendingTerminateReply return values.

Changes

Quit Confirmation Alert Presenter Refactor

Layer / File(s) Summary
QuitConfirmationAlertPresenter class definition
Sources/QuitConfirmationAlertPresenter.swift
QuitConfirmationAlertPresenter builds a localized quit NSAlert with Quit/Cancel buttons and suppression control. Presents as a sheet on available host windows (without attached sheets) or as a standalone modal panel. Wires button actions and window-close events to a single-invocation finish() that delivers the modal response and suppression-button state to the completion closure.
AppDelegate state tracking and quit/termination helpers
Sources/AppDelegate.swift, Sources/QuitConfirmationAlertPresenter.swift
AppDelegate gains activeQuitConfirmationAlertPresenter and activeQuitConfirmationOwnsTerminateRequest stored properties for re-entrancy prevention. New methods: prepareForConfirmedAppTermination() sets termination-in-progress and persists session/history; presentQuitConfirmationAlert(ownsTerminateRequest:completion:) ensures one-time presentation; handleApplicationTerminateQuitConfirmationResponse(...) applies suppression, manages termination flags, closes inspectors, defers remote kills, and replies once. pendingTerminateReply(...) and hasQuitConfirmationDirtyWorkspaces() support ownership-based reply decisions and dirty-workspace detection.
applicationShouldTerminate refactoring
Sources/AppDelegate.swift
Refactors applicationShouldTerminate(_:) to use pendingTerminateReply(...) for re-entrancy deferral. Replaces inline termination setup on the no-dialog path with prepareForConfirmedAppTermination(). Replaces NSAlert.runModal() with presentQuitConfirmationAlert(ownsTerminateRequest: true), forwarding response and suppression state to the shared handler.
handleQuitShortcutWarning refactoring
Sources/AppDelegate.swift
Refactors handleQuitShortcutWarning() to early-return when a presenter is active, preventing dialog stacking. Routes through presentQuitConfirmationAlert(ownsTerminateRequest: false) instead of inline NSAlert construction. Suppression and termination are now handled by the shared completion callback.
Xcode project configuration
cmux.xcodeproj/project.pbxproj
Registers QuitConfirmationAlertPresenter.swift and QuitConfirmationAlertPresenterTests.swift with new PBXBuildFile, PBXFileReference, and PBXGroup entries. Adds both files to the main cmux and cmuxTests target PBXSourcesBuildPhase for compilation and testing.
Presenter tests and test double
cmuxTests/QuitConfirmationAlertPresenterTests.swift
QuitConfirmationAlertSpy records sheet vs. standalone modal API usage and captures sheet completion handlers. Three tests validate: pendingTerminateReply return values for ownership/dirty-workspace combinations; sheet-modal presentation path with host window; standalone-modal path without host window. All tests verify correct modal response and suppression state .off.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AppDelegate
  participant QuitConfirmationAlertPresenter
  participant HostWindow
  participant NSApp

  User->>AppDelegate: Cmd+Q or applicationShouldTerminate(_:)
  AppDelegate->>AppDelegate: Check pendingTerminateReply()
  alt Presenter already active or dirty workspaces
    AppDelegate->>NSApp: return .terminateLater or .terminateCancel
  else Proceed with confirmation
    AppDelegate->>QuitConfirmationAlertPresenter: presentQuitConfirmationAlert(ownsTerminateRequest:)
    QuitConfirmationAlertPresenter->>QuitConfirmationAlertPresenter: activate app
    alt Host window available
      QuitConfirmationAlertPresenter->>HostWindow: beginSheetModal(completionHandler:)
      User->>HostWindow: Click Quit or Cancel
      HostWindow-->>QuitConfirmationAlertPresenter: completion(response, suppressionState)
    else No host window
      QuitConfirmationAlertPresenter->>QuitConfirmationAlertPresenter: Present standalone panel
      User->>QuitConfirmationAlertPresenter: Click Quit or Cancel
      QuitConfirmationAlertPresenter-->>QuitConfirmationAlertPresenter: finish() → completion()
    end
    QuitConfirmationAlertPresenter->>AppDelegate: completion(response, suppressionState)
    AppDelegate->>AppDelegate: handleApplicationTerminateQuitConfirmationResponse()
    alt User confirmed Quit
      AppDelegate->>AppDelegate: Set isTerminatingApp, apply suppression
      AppDelegate->>NSApp: replyToTerminate(true)
    else User cancelled
      AppDelegate->>NSApp: replyToTerminate(false)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • lawrencecchen

Poem

🐇 A dialog once ran wild and free,
Blocking threads for all to see.
Now a Presenter holds the stage,
Sheet or modal, one shared cage.
Re-entrancy guarded, one-shot done—
The quit flow hops, neat as a bun! 🎉


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error PR introduces hasQuitConfirmationDirtyWorkspaces() calling recoverableMainWindowRoutes() which sorts O(R log R) even though sort isn't needed for contains() check; performs O(T) scans on manager.ta... Replace sortedRecoverableMainWindowRoutes() with unsorted variant or inline unsorted routes iteration in hasQuitConfirmationDirtyWorkspaces() to avoid unnecessary O(R log R) sort on quit-confirmation path checking scalable workspace coll...
Cmux Full Internationalization ❌ Error Four new user-facing localization keys (dialog.quitCmux.title, dialog.quitCmux.message, dialog.quitCmux.quit, dialog.dontWarnCmdQ) use String(localized:) API correctly but lack translations for the... Add Khmer (km) translations to all four new keys in Resources/Localizable.xcstrings to match the 20 supported locales in the catalog.
Cmux Swift Auxiliary Window Close Shortcuts ❌ Error The code introduces a standalone user-visible window via NSAlert.window that becomes a key window (makeKeyAndOrderFront) without a cmux.* identifier, violating the pattern for auxiliary windows tha... Assign the alert window a stable cmux.* identifier (e.g., "cmux.quitConfirmation") and register it in cmuxAuxiliaryWindowIdentifiers in Sources/cmuxApp.swift to ensure Cmd+W ownership through cmuxWindowShouldOwnCloseShortcut.
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 (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: avoiding nested quit confirmation modal loops, which directly addresses the core Sentry hang issue.
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.
Cmux Swift Actor Isolation ✅ Passed QuitConfirmationAlertPresenter is properly annotated @MainActor as a UI-bound NSObject subclass. AppDelegate properties are private on @MainActor class. Test suite correctly marked @MainActor. No v...
Cmux Swift Blocking Runtime ✅ Passed No blocking runtime primitives introduced in new production code. Refactored quit path replaces blocking NSAlert.runModal() with async beginSheetModal and event-driven standalone presentation. Test...
Cmux Expensive Synchronous Load ✅ Passed PR adds no new RestorableAgentSessionIndex.load() or expensive loaders to quit paths; new @MainActor presenter only handles UI; existing calls use approved cache-first fallback pattern.
Cmux Cache Substitution Correctness ✅ Passed The PR doesn't involve cache substitution in persistence/snapshot paths. It replaces quit dialog runModal() with an async presenter, and all snapshot persistence uses explicit fresh indexes from Pr...
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code changes and Xcode project configuration. The "no hacky sleeps" rule explicitly excludes Swift code (covered by swift-blocking-runtime.md) and applies only to TypeScript,...
Cmux Swift Concurrency ✅ Passed PR uses completion handlers only at AppKit boundaries (NSAlert.beginSheetModal, NSWindowDelegate). No background dispatch queues, Combine state, or fire-and-forget Tasks introduced. Closures proper...
Cmux Swift @Concurrent ✅ Passed All Swift code changes follow concurrent annotation rules: no missing @concurrent on nonisolated async work, no invalid @concurrent on sync/actor-isolated functions, and no heavy async work without...
Cmux Swift File And Package Boundaries ✅ Passed QuitConfirmationAlertPresenter.swift (118 lines) has clear single UI responsibility, AppDelegate.swift actually decreases by 7 net lines, no mixed responsibilities, and all code appropriately scope...
Cmux Swiftpm Lockfiles ✅ Passed PR makes no SwiftPM package-reference, dependency, or .gitignore changes; only adds local source files and build configuration entries.
Cmux Swift Logging ✅ Passed No Swift logging violations detected. New production code contains no print/NSLog/debugPrint/dump. AppDelegate changes use existing StartupBreadcrumbLog mechanism. Tests properly excluded.
Cmux User-Facing Error Privacy ✅ Passed All user-facing strings in the new QuitConfirmationAlertPresenter comply with privacy guidelines: no vendor/internal provider names, credentials, environment variables, or sensitive implementation...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI state violations. QuitConfirmationAlertPresenter is an AppKit NSObject/NSWindowDelegate with no SwiftUI patterns; AppDelegate additions are standard instance properties, not...
Cmux Architecture Rethink ✅ Passed PR replaces blocking NSAlert.runModal() with proper async beginSheetModal/modeless window, avoids timing/blocking/observer patterns, maintains clear single ownership and invariants in AppDelegate,...
Cmux Source Artifacts ✅ Passed PR adds only legitimate hand-written source files, test code, and build config; no local artifacts, DerivedData, .claude/, .agents/, or other prohibited patterns present.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seams found in production source. All new methods in Sources/ are either private (explicitly marked) or implicitly internal (no visibility modifier) with legitimate production callers...
Description check ✅ Passed The PR description comprehensively covers the problem, solution, testing approach, and verification done, matching all template requirements.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-sentry-ui-hang-quit-confirm

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.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/ShortcutAndCommandPaletteTests.swift`:
- Around line 1951-1979: The existing test
testPresenterUsesSheetCompletionWithoutRunningNestedModalLoop only covers the
sheet presentation path. Add a new test method to cover the no-host standalone
fallback scenario by creating a QuitConfirmationAlertPresenter with a
presentingWindowProvider that returns nil, then verify that the alert spy never
calls runModal() and that the completion handler still fires when the presenter
is presented in standalone mode (through button click or window close).

In `@Sources/AppDelegate.swift`:
- Around line 502-586: Extract the entire QuitConfirmationAlertPresenter class
(including all its methods, properties, and extensions) from AppDelegate.swift
and move it to a new dedicated file named
Sources/QuitConfirmationAlertPresenter.swift. This self-contained class with its
NSWindowDelegate conformance is independently testable and should not contribute
to further bloating the large AppDelegate.swift file. After moving the class,
ensure no import statements or other code changes are needed in the new file,
and verify that AppDelegate.swift can still reference and use
QuitConfirmationAlertPresenter without any compilation errors.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78e18276-fe46-4af4-b46a-5f5c3c476619

📥 Commits

Reviewing files that changed from the base of the PR and between d886638 and 03f93e3.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift

Comment on lines +1951 to +1979
func testPresenterUsesSheetCompletionWithoutRunningNestedModalLoop() {
let alert = QuitConfirmationAlertSpy()
let hostWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 320),
styleMask: [.titled],
backing: .buffered,
defer: false
)

var completedResponse: NSApplication.ModalResponse?
var completedSuppressionState: NSControl.StateValue?
let presenter = QuitConfirmationAlertPresenter(
alert: alert,
presentingWindowProvider: { hostWindow }
) { response, suppressionState in
completedResponse = response
completedSuppressionState = suppressionState
}

presenter.present()

XCTAssertTrue(alert.didBeginSheetModal)
XCTAssertFalse(alert.didRunModal)
XCTAssertNil(completedResponse)

alert.capturedSheetCompletion?(.alertFirstButtonReturn)

XCTAssertEqual(completedResponse, .alertFirstButtonReturn)
XCTAssertEqual(completedSuppressionState, .off)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Cover the no-host standalone fallback.

This test only exercises the sheet path. A future change could reintroduce runModal() inside presentStandalone() and still pass the source scan, because the scan only checks the two AppDelegate call-site bodies. Add a provider returning nil or a host with an attached sheet and assert the spy never runs runModal() while completion still fires through the standalone button/window-close path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/ShortcutAndCommandPaletteTests.swift` around lines 1951 - 1979, The
existing test testPresenterUsesSheetCompletionWithoutRunningNestedModalLoop only
covers the sheet presentation path. Add a new test method to cover the no-host
standalone fallback scenario by creating a QuitConfirmationAlertPresenter with a
presentingWindowProvider that returns nil, then verify that the alert spy never
calls runModal() and that the completion handler still fires when the presenter
is presented in standalone mode (through button click or window close).

Comment thread Sources/AppDelegate.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces blocking runModal() quit-confirmation paths with a retained QuitConfirmationAlertPresenter that uses beginSheetModal (sheet) when a host window is available, or a modeless floating window otherwise, eliminating the nested AppKit modal loop that caused the Sentry hang. The pending-terminate reply policy is unified into pendingTerminateReply(...), and session snapshot/history flushing is deferred until after confirmation so a user cancel no longer pays the full persistence cost.

  • Introduces QuitConfirmationAlertPresenter with sheet and standalone presentation paths, didFinish guard for idempotent completion, and windowWillClose delegate for window-dismiss cancels.
  • Adds two AppDelegate state properties (activeQuitConfirmationAlertPresenter, activeQuitConfirmationOwnsTerminateRequest) to coordinate re-entry across the Cmd+Q shortcut and applicationShouldTerminate paths.
  • Moves hasQuitConfirmationDirtyWorkspaces() and the new pendingTerminateReply static helper to an AppDelegate extension in QuitConfirmationAlertPresenter.swift.

Confidence Score: 5/5

Safe to merge — the async presenter correctly handles all quit paths without introducing new blocking calls or re-entrant modal loops.

The re-entry guard, idempotent finish(), and pendingTerminateReply policy cover the three distinct quit paths (Cmd+Q shortcut, applicationShouldTerminate, and concurrent re-entry) without leaving any stale termination state. Session persistence is now correctly deferred to after confirmation. The tests exercise both sheet and standalone presentation and all pendingTerminateReply combinations.

No files require special attention.

Important Files Changed

Filename Overview
Sources/QuitConfirmationAlertPresenter.swift New 118-line presenter class implementing sheet and standalone quit dialogs without runModal(); includes idempotent finish() guard, windowWillClose delegate, and AppDelegate extension for pendingTerminateReply policy.
Sources/AppDelegate.swift Replaces inline runModal() alert blocks with presentQuitConfirmationAlert/handleApplicationTerminateQuitConfirmationResponse helpers; defers prepareForConfirmedAppTermination() to after confirmation; adds re-entry guard for handleQuitShortcutWarning.
cmuxTests/QuitConfirmationAlertPresenterTests.swift New test file covering pendingTerminateReply policy combinations and both sheet and standalone presentation paths via QuitConfirmationAlertSpy; confirms no nested modal loop.
cmux.xcodeproj/project.pbxproj Adds QuitConfirmationAlertPresenter.swift to app target and QuitConfirmationAlertPresenterTests.swift to test target; no package dependency changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant AppDelegate
    participant Presenter as QuitConfirmationAlertPresenter
    participant NSApp

    Note over AppDelegate: Cmd+Q shortcut path (ownsTerminateRequest: false)
    User->>AppDelegate: Cmd+Q shortcut
    AppDelegate->>AppDelegate: handleQuitShortcutWarning()
    AppDelegate->>Presenter: presentQuitConfirmationAlert(ownsTerminate: false)
    Presenter->>Presenter: present() → beginSheetModal / standalone
    User->>Presenter: clicks Quit
    Presenter->>AppDelegate: completion(.alertFirstButtonReturn, .off)
    AppDelegate->>AppDelegate: "isQuitWarningConfirmed = true"
    AppDelegate->>NSApp: NSApp.terminate(nil)
    NSApp->>AppDelegate: applicationShouldTerminate(_:)
    AppDelegate->>AppDelegate: pendingTerminateReply → nil (no active presenter)
    AppDelegate->>AppDelegate: shouldShowConfirmation → false (already confirmed)
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate-->>NSApp: .terminateNow

    Note over AppDelegate: Direct terminate path (ownsTerminateRequest: true)
    NSApp->>AppDelegate: applicationShouldTerminate(_:)
    AppDelegate->>AppDelegate: pendingTerminateReply → nil (no active presenter)
    AppDelegate->>Presenter: presentQuitConfirmationAlert(ownsTerminate: true)
    AppDelegate-->>NSApp: .terminateLater
    Presenter->>Presenter: present() → beginSheetModal / standalone
    User->>Presenter: clicks Quit
    Presenter->>AppDelegate: handleApplicationTerminateQuitConfirmationResponse
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate->>NSApp: replyToTerminateOnce(true)

    Note over AppDelegate: Re-entry guard
    NSApp->>AppDelegate: applicationShouldTerminate(_:) (re-entry while dialog active)
    AppDelegate->>AppDelegate: pendingTerminateReply → .terminateLater or .terminateCancel
    AppDelegate-->>NSApp: reply without showing new dialog
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant AppDelegate
    participant Presenter as QuitConfirmationAlertPresenter
    participant NSApp

    Note over AppDelegate: Cmd+Q shortcut path (ownsTerminateRequest: false)
    User->>AppDelegate: Cmd+Q shortcut
    AppDelegate->>AppDelegate: handleQuitShortcutWarning()
    AppDelegate->>Presenter: presentQuitConfirmationAlert(ownsTerminate: false)
    Presenter->>Presenter: present() → beginSheetModal / standalone
    User->>Presenter: clicks Quit
    Presenter->>AppDelegate: completion(.alertFirstButtonReturn, .off)
    AppDelegate->>AppDelegate: "isQuitWarningConfirmed = true"
    AppDelegate->>NSApp: NSApp.terminate(nil)
    NSApp->>AppDelegate: applicationShouldTerminate(_:)
    AppDelegate->>AppDelegate: pendingTerminateReply → nil (no active presenter)
    AppDelegate->>AppDelegate: shouldShowConfirmation → false (already confirmed)
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate-->>NSApp: .terminateNow

    Note over AppDelegate: Direct terminate path (ownsTerminateRequest: true)
    NSApp->>AppDelegate: applicationShouldTerminate(_:)
    AppDelegate->>AppDelegate: pendingTerminateReply → nil (no active presenter)
    AppDelegate->>Presenter: presentQuitConfirmationAlert(ownsTerminate: true)
    AppDelegate-->>NSApp: .terminateLater
    Presenter->>Presenter: present() → beginSheetModal / standalone
    User->>Presenter: clicks Quit
    Presenter->>AppDelegate: handleApplicationTerminateQuitConfirmationResponse
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate->>NSApp: replyToTerminateOnce(true)

    Note over AppDelegate: Re-entry guard
    NSApp->>AppDelegate: applicationShouldTerminate(_:) (re-entry while dialog active)
    AppDelegate->>AppDelegate: pendingTerminateReply → .terminateLater or .terminateCancel
    AppDelegate-->>NSApp: reply without showing new dialog
Loading

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift
Comment on lines +1979 to +1983
isTerminatingApp = false
clearMarkedRemoteTmuxKills()
StartupBreadcrumbLog.append("appDelegate.shouldTerminate.reply", fields: ["shouldQuit": "0"])
}
replyToTerminateOnce(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 Stale no-op isTerminatingApp = false with outdated comment

In the old code, isTerminatingApp = true was set unconditionally at the top of applicationShouldTerminate (before the dialog), so the cancel branch needed to reset it. In the new code, isTerminatingApp is only set to true inside prepareForConfirmedAppTermination(), which is called only on confirmation — so by the time this else branch runs, isTerminatingApp is already false. The isTerminatingApp = false here is a no-op, and the comment ("Reset so that the next quit attempt can show the dialog again") is now misleading — the re-entry gate is activeQuitConfirmationAlertPresenter = nil (cleared in presentQuitConfirmationAlert's wrapper closure), not this flag.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread Sources/AppDelegate.swift Outdated
super.init()
}

static func makeAlert() -> NSAlert {

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 makeAlert() is a private factory helper with no caller outside QuitConfirmationAlertPresenter; it should be private rather than internal to avoid unintended visibility through @testable import.

Suggested change
static func makeAlert() -> NSAlert {
private static func makeAlert() -> NSAlert {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +557 to +579
buttons[1].action = #selector(cancelQuit)
}

let window = alert.window
window.delegate = self
window.level = .modalPanel
window.center()
window.makeKeyAndOrderFront(nil)
}

@objc private func confirmQuit() {
finish(.alertFirstButtonReturn)
}

@objc private func cancelQuit() {
finish(.alertSecondButtonReturn)
}

func windowWillClose(_ notification: Notification) {
finish(.alertSecondButtonReturn)
}

private func finish(_ response: NSApplication.ModalResponse) {

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 presentStandalone fallback path has no test coverage

testPresenterUsesSheetCompletionWithoutRunningNestedModalLoop only exercises the beginSheetModal branch. The standalone fallback — used when presentingWindowProvider() returns nil or the host window already has an attached sheet — is untested. In the terminate-owned case (ownsTerminateRequest: true), a regression where button target/action are not set correctly would leave AppKit indefinitely waiting on .terminateLater with no recovery path. A test passing presentingWindowProvider: { nil } and asserting button clicks deliver the correct response and didRunModal == false would close this gap.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 535-537: The activation condition in AppDelegate.swift currently
checks only for the `.regular` activation policy before showing the quit
confirmation sheet. Change this condition from `NSApp.activationPolicy() ==
.regular` to `NSApp.activationPolicy() != .prohibited` to ensure the app
activates and brings the quit confirmation sheet to the foreground for all
non-prohibited policies, including `.accessory` mode.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26515b15-3807-4bcc-a356-91982fc6da99

📥 Commits

Reviewing files that changed from the base of the PR and between 03f93e3 and 853726d.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +535 to +537
if NSApp.activationPolicy() == .regular {
NSApp.activate(ignoringOtherApps: true)
}

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

Activate all non-prohibited app policies before showing quit confirmation sheet

The app currently activates only .regular mode before displaying the quit-confirmation alert. When running in .accessory mode (menu-bar-only via MenuBarExtraController), the app fails to activate before the quit sheet appears, leaving it behind the user's current application. The active-presenter guard then blocks repeated quit attempts. Change the activation condition to activate all non-prohibited policies (activationPolicy != .prohibited) to ensure the quit confirmation is always presented in the foreground.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 535 - 537, The activation condition
in AppDelegate.swift currently checks only for the `.regular` activation policy
before showing the quit confirmation sheet. Change this condition from
`NSApp.activationPolicy() == .regular` to `NSApp.activationPolicy() !=
.prohibited` to ensure the app activates and brings the quit confirmation sheet
to the foreground for all non-prohibited policies, including `.accessory` mode.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/AppDelegate.swift (1)

6085-6240: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move the Open Diff Viewer orchestration out of AppDelegate.

This adds diff-context lookup, baseline-store parsing, cwd selection, and process-launch setup to an already oversized AppDelegate. Keep the shortcut wrapper here, but move the reusable orchestration into a same-module helper such as OpenDiffViewerLauncher/OpenDiffViewerContextResolver so it can be tested and maintained independently.

As per coding guidelines, production Swift files over 800 lines and app-root features with separable core logic should be flagged rather than further mixing responsibilities in Sources/AppDelegate.swift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 6085 - 6240, Create a new helper
class (such as OpenDiffViewerLauncher or OpenDiffViewerContextResolver) in the
same module and move the orchestration logic out of AppDelegate to reduce its
responsibilities. Move the private functions focusedAgentDiffContext,
latestAgentTurnDiffRepoRoot, agentTurnDiffBaselineStoreURL,
normalizedOpenDiffViewerIdentifier, normalizedOpenDiffViewerSessionId,
normalizedOpenDiffViewerPath, and launchDiffViewerProcess into the new helper
class along with the main openDiffViewerForFocusedWorkspace logic (keeping only
the preferAgentContext parameter selection). In AppDelegate, keep only the thin
public wrapper methods openDiffViewerForFocusedWorkspace and
openDirectoryDiffViewerForFocusedWorkspace that delegate to the new helper class
instance. This separates concerns, makes the code independently testable, and
reduces AppDelegate's bloat.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/AppDelegate.swift`:
- Around line 6085-6240: Create a new helper class (such as
OpenDiffViewerLauncher or OpenDiffViewerContextResolver) in the same module and
move the orchestration logic out of AppDelegate to reduce its responsibilities.
Move the private functions focusedAgentDiffContext, latestAgentTurnDiffRepoRoot,
agentTurnDiffBaselineStoreURL, normalizedOpenDiffViewerIdentifier,
normalizedOpenDiffViewerSessionId, normalizedOpenDiffViewerPath, and
launchDiffViewerProcess into the new helper class along with the main
openDiffViewerForFocusedWorkspace logic (keeping only the preferAgentContext
parameter selection). In AppDelegate, keep only the thin public wrapper methods
openDiffViewerForFocusedWorkspace and openDirectoryDiffViewerForFocusedWorkspace
that delegate to the new helper class instance. This separates concerns, makes
the code independently testable, and reduces AppDelegate's bloat.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57e933f9-e09b-4cb2-b907-3c4c062c3a49

📥 Commits

Reviewing files that changed from the base of the PR and between adee5f8 and be3c75d.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmux.xcodeproj/project.pbxproj

This branch was successfully deployed

1 active deployment
Preview – cmux — 2f41a341 Deployed Jun 21, 2026 by vercel[bot]
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