Skip to content

Stop main window drifting down on sleep/wake - #6305

Merged
austinywang merged 2 commits into
manaflow-ai:mainfrom
sergej-koscejev:fix-window-shift-lock-constrain
Jun 19, 2026
Merged

austinywang merged 2 commits into
manaflow-ai:mainfrom
sergej-koscejev:fix-window-shift-lock-constrain

Conversation

@sergej-koscejev

@sergej-koscejev sergej-koscejev commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Note: this PR was prepared by an AI agent (Claude) on the author's behalf.

Problem

After the Mac sleeps and wakes (e.g. you lock with Ctrl+Cmd+Q and come back later, or the display sleeps), the cmux main window shifts downward, and it accumulates across sleep/wake cycles. The lock keystroke itself is not the trigger — the sleep→wake is.

Cause

This is AppKit re-constraining the window, not cmux repositioning it. None of cmux's own observers move the window here (sessionDidResignActive only saves a snapshot; didWake only restarts the socket listener; applicationDidBecomeActive only re-places windows when none is visible).

On a display/system sleep→wake, AppKit re-runs its constrain pass (constrainFrameRect(_:to:)) over every window. The default implementation does not only clamp off-screen windows back into view — it also repositions windows that are already fully on-screen, which is what shows up as the per-cycle creep. The exact reposition is AppKit-internal and depends on the display arrangement and each screen's menu-bar / safe-area insets, so:

  • it is not a fixed titlebar-height nudge (the move is larger), and
  • it is not limited to a window whose titlebar sits under the menu bar — it also hits e.g. a window occupying the bottom half of an external display, and likely other arrangements.

The main window is .fullSizeContentView, and cmux disables OS window restoration (isRestorable = false), re-applying its saved frame only at startup. Because nothing re-asserts the saved frame after wake, whatever AppKit's re-constrain produced sticks and accumulates.

Fix

Override CmuxMainWindow.constrainFrameRect(_:to:) to leave an already-reachable frame untouched, only falling back to AppKit's default constraining when the proposed frame would otherwise be stranded off-screen (e.g. a display was disconnected), so a lost window can still be pulled back into view. cmux already owns and clamps its own window placement, so deferring to AppKit's re-constrain here only produced the drift. The reachability decision is factored into a pure, testable helper.

Tests (two-commit red/green)

  1. Failing test — testConstrainPreservesOnScreenFrameOverlappingMenuBar asserts that constraining an on-screen frame leaves it untouched (a titlebar-under-menu-bar frame is used as one easy, deterministic on-screen case). Verified red-by-assertion without the fix (inherited NSWindow.constrainFrameRect moves it).
  2. Fix — adds the override plus deterministic, screen-agnostic coverage of the shouldPreserveFrameDuringConstrain helper (inside / menu-bar-overlap / stranded / barely-peeking / no-screens).

A third commit corrects code/test comments that had mis-stated the cause. All 6 tests pass green locally on macOS 26.5.1 (M2 Max). Test file is wired into project.pbxproj and passes lint-pbxproj-test-wiring.sh.

Verification

Reproduced and confirmed fixed on the reporter's macOS (26.5.1 Tahoe) via a tagged Debug build and a real sleep/wake cycle — the window now stays put with no cumulative creep.

See also the note below on overlap with #2667.

Summary by CodeRabbit

  • Bug Fixes

    • Improved window positioning during sleep/wake to prevent the app from subtly shifting the window when the proposed location is already reachable on-screen, especially when the title bar overlaps the system menu bar area.
  • Tests

    • Added debug-only regression and unit tests to verify frame-preservation behavior for on-screen, off-screen, edge-overlap, and empty visible-region scenarios.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@sergej-koscejev 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 Jun 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 263e02a4-c059-42d1-a0da-fb7f4daedc47

📥 Commits

Reviewing files that changed from the base of the PR and between db55f55 and f6cbe00.

📒 Files selected for processing (3)
  • Sources/App/CmuxMainWindow.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CmuxMainWindowConstrainFrameTests.swift

📝 Walkthrough

Walkthrough

