Skip to content

Remember and restore window geometry per monitor configuration (#2135) - #7477

Merged
austinywang merged 23 commits into
manaflow-ai:mainfrom
mxschmitt:feat-per-monitor-window-geometry
Jul 8, 2026
Merged

austinywang merged 23 commits into
manaflow-ai:mainfrom
mxschmitt:feat-per-monitor-window-geometry

Conversation

@mxschmitt

@mxschmitt mxschmitt commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

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 = false for 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

  • Stable display key (NSScreen.cmuxStableDisplayKey): prefers CGDisplayCreateUUIDFromDisplayID (stable across reboot, GPU-mux, and port/reconnect — unlike the raw CGDirectDisplayID, which macOS reassigns), with an EDID-triple fallback; nil when neither resolves, so such a display is excluded from any persisted key rather than keyed unstably.
  • Config signature (DisplayConfigurationSignature, pure, in CmuxWindowing): sorted / order-independent; excludes visibleFrame (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.
  • Per-window LRU ring (SessionConfigFrameEntry, cap 8): stored additively on SessionWindowSnapshot (optional field → older snapshots decode unchanged) and mirrored in memory.
  • Capture happens only while a configuration is stable — the session-autosave tick and window close, both correctly keyed to the current signature. Guarded so it never persists during restore/teardown/settling, never persists a stranded frame (reconciledFrameAfterScreenChange != nil), and is keyed to the write-time signature so a slipped write can only land in the currently-connected slot.
  • Restore folds into the existing Re-clamp main window when its titlebar is stranded off-screen after monitor disconnect #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 (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 through resolvedWindowFrame (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 didChangeScreenParameters handler, on the assumption that the notification fires before NSScreen.screens updates. 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

NSWindow frame autosave is single-frame, not per-configuration; cmux already tried and reverted AppKit autosave (#3882 → #3631). Prior art d3aec2f72 (PR #2667) built ~this but stalled on merge conflicts + over-bundling — this is the sharply-scoped version, keying on stable UUID and excluding visibleFrame (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

  • 9 pure signature tests (CmuxWindowing package): order-independence, visibleFrame-exclusion, resolution sensitivity, identical-panel position disambiguation, mirror distinctness, refuse-to-key, degenerate-frame exclusion, sub-pixel-jitter stability.
  • 4 app-target tests: the headline round-trip (place on external → disconnect must not mutate the external slot → reconnect restores the frame exactly), LRU upsert/eviction at cap, and clamp-when-remembered-frame-no-longer-fits. Wired into project.pbxproj.

Manual verification (real hardware, MacBook + 2 DELL monitors)

Dogfooded the full round-trip with DEBUG trace logging (monitorMemory.*). Confirmed from the logs:

  • Move window on 2-monitor config → capture under that config's signature.
  • Disconnect → window clamped onto the built-in; the external slot is not mutated.
  • Reconnect 2 monitors → restore.hit re-applies the exact remembered frame → window back on the external at the moved position.
  • The reverse (built-in-only config) remembers and restores its own frame independently.
  • Sleep/wake and same-signature changes are confirmed no-ops.

Out of scope

Summary by CodeRabbit

  • New Features

    • Session restoration now remembers and restores window frames per display configuration using a stable physical-display identity.
    • Added an order-independent “display arrangement” signature to improve matching across reconnects and mirrored setups, including per-window configuration-frame persistence with capped LRU history.
  • Bug Fixes

    • Restoration now more reliably selects the correct monitor using stable identity, with safe fallbacks when identity is missing/ambiguous.
    • Restored window positions are validated and clamped to the target display’s visible bounds to reduce off-screen placements from resolution/jitter changes.
  • Tests

    • Added coverage for signature behavior, mirroring, clamping, reconnect/restore scenarios, and LRU persistence semantics.

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 optional stableID on display snapshots/geometry feed a pure displayConfigurationSignature in CmuxWindowing (order-independent, ignores Dock/visibleFrame jitter, resolution- and mirror-aware).

Persistence: Each window keeps an LRU ring (cap 8) of SessionConfigFrameEntry values, stored additively on SessionWindowSnapshot.configFrames and in-memory windowConfigFrames.

Runtime: New AppDelegate+MonitorMemory wires CoreGraphics reconfiguration + coalesced screen reconcile: on a genuine signature change, restore remembered frames via resolvedWindowFrame (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 reassigned CGDirectDisplayID.

Tests: Signature unit tests in CmuxWindowing plus 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.

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

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

@mxschmitt is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

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

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

Changes

Display config signature and window frame memory

Layer / File(s) Summary
Stable identity and signature
Sources/App/ScreenIdentity.swift, Packages/macOS/CmuxWindowing/Sources/CmuxWindowing/Summaries/SessionDisplayGeometry.swift, .../DisplayConfigurationSignature.swift, .../CmuxWindowingTests/DisplayConfigurationSignatureTests.swift
Adds NSScreen.cmuxStableDisplayKey, a stableID field on SessionDisplayGeometry, and an order-independent displayConfigurationSignature(isMirrored:) API with tests covering ordering, mirroring, jitter, resolution changes, and missing or degenerate identity.
Frame persistence model
Sources/SessionPersistencePolicy+ConfigFrames.swift, Sources/SessionDisplaySnapshot.swift, Sources/SessionConfigFrameEntry.swift, Sources/SessionConfigFrameRing.swift, Sources/SessionPersistence.swift
Adds maxConfigFramesPerWindow, SessionDisplaySnapshot with stableID, SessionConfigFrameEntry, an LRU SessionConfigFrameRing, and optional configFrames persisted on SessionWindowSnapshot.
Display reconfiguration plumbing
Sources/AppDelegate+MonitorMemory.swift, Sources/AppDelegate.swift
Introduces configuration-signature tracking, display reconfiguration callbacks, suppression and reconcile scheduling, signature-aware restore logic, stable-ID display matching, and frame capture/persistence across session save, restore, and window close.
Xcode registration and integration tests
cmux.xcodeproj/project.pbxproj, cmuxTests/AppDelegateDisplayConfigRestoreTests.swift
Registers new source/test files in the Xcode project and adds tests covering reconnect round-trips, stable-ID reassignment/fallback/tie-break, config-frame preference, ring capping, suppression/reconciliation interaction, and clamping to a smaller display.

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
Loading
sequenceDiagram
  participant NSScreen
  participant SessionDisplayGeometry
  participant DisplayConfigurationSignature

  NSScreen->>SessionDisplayGeometry: stableID, frame, visibleFrame
  SessionDisplayGeometry->>DisplayConfigurationSignature: displayConfigurationSignature(isMirrored:)
  DisplayConfigurationSignature->>DisplayConfigurationSignature: sort and join components
Loading

Related PRs: None found.

Suggested labels: macOS, needs-testing

Suggested reviewers: None found.


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error AppDelegate+MonitorMemory adds NotificationQueue.default.enqueue(... .whenIdle) plus retry requeue for screen reconciliation, introducing runtime delayed dispatch in production code. Replace idle-based rescheduling with an explicit display-reconfiguration completion signal/state transition (or actor-owned async flow), not whenIdle requeueing.
Cmux No Ambient Global State ❌ Error The PR adds runtime monitor-restore state directly on AppDelegate (windowConfigFrames, lastAppliedConfigurationSignature, suppression flags), which the rule forbids as new app-delegate state. Move this state into an injectable display/session restore controller owned by AppDelegate; keep only the required platform callback and file-private helpers at the seam.
✅ Passed checks (23 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 New models are plain value types; AppDelegate stays @MainActor and pure helpers are isolated with nonisolated where needed. No new actor-isolation regression.
Cmux Browser Automation Off-Main ✅ Passed PR only changes display-reconfiguration/session-restore code; it doesn't touch TerminalController, ControlCommandExecutionPolicy, or browser socket-worker routing.
Cmux Expensive Synchronous Load ✅ Passed No new/moved agent-history sync load appears in the diff; changes are display/session-geometry persistence only.
Cmux Cache Substitution Correctness ✅ Passed Per-config frames are event-driven and seed from snapshots, while restore falls back to raw frame/display data; cold and stale cases are guarded.
Cmux No Hacky Sleeps ✅ Passed No new fixed sleeps, timers, or polling were added; the screen-change path is callback/notification-driven, and whenIdle scheduling was preexisting.
Cmux Algorithmic Complexity ✅ Passed New scans are over bounded display sets or an 8-entry ring; the only O(n²) window helper is pre-existing and unchanged.
Cmux Swift Concurrency ✅ Passed New async work is limited to AppKit/CoreGraphics callback boundaries and main-queue hops; no new background queues, Combine state, or unmanaged long-lived Tasks were added.
Cmux Swift @Concurrent ✅ Passed Diff only removes display-reconfiguration depth gating and routes a main-queue notification to existing MainActor sync handlers; no async/@Concurrent changes.
Cmux Swift File And Package Boundaries ✅ Passed New files are small/focused; the only giant file changed incidentally with +118 lines, and the 381-line monitor-memory helper stays under the 400-line new-file threshold.
Cmux Swiftpm Lockfiles ✅ Passed HEAD diff only touches Swift sources/tests; no Package.swift, Package.resolved, .gitignore, or Xcode package-reference files changed, so the lockfile rule isn't triggered.
Cmux Swift Logging ✅ Passed PASS: The new runtime traces are all cmuxDebugLog calls wrapped in #if DEBUG; the commit adds no print/debugPrint/dump/NSLog or new Logger constants.
Cmux User-Facing Error Privacy ✅ Passed Touched production code adds geometry/session logic and DEBUG logs only; no new user-facing alerts, error bodies, or recovery copy with sensitive details.
Cmux Full Internationalization ✅ Passed Touched code only adds internal geometry/config helpers and debug logs; no new user-facing Swift copy, xcstrings/plist, or web locale files were changed.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state/layout pattern was introduced: changed files are AppDelegate/AppKit helpers and tests, with no new ObservableObject/@StateObject/GeometryReader/List/body code.
Cmux Architecture Rethink ✅ Passed PASS: the new callback/observers are a documented CoreGraphics/AppKit bridge, and AppDelegate keeps the screen-change state and invariants in one owner.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The diff only adds display/session persistence helpers and test fixtures; no new user-visible auxiliary window, cmux.* identifier, or shortcut-routing change appears.
Cmux Source Artifacts ✅ Passed Only hand-written Swift source/test files changed; no logs, screenshots, build output, caches, temp dirs, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam was added in production Sources; the new AppDelegate method is production-used by the reconfiguration observer, not test-only.
Title check ✅ Passed The title succinctly matches the main change: remembering and restoring window geometry per monitor configuration.
Description check ✅ Passed The description covers the summary and testing thoroughly, but it omits the demo video, review trigger, and checklist sections from the template.
✨ 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.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR remembers window geometry per monitor setup and restores it when displays change.

  • Stable display identities and configuration signatures for connected monitors.
  • Per-window config-frame rings persisted in session snapshots.
  • Restore and reachability handling folded into the screen-change reconcile path.
  • Tests for signatures, restore behavior, ring eviction, and capture suppression.

Confidence Score: 4/5

This is close, but the display-change capture gate should be fixed before merging.

  • Snapshot-backed window creation now appears to seed saved config frames under the live window id.
  • The new monitor-memory state can still stop stable captures after some CoreGraphics callback orders.
  • That can leave remembered frame slots stale for later monitor switches.

Sources/AppDelegate+MonitorMemory.swift

Important Files Changed

Filename Overview
Sources/AppDelegate+MonitorMemory.swift Adds monitor-memory restore and capture suppression; the CoreGraphics callback path can keep captures suppressed without scheduling release.
Sources/AppDelegate.swift Wires monitor-memory state, observer setup, startup signature seeding, and snapshot-backed config-frame seeding.
Sources/SessionConfigFrameRing.swift Adds the capped per-configuration frame ring used by session snapshots.
Packages/macOS/CmuxWindowing/Sources/CmuxWindowing/Summaries/DisplayConfigurationSignature.swift Adds the pure display configuration signature used to key remembered window frames.

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

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate.swift Outdated
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.
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate.swift Outdated
@mxschmitt
mxschmitt marked this pull request as draft July 7, 2026 00:09

@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 (2)
Sources/AppDelegate.swift (2)

3918-3918: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Capture 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 captureWindowConfigFrame recomputes the live signature and reachability from currentDisplayGeometries(). During dock/reconnect this can overwrite the destination configuration’s remembered frame before restoreRememberedFrames(for:); during disconnect it can reject a valid old-monitor frame as strandedFrame. 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.md requires 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 lift

Replace the fixed settling grace window with an explicit transition boundary.

beginSettlingScreenChange() plus endSettlingScreenChangeAfterGrace() 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.md disallows 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0282671 and 2530b6d.

📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

mxschmitt added 2 commits July 6, 2026 17:17
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.
@mxschmitt
mxschmitt marked this pull request as ready for review July 7, 2026 01:00
Comment thread Sources/AppDelegate.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2530b6d and 5e13cad.

📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

Comment thread Sources/AppDelegate.swift Outdated
…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.
Comment thread Sources/AppDelegate.swift
Comment thread Sources/AppDelegate.swift Outdated
- 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>
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated

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

📥 Commits

Reviewing files that changed from the base of the PR and between 078156b and 78035ba.

📒 Files selected for processing (6)
  • Sources/AppDelegate+MonitorMemory.swift
  • Sources/AppDelegate.swift
  • Sources/SessionDisplayFrames.swift
  • Sources/SessionPersistence.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AppDelegateDisplayConfigRestoreTests.swift
💤 Files with no reviewable changes (1)
  • Sources/SessionPersistence.swift

Comment thread cmuxTests/AppDelegateDisplayConfigRestoreTests.swift Outdated
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc3e15 and 39c029c.

📒 Files selected for processing (3)
  • Sources/AppDelegate+MonitorMemory.swift
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateDisplayConfigRestoreTests.swift

Comment thread cmuxTests/AppDelegateDisplayConfigRestoreTests.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/AppDelegate+MonitorMemory.swift (1)

6-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared display-reconfiguration notification constant

Sources/AppDelegate+MonitorMemory.swift should post AppDelegate.displayReconfigurationNotification instead of repeating the raw Notification.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

📥 Commits

Reviewing files that changed from the base of the PR and between 39c029c and 1de4a76.

📒 Files selected for processing (3)
  • Sources/AppDelegate+MonitorMemory.swift
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateDisplayConfigRestoreTests.swift

@vercel

vercel Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 8, 2026 10:10am

Comment thread Sources/AppDelegate+MonitorMemory.swift
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated

@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 c0d80a7. Configure here.

Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift Outdated
Comment thread Sources/AppDelegate+MonitorMemory.swift

This branch was successfully deployed

1 active deployment
Preview – cmux — ef974391 Deployed Jul 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Window position not restored after external display reconnection

2 participants