Skip to content

Clamp large window stranded off-screen after display disconnect (#2824) - #7084

Closed
ruan11223344 wants to merge 1 commit into
manaflow-ai:mainfrom
ruan11223344:issue-2824-clamp-stranded-window-after-display-disconnect
Closed

ruan11223344 wants to merge 1 commit into
manaflow-ai:mainfrom
ruan11223344:issue-2824-clamp-stranded-window-after-display-disconnect

Conversation

@ruan11223344

@ruan11223344 ruan11223344 commented Jun 30, 2026 •

Copy link
Copy Markdown

Fixes #2824.

Problem

When an external display is disconnected, the cmux window is left hanging off the right edge of the remaining screen — "effectively off screen" (per the issue), recoverable only by dragging it back or moving it to a new Space.

Reproduced on 0.64.17. Measuring the window via the Accessibility API right after a physical disconnect (built-in display, logical bounds 0,0 → 1512,982):

window: x=956  y=-324  w=1512  h=968

So a 1512pt-wide window with only ~556pt visible on the right edge, its titlebar above the menu bar.

Root cause

CmuxMainWindow.constrainFrameRect deliberately refuses AppKit's constrain pass for any frame judged "reachable", so an on-screen window is never repositioned and cannot creep on sleep/wake. shouldPreserveFrameDuringConstrain judged reachability as ≥60pt of overlap in each dimension.

That absolute floor is too lenient for large windows: a full-width window stranded off the right edge keeps hundreds of points visible — far above 60pt — so it was treated as reachable and left stranded. The existing tests only covered a ~20pt sliver, so this large-but-mostly-off case slipped through.

Fix

Require a proportional slice of the window to be visible: the larger of the 60pt floor and 60% of each dimension, capped at the window's own extent.

  • A window that is mostly or fully on-screen — including one whose titlebar merely pokes into the menu bar (the sleep/wake creep case) — is still preserved untouched.
  • A window that is mostly off-screen is handed back to AppKit's default constrain pass and pulled into view.

Tests

All existing CmuxMainWindowConstrainFrameTests cases keep their results. Two regression cases added:

  • testDoesNotPreserveLargeWindowStrandedOffRightEdge — an 800pt window 42% past the right edge.
  • testDoesNotPreserveFullWidthWindowStrandedAfterDisconnect — the measured 1512pt full-width window left ~556pt visible.

🤖 Generated with Claude Code


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


Summary by cubic

Pulls windows back into view after a display disconnect by requiring a proportional visible area before skipping AppKit’s constrain pass. Fixes #2824.

  • Bug Fixes
    • Require the larger of 60pt or 60% visible per dimension (capped by window size); AppKit re-constrains stranded windows while on-screen windows remain stable.
    • Add two regression tests for off-right-edge cases, including the measured full-width scenario.

Written for commit dffd54e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved how the app decides whether to keep a window’s position when screen layouts change.
    • Windows are now less likely to stay partially off-screen or become hard to reach after an external display is disconnected.
    • Added regression coverage for edge cases involving large windows on the edge of a missing display.

…flow-ai#2824)

CmuxMainWindow.constrainFrameRect refuses AppKit's constrain pass for any
frame deemed "reachable", so an already-on-screen window is never nudged and
cannot creep on sleep/wake. The reachability check required only 60pt of the
window to overlap a screen in each dimension.

That absolute floor is too lenient for large windows. When an external display
is unplugged, a full-width window can be left hanging off the right edge of the
remaining screen with several hundred points still visible — far above 60pt,
so it was treated as reachable and left stranded, effectively off-screen and
unusable.

Require a proportional slice of the window to be visible (the larger of a 60pt
floor and 60% of each dimension, capped at the window's extent). A window that
is mostly or fully on-screen — including one whose titlebar merely pokes into
the menu bar — is still preserved untouched, so the sleep/wake creep fix is
unaffected; a window that is mostly off-screen is handed back to AppKit and
pulled into view.

Adds two regression cases to CmuxMainWindowConstrainFrameTests covering an
800pt window 42% past the right edge and a measured 1512pt full-width window
left ~556pt visible after disconnect.

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

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

@ruan11223344 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 30, 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: 541abd26-7a5f-4747-bafe-69b8e1c6fc5e

📥 Commits

Reviewing files that changed from the base of the PR and between 1123a9c and dffd54e.

📒 Files selected for processing (2)
  • Sources/App/CmuxMainWindow.swift
  • cmuxTests/CmuxMainWindowConstrainFrameTests.swift

📝 Walkthrough

Walkthrough

shouldPreserveFrameDuringConstrain gains a minimumVisibleFraction parameter (default 0.6). The required visible width and height are now computed as max(minimumVisibleExtent, proposedDimension * minimumVisibleFraction), capped to the proposed frame size. Two regression tests cover windows stranded off-screen after monitor disconnect.

Frame Preservation Threshold

Layer / File(s) Summary
Combined threshold logic and docs
Sources/App/CmuxMainWindow.swift
Adds minimumVisibleFraction: CGFloat = 0.6 parameter and replaces the absolute-only threshold with max(minimumVisibleExtent, proposedDimension * minimumVisibleFraction) for both width and height, with updated doc comments.
Regression tests for stranded windows
cmuxTests/CmuxMainWindowConstrainFrameTests.swift
Two tests assert frames are not preserved when a large or full-width window is left off the right edge after an external display is unplugged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • manaflow-ai/cmux#6305: Directly modifies shouldPreserveFrameDuringConstrain and its tests to control frame reachability on screen configuration changes, the same method updated in this PR.

Poem

🐇 A window once lost to the right,
Now caught by a fraction of light.
Sixty percent must remain,
Or we'll move it again—
No more hiding just out of sight! ✨

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main fix for stranded windows after monitor disconnect.
Description check ✅ Passed Includes summary, problem, fix, and testing details tied to #2824; only non-critical template sections like demo video and checklist are omitted.
Linked Issues check ✅ Passed The code and tests directly address #2824 by re-clamping mostly off-screen windows after display disconnect.
Out of Scope Changes check ✅ Passed The changes stay focused on the window-constrain logic and matching regression tests, with no obvious unrelated additions.
Cmux Swift Actor Isolation ✅ Passed PASS: The diff only adds @MainActor UI/window code plus nonisolated pure helpers/tests; it doesn't introduce shared mutable Sendable state or background UI access.
Cmux Swift Blocking Runtime ✅ Passed The PR only changes frame-visibility math and deterministic tests; it adds no semaphores, waits, sleeps, asyncAfter, sync, or locks.
Cmux Browser Automation Off-Main ✅ Passed PR only changes CmuxMainWindow constrain logic and tests; no browser.* socket commands or worker/main-actor automation paths were touched.
Cmux Expensive Synchronous Load ✅ Passed Diff only changes window-frame geometry logic and tests; no synchronous agent-history/JSON loads were added or moved onto interactive/main-actor paths.
Cmux Cache Substitution Correctness ✅ Passed The PR only adjusts live window-constrain heuristics from NSScreen data; it doesn’t replace any authoritative read with a cached value in a persistence/history/undo/snapshot path.
Cmux No Hacky Sleeps ✅ Passed PR only changes Swift window-preservation logic and deterministic tests; no new non-Swift sleeps, timers, polling, or fixed waits appear in the diff.
Cmux Algorithmic Complexity ✅ Passed The change keeps a single linear pass over NSScreen.visibleFrames, a tiny fixed-size collection, with no nested rescans or hot-path sorting/filtering.
Cmux Swift Concurrency ✅ Passed Diff only changes a pure frame-visibility check and adds XCTest regressions; no DispatchQueue, Task, Combine, or completion-handler async patterns were introduced.
Cmux Swift @Concurrent ✅ Passed Changed code only adjusts a synchronous nonisolated helper and tests; no nonisolated async or @concurrent misuse appears.
Cmux Swift File And Package Boundaries ✅ Passed CmuxMainWindow.swift is only 251 lines, the change is a small focused AppKit bug fix, and no new mixed-responsibility or oversized-file/package-boundary issue appears.
Cmux Swiftpm Lockfiles ✅ Passed PR only changes two Swift source files; no .gitignore, Package.swift, xcodeproj, or Package.resolved files are touched, so the SwiftPM lockfile rule isn’t triggered.
Cmux Swift Logging ✅ Passed The diff only changes frame-preservation logic and tests; it adds no print/debugPrint/dump/NSLog, no ad hoc stdout/file logging, and no new Logger constants.
Cmux User-Facing Error Privacy ✅ Passed Diff only changes window-constrain logic and tests; no user-facing errors, alerts, command output, or recovery copy were added or altered.
Cmux Full Internationalization ✅ Passed Diff only changes window-constraint logic, comments, and tests; no user-facing localized text or catalog/Info.plist entries were added.
Cmux Swiftui State Layout ✅ Passed The diff only adjusts AppKit window geometry and tests; the SwiftUI bridge view is legacy/contained, with no new ObservableObject, GeometryReader, lazy-list store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed Small local correctness fix: existing MainWindow owns frame-constrain invariant; no new timers, observers, side channels, or split lifecycle ownership added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The diff only changes main CmuxMainWindow frame-constrain logic and tests; it adds no standalone auxiliary windows, identifiers, or close-shortcut ownership.
Cmux Source Artifacts ✅ Passed Only source/test paths changed: Sources/App/CmuxMainWindow.swift and cmuxTests/CmuxMainWindowConstrainFrameTests.swift; no artifact/temp/build paths from the rule appear.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seam was added in production source; the PR only changes preserve-frame logic and adds tests under cmuxTests via @testable import.
Cmux No Ambient Global State ✅ Passed PR only changes an existing static method on CmuxMainWindow and adds tests; it adds no new top-level funcs, mutable globals, or singletons.
✨ 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 Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a window-placement regression (#2824) where disconnecting an external display left the cmux window stranded off the right edge of the remaining screen. The existing absolute-floor visibility check (60 pt) treated a large window with hundreds of points still technically on-screen as "reachable", so it was never handed back to AppKit's constrain pass.

  • CmuxMainWindow.swift: shouldPreserveFrameDuringConstrain now computes a proportional required-overlap — the larger of the 60 pt absolute floor and 60 % of each window dimension — so large windows that are mostly off-screen are correctly clamped while small on-screen windows (including the sleep/wake titlebar-in-menu-bar case) are still preserved untouched.
  • CmuxMainWindowConstrainFrameTests.swift: Two new deterministic regression cases cover the exact 1512 pt full-width scenario from the bug report and a generic 800 pt window at ~42 % visibility; all existing test cases continue to pass.

Confidence Score: 5/5

Safe to merge; the change is a self-contained threshold adjustment in a static helper with no side-effects on other code paths.

The proportional threshold calculation is mathematically correct for all window sizes (verified by manually tracing each test case, including the tiny-window edge case where the outer min prevents requiring more than 100 % visibility). All five pre-existing test scenarios still produce the same result under the new formula, and the two new regression cases match the exact real-world measurements from the bug report. The nonisolated static method has no actor-isolation implications, introduces no new global state, and touches no hot path.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/CmuxMainWindow.swift Replaces the pure absolute-floor overlap threshold with a proportional threshold (max of 60 pt floor or 60 % of window extent); logic is mathematically sound and all existing + new test cases verify correctly.
cmuxTests/CmuxMainWindowConstrainFrameTests.swift Adds two focused regression tests for the display-disconnect stranding case; both are deterministic (no real screen required) and correctly assert the new proportional behaviour.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["constrainFrameRect called by AppKit"] --> B["shouldPreserveFrameDuringConstrain"]
    B --> C["Compute requiredWidth\nmin(w, max(60pt, w × 0.6))"]
    C --> D["Compute requiredHeight\nmin(h, max(60pt, h × 0.6))"]
    D --> E["For each screen visibleFrame"]
    E --> F{"intersection ≥ required\nin both dimensions?"}
    F -- "Yes (≥60% visible)" --> G["Return proposedFrame unchanged\n(sleep/wake creep prevented)"]
    F -- "No (mostly off-screen)" --> H["Check next screen"]
    H -- "No screens left" --> I["Return super.constrainFrameRect\n(AppKit pulls window back)"]
    I --> J["Window clamped back into view\n✅ Fixes #2824"]
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["constrainFrameRect called by AppKit"] --> B["shouldPreserveFrameDuringConstrain"]
    B --> C["Compute requiredWidth\nmin(w, max(60pt, w × 0.6))"]
    C --> D["Compute requiredHeight\nmin(h, max(60pt, h × 0.6))"]
    D --> E["For each screen visibleFrame"]
    E --> F{"intersection ≥ required\nin both dimensions?"}
    F -- "Yes (≥60% visible)" --> G["Return proposedFrame unchanged\n(sleep/wake creep prevented)"]
    F -- "No (mostly off-screen)" --> H["Check next screen"]
    H -- "No screens left" --> I["Return super.constrainFrameRect\n(AppKit pulls window back)"]
    I --> J["Window clamped back into view\n✅ Fixes #2824"]
Loading

Reviews (1): Last reviewed commit: "Clamp large window stranded off-screen a..." | Re-trigger Greptile

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thank you for this! The display-disconnect recovery shipped on main in #12053, including the regression coverage, so I’m closing this superseded PR :)

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 moves to the right of screen when disconnected from external monitor

2 participants