Skip to content

Make Settings open structurally reliable: AppKit-owned window lifecycle - #7783

Merged
austinywang merged 10 commits into
mainfrom
issue-7777-settings-always-open
Jul 10, 2026
Merged

austinywang merged 10 commits into
mainfrom
issue-7777-settings-always-open

Conversation

@austinywang

@austinywang austinywang commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7777
Fixes #7775
Fixes #4053

Problem

Settings intermittently cannot be opened at all: cmux → Settings…, ⌘,, and CLI settings open all silently do nothing, and only an app restart recovers. This has recurred across builds (#5770 → closed, still recurring; #4053; #7775), and the point fix in PR #5806 (deferred verification + one retry) did not hold.

The #7775 capture shows the worst shape: the tag-bound socket returns OK target=account while accessibility inspection proves no Settings window exists at all — not offscreen, not hidden, never created.

Root cause

Settings window creation was delegated to a SwiftUI single-Window scene through openWindow(id: "settings"). That call returns nothing, reports nothing, and its internal scene state can wedge permanently (relaunch-while-open per #7775, scene mid-teardown per the live root-cause confirmation on #5770). The presenter could only request-and-hope, so each observed failure mode grew its own patch: an isOpeningSettingsWindow flag, a shouldOpenWhenConfigured deferral, a 500 ms verification timer, and a bounded retry. When the scene wedged, the retry retried into the same wedged scene and gave up — the exact "accepted but nothing happened" symptom.

Fix: AppKit-owned window lifecycle, SwiftUI only for content

SettingsWindowPresenter is rewritten as the single source of truth for the Settings window lifecycle. It constructs the NSWindow itself (SettingsWindowFactory: NSHostingController(SettingsWindowRoot) with sceneBridgingOptions = [.toolbars, .title]), the same ownership model the main window (AppDelegate.createMainWindow) and TaskManagerWindowController already use. The SwiftUI settings Window scene is deleted.

show() is now synchronous and self-healing:

  1. Reuse the tracked/scanned window only if usable; a window in any unusable state (torn-down content, degenerate frame) is demolished on the spot.
  2. Otherwise build a fresh window synchronously — creation cannot no-op.
  3. Clamp the frame onto a visible screen (multi-monitor recovery kept from Fix #5770: recover Settings open from offscreen frames and silent no-ops #5806, incl. cursor-screen recovery for frames stranded on disconnected displays).
  4. Order front and verify isVisible before returning; on failure, tear down and recreate once, then fail loudly (Logger fault with full window/app/screen state).
  5. On close, the presenter strips the window's identifier and releases its content tree, so a closed window can never absorb a future open request (the Settings window is blank on every open after the first (⌘,) #4964 blank-reopen / Auxiliary windows (Settings, etc.) don't truly close: reappear, linger in window switcher, and absorb the Close shortcut #5321 lingering-window classes).

Deleted with the scene: the openWindow closures, configure(openWindow:)/configure(window:) + WindowAccessor wiring, shouldOpenWhenConfigured, isOpeningSettingsWindow, the verification timer, SettingsWindowOpenOutcome, and the retry machinery.

Invariant established: every open request ends in exactly one of — a visible Settings window, a window ordered front under a hidden app (non-activating CLI opens only), or a loud diagnostic failure carried in the return value. "Returned OK but nothing happened" is unrepresentable.

CLI truth-telling (#7775)

ControlSystemContext is @MainActor, so controlSettingsOpen now presents synchronously and replies from the actual outcome: ControlSettingsOpenResolution gains .failed(message:), mapped to an unavailable socket error. settings.open returns opened=true if-and-only-if a window actually materialized. The activate flag now also gates app activation on the shared path (socket focus policy), instead of show() unconditionally activating.

Navigation targets are also now delivered on first open: previously a fresh window's pending target was never consumed (dead consumePendingNavigationTarget); the new SettingsWindowHostRoot consumes it once the content is live.

Why this also closes #4053

#4053 is the same "menu click produces nothing" symptom on a stable build with no deterministic repro. The live root-cause confirmation on #5770 (same symptom family) proved the mechanism: openPreferencesWindow → show() → openWindow(id:) silently no-ops with no window created. That entire creation path no longer exists; any wedge inside it is gone by construction, and if AppKit itself ever refused to present, the CLI/log now say so explicitly instead of no-oping.

Regression tests (two-commit red/green)

Commit 1 adds SettingsWindowOpenRegressionTests — end-to-end, real-factory tests that are red on the old code and unchanged by the fix commit:

  • show() always produces a visible Settings window (fresh state)
  • reopen after close produces a fresh visible window
  • open/close churn never wedges the open path
  • show() while the previous window is mid-close still produces a visible window
  • show() recovers a window parked off every active screen

Commit 2 (the fix) rewrites SettingsWindowPresenterTests for the new lifecycle using an injected window factory: creation/reuse/fresh-after-close, content released on close, contentless-husk teardown + recreation, mid-close reopen, bounded recreate → loud .failed when no window becomes visible, min-size + clamp repair on show, immediate vs pending navigation delivery, and the pure usability policy. The multi-monitor recovery and clamp-geometry unit tests are carried over unchanged.

Interactive-only modes (stated plainly, not faked): the #7775 relaunch-while-open recipe, real display disconnect/reconnect, and hidden-app CLI opens are not reproducible in unit tests. The relaunch wedge is eliminated structurally (no scene state survives to wedge); display changes are covered by the pure targetVisibleFrame/clampedFrame tests plus the offscreen-frame end-to-end test; the hidden-app path is covered by the orderedWhileAppHidden result mapping.

Localization audit

  • New user-facing string: settings.window.runtimeUnavailable (fallback shown only if the settings runtime is missing at window creation — loud instead of silent). Added to Resources/Localizable.xcstrings with translations for all 19 catalog locales (ar, bs, da, de, en, es, fr, it, ja, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant).
  • Window title reuses the existing settings.title key.
  • Removed scene used the same settings.title key; no keys were orphaned. All other new strings are os.Logger/debug diagnostics (not localized by policy).
  • No keyboard shortcuts added or changed.

Budgets / guards

  • swift_file_length_budget.py passes with no TSV changes: new files SettingsWindowFactory.swift (81) and rewritten SettingsWindowPresenter.swift (462) are under 500; SettingsWindowPresenterTests.swift shrank 668 → 542; BrowserPanelView.swift, cmuxApp.swift, SettingsWindowScene.swift (dead scene struct deleted) all shrank.
  • lint-pbxproj-test-wiring.sh, check-workspace-package-groups.py --check, check-package-resolved-policy.py all pass.
  • No new keyboard shortcuts; no swift-warning-budget.tsv changes.

Red/green CI proof

Both commits were pushed together, so in addition to the PR checks:

Review follow-ups (pushed in 8977ae1)

  • Reused live Settings window now receives the requested navigation target even when the app is hidden and activate=false (Cursor medium / Greptile P1).
  • presentPreferencesWindow consumes the show result: .failed beeps instead of silently activating, matching the CLI's error surfacing (Cursor medium).
  • The DEBUG UI-test capture records opened from the verified outcome, not the request (Cursor low).

Review follow-ups round 2 (codex structured review, pushed in ee23c79 and 708080a)

  • settings.open --activate=false no longer keys the window, raises the main window, or unhides/activates the app (socket no-focus-steal contract).
  • SettingsHostWindow records close-begin, so show() deterministically refuses to reuse a dying window regardless of notification-observer order.
  • settings.open with a missing control-system context fails closed (unavailable) instead of synthesizing opened=true.
  • A fresh window's deferred navigation post is generation-guarded so it can't override a newer targeted open.
  • The shared Toggle Left Sidebar command now routes to the Settings split view when the Settings window is key (the AppKit-hosted window lost SwiftUI SidebarCommands); covered by new SettingsWindowNavigationRoutingTests.
  • Pure geometry statics moved to SettingsWindowGeometry.swift to keep the presenter under the 500-line budget threshold.

Review follow-ups round 3–4 (pushed in 40edeae, 7788d21, 43e05e5)

  • presentPreferencesWindow/openPreferencesWindow moved to Sources/App/AppDelegateSettingsPresentation.swift — AppDelegate.swift is over the 900-line hard cap and may not grow, which failed workflow-guard-tests; it now shrinks 29 lines net.
  • Navigation posts only after the Settings content signals readiness via the host root's onAppear — an NSWindow existing is not enough, so rapid targeted opens can no longer post into a not-yet-subscribed window and lose the pane.
  • An untargeted show() (menu click) no longer erases a still-undelivered pending navigation target (Cursor findings).
  • openBrowserImportSettings routes through the shared AppDelegate.presentPreferencesWindow, so a failed presentation beeps there too instead of silently no-oping.
  • Added the Khmer (km) translation for settings.window.runtimeUnavailable (the catalog ships km; the key now covers all 20 catalog locales).
  • Test-side sidebar-toggle recorder observes the raw notification name instead of the package symbol — @testable import package-symbol visibility differs across toolchains and broke the CI test compile while local Xcode accepted it.
  • Warning budget: dropped the deprecated no-op activateIgnoringOtherApps from the moved default closure; the sidebar-toggle helper takes keyWindow explicitly (a NSApp.keyWindow default argument warns under strict concurrency) (ebbe77b).
  • Reentrancy-safe teardown recovery (9526370): the retry loop re-checks for a usable window on every attempt so a re-entrant show() from a willClose observer is adopted instead of duplicated, and a small depth bound turns a pathological reopen-on-close observer plus persistent presentation failure into a loud .failed instead of unbounded recursion — both covered by new tests.

🤖 Generated with Claude Code

austinywang and others added 2 commits July 9, 2026 18:37
…ible window

End-to-end tests for the recurring "Settings won't open" family
(#7777, #7775, #5770, #4053): every SettingsWindowPresenter.show() must
end with a visible Settings window — from a fresh state, after close,
after open/close churn, while a previous window is mid-close, and after
the window was stranded off every active screen.

These are red on the current SwiftUI-scene-based open path, which hands
the request to openWindow(id:) and can silently no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #7777. The Settings window is no longer created through a SwiftUI
Window scene's openWindow(id:), which has no failure callback and could
wedge permanently (relaunch-while-open #7775, scene mid-teardown #5770),
leaving menu, Cmd-comma, and CLI opens silently dead until app restart.

SettingsWindowPresenter is now the single source of truth for the
window lifecycle: it synchronously builds the NSWindow itself
(SettingsWindowFactory: NSHostingController(SettingsWindowRoot) with
toolbar/title scene bridging — the same AppKit-owned model as the main
window and TaskManagerWindowController), tears down any unusable
existing window instead of re-fronting it, clamps stranded frames onto
a visible screen, verifies isVisible before returning, recreates once
on failure, and fails loudly otherwise. Closing strips the identifier
and releases the content tree so a closed window can never absorb a
future open request.

The deferral flags, 500ms verification timer, and retry machinery from
PR #5806 are deleted; the CLI settings.open reply now reflects the real
outcome (opened iff a window materialized; new .failed resolution maps
to a socket error), and navigation targets are delivered on first open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 10, 2026 7:20am
cmux-staging Building Building Preview, Comment Jul 10, 2026 7:20am

@coderabbitai

coderabbitai Bot commented Jul 10, 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

Settings presentation now uses an AppKit-owned window with synchronous result reporting, explicit failure diagnostics, persisted selection state, navigation delivery, lifecycle recovery, and expanded regression coverage.

Changes

Settings window presentation

Layer / File(s) Summary
AppKit window host
Sources/App/SettingsWindowFactory.swift, Packages/macOS/CmuxSettingsUI/..., Sources/cmuxApp.swift, Resources/Localizable.xcstrings
Settings windows are constructed through an AppKit factory, hosted with SwiftUI content, and use app-scoped selection persistence. Sidebar toggling is routed through a notification, with localized fallback content when the runtime is unavailable.
Presenter lifecycle and recovery
Sources/App/SettingsWindowPresenter.swift, Sources/App/SettingsWindowGeometry.swift
The presenter synchronously reuses or recreates identified windows, repairs frames, delivers navigation, handles closing and unusable windows, and returns explicit presentation outcomes.
Open-result propagation
Sources/TerminalController+ControlSystemContext.swift, Packages/macOS/CmuxControlSocket/..., Sources/App/AppDelegateSettingsPresentation.swift, Sources/AppDelegate.swift, Sources/Panels/BrowserPanelView.swift, cmux.xcodeproj/project.pbxproj
Settings-open callers propagate presenter failures with diagnostics, while application entrypoints invoke the AppKit presenter directly and activate the app only after successful presentation.
Lifecycle and integration validation
cmuxTests/*, cmuxUITests/*
Tests cover creation, reuse, closure races, geometry recovery, navigation, sidebar routing, bounded failure, and updated browser-import outcomes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ControlCommandCoordinator
  participant TerminalController
  participant SettingsWindowPresenter
  participant SettingsWindowFactory
  participant SettingsWindowHostRoot
  ControlCommandCoordinator->>TerminalController: settings.open(target)
  TerminalController->>SettingsWindowPresenter: show(navigationTarget:activateApp:)
  SettingsWindowPresenter->>SettingsWindowFactory: makeSettingsWindow()
  SettingsWindowFactory-->>SettingsWindowPresenter: NSWindow
  SettingsWindowPresenter->>SettingsWindowHostRoot: order window front
  SettingsWindowHostRoot-->>SettingsWindowPresenter: deliver pending navigation
  SettingsWindowPresenter-->>TerminalController: presented or failed result
  TerminalController-->>ControlCommandCoordinator: opened or failed resolution
Loading

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#3164: Concerns the SettingsWindowPresenter implementation and related presentation behavior changed here.
  • manaflow-ai/cmux-dev-artifacts#3156: Concerns Settings-window visibility and bounded failure handling covered by this change.
  • manaflow-ai/cmux-dev-artifacts#3154: Concerns the SettingsWindowOpenRegressionTests suite added by this change.

Possibly related PRs

  • manaflow-ai/cmux#3244: Both changes modify the Settings-window presentation and navigation pipeline around SettingsWindowPresenter.

Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error New settings.open error responses use hardcoded English strings and aren’t backed by String(localized:)/xcstrings. Add catalog keys + translations for the settings.open errors and return localized messages from the coordinator, or mark them non-user-facing if intended.
Cmux No Test Or Debug Seam In Production Source ❌ Error PR adds production test/debug seams: SettingsWindowPresenter.show() writes a DEBUG-only UI-test capture, and consumePendingNavigationTarget() is only used by tests. Move UI-test observation into Tests/ via @testable import, and remove or isolate the test-only accessor from production code.
Cmux No Ambient Global State ❌ Error The diff adds ambient state via SettingsWindowPresenter.shared and a pure static namespace SettingsWindowFactory, both disallowed by the no-ambient-global-state rule. Remove the singleton; inject a constructable SettingsWindowPresenter from the app seam, and replace SettingsWindowFactory with a private helper or instance-owned factory object.
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and omits Summary, Testing, Demo Video, Review Trigger, and Checklist sections. Reformat it to the repo template and add the missing Summary, Testing/manual verification, Demo Video link, Review Trigger block, and checklist items.
✅ Passed checks (20 passed)
Check name Status Explanation
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 The changed UI and socket types are already @MainActor, and I found no new Sendable/shared-state or background-store isolation issue.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits, sleeps, semaphores, or locks were added; the only timing is a one-hop MainActor Task for callback ordering, and the old verification timer was removed.
Cmux Browser Automation Off-Main ✅ Passed Changed files only reroute settings presentation; no browser.* wait commands or socketWorkerMethods/processV2Command routing were modified.
Cmux Expensive Synchronous Load ✅ Passed The PR’s touched settings-path code adds no agent-history loads; settings.open just calls SettingsWindowPresenter.show, and no changed file introduces RestorableAgentSessionIndex.load() or he...
Cmux Cache Substitution Correctness ✅ Passed No freshness-sensitive cache substitution was introduced; persisted window/frame state handles cold/stale cases, and the remaining cached state is transient UI/navigation hinting.
Cmux No Hacky Sleeps ✅ Passed PR changes only Swift, xcstrings, and pbxproj files; no non-Swift runtime/build scripts or added waits/sleeps were found.
Cmux Algorithmic Complexity ✅ Passed No new nested rescans or repeated sort/filter paths: window lookup is a single scan, retries are bounded to 2, and screen geometry only scans the small NSScreen list.
Cmux Swift Concurrency ✅ Passed Only UI-bound hops were added (SwiftUI Task/main-queue callbacks); no new background queues, Combine state, or completion-handler APIs in production code.
Cmux Swift @Concurrent ✅ Passed No touched Swift file introduces @concurrent misuse or nonisolated async; the new async work is explicitly @MainActor UI-bound.
Cmux Swift File And Package Boundaries ✅ Passed Focused AppKit glue and protocol plumbing; the 478-line presenter has one clear settings-window responsibility, and oversized touched files were incidental/small.
Cmux Swiftpm Lockfiles ✅ Passed PASS: cmux.xcodeproj/project.pbxproj only changed source file lists; no SwiftPM package-reference lines, Package.resolved, or .gitignore changes appear in the PR.
Cmux Swift Logging ✅ Passed The added runtime diagnostics use os.Logger with nonisolated constants; only DEBUG-gated cmuxDebugLog calls were added, and no print/NSLog or sensitive-data logging appears in runtime code.
Cmux User-Facing Error Privacy ✅ Passed New user-facing copy is generic (“Settings could not load”, “Settings context not attached”); no vendor names, secrets, or raw upstream payloads were added.
Cmux Swiftui State Layout ✅ Passed No new ObservableObject/@Published/GeometryReader/layout-state anti-patterns were introduced; touched SwiftUI views keep legacy @State/@AppStorage and snapshot rows only.
Cmux Architecture Rethink ✅ Passed Settings lifecycle has one owner (SettingsWindowPresenter) with required bridge notifications; no sleeps/polling or split ownership in production, and the fallback seam is test-only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: the new Settings NSWindow gets stable identifier cmux.settings in SettingsWindowPresenter and cmuxAuxiliaryWindowIdentifiers already includes it; lint_auxiliary_window_close_shortcuts.py passed.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test/localization/project files; no logs, caches, build outputs, tmp, or other artifact directories appear.
Title check ✅ Passed The title accurately summarizes the main change: moving Settings to an AppKit-owned window lifecycle for more reliable opens.
✨ 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 issue-7777-settings-always-open

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.

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

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves Settings window presentation to an AppKit-owned lifecycle. The main changes are:

  • A new SettingsWindowPresenter creates, reuses, verifies, and tears down the Settings window.
  • The SwiftUI Settings scene is removed, while SwiftUI remains the hosted window content.
  • settings.open now reports real presentation success or failure.
  • Settings navigation delivery is deferred until hosted content is ready.
  • Tests and localization were updated for the new window path.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Sources/App/SettingsWindowPresenter.swift Replaces the old Settings presentation flow with synchronous AppKit window ownership, visibility checks, close teardown, and navigation delivery.
Sources/App/SettingsWindowFactory.swift Adds the AppKit window factory and SwiftUI host root for Settings content.
Sources/TerminalController+ControlSystemContext.swift Maps settings.open to the verified Settings presenter result.
Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift Removes SwiftUI scene ownership and adapts the Settings root for AppKit hosting.

Reviews (9): Last reviewed commit: "Address review round 4: reentrancy-safe ..." | Re-trigger Greptile

Comment on lines +126 to +133
if NSApp.isHidden && !activateApp {
// Ordering front succeeded as far as AppKit allows without
// unhiding the app; the window appears on unhide.
Self.log.notice(
"settings.window.show ordered front while app is hidden; deferring visibility to unhide"
)
return .orderedWhileAppHidden
}

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.

P1 Hidden Reuse Skips Navigation

When settings.open runs with activate=false while the app is hidden and a settings window already exists, this branch returns orderedWhileAppHidden before posting the requested target. The reused window will not run the host root's first-appear consumer again, so the CLI can report opened=true and later unhide to the old pane instead of the requested one.

Suggested change
if NSApp.isHidden && !activateApp {
// Ordering front succeeded as far as AppKit allows without
// unhiding the app; the window appears on unhide.
Self.log.notice(
"settings.window.show ordered front while app is hidden; deferring visibility to unhide"
)
return .orderedWhileAppHidden
}
if NSApp.isHidden && !activateApp {
// Ordering front succeeded as far as AppKit allows without
// unhiding the app; the window appears on unhide.
deliverNavigation(reusedExistingWindow: reusedExisting)
Self.log.notice(
"settings.window.show ordered front while app is hidden; deferring visibility to unhide"
)
return .orderedWhileAppHidden
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8977ae1 exactly as suggested — deliverNavigation(reusedExistingWindow:) is called before returning orderedWhileAppHidden.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed across rounds 1/3 of this PR and completed in follow-up PR #7800: the hidden-app branch delivers navigation to ready reused content before returning orderedWhileAppHidden, an unready/fresh window keeps the target pending until the content's onAppear drains it, and #7800 makes that readiness signal instance-scoped (routed to the presenter that owns the window rather than the singleton), so the CLI can no longer report opened=true while the requested pane is dropped.

— Claude Code

…h failure, truthful UI-test capture

- deliverNavigation now runs before the orderedWhileAppHidden return, so a
  reused live Settings window still receives the requested pane when the
  app is hidden and activate=false (Cursor/Greptile finding).
- presentPreferencesWindow consumes the show result: on .failed it beeps
  instead of silently activating, matching the CLI's error surfacing.
- The DEBUG UI-test capture records opened from the verified outcome, not
  the request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/App/SettingsWindowPresenter.swift

@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/SettingsWindowPresenterTests.swift`:
- Around line 490-541: Extract the duplicated private ReopenSettingsOnWillClose
helper into a shared test helper file, preserving its
NSWindow.willCloseNotification observation, reopen callback, and stopObserving
behavior. Remove the local class definitions from both
SettingsWindowOpenRegressionTests.swift and SettingsWindowPresenterTests.swift,
and update both suites to use the shared helper.

In `@Sources/App/SettingsWindowPresenter.swift`:
- Around line 104-133: In the hidden-app branch of the presentation loop in
SettingsWindowPresenter, call deliverNavigation(reusedExistingWindow:
reusedExisting) before returning .orderedWhileAppHidden, ensuring reused windows
receive the pending navigation target before unhide. Preserve the existing
logging and return behavior.
🪄 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: b992813c-4b80-46de-afc3-df6dd910d9b9

📥 Commits

Reviewing files that changed from the base of the PR and between 98b86ec and 94b7af2.

📒 Files selected for processing (15)
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/System/ControlCommandCoordinator+SystemMisc.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/System/ControlSettingsOpenResolution.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
  • Resources/Localizable.xcstrings
  • Sources/App/SettingsWindowFactory.swift
  • Sources/App/SettingsWindowOpenOutcome.swift
  • Sources/App/SettingsWindowPresenter.swift
  • Sources/AppDelegate.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/TerminalController+ControlSystemContext.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SettingsWindowOpenRegressionTests.swift
  • cmuxTests/SettingsWindowPresenterTests.swift
  • cmuxUITests/BrowserImportProfilesUITests.swift
💤 Files with no reviewable changes (2)
  • Sources/App/SettingsWindowOpenOutcome.swift
  • cmuxUITests/BrowserImportProfilesUITests.swift

Comment thread cmuxTests/SettingsWindowPresenterTests.swift
Comment thread Sources/App/SettingsWindowPresenter.swift Outdated
…tic mid-close rejection, fail-closed nil context

- show(activateApp: false) now only orders the window in (no makeKey, no
  parent-window raise, no unhide/activate), honoring the socket
  no-focus-steal contract for settings.open --activate=false.
- SettingsHostWindow records close-begin so show() deterministically
  refuses to reuse a dying window regardless of notification-observer
  order; demolish/strip split keeps mid-close teardown re-entrant-safe.
- settings.open with a missing control-system context now returns an
  unavailable error instead of synthesizing opened=true.
- Pure geometry statics moved to SettingsWindowGeometry.swift so the
  presenter stays under the 500-line budget threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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/App/SettingsWindowPresenter.swift (1)

157-169: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Do not return AppKit diagnostics to socket callers.

failureReason exposes window state, screen count, and frame geometry; controlSettingsOpen forwards it verbatim in .failed(message:). Keep these diagnostics in Logger and return a stable product-safe error.

Proposed fix
-        return .failed(reason: failureReason)
+        return .failed(reason: "Settings window could not be opened")

As per coding guidelines, “User-facing errors, alerts, command output, API error bodies, and recovery copy must not expose implementation details.”

Also applies to: 298-309

🤖 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/App/SettingsWindowPresenter.swift` around lines 157 - 169, Do not
expose the AppKit diagnostic failureReason through the socket response. In the
presenter’s failed result path and the controlSettingsOpen caller, continue
logging failureReason for diagnostics but return a stable, product-safe error
message in .failed(message:) instead of forwarding the window state, screen
count, or frame geometry.

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/App/SettingsWindowPresenter.swift`:
- Around line 157-169: Do not expose the AppKit diagnostic failureReason through
the socket response. In the presenter’s failed result path and the
controlSettingsOpen caller, continue logging failureReason for diagnostics but
return a stable, product-safe error message in .failed(message:) instead of
forwarding the window state, screen count, or frame geometry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7cf06dbf-534e-408d-965c-18c5b1e196ca

📥 Commits

Reviewing files that changed from the base of the PR and between 94b7af2 and 8977ae1.

📒 Files selected for processing (2)
  • Sources/App/SettingsWindowPresenter.swift
  • Sources/AppDelegate.swift

@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/App/SettingsWindowPresenter.swift (1)

16-19: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Sanitize the settings-open failure message

Sources/App/SettingsWindowPresenter.swift:16-18 builds failureReason from AppKit window/app state, and Sources/TerminalController+ControlSystemContext.swift:238-242 passes it straight through as .failed(message: reason). Return a product-level error to the caller and keep the detailed state in logs only.

🤖 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/App/SettingsWindowPresenter.swift` around lines 16 - 19, Update the
settings-open failure flow around SettingsWindowPresenter’s failed(reason:) and
TerminalController’s .failed(message: reason) handling so callers receive a
stable product-level error message instead of diagnostic window/app state;
retain the detailed failureReason only in the appropriate log entry.
🤖 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/App/SettingsWindowPresenter.swift`:
- Around line 16-19: Update the settings-open failure flow around
SettingsWindowPresenter’s failed(reason:) and TerminalController’s
.failed(message: reason) handling so callers receive a stable product-level
error message instead of diagnostic window/app state; retain the detailed
failureReason only in the appropriate log entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f635f29a-d7cf-44c1-bc0a-d6e71d0a6aaf

📥 Commits

Reviewing files that changed from the base of the PR and between 8977ae1 and ee23c79.

📒 Files selected for processing (6)
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/System/ControlCommandCoordinator+SystemMisc.swift
  • Sources/App/SettingsWindowFactory.swift
  • Sources/App/SettingsWindowGeometry.swift
  • Sources/App/SettingsWindowPresenter.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SettingsWindowPresenterTests.swift

austinywang and others added 2 commits July 9, 2026 19:18
…debar toggle routing

- A fresh window's deferred navigation post is generation-guarded: a
  newer targeted show that delivered in the meantime supersedes the
  queued post instead of being overridden by it.
- The shared Toggle Left Sidebar command now routes to the Settings
  split view when the Settings window is key (the AppKit-hosted window
  lost SwiftUI SidebarCommands with the scene removal); the package root
  listens for the toggle request and flips columnVisibility.
- New SettingsWindowNavigationRoutingTests cover queued delivery,
  supersession, and sidebar-command routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…legate.swift

AppDelegate.swift is over the 900-line hard cap, so it may not grow at
all in a PR; the presentPreferencesWindow failure-surfacing change added
5 net lines. Move presentPreferencesWindow/openPreferencesWindow into
Sources/App/AppDelegateSettingsPresentation.swift (AppDelegate now
shrinks by 29 lines net).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/App/SettingsWindowPresenter.swift
Comment thread Sources/App/SettingsWindowPresenter.swift
austinywang and others added 3 commits July 9, 2026 19:33
…ser entrypoint failure surfacing, Khmer localization

- Navigation posts only after the Settings content signals readiness
  (host root onAppear); an NSWindow existing is not enough, so a rapid
  second targeted open can no longer post into the void and lose the
  pane. Latest target stays pending until the content is live.
- openBrowserImportSettings routes through the shared
  AppDelegate.presentPreferencesWindow so a failed presentation beeps
  instead of silently doing nothing.
- settings.window.runtimeUnavailable gains the Khmer (km) translation
  the catalog ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ymbol, preserve pending target on untargeted shows

- SettingsSidebarToggleRecorder observes the raw notification name so
  the test target does not depend on package-symbol visibility through
  @testable import, which differs across toolchains (CI failed with
  'cannot find SettingsWindowRoot in scope' while local Xcode compiled).
- An untargeted show() no longer erases a still-undelivered pending
  navigation target (Cursor findings); regression test added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ain-actor default argument

- activateIgnoringOtherApps is a documented no-op on macOS 14+ (this
  target's minimum); dropping it from the moved presentPreferencesWindow
  default closure is behavior-neutral and keeps the new file
  warning-free.
- handleSidebarToggleIfSettingsWindowIsKey takes keyWindow explicitly;
  a NSApp.keyWindow default argument is evaluated outside the main
  actor and warns under strict concurrency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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/App/AppDelegateSettingsPresentation.swift`:
- Around line 8-34: Remove the showFallbackSettingsWindow parameter and its
conditional branch from presentPreferencesWindow, making
SettingsWindowPresenter.show(navigationTarget:) the sole presentation path.
Preserve the existing failed-result handling, beep, early return, and
activateApplication behavior only after successful presentation.
🪄 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: cebbe4dc-7156-44fe-adbd-4ba2c40d8511

📥 Commits

Reviewing files that changed from the base of the PR and between 708080a and ebbe77b.

📒 Files selected for processing (10)
  • Resources/Localizable.xcstrings
  • Sources/App/AppDelegateSettingsPresentation.swift
  • Sources/App/SettingsWindowFactory.swift
  • Sources/App/SettingsWindowPresenter.swift
  • Sources/AppDelegate.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SettingsWindowNavigationRoutingTests.swift
  • cmuxTests/SettingsWindowPresenterTests.swift

Comment thread Sources/App/AppDelegateSettingsPresentation.swift
demolish() closes windows synchronously, and a foreign willClose
observer may re-enter show() from that close. The retry loop now
re-checks for a usable existing window on every attempt, so a healthy
replacement created by re-entrant show() is adopted instead of
duplicated, and a small re-entrant depth bound converts a pathological
reopen-on-close observer combined with persistent presentation failure
into a loud .failed instead of unbounded recursion. Covered by
reentrantReopenDuringTeardownAdoptsReplacementWindow and
pathologicalReopenOnCloseFailsLoudlyInsteadOfRecursing.
unusableWindowReason moved to the pure-helpers file for the length
budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang
austinywang enabled auto-merge (squash) July 10, 2026 03:08

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9526370. Configure here.

reusedExisting: reusedExisting
)
Self.log.error("settings.window.show \(failureReason, privacy: .public)")
demolish(window)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Miniaturized window visibility demolish

Medium Severity

After orderFront, success is gated on window.isVisible (or hidden-app orderedWhileAppHidden). A reused Settings window that is still in the Dock or not yet visible on the same run-loop turn after deminiaturize can fail that check while the app is visible, so performShow treats a valid reuse as failure and demolish tears down the live window instead of presenting it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9526370. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in follow-up PR #7800 (this PR merged before the fix could land here). performShow now captures wasMiniaturized before ordering front; a window that was miniaturized and is deminiaturizing is treated as a successful presentation (visibility follows the unminiaturize animation) instead of being demolished as a failure. Covered by reusedMiniaturizedWindowIsNotDemolishedWhileDeminiaturizing, using a test window that models AppKit's deferred post-deminiaturize visibility.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Design update in #7800 after its own review round: preserving the window through the async animation created a pending state every consumer mistook for final success, so the final fix replaces a Dock-miniaturized window upfront with a fresh, immediately visible one (saved frame restored). Your reported bug shape — a valid miniaturized reuse consuming a failure attempt / being demolished as an error — is gone either way: the replacement is intentional, logged, and always ends in a visible window with a synchronous .presented result.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Final design in #7800 (after an empirical AppKit probe): the miniaturized window is reused, not replaced — probing shows deminiaturize alone leaves isVisible false on the same run-loop turn, but deminiaturize immediately followed by orderFrontRegardless (the presenter's exact sequence) commits visibility on the same turn. So the reuse keeps unsaved Settings state AND satisfies the verified visible-on-return contract synchronously. A stalled-commit simulation test pins the fallback (replacement with a fresh visible window) for any OS where that ever changes.

— Claude Code

@austinywang
austinywang merged commit 25b5fd8 into main Jul 10, 2026
35 of 37 checks passed
austinywang added a commit that referenced this pull request Jul 10, 2026
- presentPreferencesWindow: replace the result-less showFallbackSettingsWindow
  seam with a presentSettingsWindow seam that must report a
  SettingsWindowShowResult; the failed -> beep gate now applies to every path
  (CodeRabbit)
- performShow: a reused window mid-deminiaturize from the Dock is a
  successful presentation, not a failure to demolish (Cursor Bugbot)
- performShow: a failed request clears its own pending navigation target so
  it cannot leak into a later untargeted open (codex P2)
- Content readiness is instance-scoped: the factory takes onContentAppear and
  the presenter passes itself into the factory; singleton shims removed
  (codex P2)
- The three settings-window suites nest under one serialized parent suite so
  they cannot interleave on NSApp windows / frame autosave state (codex P2)
- Budget mechanics: presentPreferencesWindow tests extracted to
  AppDelegatePresentPreferencesWindowTests.swift; presentationFailureReason
  moved to SettingsWindowGeometry.swift; no TSV changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 10, 2026
)

* Address post-merge review findings from #7783

- presentPreferencesWindow: replace the result-less showFallbackSettingsWindow
  seam with a presentSettingsWindow seam that must report a
  SettingsWindowShowResult; the failed -> beep gate now applies to every path
  (CodeRabbit)
- performShow: a reused window mid-deminiaturize from the Dock is a
  successful presentation, not a failure to demolish (Cursor Bugbot)
- performShow: a failed request clears its own pending navigation target so
  it cannot leak into a later untargeted open (codex P2)
- Content readiness is instance-scoped: the factory takes onContentAppear and
  the presenter passes itself into the factory; singleton shims removed
  (codex P2)
- The three settings-window suites nest under one serialized parent suite so
  they cannot interleave on NSApp windows / frame autosave state (codex P2)
- Budget mechanics: presentPreferencesWindow tests extracted to
  AppDelegatePresentPreferencesWindowTests.swift; presentationFailureReason
  moved to SettingsWindowGeometry.swift; no TSV changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Deminiaturize outcome is honest: .deminiaturizing result with bounded self-heal verification

Review P2 on the previous commit: returning .presented while the window is
verifiably not visible violated the presenter's verified-result contract —
a stalled Dock transition would be a silent success. The branch now:

- returns a distinct .deminiaturizing result (window live, AppKit animating
  it in) instead of claiming .presented
- runs a bounded follow-up that demolishes the window loudly if visibility
  never arrives (skipped if re-miniaturized or the app hid meanwhile), so
  the next open self-heals with a fresh window
- checks the hidden-app branch before the deminiaturize branch, since a
  hidden app defers visibility regardless of the animation
- maps .deminiaturizing to opened in the settings.open socket reply

Also fixes the workflow-guard-tests failure: the CI budget enforces per-PR
growth (+63 > 25 allowance) on tracked SettingsWindowPresenterTests.swift,
so the miniaturized-reuse test and Dock-simulation window moved to the
untracked SettingsWindowNavigationRoutingTests.swift, and
clampToVisibleAreaIfNeeded moved to SettingsWindowGeometry.swift to keep the
presenter under the 500-line tracking threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace async .deminiaturizing state with synchronous upfront replacement; activate only after visibility

Review round 2 found four P2s all rooted in .deminiaturizing being an
asynchronous state that consumers (socket reply, UI-test capture, repeated
shows) treated as final success. Eliminate the state instead of patching
each consumer:

- A Dock-miniaturized window can never satisfy the verified
  visible-on-return contract (deminiaturization animates across run-loop
  turns), so usableExistingWindow now demolishes it upfront and the show
  presents a fresh, immediately visible window at the saved frame. Every
  result is synchronous truth again: .presented / .orderedWhileAppHidden /
  .failed. The socket mapping, UI-test capture, and repeated-show paths
  need no special cases.
- Activation is a post-verification step: orderFrontWithoutActivation
  orders the window in first, and unhide/activate/makeKey run only after
  the window is verifiably visible (or when unhiding a hidden app is
  itself what visibility waits on). A failed presentation can no longer
  activate the app or steal focus as a side effect.

Tests: miniaturizedSettingsWindowIsReplacedWithAFreshVisibleWindow and
failedShowNeverActivatesOrKeysAnInvisibleWindow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Preserve a miniaturized Settings window: deminiaturize+orderFront commits visibility synchronously

Review round 3: replacing a Dock-miniaturized window destroyed unsaved
Settings drafts (P1), and the hidden-app verification unhide left the app
activated on failure (P2).

Empirical probe (macOS 26): NSWindow.deminiaturize alone leaves isVisible
false on the same run-loop turn, but deminiaturize followed immediately by
orderFrontRegardless commits visibility on the SAME turn. That is exactly
the presenter's sequence, so the miniaturized window can be REUSED — its
SwiftUI tree and unsaved edits intact — while the verified
visible-on-return contract still holds with no async state:

- usableExistingWindow treats a miniaturized window as usable again;
  orderFrontWithoutActivation deminiaturizes before ordering front
- if an OS ever breaks the same-turn commit, the attempt loop falls back
  to replacing the window with a fresh visible one (covered by a stalled-
  commit simulation test) — never a pending state reported as success
- the hidden-app unhide gamble is recorded and the failure exit re-hides
  the app, so a failed presentation leaves focus exactly as found

Tests: miniaturizedSettingsWindowIsReusedAndVisibleOnReturn (simulation
faithful to the probed AppKit behavior) and
stalledDeminiaturizeCommitFallsBackToAFreshVisibleWindow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Preserve minimized windows across async deminiaturize; never activate to verify

Review round 4:

- P1: the same-turn deminiaturize commit was probed only on macOS 26 while
  cmux supports macOS 14+. performShow now waits out an initiated
  deminiaturization with a bounded main-run-loop pump (the same nested
  pumping AppKit's modal/menu tracking uses) before concluding anything,
  so a live window full of unsaved edits survives on every supported OS;
  replacement remains only for a transition that never lands (genuinely
  wedged). New test asyncDeminiaturizeCommitIsAwaitedWithoutReplacingTheWindow
  proves the later-turn commit is awaited without replacing the window;
  the stalled test now overrides the settle timeout to stay fast.
- P2: hidden-app verification uses NSApp.unhideWithoutActivation() (the
  API that exists for exactly this) instead of full activation, so a
  request that ultimately fails has never activated the app or redirected
  focus; activation runs strictly after verified visibility, and the
  failure exit re-hides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Split presentation-contract tests into their own file to stay under the length threshold

SettingsWindowNavigationRoutingTests.swift reached 540 lines (untracked
files must stay under 500). The Dock-deminiaturize, failed-activation, and
re-entrant teardown tests move to SettingsWindowPresentationContractTests
(wired into the pbxproj, nested under the same serialized parent suite);
the window helpers become target-internal and are shared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Skip the deminiaturize wait for hidden non-activating opens; abort on mid-wait ownership change

Review round 5:

- The hidden-app non-activating branch (.orderedWhileAppHidden) now runs
  before the deminiaturize wait: under a hidden app no amount of waiting
  produces visibility, and this is the synchronous socket path that must
  not stall for the full settle timeout.
- The nested run-loop pump can process a close (or a re-entrant show)
  while the outer attempt still holds the window: awaitVisibility now also
  exits when window ownership changes, and the attempt aborts — adopting
  an already-visible replacement, or failing loudly — instead of building
  a fresh window that resurrects one the user just closed. New test:
  closingTheWindowDuringTheDeminiaturizeWaitIsNotResurrected.
- logExistingWindowState moved to SettingsWindowGeometry.swift to keep the
  presenter under the 500-line threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Coalesce re-entrant opens onto an in-flight deminiaturization; adopted replacements honor activation

Review round 6 (Cursor + codex):

- Cursor: adopting a visible replacement window after a mid-wait ownership
  change returned .presented without honoring activateApp — an activating
  open (menu, CLI with activation) could report success while the window
  was never keyed and the app stayed inactive. activateAndSurface now runs
  for the adopted window. Test:
  reentrantReplacementDuringDeminiaturizeWaitIsActivated.
- codex P1: deminiaturize clears isMiniaturized before visibility lands,
  so a re-entrant show during the bounded wait saw a plain invisible
  window and demolished the live transition (destroying unsaved edits).
  The presenter now tracks the window whose deminiaturization is being
  awaited and re-entrant shows coalesce onto the transition. Test:
  reentrantShowDuringDeminiaturizeWaitCoalescesOntoTheTransition.
- Navigation delivery moved to SettingsWindowNavigationDelivery.swift
  (wired into the app target) to keep the presenter under the 500-line
  threshold; its state properties become target-internal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Convert AppDelegatePresentPreferencesWindowTests to Swift Testing

Review round 7: the extraction from AppDelegateShortcutRoutingTests carried
its XCTest style along, but repo policy reserves XCTest for UI suites —
non-UI unit tests use Swift Testing. Same five tests, now @Suite/@Test/#expect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix deminiaturize-wait tests on CI: schedule test events on the run loop, not the main queue

The three mid-wait tests failed on CI: they scheduled their mid-wait events
(window close, re-entrant show, delayed commit) via
DispatchQueue.main.asyncAfter, but the presenter's bounded wait pumps the
run loop from inside the current main-queue job, and a nested pump cannot
drain the serial main dispatch queue — the events never fired and every
wait timed out into the replacement fallback. Timer is a run-loop source,
fires during the pump, and faithfully models how AppKit delivers the real
transition (CA/WindowServer events, not main-queue blocks).

This also documents a property of the production design: main-queue work
(including queued socket commands) cannot re-enter the presenter during
the bounded wait; only run-loop-source events (user input, notifications)
can, and those paths are covered by the coalescing/ownership tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 14, 2026
Pin the structure the SwiftUI-owned WindowGroup scene produced for the
Settings window, on top of the AppKit-owned lifecycle from #7783:

- .fullSizeContentView in the styleMask (the sidebar extends under the
  titlebar), default titlebar styling otherwise — probe-app verified
  as exactly what SwiftUI sets on its own NavigationSplitView window
- an AppKit-owned toolbar of [flexible space, sidebar toggle, sidebar
  tracking separator], the exact item layout SwiftUI builds; the
  NSHostingController scene bridge never materializes the implicit
  toggle in an AppKit-hosted window, so .toolbars bridging is pinned
  OFF and the toolbar is deterministic (CI-testable, no bridge wait)
- the toolbar toggle posts the same notification the Toggle Left
  Sidebar menu command uses (one shared mutation path)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 14, 2026
The AppKit-hosted Settings window (#7783) lost two structural pieces
of the SwiftUI WindowGroup chrome:

1. .fullSizeContentView, without which the NavigationSplitView sidebar
   stops at an opaque titlebar band instead of running full height.
2. The sidebar toggle. NSHostingController's .toolbars scene bridging
   never materializes NavigationSplitView's implicit toggle in an
   AppKit-hosted window, so the factory now owns the toolbar in
   AppKit with SwiftUI's exact item layout: [flexible space, toggle,
   sidebar tracking separator]. The toggle posts the same notification
   the Toggle Left Sidebar menu command routes, keeping one mutation
   path for the sidebar state, and the tracking separator follows the
   split divider so the title renders at the detail column's leading
   edge.

The window renders in the system's current design (Liquid Glass on
macOS 26 SDK builds); no design opt-outs.

Localization: the toolbar toggle reuses the existing fully-localized
shortcut.toggleLeftSidebar.label and titlebar.sidebar.tooltip keys;
no new user-facing strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 14, 2026
Pin the structure the SwiftUI-owned WindowGroup scene produced for the
Settings window, on top of the AppKit-owned lifecycle from #7783:

- .fullSizeContentView in the styleMask (the sidebar extends under the
  titlebar), default titlebar styling otherwise — probe-app verified
  as exactly what SwiftUI sets on its own NavigationSplitView window
- an AppKit-owned toolbar of [flexible space, sidebar toggle, sidebar
  tracking separator], the exact item layout SwiftUI builds; the
  NSHostingController scene bridge never materializes the implicit
  toggle in an AppKit-hosted window, so .toolbars bridging is pinned
  OFF and the toolbar is deterministic (CI-testable, no bridge wait)
- the toolbar toggle posts the same notification the Toggle Left
  Sidebar menu command uses (one shared mutation path)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 14, 2026
The AppKit-hosted Settings window (#7783) lost two structural pieces
of the SwiftUI WindowGroup chrome:

1. .fullSizeContentView, without which the NavigationSplitView sidebar
   stops at an opaque titlebar band instead of running full height.
2. The sidebar toggle. NSHostingController's .toolbars scene bridging
   never materializes NavigationSplitView's implicit toggle in an
   AppKit-hosted window, so the factory now owns the toolbar in
   AppKit with SwiftUI's exact item layout: [flexible space, toggle,
   sidebar tracking separator]. The toggle posts the same notification
   the Toggle Left Sidebar menu command routes, keeping one mutation
   path for the sidebar state, and the tracking separator follows the
   split divider so the title renders at the detail column's leading
   edge.

The window renders in the system's current design (Liquid Glass on
macOS 26 SDK builds); no design opt-outs.

Localization: the toolbar toggle reuses the existing fully-localized
shortcut.toggleLeftSidebar.label and titlebar.sidebar.tooltip keys;
no new user-facing strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jul 14, 2026
…-hosted window (#8047)

* test: cover Settings sidebar chrome regression

* test: require SwiftUI Settings toolbar ownership

* fix: restore Settings sidebar-owned chrome

* fix: preserve native Settings sidebar toolbar

* test: exercise native Settings sidebar toolbar

* test: verify bridged Settings sidebar toggle

* test: require 0.64.17 Settings chrome

* fix: restore 0.64.17 Settings window chrome

* test: verify Settings sidebar visibility toggle

* test: keep Settings chrome coverage deterministic

* test: pin the native Settings split-view chrome contract

Pin the structure the SwiftUI-owned WindowGroup scene produced for the
Settings window, on top of the AppKit-owned lifecycle from #7783:

- .fullSizeContentView in the styleMask (the sidebar extends under the
  titlebar), default titlebar styling otherwise — probe-app verified
  as exactly what SwiftUI sets on its own NavigationSplitView window
- an AppKit-owned toolbar of [flexible space, sidebar toggle, sidebar
  tracking separator], the exact item layout SwiftUI builds; the
  NSHostingController scene bridge never materializes the implicit
  toggle in an AppKit-hosted window, so .toolbars bridging is pinned
  OFF and the toolbar is deterministic (CI-testable, no bridge wait)
- the toolbar toggle posts the same notification the Toggle Left
  Sidebar menu command uses (one shared mutation path)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: restore the Settings sidebar toggle and full-height sidebar

The AppKit-hosted Settings window (#7783) lost two structural pieces
of the SwiftUI WindowGroup chrome:

1. .fullSizeContentView, without which the NavigationSplitView sidebar
   stops at an opaque titlebar band instead of running full height.
2. The sidebar toggle. NSHostingController's .toolbars scene bridging
   never materializes NavigationSplitView's implicit toggle in an
   AppKit-hosted window, so the factory now owns the toolbar in
   AppKit with SwiftUI's exact item layout: [flexible space, toggle,
   sidebar tracking separator]. The toggle posts the same notification
   the Toggle Left Sidebar menu command routes, keeping one mutation
   path for the sidebar state, and the tracking separator follows the
   split divider so the title renders at the detail column's leading
   edge.

The window renders in the system's current design (Liquid Glass on
macOS 26 SDK builds); no design opt-outs.

Localization: the toolbar toggle reuses the existing fully-localized
shortcut.toggleLeftSidebar.label and titlebar.sidebar.tooltip keys;
no new user-facing strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jul 15, 2026
6 tasks done
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jul 16, 2026
…-hosted window (manaflow-ai#8047)

* test: cover Settings sidebar chrome regression

* test: require SwiftUI Settings toolbar ownership

* fix: restore Settings sidebar-owned chrome

* fix: preserve native Settings sidebar toolbar

* test: exercise native Settings sidebar toolbar

* test: verify bridged Settings sidebar toggle

* test: require 0.64.17 Settings chrome

* fix: restore 0.64.17 Settings window chrome

* test: verify Settings sidebar visibility toggle

* test: keep Settings chrome coverage deterministic

* test: pin the native Settings split-view chrome contract

Pin the structure the SwiftUI-owned WindowGroup scene produced for the
Settings window, on top of the AppKit-owned lifecycle from manaflow-ai#7783:

- .fullSizeContentView in the styleMask (the sidebar extends under the
  titlebar), default titlebar styling otherwise — probe-app verified
  as exactly what SwiftUI sets on its own NavigationSplitView window
- an AppKit-owned toolbar of [flexible space, sidebar toggle, sidebar
  tracking separator], the exact item layout SwiftUI builds; the
  NSHostingController scene bridge never materializes the implicit
  toggle in an AppKit-hosted window, so .toolbars bridging is pinned
  OFF and the toolbar is deterministic (CI-testable, no bridge wait)
- the toolbar toggle posts the same notification the Toggle Left
  Sidebar menu command uses (one shared mutation path)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: restore the Settings sidebar toggle and full-height sidebar

The AppKit-hosted Settings window (manaflow-ai#7783) lost two structural pieces
of the SwiftUI WindowGroup chrome:

1. .fullSizeContentView, without which the NavigationSplitView sidebar
   stops at an opaque titlebar band instead of running full height.
2. The sidebar toggle. NSHostingController's .toolbars scene bridging
   never materializes NavigationSplitView's implicit toggle in an
   AppKit-hosted window, so the factory now owns the toolbar in
   AppKit with SwiftUI's exact item layout: [flexible space, toggle,
   sidebar tracking separator]. The toggle posts the same notification
   the Toggle Left Sidebar menu command routes, keeping one mutation
   path for the sidebar state, and the tracking separator follows the
   split divider so the title renders at the detail column's leading
   edge.

The window renders in the system's current design (Liquid Glass on
macOS 26 SDK builds); no design opt-outs.

Localization: the toolbar toggle reuses the existing fully-localized
shortcut.toggleLeftSidebar.label and titlebar.sidebar.tooltip keys;
no new user-facing strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1574b7b)

This branch was successfully deployed

1 active deployment
Preview – cmux — 95263705 Deployed Jul 10, 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

1 participant