CmuxMainWindow gains a constrainFrameRect(_:to:) override that returns the proposed frame unchanged when a new static helper shouldPreserveFrameDuringConstrain finds sufficient screen overlap (≥ 60 pt in each dimension). A new XCTest file with regression and unit tests is added and wired into the Xcode project.

Changes

Window Frame Constraining

Layer / File(s) Summary
constrainFrameRect override and visibility predicate
Sources/App/CmuxMainWindow.swift
Overrides constrainFrameRect(_:to:) to return the proposed frame unchanged when shouldPreserveFrameDuringConstrain finds any screen's visibleFrame intersects the proposed rect by at least 60 pt in both dimensions; otherwise falls through to super.
XCTest suite and Xcode project wiring
cmuxTests/CmuxMainWindowConstrainFrameTests.swift, cmux.xcodeproj/project.pbxproj
Adds CmuxMainWindowConstrainFrameTests (DEBUG-only, @MainActor) with a regression test for the menu-bar overlap case and deterministic unit tests for the static helper covering full-inside, menu-bar-overlap, fully-off-screen, minimal-corner, and no-screens cases; wires the file into the cmuxTests Xcode target via four pbxproj entries.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A window once wandered too far from the screen,
Now a bunny checks intersection — knows where frames have been.
Sixty points of overlap, a grabbable zone,
The titlebar peeks past the menu bar chrome.
Constrain? Not today! The frame stays right here. 🐇✨


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Source Artifacts ❌ Error Commit adds 31 artifact files from local AI development tools (.claude/, .cursor/, .greptile/, .agents/, .coderabbit.yaml) that violate source-control-artifacts.md as local tool output without deli... Remove all .claude/, .cursor/, .greptile/, .agents/ directories and .coderabbit.yaml from the commit, or add their patterns to .gitignore.
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: preventing the main window from drifting downward during sleep/wake cycles.
Description check ✅ Passed The PR description is comprehensive and addresses all template sections: Problem/Cause/Fix are well explained, Testing section documents the test-driven approach with two commits, Testing verification is detailed, but Demo Video URL and Review Trigger sections are missing, and checklist is not provided.
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 Production code properly observes Swift 6 actor isolation: CmuxMainWindow is explicitly @MainActor, overridden constrainFrameRect inherits MainActor, and pure helper shouldPreserveFrameDuringConstr...
Cmux Swift Blocking Runtime ✅ Passed Production code introduces no blocking or timing-based synchronization. The two new methods in CmuxMainWindow use only deterministic frame geometry calculations and loop-based intersection testing....
Cmux Expensive Synchronous Load ✅ Passed PR adds only lightweight geometry calculations (NSRect intersection, CGFloat arithmetic) to constrain-frame override—no RestorableAgentSessionIndex.load(), sysctl, FileManager I/O, or other expensi...
Cmux Cache Substitution Correctness ✅ Passed The PR adds a constrainFrameRect override that reads fresh parameters and live system state (NSScreen.screens) without substituting cached values in persistence/history/undo/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed All changes are Swift code or Xcode configuration; the rule explicitly excludes Swift from its scope (deferred to swift-blocking-runtime.md), so this check does not apply.
Cmux Algorithmic Complexity ✅ Passed The code iterates only over NSScreen.screens (a fixed, tiny collection of 1-4 physical monitors), performing O(1) operations per screen—not a scalable collection scan like the rule targets (workspa...
Cmux Swift Concurrency ✅ Passed No legacy async patterns found. The PR adds synchronous frame-constraining logic with proper @MainActor isolation and a nonisolated pure helper—no Dispatch queues, Combine, completion handlers, or...
Cmux Swift @Concurrent ✅ Passed Both new Swift methods comply with concurrency rules: constrainFrameRect is a synchronous @MainActor override (correctly unannotated), and shouldPreserveFrameDuringConstrain is a pure nonisolated s...
Cmux Swift File And Package Boundaries ✅ Passed New CmuxMainWindow.swift is 181 lines, well under the 400-line threshold for new files. It maintains coherent responsibility (main window UI/AppKit integration and frame management). The new constr...
Cmux Swift Logging ✅ Passed Production code (CmuxMainWindow.swift) contains no print/debugPrint/dump/NSLog/ad-hoc logging. Test code (CmuxMainWindowConstrainFrameTests.swift) is properly guarded with #if DEBUG.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error messages, alerts, or warnings are introduced. New production code contains only internal APIs with no user-visible text. Test file is developer-only, which is explicitly allowed.
Cmux Full Internationalization ✅ Passed PR adds only technical implementation (window-frame calculations), developer-only tests (guarded by #if DEBUG), and comments—no user-facing text requiring localization.
Cmux Swiftui State Layout ✅ Passed This PR contains AppKit window management code (NSWindow constrainFrameRect override and tests) with zero SwiftUI state changes. No ObservableObject, @Published, @StateObject, GeometryReader, lazy...
Cmux Architecture Rethink ✅ Passed PR implements a required platform bridge override (constrainFrameRect) with clear owner, documented invariant, and pure helper; contains no timing repairs, observers, side channels, or split lifecy...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies existing CmuxMainWindow frame-constraining behavior and adds test-only fixtures (wrapped in #if DEBUG), not new standalone windows requiring cmux.* identifiers.
✨ 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.

@sergej-koscejev

Copy link
Copy Markdown
Contributor Author

The PR is 100 % vibe-coded by Claude Code (Opus 4.8), let me know if that's a problem. I have no experience with Swift whatsoever, I just was annoyed by the bug. Thanks!

@greptile-apps

greptile-apps Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Overrides CmuxMainWindow.constrainFrameRect(_:to:) to suppress AppKit's sleep/wake constrain pass for windows that are already reachable on any screen, while preserving fallback to AppKit's default for genuinely stranded windows. The reachability decision is extracted into a nonisolated static pure helper (shouldPreserveFrameDuringConstrain) that is fully unit-tested independently of display hardware.

  • Production fix (CmuxMainWindow.swift): The override guards against cumulative per-cycle window drift by returning the proposed frame unchanged when it overlaps at least 60 pts in both dimensions on some connected screen; it delegates to super otherwise, so windows displaced by a display disconnection still get rescued by AppKit.
  • Tests (CmuxMainWindowConstrainFrameTests.swift): Six tests cover the decision helper exhaustively (fully inside, menu-bar-overlap, stranded, barely-peeking, no-screens) and include one integration regression that instantiates a real CmuxMainWindow and calls constrainFrameRect against the live screen geometry.
  • Project wiring (project.pbxproj): The test file is correctly registered across all required PBXBuildFile, PBXFileReference, group, and Sources phases.

Confidence Score: 5/5

Safe to merge — the change is a narrow, well-scoped override of a single AppKit callback with a clear fallback path and full test coverage.

The override correctly names and enforces cmux's existing invariant (cmux owns window placement). The pure helper handles all boundary conditions: CGRect.intersection of non-overlapping rects returns a null rect whose width is 0, so the 60 pt threshold correctly rejects it. The nonisolated annotation on the helper is appropriate, actor isolation is consistent with the @MainActor class, and no production logging, i18n, or blocking primitives are introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/CmuxMainWindow.swift Adds constrainFrameRect override to prevent AppKit drift on sleep/wake, with a nonisolated pure helper that correctly gates the bypass on per-screen reachability; logic and actor isolation are sound.
cmuxTests/CmuxMainWindowConstrainFrameTests.swift Six deterministic tests cover the full reachability decision matrix (fully inside, menu-bar overlap, stranded, barely-peeking, no-screens) plus an integration regression against a real window; coverage is thorough.
cmux.xcodeproj/project.pbxproj Adds the new test file to PBXBuildFile, PBXFileReference, test group, and test target Sources — standard and complete wiring.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[AppKit calls constrainFrameRect] --> B{shouldPreserveFrameDuringConstrain
proposedFrame, visibleFrames}
    B -->|intersection >= 60pt both dims| C{Any screen qualifies?}
    C -->|Yes| D[Return proposedFrame unchanged]
    C -->|No| E[super.constrainFrameRect]
    E --> F[AppKit rescues stranded window]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[AppKit calls constrainFrameRect] --> B{shouldPreserveFrameDuringConstrain
proposedFrame, visibleFrames}
    B -->|intersection >= 60pt both dims| C{Any screen qualifies?}
    C -->|Yes| D[Return proposedFrame unchanged]
    C -->|No| E[super.constrainFrameRect]
    E --> F[AppKit rescues stranded window]
Loading

Reviews (3): Last reviewed commit: "Stop AppKit re-constraining the main win..." | Re-trigger Greptile

@sergej-koscejev

Copy link
Copy Markdown
Contributor Author

Commit adds multiple artifact files violating source-control-artifacts.md: .vercel/ (explicitly prohibited), .cursor/, .greptile/, .coderabbit.yaml (tool configs), .agents/skills, and .claude/

Huh? It does no such thing?

@sergej-koscejev

sergej-koscejev commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Note: this comment was written by an AI agent (Claude) on the author's behalf.

Edit: corrected the cause framing — the trigger is sleep→wake, not the lock keystroke; see the updated PR description.

Overlap with #2667

Heads-up for reviewers: #2667 ("Fix window collapsing to sliver after disconnecting external displays") works in the same window-frame-on-display-reconfiguration machinery, so the two should be reconciled even though they don't conflict textually.

Flagging so whoever lands either PR knows CmuxMainWindow.constrainFrameRect no longer always clamps.

@sergej-koscejev sergej-koscejev changed the title Stop main window drifting down on lock/unlock (Ctrl+Cmd+Q) Stop main window drifting down on sleep/wake Jun 17, 2026
After the Mac sleeps and wakes, AppKit re-runs its constrain pass over
every window. Its default constrainFrameRect does not only clamp
off-screen windows back into view — it also repositions windows that are
already fully on-screen. CmuxMainWindow is .fullSizeContentView and
disables AppKit window restoration (isRestorable = false), re-applying
its saved frame only at startup, so nothing re-asserts the frame after
wake and the reposition sticks and accumulates each cycle.

This test pins the desired behavior: constraining an on-screen frame
must leave it untouched (a titlebar-under-menu-bar frame is used as one
easy, deterministic on-screen case). It fails today because the
inherited NSWindow.constrainFrameRect moves the frame.
After a display/system sleep→wake, AppKit re-runs its constrain pass over
every window, and its default constrainFrameRect repositions windows that
are already fully on-screen — not just off-screen ones. The move is
AppKit-internal: it is not a fixed titlebar-height nudge and is not
limited to a titlebar sitting under the menu bar (it also hits e.g. a
window in the bottom half of an external display), and it depends on the
display arrangement and per-screen menu-bar/safe-area insets. Because
cmux owns its own frames, disables AppKit window restoration, and
re-applies the saved frame only at startup, nothing re-asserts it after
wake, so the reposition sticks and accumulates each cycle.

Override CmuxMainWindow.constrainFrameRect to leave an already-reachable
frame untouched, deferring to AppKit's default only when the frame would
otherwise be stranded off-screen (e.g. a display was disconnected) so a
genuinely lost window can still be pulled back into view. cmux already
owns and clamps its own placement, so deferring to AppKit's re-constrain
here only caused the drift.

Adds deterministic, screen-agnostic coverage of the reachability helper.
@sergej-koscejev
sergej-koscejev force-pushed the fix-window-shift-lock-constrain branch from db55f55 to f6cbe00 Compare June 17, 2026 11:13
@vercel

vercel Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 19, 2026 10:34pm

@austinywang
austinywang merged commit 67cb000 into manaflow-ai:main Jun 19, 2026
19 checks passed
austinywang pushed a commit that referenced this pull request Jun 26, 2026
When the app is in another app and the display sleeps/wakes (or the
screen is reconfigured), a maximized or native-fullscreen main window can
come back genuinely resized to the ~2/3 default (1000x700) frame. The
existing constrainFrameRect override (#6305) only vetoes AppKit's
re-constrain of an already-good frame; it cannot undo a real resize
applied through another path (native-fullscreen exit, un-zoom revert, or
a display-mode resize), so the shrunken frame sticks.

A frame change while the app is inactive is never user-driven, so we
snapshot each main window's frame on resignActive, arm a flag when a
display sleep/wake or screen-parameter change is seen while inactive, and
on the next activation restore any window that shrank to a still-reachable
earlier frame. Native-fullscreen windows and frames whose display was
unplugged are left untouched.

Implements CmuxMainWindow.restoredFrameAfterInactiveDisplayTransition,
turning the previous commit's regression test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
austinywang pushed a commit that referenced this pull request Jun 26, 2026
When a maximized or native-fullscreen cmux window loses focus and the
display sleeps/wakes (or is reconfigured) while the user is in another
app, the window can come back genuinely resized to the ~2/3 default
(1000x700) frame. The existing constrainFrameRect override (#6305) only
vetoes AppKit's re-constrain of an already-good frame; it cannot undo a
real resize applied through another path (native-fullscreen exit, an
un-zoom revert, or a display-mode resize), so the shrunken frame sticks.

A frame change while the app is inactive is never user-driven (the user
is in another app), so we snapshot each CmuxMainWindow frame on
resignActive and, on the next activation, restore any window that shrank
to a still-reachable earlier frame. Windows still in native fullscreen
and frames whose display was unplugged are left untouched. No flag is
needed because an inactive resize is by definition not user-initiated —
this also avoids the laptop race where the display wakes exactly as the
user returns.

Implements CmuxMainWindow.restoredFrameAfterInactiveDisplayTransition,
turning the previous commit's regression test green. Refreshes the Swift
file-length budget for AppDelegate.swift via scripts/swift_file_length_budget.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
austinywang pushed a commit that referenced this pull request Jun 26, 2026
When a maximized or native-fullscreen cmux window loses focus and the
display sleeps/wakes (or is reconfigured) while the user is in another
app, the window can come back genuinely resized to the ~2/3 default
(1000x700) frame. The existing constrainFrameRect override (#6305) only
vetoes AppKit's re-constrain of an already-good frame; it cannot undo a
real resize applied through another path (native-fullscreen exit, an
un-zoom revert, or a display-mode resize), so the shrunken frame sticks.

New MainWindowFrameRestorer (in CmuxMainWindow.swift) owns the state and
decisions: AppDelegate snapshots each main window frame on resignActive
and, on the next activation, restores any window that shrank to a
still-reachable earlier frame. The restore is gated on a real display
transition (display/system sleep or a screen-parameter change) observed
while the app was inactive, so a deliberate background resize by a window
manager, AppleScript, or macOS window management is left untouched.
Arming on the sleep side sets the flag while the user is still away,
avoiding the laptop race where the display wakes as the user returns.
Native-fullscreen and unplugged-display frames are left alone.

Implements CmuxMainWindow.restoredFrameAfterInactiveDisplayTransition,
turning the previous commit's regression test green, and adds
MainWindowFrameRestorer gating tests. Refreshes the Swift file-length
budget for AppDelegate.swift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mxschmitt added a commit to mxschmitt/cmux that referenced this pull request Jul 6, 2026
When an external monitor positioned above the built-in display is
disconnected, the cmux main window can be left with its body's bottom
edge dipping into the built-in display while its titlebar sits far above
the only remaining screen — off the top, unreachable. Because the window
is non-movable (isMovable=false) and can only be dragged by its titlebar
handle (WindowDragHandleView), the user cannot pull it back down. This is
the same family as manaflow-ai#2824 / manaflow-ai#2135 / manaflow-ai#1620.

The runtime defense against this is CmuxMainWindow.constrainFrameRect
(added in manaflow-ai#6305 to stop sleep/wake drift), which vetoes AppKit's
corrective re-constrain whenever shouldPreserveFrameDuringConstrain
returns true. That predicate only checks for a 60x60pt overlap with ANY
screen in both dimensions — it has no requirement that the titlebar / top
of the window be reachable. So a window whose bottom 60pt overlaps the
built-in display but whose titlebar is hundreds of points above it is
preserved unchanged, and AppKit's clamp (which would rescue it) is
refused.

This test pins the desired behavior: a frame whose titlebar is stranded
above the only screen must NOT be preserved. It fails today, reproducing
the bug deterministically with synthetic visibleFrames (no display
hardware needed).
mxschmitt added a commit to mxschmitt/cmux that referenced this pull request Jul 6, 2026
Strengthen the runtime constrain veto so it only preserves a frame whose
titlebar (top strip) remains reachable, rather than any frame with a
60x60pt overlap somewhere on screen.

The manaflow-ai#6305 override (constrainFrameRect) was added to stop the main window
drifting on sleep/wake by refusing AppKit's re-constrain of an
already-reachable frame. But its reachability test only required a 60x60
overlap with any screen in both dimensions, with no titlebar requirement.
When an external monitor positioned above the built-in display is
disconnected, the window can be left with only its lower body overlapping
the built-in display while its titlebar sits far above the only remaining
screen. The lax predicate counted that as "reachable" and vetoed AppKit's
corrective clamp, so the window stayed stranded above the screen — and
because the window is non-movable and only draggable by its (now
off-screen) titlebar handle, the user could not pull it back down.

Require a grabbable slice of the top strip (120x24pt of a 64pt-tall
titlebar band) to be on a visible frame, mirroring the restore-path test
AppDelegate.shouldPreserveAccessibleFrame. A titlebar-under-menu-bar
frame and a fully on-screen frame still qualify (so the sleep/wake drift
fix is preserved), but a frame whose titlebar is above every screen now
defers to AppKit's clamp, which pulls it back into view.
mxschmitt added a commit to mxschmitt/cmux that referenced this pull request Jul 6, 2026
The manaflow-ai#6305 constrainFrameRect override cannot fix a window stranded by a
monitor disconnect: cmux main windows set isMovable = false for their
custom titlebar drag handling, and AppKit excludes a non-movable NSWindow
from its automatic on-screen constraining when displays change. So
constrainFrameRect is never invoked on that path, and nothing pulls the
window back — when an external monitor positioned above the built-in
display is disconnected (or the lid is reopened), the window keeps its
titlebar in the now-gone monitor's coordinate space, above every
remaining screen and unreachable. Because the only drag affordance is
that off-screen titlebar, the user cannot recover it.

Add a reactive reconcile: observe didChangeScreenParametersNotification
(coalesced with a short settle delay) and, for each main window whose
titlebar is no longer reachable, clamp it back onto the display it most
overlaps so a grabbable slice of the titlebar returns on-screen. Windows
that already fit are left untouched, so displays the reconfiguration did
not affect are undisturbed.

The decision is a pure, nonisolated reconciledFrameAfterScreenChange that
reuses the existing shouldPreserveAccessibleFrame / clampFrame helpers and
is unit-tested without live NSScreens (stranded → pulled back, reachable
→ nil, no displays → nil). A DEBUG cmuxDebugLog records each correction.
mxschmitt added a commit to mxschmitt/cmux that referenced this pull request Jul 6, 2026
…red reachability

Review follow-ups on the display-reconfiguration reconcile:

- Replace DispatchQueue.main.asyncAfter with a cancellable Task + Task.sleep
  (cmux-architecture bans asyncAfter in new code, and using a delay to let
  state "settle" is called out specifically). The task is cancelled on each
  new screen-change event and on teardown, so it can never fire against a
  half-torn-down app; collapses the notification burst into one pass.

- Skip native-fullscreen windows in the reconcile: an NSWindow in a
  fullscreen Space is owned by AppKit's Space machinery, and calling setFrame
  on it mid-transition (e.g. its display was just disconnected) fights the
  fullscreen teardown.

- Gate the reconcile on !isApplyingSessionRestore and !isTerminatingApp, like
  the sibling lifecycle handlers, so it never races the restore path's
  deliberate setFrame nor persists a frame clamped against transient
  mid-teardown display geometry.

- Extract one shared CmuxMainWindow.isTitlebarReachable predicate used by both
  the runtime constrain veto and the restore-time clamp
  (shouldPreserveAccessibleFrame), removing the duplicated 120/64/24 top-strip
  math that was "kept in sync" by a comment. Retune the thresholds to 60pt
  width / 16pt height so a window parked at a side edge (60-119pt of titlebar
  visible) and a window flush to the top of a large-menu-bar / notch display
  are still preserved — the old 120/24 values would have re-introduced the
  manaflow-ai#6305 sleep/wake drift for those. Adds regression tests for both.

- Drop the debug-only reconcile "source" plumbing (a stored property + method
  param used only in a #if DEBUG log of a constant string).
@Caligone Caligone mentioned this pull request Jul 8, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — f6cbe007 Deployed Jun 19, 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.

2 participants