Skip to content

Fix blank Settings window on every open after the first - #4965

Open
philip-zhan wants to merge 2 commits into
manaflow-ai:mainfrom
philip-zhan:fix-settings-blank-on-reopen-upstream
Open

philip-zhan wants to merge 2 commits into
manaflow-ai:mainfrom
philip-zhan:fix-settings-blank-on-reopen-upstream

Conversation

@philip-zhan

@philip-zhan philip-zhan commented May 28, 2026 •

Copy link
Copy Markdown

Summary

  • What changed? The Settings window (⌘,) was blank on every open after the first within an app session — titlebar only, empty content — in any appearance mode. This fixes it so reopening always renders the full settings UI.
  • Why? SettingsWindowRootView (Sources/cmuxApp.swift) chooses between the real settings content and a Color.clear placeholder via a @State flag plus a weak observed-window reference (the CPU optimization from Fix hidden Settings CPU during Codex output #4661). On willClose it both hid content and cleared windowReference.window. SwiftUI reuses the Settings Window scene across close/reopen, so the @State placeholder flag persists and the WindowAccessor (which dedupes by the reused NSWindow) never fires again. On reopen, didBecomeKey/didBecomeMain fire but are gated by isObservedWindow(...), which returns false because the reference was cleared — so content visibility never flips back on. Result: a permanently blank Settings window until app restart.

Fixes #4964.

Approach

  • Extracted the window-lifecycle decision into a small, pure, unit-testable SettingsWindowContentVisibility model (Sources/SettingsWindowContentVisibility.swift).
  • When the window becomes visible again, the model re-adopts the Settings window by its stable identifier (cmux.settings) if a prior close cleared the reference — the only signal available, since the accessor doesn't re-fire for the reused scene.
  • Rewired SettingsWindowRootView to delegate to the model.

Testing

  • Reproduced on a Debug build (reload.sh): open Settings → ⌘W → reopen → blank window (titlebar only, empty content area).
  • Verified the fix on the same build: 4 consecutive close→reopen cycles all render the full settings UI (sidebar + detail).
  • Added cmuxTests/SettingsWindowContentVisibilityTests.swift (6 Swift Testing cases) covering the exact close→reopen repro, miniaturize/deminiaturize, repeated cycles, and unrelated-window guards. Wired into project.pbxproj; passes scripts/lint-pbxproj-test-wiring.sh and scripts/check-pbxproj.sh.
  • Ran xcodebuild -scheme cmux-unit -only-testing:cmuxTests/SettingsWindowContentVisibilityTests test → TEST SUCCEEDED (6/6).
  • Two-commit red/green: the first commit lands the test + seam in the bug-reproducing state (restoresContentWhenReusedWindowReopensAfterClose and survivesRepeatedCloseReopenCycles fail); the second commit adds the fix and turns them green — so the Commits tab proves the test catches the bug.

Demo Video

Before

CleanShot.2026-05-28.at.17.00.31.mp4

After

CleanShot.2026-05-28.at.17.09.01.mp4

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (N/A — internal bug fix, no user-facing API/docs change)
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

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


Summary by cubic

Fixes #4964 by re-adopting the reused Settings window so reopening always shows the full Settings UI. Centralizes window lifecycle logic to prevent the blank window after the first open.

  • Bug Fixes
    • Introduced SettingsWindowContentVisibility to control content rendering based on window lifecycle.
    • Re-adopts the Settings window by stable id cmux.settings on become-visible when the scene is reused.
    • Replaced weak-window logic in SettingsWindowRootView with the model; simplified event handling.
    • Added unit tests covering close→reopen, miniaturize/deminiaturize, and repeated cycles.

Written for commit 9dd49a7. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved Settings window content rendering during miniaturization and restoration.
    • Fixed issue where Settings window content would not properly display after reopening.
  • Tests

    • Added comprehensive test suite for Settings window visibility handling across various window state transitions.

Review Change Stack

philip-zhan and others added 2 commits May 28, 2026 16:27
The Settings window renders a blank placeholder on every open after the first
within a session (open -> close -> reopen), in any appearance mode. SwiftUI
reuses the Settings Window scene across close/reopen, so its content-visibility
state persists and the WindowAccessor (which dedupes by window) never fires
again on the second open. The content-visibility optimization from manaflow-ai#4661
forgets the observed window on close and then rejects every subsequent
"became visible" notification, so content never renders again.

Extract the lifecycle logic into a pure SettingsWindowContentVisibility model
and rewire SettingsWindowRootView to delegate to it, so the close -> reopen
path can be exercised in isolation. This commit intentionally reproduces the
bug (no re-adoption on reopen): SettingsWindowContentVisibilityTests'
restoresContentWhenReusedWindowReopensAfterClose and
survivesRepeatedCloseReopenCycles fail here, proving the test catches the bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the Settings window becomes visible again, re-adopt it by its stable
identifier (cmux.settings) if a prior close cleared the observed-window
reference. SwiftUI reuses the Window scene across close/reopen and the
WindowAccessor does not fire a second time, so this is the only signal that
the reused window is back. Restores content rendering on the second and
subsequent opens within a session.

All SettingsWindowContentVisibilityTests now pass (6/6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown

@philip-zhan is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai

coderabbitai Bot commented May 28, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces SettingsWindowContentVisibility, a new state machine that centralizes Settings window rendering visibility across window lifecycle events, and refactors SettingsWindowRootView to use this model instead of a fragile boolean flag, fixing the blank-window-on-reopen bug caused by SwiftUI window reuse.

Changes

Settings Window Content Visibility Fix

Layer / File(s) Summary
SettingsWindowContentVisibility state machine
Sources/SettingsWindowContentVisibility.swift
New @MainActor value type that encapsulates window visibility state: tracks observed NSWindow identity via ObjectIdentifier, toggles shouldRenderContent on miniaturize/deminiaturize/visibility/close events, and handles reused-window reopen by re-adopting the window when its stable identifier matches.
SettingsWindowRootView integration
Sources/cmuxApp.swift
Replaces @State boolean flag and weak window reference with visibility state model. Refactored notification handlers configure the window, listen for miniaturize/deminiaturize/key/main/close events, and introduce handleWindowDidBecomeVisible to re-identify the window post-reuse using stable identifiers from SettingsWindowPresenter.windowIdentifier.
Test suite
cmuxTests/SettingsWindowContentVisibilityTests.swift
Validates SettingsWindowContentVisibility state transitions: initial render, miniaturize/deminiaturize cycles, reused-window reopen regression, repeated close/reopen, and filtering unrelated-window notifications. Uses private WindowToken to simulate distinct window identities.
Project build configuration
cmux.xcodeproj/project.pbxproj
Adds PBXBuildFile, PBXFileReference, and group entries to wire SettingsWindowContentVisibility.swift into main target and test file into test target.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4661: Introduced the original content-visibility optimization in SettingsWindowRootView that this PR refactors to fix the blank-window-on-reopen regression.

Poem

🐰 A window reused, but state forgot—
Fresh visibility wired just right,
The Settings now shine on reopen, no blot,
State machine brings content back to light! ✨

🚥 Pre-merge checks | ✅ 17 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary fix: resolving the blank Settings window on reopens after the first open, which is the main objective from issue #4964.
Linked Issues check ✅ Passed The PR fully addresses issue #4964's objectives: reproduces the bug, implements a fix via SettingsWindowContentVisibility model, adds regression tests covering close→reopen and related cases, and verifies the fix prevents blank windows on reopens.
Out of Scope Changes check ✅ Passed All changes are in scope: new SettingsWindowContentVisibility model, integration into SettingsWindowRootView, test suite additions, and project file updates are all directly related to fixing issue #4964.
Cmux Swift Actor Isolation ✅ Passed SettingsWindowContentVisibility is explicitly @MainActor, a state-machine value type with only Sendable members, used only within MainActor View context. No isolation violations or debt introduced.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization patterns (Task.sleep, semaphores, DispatchQueue.main.sync, NSLock, asyncAfter) introduced in production code. Uses @MainActor state machine.
Cmux No Hacky Sleeps ✅ Passed All changes are Swift code. The check explicitly excludes Swift ("covered by swift-blocking-runtime.md"). No hacky sleeps, timers, or delays found in any changed files.
Cmux Algorithmic Complexity ✅ Passed Production code uses only O(1) constant-time operations on fixed-size state. Tests use fixed-size iterations (5 loops), not scalable collections. No nested scans or complex algorithms on user paths.
Cmux Swift Concurrency ✅ Passed New @MainActor value type; .onReceive() with NSWindow notifications allowed for AppKit boundaries; no Dispatch, Combine, or problematic Task patterns.
Cmux Swift @Concurrent ✅ Passed All Swift changes properly use @MainActor isolation on synchronous-only code; no async functions, no missing @concurrent, no violations of the swift-concurrent-annotation.md rules.
Cmux Swift File And Package Boundaries ✅ Passed 66-line single-responsibility state machine with clear extraction path; net -14 lines to cmuxApp.swift. Meets "focused bug fix" exception for existing oversized files.
Cmux Swift Logging ✅ Passed The PR adds SettingsWindowContentVisibility.swift, modifies cmuxApp.swift, and adds tests—none contain forbidden logging (print, debugPrint, dump, NSLog) or Logger violations.
Cmux User-Facing Error Privacy ✅ Passed PR adds internal state management for Settings window visibility with no user-facing error messages, alerts, or privacy-sensitive information exposed.
Cmux Full Internationalization ✅ Passed PR introduces no user-facing text, localized APIs, or i18n changes. All modifications are internal: state machine struct, test code, and refactored view logic without new localization requirements.
Cmux Swiftui State Layout ✅ Passed SettingsWindowContentVisibility is a value struct with @State, state mutations only in .onReceive handlers, no ObservableObject/Published antipatterns, and no render-time mutations.
Cmux Architecture Rethink ✅ Passed No architectural violations: required NSWindow platform callbacks, single @State owner, sound invariants, no timing/locks/polling, properly documented.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Settings window already has stable cmux.settings identifier registered in cmuxAuxiliaryWindowIdentifiers; PR only refactors content visibility logic. Lint script passes with 25 identifiers checked.
Description check ✅ Passed The PR description comprehensively covers all required template sections: Summary (what changed and why), Testing (reproduction, verification, unit tests, and test execution), Demo Video (before/after), Review Trigger, and Checklist with appropriate item completion.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@philip-zhan

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented May 28, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@philip-zhan I have started the AI code review. It will take a few minutes to complete.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

@philip-zhan Sure! I'll kick off a review of the PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​posthog-js@​1.373.47410082100100
Addednpm/​tailwindcss@​4.1.181001008498100
Addednpm/​typescript@​5.9.3100100909590
Addednpm/​zod@​4.3.610010010095100

View full report

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a bug where the Settings window showed only its title bar (blank content area) on every open after the first within a session. SwiftUI reuses the Window scene's NSWindow across close/reopen, so WindowAccessor never re-fires and the old @State placeholder flag stayed stuck after the willClose handler cleared the window reference.

  • Extracts the window lifecycle state machine into SettingsWindowContentVisibility, a small @MainActor value-type model that handles configure / miniaturize / become-visible / close transitions and re-adopts the reused Settings window (identified by SettingsWindowPresenter.windowIdentifier) when a prior close cleared the observed reference.
  • Rewires SettingsWindowRootView to delegate all lifecycle events to the new model, removing WeakSettingsWindowReference and the two separate @State flags it replaced.
  • Adds six Swift Testing regression cases (close→reopen, repeated cycles, miniaturize/deminiaturize, unrelated-window guards) and wires both files into project.pbxproj.

Confidence Score: 4/5

The fix is correct and well-tested; the only observation is a style point on an explicit @MainActor annotation that is unnecessary for a pure value struct.

The close→reopen state machine is logically sound, idempotent across repeated notifications, and correctly guards re-adoption behind the window's stable cmux.settings identifier. The new model is covered by six regression tests that would have caught the original bug. The sole finding is the unnecessary @MainActor annotation on a struct whose fields are Bool and ObjectIdentifier? — no UI state, no actor-bound APIs — which is a style concern rather than a defect.

Sources/SettingsWindowContentVisibility.swift — worth a second look only for the @MainActor annotation; everything else in the changeset is straightforward.

Important Files Changed

Filename Overview
Sources/SettingsWindowContentVisibility.swift New pure-value state machine for the Settings window lifecycle; correct logic but carries an unnecessary @MainActor annotation for a struct with no UI state.
Sources/cmuxApp.swift Replaces two-field @State with the new visibility model; notification wiring is correct and the re-adoption guard is properly scoped to the settings window identifier.
cmuxTests/SettingsWindowContentVisibilityTests.swift Six Swift Testing cases covering the core close→reopen regression, miniaturize/deminiaturize, repeated cycles, and unrelated-window guards; all paths correct.
cmux.xcodeproj/project.pbxproj Wires both new files into the app and test targets; UUIDs are hand-crafted sequential values (not Xcode-generated) but unique and valid.

Sequence Diagram

sequenceDiagram
    participant SC as Settings Scene (SwiftUI)
    participant WA as WindowAccessor
    participant SWCV as SettingsWindowContentVisibility
    participant SRV as SettingsWindowRootView

    Note over SC,SRV: First open
    SC->>WA: background callback fires
    WA->>SWCV: windowConfigured(id, isMiniaturized: false)
    SWCV-->>SRV: "shouldRenderContent = true"

    Note over SC,SRV: Close (⌘W)
    SC->>SRV: willCloseNotification
    SRV->>SWCV: windowWillClose(id)
    SWCV-->>SRV: "shouldRenderContent = false, observedWindow = nil"

    Note over SC,SRV: Reopen — WindowAccessor does NOT re-fire (reused scene)
    SC->>SRV: didBecomeKeyNotification
    SRV->>SWCV: windowDidBecomeVisible(id, isSettingsWindow: true)
    Note over SWCV: observedWindow == nil → re-adopt
    SWCV-->>SRV: "observedWindow = id, shouldRenderContent = true"

    Note over SC,SRV: Miniaturize
    SC->>SRV: didMiniaturizeNotification
    SRV->>SWCV: windowDidMiniaturize(id)
    SWCV-->>SRV: "shouldRenderContent = false"

    Note over SC,SRV: Deminiaturize
    SC->>SRV: didDeminiaturizeNotification
    SRV->>SWCV: windowDidBecomeVisible(id, isSettingsWindow: true)
    SWCV-->>SRV: "shouldRenderContent = true"
Loading

Reviews (1): Last reviewed commit: "Fix blank Settings window on reopen by r..." | Re-trigger Greptile

Comment on lines +11 to +12
@MainActor
struct SettingsWindowContentVisibility: Equatable {

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 SettingsWindowContentVisibility is a pure value type whose fields are Bool and ObjectIdentifier? — it holds no UI state and has no dependency on the main actor. Marking it @MainActor couples every declaration and mutating method to the main actor unnecessarily, which is why the test suite also has to carry @MainActor. Per the actor-isolation guideline, pure value model types should be nonisolated (no annotation); callers on the main actor can still create and mutate them freely, and future non-UI callsites won't be forced onto the main thread.

Suggested change
@MainActor
struct SettingsWindowContentVisibility: Equatable {
struct SettingsWindowContentVisibility: Equatable {

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

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!

@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the blank Settings window on re-open by extracting a pure, testable SettingsWindowContentVisibility value type that re-adopts the reused NSWindow (by its stable cmux.settings identifier) when a prior close cleared the observed reference — the path that the old WeakSettingsWindowReference/flag pair could never recover from.

  • SettingsWindowContentVisibility.swift: New @MainActor struct encoding the four-state lifecycle (unconfigured → configured → miniaturized/closed → re-adopted); replaces the old weak-ref + boolean pair with a named, Equatable state machine backed by six passing unit tests.
  • cmuxApp.swift: SettingsWindowRootView wired to the new model; WindowAccessor now calls windowConfigured, and handleWindowDidBecomeVisible passes the isSettingsWindow predicate that enables re-adoption on reopen.
  • cmux.xcodeproj/project.pbxproj: Both new files registered in the app and test targets with PBX UUIDs.

Confidence Score: 4/5

Safe to merge; the fix correctly addresses the blank-window repro with no behavioral regressions on the normal open/close/reopen path.

The re-adoption logic in windowDidBecomeVisible is sound — re-adoption only fires when observedWindow is nil and the window carries the stable cmux.settings identifier, and the five-cycle stress test plus unrelated-window guard tests confirm the boundaries. The only open items are a missing dedup guard in windowConfigured and a missing test for the isMiniaturized: true configure path.

Sources/SettingsWindowContentVisibility.swift — the windowConfigured dedup and the test gap for the miniaturized-on-configure path.

Important Files Changed

Filename Overview
Sources/SettingsWindowContentVisibility.swift New 66-line @mainactor struct encoding the Settings window content state machine; logic is clean and well-documented. Minor gap: windowConfigured always unconditionally writes both fields (no dedup guard), unlike the old setContentVisibility.
Sources/cmuxApp.swift SettingsWindowRootView simplified by delegating to SettingsWindowContentVisibility; re-adoption path wired via handleWindowDidBecomeVisible with identifier check against SettingsWindowPresenter.windowIdentifier.
cmuxTests/SettingsWindowContentVisibilityTests.swift Six Swift Testing cases cover the core repro, miniaturize/deminiaturize, 5-cycle stress test, and unrelated-window guards. Missing a test for windowConfigured(isMiniaturized: true) but all critical paths are covered.
cmux.xcodeproj/project.pbxproj Both new files correctly registered in app Sources and test Sources build phases with PBX UUIDs.

Sequence Diagram

sequenceDiagram
    participant SWR as SettingsWindowRootView
    participant WA as WindowAccessor
    participant SWCV as SettingsWindowContentVisibility
    participant NC as NotificationCenter

    Note over SWR,SWCV: First open
    WA->>SWR: callback(window)
    SWR->>SWCV: windowConfigured(id, isMiniaturized: false)
    SWCV-->>SWCV: "observedWindow = id, shouldRenderContent = true"
    NC->>SWR: didBecomeKeyNotification
    SWR->>SWCV: windowDidBecomeVisible(id, isSettingsWindow: true)
    SWCV-->>SWCV: "observedWindow already set, shouldRenderContent = true"

    Note over SWR,SWCV: User closes with ⌘W
    NC->>SWR: willCloseNotification
    SWR->>SWCV: windowWillClose(id)
    SWCV-->>SWCV: "shouldRenderContent = false, observedWindow = nil"

    Note over SWR,SWCV: Reopen (scene reused — WindowAccessor does NOT fire again)
    NC->>SWR: didBecomeKeyNotification
    SWR->>SWCV: windowDidBecomeVisible(id, isSettingsWindow: true)
    SWCV-->>SWCV: "observedWindow == nil → re-adopt, observedWindow = id"
    SWCV-->>SWCV: "shouldRenderContent = true ✅"
Loading

Reviews (2): Last reviewed commit: "Fix blank Settings window on reopen by r..." | Re-trigger Greptile

Comment on lines +32 to +35
mutating func windowConfigured(_ window: ObjectIdentifier, isMiniaturized: Bool) {
observedWindow = window
shouldRenderContent = !isMiniaturized
}

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 The old setContentVisibility had a guard shouldRenderSettingsContent != isVisible else { return } dedup to prevent no-op @State mutations (which still trigger a SwiftUI re-render even when the value doesn't change). windowConfigured unconditionally writes both observedWindow and shouldRenderContent on every call. WindowAccessor typically fires once, but on a window-moved-between-screens event or any future path that re-triggers the accessor, this will cause an avoidable render. Adding an early exit when the values are already correct restores the prior optimization.

Suggested change
mutating func windowConfigured(_ window: ObjectIdentifier, isMiniaturized: Bool) {
observedWindow = window
shouldRenderContent = !isMiniaturized
}
mutating func windowConfigured(_ window: ObjectIdentifier, isMiniaturized: Bool) {
let newShouldRender = !isMiniaturized
guard observedWindow != window || shouldRenderContent != newShouldRender else { return }
observedWindow = window
shouldRenderContent = newShouldRender
}

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 on lines +22 to +29
@Test func rendersContentOnFirstConfigure() {
var visibility = SettingsWindowContentVisibility()
let window = WindowToken()

visibility.windowConfigured(ObjectIdentifier(window), isMiniaturized: false)

#expect(visibility.shouldRenderContent)
}

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 Missing test: windowConfigured(isMiniaturized: true)

The suite has no case that calls windowConfigured(id, isMiniaturized: true) and verifies shouldRenderContent == false. If a future refactor accidentally flips the negation (e.g. shouldRenderContent = isMiniaturized), none of the six existing tests would catch it — they all configure with isMiniaturized: false.

@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the Settings window going blank on every open after the first by extracting window-lifecycle state into a new SettingsWindowContentVisibility value type and adding re-adoption logic for the reused SwiftUI Window scene.

  • SettingsWindowContentVisibility replaces the old WeakSettingsWindowReference + shouldRenderSettingsContent flag pair with a single, testable struct that tracks the observed window via ObjectIdentifier and re-adopts the reused NSWindow on didBecomeKey/didBecomeMain by matching the stable cmux.settings identifier.
  • Six regression tests in SettingsWindowContentVisibilityTests.swift cover the close→reopen cycle, miniaturize/deminiaturize, repeated cycles, and unrelated-window guards; the project.pbxproj wiring passes the existing lint scripts.

Confidence Score: 4/5

Safe to merge; the fix correctly restores Settings content on every reopen and is backed by targeted regression tests.

The re-adoption logic in windowDidBecomeVisible is correct for all tested paths. The two observations are: @MainActor on a pure value struct tightens coupling unnecessarily, and the re-adoption's isSettingsWindow check implicitly depends on SettingsWindowPresenter.configure(window:) having already run — a precondition not expressed anywhere in the model's interface.

Sources/SettingsWindowContentVisibility.swift — re-adoption coupling and actor annotation; everything else is straightforward.

Important Files Changed

Filename Overview
Sources/SettingsWindowContentVisibility.swift New value type encapsulating the settings window show/hide state machine. Logic is correct and well-documented; @MainActor on the struct is unnecessarily restrictive for a pure value type.
Sources/cmuxApp.swift Rewires SettingsWindowRootView to delegate lifecycle events to SettingsWindowContentVisibility; removes WeakSettingsWindowReference and the isObservedWindow helper. The delegation and handleWindowDidBecomeVisible re-adoption path are correct.
cmuxTests/SettingsWindowContentVisibilityTests.swift Six focused regression tests covering first configure, miniaturize/deminiaturize, the core close→reopen repro, repeated cycles, and unrelated-window guards. All paths exercised correctly.
cmux.xcodeproj/project.pbxproj Wires SettingsWindowContentVisibility.swift into the main target and SettingsWindowContentVisibilityTests.swift into the test target; no structural issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([SettingsWindowContentVisibility.init
shouldRenderContent=true
observedWindow=nil]) --> B

    B[WindowAccessor fires
first open only] -->|windowConfigured| C
    C[observedWindow = id
shouldRenderContent = !isMiniaturized]

    C -->|NSWindow.willCloseNotification| D
    D[observedWindow = nil
shouldRenderContent = false]

    D -->|NSWindow.didBecomeKey or
didBecomeMain notification| E{observedWindow == nil
AND isSettingsWindow?}
    E -->|YES – re-adopt| F[observedWindow = window
shouldRenderContent = true]
    E -->|NO – unrelated window| G([no-op, early return])

    C -->|NSWindow.didMiniaturizeNotification| H[shouldRenderContent = false]
    H -->|NSWindow.didDeminiaturizeNotification
or didBecomeKey/Main| F

    F -->|Next close| D
Loading

Reviews (3): Last reviewed commit: "Fix blank Settings window on reopen by r..." | Re-trigger Greptile

/// window on every open after the first. This value type centralizes that
/// state machine so the close → reopen path can be exercised in isolation.
@MainActor
struct SettingsWindowContentVisibility: Equatable {

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 @MainActor on a pure value type

SettingsWindowContentVisibility holds only value-typed fields (Bool, ObjectIdentifier?) with no AppKit objects and no shared mutable reference semantics. The @MainActor annotation couples the type to the main actor unnecessarily — it cannot be called from non-@MainActor contexts (e.g., a future actor-isolated store, or a plain XCTest target without @MainActor). The cmux actor-isolation rule's preferred shape for pure value types is nonisolated struct; the @MainActor should live on the call sites (already the case — SettingsWindowRootView is @MainActor), not on the model itself.

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

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 on lines +52 to +58
mutating func windowDidBecomeVisible(_ window: ObjectIdentifier, isSettingsWindow: Bool) {
if observedWindow == nil, isSettingsWindow {
observedWindow = window
}
guard window == observedWindow else { return }
shouldRenderContent = true
}

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 Re-adoption relies on an implicit ordering dependency with SettingsWindowPresenter

windowDidBecomeVisible re-adopts the Settings window by checking isSettingsWindow, which the caller resolves via window.identifier?.rawValue == SettingsWindowPresenter.windowIdentifier. This check only returns true because SettingsWindowPresenter.configure(window:) has already stamped the identifier on the reused NSWindow during the first open. Nothing in SettingsWindowContentVisibility's interface encodes that precondition, so a future refactor that delays or skips configure(window:) would silently break re-adoption: isSettingsWindow would be false, observedWindow would stay nil, and the window would go blank again — exactly the bug this PR fixed — without any compile-time or runtime signal.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@teamleaderleo teamleaderleo added area: settings Settings UI and the config file S2: major A crash, hang, lost state, broken connection, or a regression on a path people use labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: settings Settings UI and the config file S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings window is blank on every open after the first (⌘,)

2 participants