Repository navigation
Remember and restore window geometry per monitor configuration (#2135) - #7477
Conversation
…low-ai#2135) When the user switches between monitor setups (dock/undock, reconnect a display, home-dual ↔ office-single ↔ laptop-only), each main window now returns to the position and size it last had on that configuration. Previously cmux kept only one last frame, so reconnecting the external monitor left the window on the built-in display (manaflow-ai#2135); the just-merged manaflow-ai#6913 reconcile keeps a stranded window reachable but does not remember where it was. Design (per-window config-frame ring, folded into the manaflow-ai#6913 reconcile): - Stable display key: NSScreen.cmuxStableDisplayKey prefers CGDisplayCreateUUIDFromDisplayID (stable across reboot, GPU-mux, and port/reconnect — unlike the raw CGDirectDisplayID) with an EDID-triple fallback; nil when neither resolves, so such a display is excluded from any persisted key rather than keyed unstably. - Display-configuration signature (DisplayConfigurationSignature, pure / in CmuxWindowing): sorted and order-independent; excludes visibleFrame (so Dock/menu-bar/notch changes don't re-key the same setup); includes frame origin+size (so resolution changes re-key, and two identical-model monitors are disambiguated left/right by position); mirror sets get a distinct marker so they never collide with laptop-only. - Per-window LRU ring (SessionConfigFrameEntry, cap 8) stored additively on SessionWindowSnapshot (optional field → older snapshots decode unchanged) and mirrored in memory for capture/restore. - Capture firewall (the manaflow-ai#2135 corruption guard): captures run only on non-reactive paths (session snapshot, window close, plus a leading-edge snapshot at the top of the didChangeScreenParameters handler that records the still-good frame under the PRE-change signature). Every write is suppressed during session restore, teardown, and the settling window after a screen change; skipped when reconciledFrameAfterScreenChange would move the frame (never persist a stranded frame); and keyed to the write-time signature so a slipped write can only land in the currently-connected slot, never overwrite a disconnected one. - Restore folds into the existing manaflow-ai#6913 debounced reconcile pass (no second observer). It fires only when the connected-display signature genuinely changes vs lastAppliedConfigurationSignature (seeded at didFinishLaunching), so sleep/wake and Dock resize are no-ops. Remembered frames are routed through resolvedWindowFrame (re-clamped if they no longer fit), fullscreen windows are skipped, and the reachability safety net still runs afterward. Tests: 9 pure signature tests (CmuxWindowing package) covering order-independence, visibleFrame-exclusion, resolution sensitivity, identical-panel position disambiguation, mirror distinctness, and refuse-to-key; 4 app-target tests including the headline round-trip (place on external → disconnect must NOT mutate the external slot → reconnect restores exactly), LRU upsert/eviction, and clamp-when-oversized. New test file wired into project.pbxproj.
|
@mxschmitt is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds stable display identity and configuration signatures, persists per-window frame history by signature, and updates AppDelegate to capture, restore, and reconcile window frames across display changes. It also adds tests and Xcode project entries for the new files. ChangesDisplay config signature and window frame memory
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CoreGraphics
participant AppDelegate
participant SessionConfigFrameRing
participant Window
CoreGraphics->>AppDelegate: begin/end display reconfiguration
AppDelegate->>AppDelegate: beginScreenChangeCaptureSuppression
AppDelegate->>AppDelegate: scheduleScreenChangeReconcileWhenIdle
AppDelegate->>SessionConfigFrameRing: entry(for: signature)
SessionConfigFrameRing-->>AppDelegate: remembered frame entry
AppDelegate->>Window: apply resolved frame
sequenceDiagram
participant NSScreen
participant SessionDisplayGeometry
participant DisplayConfigurationSignature
NSScreen->>SessionDisplayGeometry: stableID, frame, visibleFrame
SessionDisplayGeometry->>DisplayConfigurationSignature: displayConfigurationSignature(isMirrored:)
DisplayConfigurationSignature->>DisplayConfigurationSignature: sort and join components
Related PRs: None found. Suggested labels: macOS, needs-testing Suggested reviewers: None found. Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors)
✅ Passed checks (23 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR remembers window geometry per monitor setup and restores it when displays change.
Confidence Score: 4/5This is close, but the display-change capture gate should be fixed before merging.
Sources/AppDelegate+MonitorMemory.swift Important Files Changed
Reviews (22): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
DEBUG-only cmuxDebugLog tracing so the dock/undock round-trip can be verified from the event log: - monitorMemory.screenChange — a display reconfiguration was observed (with display count + names). - monitorMemory.capture / .capture.skip — a window frame was recorded under a configuration signature, or which guard rejected the capture (settling/restore/teardown, fullscreen, noStableSignature, strandedFrame). - monitorMemory.reconcile — whether the connected-display signature changed since the last applied one (drives whether restore runs). - monitorMemory.restore.hit / .restore.miss — a remembered frame was restored for the new configuration, or none was found. Signatures are rendered via a compact token (display count + short suffix) so lines stay readable with several displays.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/AppDelegate.swift (2)
3918-3918: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winCapture screen-change leading-edge frames under the pre-change configuration.
The leading-edge path is meant to preserve the frame for the configuration being left, but
captureWindowConfigFramerecomputes the live signature and reachability fromcurrentDisplayGeometries(). During dock/reconnect this can overwrite the destination configuration’s remembered frame beforerestoreRememberedFrames(for:); during disconnect it can reject a valid old-monitor frame asstrandedFrame. Pass the previous signature and previous display geometries into the screen-change capture path, and reserve live write-time signatures for ordinary snapshot/close captures. As per path instructions,.github/review-bot-rules/reliability-single-source-of-truth.mdrequires correctness-critical geometry identity to come from a reliable keyed source rather than an unreliable fallback that can apply or persist the wrong geometry.Also applies to: 4075-4095
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AppDelegate.swift` at line 3918, The screen-change leading-edge capture is using live geometry from currentDisplayGeometries() inside captureWindowConfigFrame, which can overwrite or reject the frame for the configuration being left. Update the screen-change path in AppDelegate so captureAllWindowConfigFrames(reason:) passes the previous display signature and previous display geometries into the capture flow, and have captureWindowConfigFrame use those pre-change values for leading-edge capture while keeping live signature recomputation only for ordinary snapshot/close captures. Make sure the restoreRememberedFrames(for:) path reads the preserved pre-change frame rather than the current configuration’s geometry.Source: Path instructions
3919-3919: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftReplace the fixed settling grace window with an explicit transition boundary.
beginSettlingScreenChange()plusendSettlingScreenChangeAfterGrace()makes capture correctness depend on a 500ms timing window. That can drop legitimate captures on fast transitions or re-enable writes while AppKit is still adjusting windows on slow display changes. Scope suppression to the explicit reconcile/restore operation or a documented AppKit signal instead. As per coding guidelines, non-test Swift must flag timing-based coordination such as sleeps, delayed dispatch, timers, and polling; as per path instructions,.github/review-bot-rules/swift-blocking-runtime.mddisallows timing-based synchronization in production Swift.Also applies to: 3949-3950
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AppDelegate.swift` at line 3919, The settling logic in beginSettlingScreenChange/endSettlingScreenChangeAfterGrace relies on a fixed 500ms grace window, which is timing-based coordination. Replace that with an explicit boundary around the reconcile/restore flow or a documented AppKit-driven signal so capture suppression only spans the actual transition in AppDelegate’s screen-change handling. Update the related callers around beginSettlingScreenChange, endSettlingScreenChangeAfterGrace, and the reconcile/restore path to avoid delayed dispatch or any timer-based re-enabling of writes.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/AppDelegate.swift`:
- Line 3918: The screen-change leading-edge capture is using live geometry from
currentDisplayGeometries() inside captureWindowConfigFrame, which can overwrite
or reject the frame for the configuration being left. Update the screen-change
path in AppDelegate so captureAllWindowConfigFrames(reason:) passes the previous
display signature and previous display geometries into the capture flow, and
have captureWindowConfigFrame use those pre-change values for leading-edge
capture while keeping live signature recomputation only for ordinary
snapshot/close captures. Make sure the restoreRememberedFrames(for:) path reads
the preserved pre-change frame rather than the current configuration’s geometry.
- Line 3919: The settling logic in
beginSettlingScreenChange/endSettlingScreenChangeAfterGrace relies on a fixed
500ms grace window, which is timing-based coordination. Replace that with an
explicit boundary around the reconcile/restore flow or a documented
AppKit-driven signal so capture suppression only spans the actual transition in
AppDelegate’s screen-change handling. Update the related callers around
beginSettlingScreenChange, endSettlingScreenChangeAfterGrace, and the
reconcile/restore path to avoid delayed dispatch or any timer-based re-enabling
of writes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 59501fcd-8f94-49e6-8394-c7b91787cbba
📒 Files selected for processing (1)
Sources/AppDelegate.swift
Dogfood on real hardware showed the window landing on the wrong screen
after reconnecting monitors. Debug logs pinpointed the cause: the
"leading-edge" capture in the didChangeScreenParameters handler was built
on a false assumption — that the notification fires BEFORE NSScreen.screens
updates. It fires AFTER. So the leading-edge capture stored the outgoing
window frame (still the laptop position) under the INCOMING configuration
signature, overwriting the good remembered frame for that configuration.
Restore then faithfully applied the corrupted frame.
The log sequence on reconnect:
capture reason=screenChange.leadingEdge sig=[2-monitor] frame={laptop pos}
restore.hit sig=[2-monitor] applied={laptop pos} ← wrong screen
A DELL monitor's ~2s EDID handshake made it worse: each transient
screen-change fired another leading-edge capture that wrote junk under a
transient signature.
Fix: remove the leading-edge capture entirely — there is no correct
"pre-change" frame to capture once the screens have already changed. The
per-config ring is populated only while a configuration is STABLE (the
session-autosave tick and window close), which are correctly keyed to the
current signature. Transient handshake configs now simply produce a
restore.miss (harmless; the window stays put) and the final real config
restores from the correctly-captured slot.
Address PR review (Greptile): endSettlingScreenChangeAfterGrace cleared the capture firewall via a fixed Task.sleep(500ms) — timing used as synchronization, which the cmux-architecture rules flag (same family as the manaflow-ai#6913 finding). A slow reconfiguration could deliver a late frame change after 500ms; a fast one needlessly suppressed session-snapshot and window-close captures for the full grace period. Drive the flag from real completion state instead. The reconcile task already debounces on didChangeScreenParameters (200ms of notification quiet), so its firing IS the "display list has stopped changing" signal, and a straggler notification re-arms settling and reschedules the pass. So the flag is now cleared when reconcileMainWindowFramesAfterScreenChange actually runs — no wall-clock grace, no separate grace Task.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 3953-3959: The settle flag is still being cleared from the
debounce quiet period rather than from an authoritative screen-state transition,
so capture can resume too early. Update the logic around the screen-change
reconcile path and the isSettlingScreenChange lifecycle so it only flips false
after the latest-signature/state transition has been observed, or keep the
firewall closed until that explicit signal exists; avoid relying on the 200ms
no-notification window in the related screen-parameter handling code.
🪄 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: 7ab0180c-9e94-4703-9879-8f3c67521323
📒 Files selected for processing (1)
Sources/AppDelegate.swift
…space enum CI's package-conventions-lint flagged `enum DisplayConfigurationSignature` as a namespace-type (all-static public surface, not instantiable). The repo rule: receiver-natural pure transforms belong in an extension on the receiver type, not a caseless namespace enum. Convert to `extension [SessionDisplayGeometry]` with a `displayConfigurationSignature(isMirrored:)` method (and a fileprivate per-element `displayConfigurationComponent`). Call sites read better too: `displays.displayConfigurationSignature(...)`. Pure logic unchanged; the 9 package signature tests and the app-target round-trip tests are updated to the new call form and still pass. The ios-tests / workflow-guard failures were gated on this lint and clear with it.
- Seed windowConfigFrames in createMainWindow(sessionWindowSnapshot:) so additional startup windows, previous-session reopen, and closed-window restore keep their per-configuration frames under the assigned window id - Reschedule the screen-change reconcile when session restore completes so a display change that lands mid-restore is consumed instead of dropped - Clear the capture firewall only when a reconcile pass actually consumes the settled configuration; skipped passes now fail closed instead of reopening on elapsed time - Extract AppDelegate+MonitorMemory.swift and SessionDisplayFrames.swift so AppDelegate.swift and SessionPersistence.swift return under the Swift file length budget without touching the budget TSVs - Add regression tests for snapshot-window seeding, fail-closed settling during restore, and the post-restore reconcile reschedule Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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 `@cmuxTests/AppDelegateDisplayConfigRestoreTests.swift`:
- Line 194: The AppDelegate setup is duplicated across the new tests, so factor
the repeated AppDelegate.shared ?? AppDelegate() expression into a small private
helper such as testAppDelegate() -> AppDelegate. Update the affected tests to
use that helper, keeping the existing closeCreatedWindow and emptyWindowSnapshot
helpers for consistency and easier maintenance.
🪄 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: e211bc53-eae9-45b0-a6d7-c24b319bc02b
📒 Files selected for processing (6)
Sources/AppDelegate+MonitorMemory.swiftSources/AppDelegate.swiftSources/SessionDisplayFrames.swiftSources/SessionPersistence.swiftcmux.xcodeproj/project.pbxprojcmuxTests/AppDelegateDisplayConfigRestoreTests.swift
💤 Files with no reviewable changes (1)
- Sources/SessionPersistence.swift
There was a problem hiding this comment.
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 `@cmuxTests/AppDelegateDisplayConfigRestoreTests.swift`:
- Around line 383-390: The test setup in AppDelegateDisplayConfigRestoreTests
around handleDisplayReconfiguration is assigning
screenChangeCaptureSuppressionSignature too early, so the begin transition
clears it before the final expectation. Move the signature assignment to after
the full begin/end reconfiguration cycle, or invoke the reconcile path that
re-records the signature, so shouldReleaseScreenChangeCaptureSuppression(for:)
evaluates true at the end.
🪄 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: a14e8b79-77b0-4232-bf50-662c57761534
📒 Files selected for processing (3)
Sources/AppDelegate+MonitorMemory.swiftSources/AppDelegate.swiftcmuxTests/AppDelegateDisplayConfigRestoreTests.swift
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/AppDelegate+MonitorMemory.swift (1)
6-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the shared display-reconfiguration notification constant
Sources/AppDelegate+MonitorMemory.swiftshould postAppDelegate.displayReconfigurationNotificationinstead of repeating the rawNotification.Name("com.cmuxterm.app.displayReconfiguration")string. That keeps the poster and observer on one source of truth.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AppDelegate`+MonitorMemory.swift around lines 6 - 19, The display reconfiguration callback is posting a hardcoded notification name instead of the shared constant. Update cmuxDisplayReconfigurationCallback in AppDelegate+MonitorMemory to use AppDelegate.displayReconfigurationNotification when calling NotificationCenter.default.post, so the poster and observers rely on the same source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/AppDelegate`+MonitorMemory.swift:
- Around line 6-19: The display reconfiguration callback is posting a hardcoded
notification name instead of the shared constant. Update
cmuxDisplayReconfigurationCallback in AppDelegate+MonitorMemory to use
AppDelegate.displayReconfigurationNotification when calling
NotificationCenter.default.post, so the poster and observers rely on the same
source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 02a08bff-f4b5-410e-a3a9-7a17f097b88a
📒 Files selected for processing (3)
Sources/AppDelegate+MonitorMemory.swiftSources/AppDelegate.swiftcmuxTests/AppDelegateDisplayConfigRestoreTests.swift
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c0d80a7. Configure here.
…ow-geometry # Conflicts: # cmux.xcodeproj/project.pbxproj
Fixes #2135 (and the reconnect half of the #1620 / #1331 / #1836 cluster).
Problem
cmux remembers only one last window frame. When you switch monitor setups — undock, reconnect a display, or move between home-dual / office-single / laptop-only — the window does not return to where it was on that setup; reconnecting the external monitor leaves it on the built-in display. The just-merged #6913 keeps a stranded window reachable; this PR makes it return to where it was.
Note macOS provides this "return to its original display on reconnect" behavior automatically for movable windows — but cmux main windows set
isMovable = falsefor custom titlebar dragging, which excludes them from AppKit's automatic display-reconfiguration handling (the same reason #6913 exists). So cmux has to reproduce that behavior itself.Design — per-window config-frame ring, folded into the #6913 reconcile
NSScreen.cmuxStableDisplayKey): prefersCGDisplayCreateUUIDFromDisplayID(stable across reboot, GPU-mux, and port/reconnect — unlike the rawCGDirectDisplayID, which macOS reassigns), with an EDID-triple fallback;nilwhen neither resolves, so such a display is excluded from any persisted key rather than keyed unstably.DisplayConfigurationSignature, pure, inCmuxWindowing): sorted / order-independent; excludesvisibleFrame(Dock/menu-bar/notch changes don't re-key the same setup); includes frame origin + size (resolution changes re-key; two identical-model monitors are disambiguated left/right by position); mirror sets get a distinct marker so they never collide with laptop-only.SessionConfigFrameEntry, cap 8): stored additively onSessionWindowSnapshot(optional field → older snapshots decode unchanged) and mirrored in memory.reconciledFrameAfterScreenChange != nil), and is keyed to the write-time signature so a slipped write can only land in the currently-connected slot.lastAppliedConfigurationSignature(seeded atdidFinishLaunching), so sleep/wake and Dock resize are no-ops (honoring the Stop main window drifting down on sleep/wake #6305/Window moves to wrong position after sleep/wake with multiple external monitors #2791/Rescue main windows stranded by display disconnect #7265 constraint). Remembered frames go throughresolvedWindowFrame(re-clamped if they no longer fit), fullscreen windows are skipped, and the reachability safety net still runs afterward.A corruption bug caught in dogfood (fixed here)
An earlier revision took a "leading-edge" capture inside the
didChangeScreenParametershandler, on the assumption that the notification fires beforeNSScreen.screensupdates. It fires after — so that capture stored the outgoing (laptop) frame under the incoming (multi-monitor) signature, corrupting the slot restore then read from, and the window landed on the wrong screen. A DELL monitor's ~2s EDID handshake made it worse (each transient screen-change fired another bad capture). Fix: removed the leading-edge capture entirely — there is no valid "pre-change" frame once the screens have changed; the stable-time captures are the source of truth. (See the commit for the exact log trace.)Why not lean on the platform / prior art
NSWindowframe autosave is single-frame, not per-configuration; cmux already tried and reverted AppKit autosave (#3882 → #3631). Prior artd3aec2f72(PR #2667) built ~this but stalled on merge conflicts + over-bundling — this is the sharply-scoped version, keying on stable UUID and excludingvisibleFrame(the fingerprint bug #2667 had), and reusing the merged #6913 machinery instead of adding a new observer.We also checked Chromium/Electron: their app code restores saved bounds only at window creation and never repositions a visible window on a live display change — but Chrome's windows are movable, so macOS moves them back to their original display on reconnect. The end-user behavior this PR produces (window returns to its monitor on reconnect) matches what Chrome exhibits; cmux just implements manually what the OS does for movable windows.
Tests
CmuxWindowingpackage): order-independence,visibleFrame-exclusion, resolution sensitivity, identical-panel position disambiguation, mirror distinctness, refuse-to-key, degenerate-frame exclusion, sub-pixel-jitter stability.project.pbxproj.Manual verification (real hardware, MacBook + 2 DELL monitors)
Dogfooded the full round-trip with DEBUG trace logging (
monitorMemory.*). Confirmed from the logs:restore.hitre-applies the exact remembered frame → window back on the external at the moved position.Out of scope
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Note
Medium Risk
Touches main-window frame logic on every display change and session save/restore; mistakes could misplace windows or corrupt remembered geometry, though guards and additive snapshot fields limit blast radius.
Overview
Adds per-monitor window geometry memory so main windows can return to their last position when you reconnect a display or switch between laptop-only and multi-monitor setups (issue #2135), on top of the existing #6913 reachability clamp.
Identity & keys:
NSScreen.cmuxStableDisplayKey(display UUID with EDID fallback) and optionalstableIDon display snapshots/geometry feed a puredisplayConfigurationSignatureinCmuxWindowing(order-independent, ignores Dock/visibleFramejitter, resolution- and mirror-aware).Persistence: Each window keeps an LRU ring (cap 8) of
SessionConfigFrameEntryvalues, stored additively onSessionWindowSnapshot.configFramesand in-memorywindowConfigFrames.Runtime: New
AppDelegate+MonitorMemorywires CoreGraphics reconfiguration + coalesced screen reconcile: on a genuine signature change, restore remembered frames viaresolvedWindowFrame(prefers config entry for current signature), then run the stranded-frame safety net. Captures only when stable (autosave, close, session snapshot), with a capture firewall during display flaps so transient frames cannot corrupt other configs. Display matching for restore now prefers stable ID over reassignedCGDirectDisplayID.Tests: Signature unit tests in
CmuxWindowingplus app-target round-trip, LRU, and suppression tests.Reviewed by Cursor Bugbot for commit ef97439. Bugbot is set up for automated code reviews on this repo. Configure here.