Skip to content

Fix main window refit after display disconnect - #9698

Closed
austinywang wants to merge 5 commits into
mainfrom
issue-9696-display-disconnect-refit
Closed

austinywang wants to merge 5 commits into
mainfrom
issue-9696-display-disconnect-refit

Conversation

@austinywang

@austinywang austinywang commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Validation

Issues

aashishtamsya and others added 4 commits August 5, 2026 23:07
When a monitor is unplugged, main windows could remain stranded off-screen
because constrainFrameRect only runs on AppKit's own schedule. Settings
already recovers off-screen frames on open (#5770); mirror that for main
windows by listening for didChangeScreenParameters and clamping any window
whose frame is no longer grabbable onto a connected display.
Move target-screen selection and frame clamping into
MultiMonitorWindowGeometry so main-window recovery no longer depends on
SettingsWindowPresenter. Unify off-screen recovery through recoveredFrame for
both constrainFrameRect and display-disconnect paths, narrow
applyOffscreenRecoveryIfNeeded to CmuxMainWindow, and derive a guaranteed
off-screen test frame from connected display bounds.
Use frameRect(forContentRect:styleMask:) so recoveredOffscreenFrame
clamps against the true frame minimum (including title bar chrome) instead
of treating content-size mins as frame dimensions.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds shared multi-monitor frame selection and clamping. Main windows recover after display changes, while settings windows reuse the shared geometry. Regression tests and a DEBUG-only socket method cover recovery behavior.

Changes

Window geometry recovery

Layer / File(s) Summary
Shared geometry and settings integration
Sources/App/MultiMonitorWindowGeometry.swift, Sources/App/SettingsWindowGeometry.swift, cmux.xcodeproj/project.pbxproj
Adds target-screen selection, frame clamping, and shared recovery. Settings-window recovery uses the shared utility.
Main-window display-change recovery
Sources/App/CmuxMainWindow.swift, Sources/AppDelegate.swift
Preserves reachable frames and recovers unreachable or oversized frames. Display-parameter changes trigger recovery for visible main windows.
Window recovery regression coverage
cmuxTests/CmuxMainWindowConstrainFrameTests.swift, cmuxTests/SettingsWindowPresenterTests.swift
Adds main-window recovery tests and updates geometry tests to use the shared utility.
Display-change recovery diagnostics
Sources/TerminalController.swift, Sources/TerminalController+DebugMethodNames.swift
Adds a DEBUG-only socket method that recovers a selected window and returns before-and-after frame data.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#9222: Concerns the same display-disconnect recovery tests and behavior.
  • manaflow-ai/cmux-dev-artifacts#9224: Concerns the same main-window off-screen recovery behavior.

Possibly related PRs

Suggested reviewers: lawrencecchen

Sequence Diagram(s)

sequenceDiagram
  participant NSApplication
  participant AppDelegate
  participant CmuxMainWindow
  participant MultiMonitorWindowGeometry
  NSApplication->>AppDelegate: didChangeScreenParametersNotification
  AppDelegate->>CmuxMainWindow: applyOffscreenRecoveryIfNeeded
  CmuxMainWindow->>MultiMonitorWindowGeometry: request recoveredFrame
  MultiMonitorWindowGeometry-->>CmuxMainWindow: return clamped frame
  CmuxMainWindow-->>AppDelegate: apply frame
Loading

Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error MultiMonitorWindowGeometry.swift keeps 96 lines of pure AppKit-independent geometry in Sources/App, shared by main and Settings callers; existing CmuxWindowing already has a CoreGraphics geometry/t... Move the pure math to Packages/macOS/CmuxWindowing, exposing MultiMonitorWindowGeometry or a display-geometry value type; keep NSScreen/NSWindow and lifecycle wiring in the app target.
Cmux User-Facing Error Privacy ❌ Error The DEBUG socket API returns No CmuxMainWindow for window_id, exposing an internal implementation class name in an API error body. Replace the message with product-level text such as Main window not found; keep class names and other implementation details in debug logs.
Cmux Architecture Rethink ❌ Error AppDelegate adds a second didChangeScreenParametersNotification observer while the existing observer already reconciles frames, creating duplicate recovery owners and frame mutations. Remove the new observer and route shared geometry through reconcileMainWindowFramesAfterScreenChange, leaving one display-change owner and one recovery invariant.
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/TerminalController.swift adds DEBUG-only v2DebugWindowRecoverAfterDisplayChange; it has no test or production caller and is not isolated in a dedicated debug source file. Move the debug endpoint and related dispatch into a dedicated debug file/folder, or remove the seam; use @testable import with private-to-internal widening for test observation.
Cmux No Ambient Global State ❌ Error Sources/App/MultiMonitorWindowGeometry.swift:6 adds a caseless enum used only as a namespace for three static functions. Make MultiMonitorWindowGeometry constructable and injectable, with instance methods owned by the window-geometry seam; inject it into CmuxMainWindow and SettingsWindowPresenter instead of using static namespace calls.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% 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
Linked Issues check ✅ Passed The changes address display-change recovery, titlebar visibility, and oversized-frame clamping for [#9696], [#7600], [#7852], and [#2824].
Out of Scope Changes check ✅ Passed All code changes support display-disconnect recovery, shared geometry handling, regression coverage, or its debug hook; no unrelated changes are evident.
Cmux Swift Actor Isolation ✅ Passed The new geometry enum is stateless, UI recovery stays in @MainActor types, and display callbacks use .main plus MainActor.assumeIsolated; no new Sendable or background UI isolation issue appears.
Cmux Swift Blocking Runtime ✅ Passed The PR adds no semaphores, waits, sleeps, timers, asyncAfter, main.sync, or locks; recovery is event-driven by didChangeScreenParametersNotification.
Cmux Browser Automation Off-Main ✅ Passed The PR adds only debug.window.recover_after_display_change and display-geometry code; no browser.* or WebKit automation lines changed, and worker policy/tests are unchanged.
Cmux Expensive Synchronous Load ✅ Passed The PR adds only NSScreen/window-frame geometry and a DEBUG frame-recovery socket hook; no agent-history stores, transcript scans, JSONL parsing, or synchronous load APIs are added or moved onto Ma...
Cmux Cache Substitution Correctness ✅ Passed The aggregate diff adds screen/frame recovery and a DEBUG hook only; targeted searches found no cache substitution in persistence, history, undo, or snapshot paths.
Cmux No Hacky Sleeps ✅ Passed The PR changes only Swift source/tests plus Xcode project wiring; no covered TypeScript, JavaScript, shell, or runtime-script timing additions were found.
Cmux Algorithmic Complexity ✅ Passed Added scans target NSScreen.screens, a small OS-bounded collection; recovery visits each main window once and does not rescan the window collection per target.
Cmux Swift Concurrency ✅ Passed The diff adds no forbidden Task, background Dispatch, Combine, or completion-handler flow; its only async-related addition is an AppKit screen-change observer on .main with MainActor.assumeIsolated...
Cmux Swift @Concurrent ✅ Passed The PR adds no async, nonisolated, or @concurrent Swift declarations. New recovery code is synchronous UI work, and the notification callback explicitly uses MainActor.assumeIsolated.
Cmux Swiftpm Lockfiles ✅ Passed The PR adds only source-file entries to cmux.xcodeproj; it changes no SwiftPM package references, Package.swift, Package.resolved, .gitignore, or workflows.
Cmux Swift Logging ✅ Passed The PR diff adds no print, debugPrint, dump, NSLog, Logger, file, stdout, or stderr logging; existing SettingsWindowGeometry logging remains unchanged and sanitized.
Cmux Full Internationalization ✅ Passed The PR adds no production user-facing text or catalog changes; the only prose literals are confined to the #if DEBUG recovery API, while tests and protocol tokens are allowed.
Cmux Swiftui State Layout ✅ Passed PASS: The diff adds AppKit window geometry, screen recovery, debug handling, and tests; it introduces no SwiftUI state, GeometryReader, lazy-row store, or render-time state mutation.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only recovers existing CmuxMainWindow instances and adds geometry; it introduces no standalone auxiliary window. scripts/lint_auxiliary_window_close_shortcuts.py passed.
Cmux Source Artifacts ✅ Passed The cumulative diff contains only nine Swift source/test files and one Xcode project file; the artifact-name/content scans found no logs, media, caches, build output, temp, or scratch paths.
Title check ✅ Passed The title clearly and concisely describes the main-window recovery fix after a display disconnect.
Description check ✅ Passed The description clearly covers the changes, motivation, validation, manual proof, and linked issues; omitted template checklist and demo-video fields are non-critical.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-9696-display-disconnect-refit

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.

@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: 2

🤖 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/App/MultiMonitorWindowGeometry.swift`:
- Around line 6-96: Convert MultiMonitorWindowGeometry from an enum with static
APIs into a constructable type that stores screens, fallbackVisibleFrame,
mouseLocation, and inset through its initializer. Make recoveredFrame an
instance method using that stored state, and convert targetVisibleFrame and
clampedFrame to instance or private helper methods as appropriate; retain static
only for private helpers if needed.

In `@Sources/App/SettingsWindowGeometry.swift`:
- Around line 14-17: Update the minimumFrameSize calculation in the Settings
window geometry recovery flow to convert window.contentMinSize into frame
coordinates using NSWindow.frameRect(forContentRect:size:styleMask:) with the
window’s current style mask before comparing it with window.minSize. Use the
converted frame dimensions when enforcing the recovered window’s minimum size.
🪄 Autofix

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 Plus

Run ID: 5feafd37-08af-425d-8a3b-fdbb997652d2

📥 Commits

Reviewing files that changed from the base of the PR and between 79a3f64 and 3837963.

📒 Files selected for processing (7)
  • Sources/App/CmuxMainWindow.swift
  • Sources/App/MultiMonitorWindowGeometry.swift
  • Sources/App/SettingsWindowGeometry.swift
  • Sources/AppDelegate.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CmuxMainWindowConstrainFrameTests.swift
  • cmuxTests/SettingsWindowPresenterTests.swift

Comment on lines +6 to +96
enum MultiMonitorWindowGeometry {
/// Pure selection of the visible-screen frame a window should be clamped
/// into. When the window's saved frame is off every active screen (e.g.
/// restored onto a now-disconnected display) it recovers onto the screen
/// under the cursor, then the main/first screen. Cursor hit-testing uses
/// each screen's *full* frame: `visibleFrame` excludes the menu bar and
/// Dock strips, and the cursor sits exactly there when a window is opened
/// from the menu bar, which would misroute recovery to the main screen.
static func targetVisibleFrame(
windowFrame: NSRect,
screens: [(frame: NSRect, visibleFrame: NSRect)],
mouseLocation: NSPoint?,
fallbackVisibleFrame: NSRect?
) -> NSRect? {
guard !screens.isEmpty else { return fallbackVisibleFrame }

// Prefer the screen the window already overlaps the most so a window
// that is mostly visible stays where the user put it.
var bestFrame: NSRect?
var bestArea: CGFloat = 0
for screen in screens {
let intersection = screen.visibleFrame.intersection(windowFrame)
let area = intersection.isNull ? 0 : intersection.width * intersection.height
if area > bestArea {
bestArea = area
bestFrame = screen.visibleFrame
}
}
if let bestFrame, bestArea > 0 {
return bestFrame
}

// The window is off every active screen. Recover onto the screen under
// the cursor when possible so the window appears where the user is looking.
if let mouseLocation,
let mouseScreen = screens.first(where: { $0.frame.contains(mouseLocation) }) {
return mouseScreen.visibleFrame
}
return fallbackVisibleFrame ?? screens.first?.visibleFrame
}

/// Pure clamp geometry: fit `frame` within `visibleFrame` (honoring `inset`
/// and a minimum size).
static func clampedFrame(
_ frame: NSRect,
minimumSize: NSSize,
into visibleFrame: NSRect,
inset: CGFloat
) -> NSRect {
var result = frame
let maxVisibleSize = NSSize(
width: max(minimumSize.width, visibleFrame.width - 2 * inset),
height: max(minimumSize.height, visibleFrame.height - 2 * inset)
)
result.size.width = min(result.size.width, maxVisibleSize.width)
result.size.height = min(result.size.height, maxVisibleSize.height)
let minX = visibleFrame.minX + inset
let minY = visibleFrame.minY + inset
let maxX = max(minX, visibleFrame.maxX - inset - result.width)
let maxY = max(minY, visibleFrame.maxY - inset - result.height)
result.origin = NSPoint(
x: min(max(result.origin.x, minX), maxX),
y: min(max(result.origin.y, minY), maxY)
)
return result
}

/// Clamp `frame` onto a connected display using the shared target-screen
/// selection and clamp geometry. Returns `nil` when no screen is available.
static func recoveredFrame(
_ frame: NSRect,
minimumSize: NSSize,
screens: [(frame: NSRect, visibleFrame: NSRect)],
mouseLocation: NSPoint?,
fallbackVisibleFrame: NSRect?,
inset: CGFloat
) -> NSRect? {
guard let targetVisibleFrame = targetVisibleFrame(
windowFrame: frame,
screens: screens,
mouseLocation: mouseLocation,
fallbackVisibleFrame: fallbackVisibleFrame
) else { return nil }
return clampedFrame(
frame,
minimumSize: minimumSize,
into: targetVisibleFrame,
inset: inset
)
}
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the static-only namespace with a constructable geometry owner.

MultiMonitorWindowGeometry contains only static behavior. The Sources/**/*.swift rule prohibits new static-only namespaces. Store the screen snapshot, fallback frame, cursor location, and inset in an initializer. Expose recovery as instance behavior. Keep only private implementation helpers static if needed.

As per coding guidelines, “do not add ... static-only namespaces ... Put state and behavior on a constructable, injectable owning type.”

🤖 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/App/MultiMonitorWindowGeometry.swift` around lines 6 - 96, Convert
MultiMonitorWindowGeometry from an enum with static APIs into a constructable
type that stores screens, fallbackVisibleFrame, mouseLocation, and inset through
its initializer. Make recoveredFrame an instance method using that stored state,
and convert targetVisibleFrame and clampedFrame to instance or private helper
methods as appropriate; retain static only for private helpers if needed.

Source: Coding guidelines

Comment on lines 14 to 17
let minimumFrameSize = NSSize(
width: max(window.minSize.width, window.contentMinSize.width),
height: max(window.minSize.height, window.contentMinSize.height)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'contentMinSize|frameRect\(forContentRect:|minimumFrameSize|clampToVisibleAreaIfNeeded|recoveredFrame' \
  Sources cmuxTests

Repository: manaflow-ai/cmux

Length of output: 15408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SettingsWindowGeometry.swift =="
cat -n Sources/App/SettingsWindowGeometry.swift

echo
echo "== SettingsWindowPresenter size setup and clamp call =="
sed -n '340,380p' Sources/App/SettingsWindowPresenter.swift | cat -n

echo
echo "== CmuxMainWindow minimumFrameSize pattern =="
sed -n '440,468p' Sources/App/CmuxMainWindow.swift | cat -n

echo
echo "== MultiMonitorWindowGeometry clamp implementation =="
cat -n Sources/App/MultiMonitorWindowGeometry.swift | sed -n '1,130p'

echo
echo "== Apple NSWindow setFrame documentation reference from local repo/docs? =="
rg -n "setFrame\\(_:display:|contentMinSize|minSize|constrains" Sources/App Sources/PanelsSources/Mobile 2>/dev/null || true

Repository: manaflow-ai/cmux

Length of output: 13172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SettingsWindowGeometry.swift =="
cat -n Sources/App/SettingsWindowGeometry.swift

echo
echo "== SettingsWindowPresenter size setup and clamp call =="
sed -n '340,380p' Sources/App/SettingsWindowPresenter.swift | cat -n

echo
echo "== CmuxMainWindow minimumFrameSize pattern =="
sed -n '440,468p' Sources/App/CmuxMainWindow.swift | cat -n

echo
echo "== MultiMonitorWindowGeometry clamp implementation =="
cat -n Sources/App/MultiMonitorWindowGeometry.swift | sed -n '1,130p'

echo
echo "== Apple NSWindow setFrame documentation reference from local repo/docs? =="
rg -n "setFrame\\(_:display:|contentMinSize|minSize|constrains" Sources/App Sources/Panels 2>/dev/null || true

Repository: manaflow-ai/cmux

Length of output: 13500


🌐 Web query:

Apple NSWindow setFrame(_:display:) does not enforce contentMinSize

💡 Result:

That is correct. According to Apple's official documentation, the contentMinSize (and contentMaxSize) constraints are explicitly not enforced by the setFrame(:display:) and setFrame(:display:animate:) methods [1][2]. These constraints are intended to limit the user's ability to resize a window interactively [3][4]. Because setFrame(_:display:) is a programmatic way to set the window's frame directly, it bypasses the enforcement of these size constraints [1][2]. If you need to programmatically resize a window while respecting your defined minimum or maximum size constraints, you must manually clamp the values before passing them to the setFrame method, or use other methods if applicable [4].

Citations:


Convert contentMinSize to frame coordinates before recovery.

NSWindow.setFrame(_:display:) does not enforce contentMinSize, and the current minimum mixes window.minSize (frame coordinates) with window.contentMinSize (content-view coordinates). If recovery shrinks an oversized Settings window, the returned frame can end up smaller than the required content once title-bar and border chrome are included. Compute the frame from window.contentMinSize with NSWindow.frameRect(forContentRect:size:styleMask:) before combining it with window.minSize.

Proposed fix
+        let contentMinimumFrameSize = NSWindow.frameRect(
+            forContentRect: NSRect(origin: .zero, size: window.contentMinSize),
+            styleMask: window.styleMask
+        ).size
         let minimumFrameSize = NSSize(
-            width: max(window.minSize.width, window.contentMinSize.width),
-            height: max(window.minSize.height, window.contentMinSize.height)
+            width: max(window.minSize.width, contentMinimumFrameSize.width),
+            height: max(window.minSize.height, contentMinimumFrameSize.height)
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let minimumFrameSize = NSSize(
width: max(window.minSize.width, window.contentMinSize.width),
height: max(window.minSize.height, window.contentMinSize.height)
)
let contentMinimumFrameSize = NSWindow.frameRect(
forContentRect: NSRect(origin: .zero, size: window.contentMinSize),
styleMask: window.styleMask
).size
let minimumFrameSize = NSSize(
width: max(window.minSize.width, contentMinimumFrameSize.width),
height: max(window.minSize.height, contentMinimumFrameSize.height)
)
🤖 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/App/SettingsWindowGeometry.swift` around lines 14 - 17, Update the
minimumFrameSize calculation in the Settings window geometry recovery flow to
convert window.contentMinSize into frame coordinates using
NSWindow.frameRect(forContentRect:size:styleMask:) with the window’s current
style mask before comparing it with window.minSize. Use the converted frame
dimensions when enforcing the recovered window’s minimum size.

Source: Path instructions

@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: 2

🤖 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/TerminalController.swift`:
- Around line 10664-10668: Update the not-found error returned by the window
lookup guard in the relevant TerminalController flow to use a product-neutral
message such as “Main window not found” instead of exposing the internal
CmuxMainWindow type name; preserve the existing error code and window_id data.
- Around line 10660-10689: Move the debug-only
v2DebugWindowRecoverAfterDisplayChange handler out of TerminalController.swift
into the project’s dedicated debug source location, preserving its parameter
validation, window lookup, recovery call, and response payload. Remove the
production implementation and retain only minimal routing glue in
TerminalController.swift if the socket dispatch requires it.
🪄 Autofix

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 Plus

Run ID: f3a65f4f-d2ba-441b-94c7-d05847228d47

📥 Commits

Reviewing files that changed from the base of the PR and between 3837963 and ee3d144.

📒 Files selected for processing (2)
  • Sources/TerminalController+DebugMethodNames.swift
  • Sources/TerminalController.swift

Comment on lines +10660 to +10689
private func v2DebugWindowRecoverAfterDisplayChange(params: [String: Any]) -> V2CallResult {
guard let windowId = v2UUID(params, "window_id") else {
return .err(code: "invalid_params", message: "Missing or invalid window_id", data: nil)
}
guard let window = AppDelegate.shared?.windowForMainWindowId(windowId) as? CmuxMainWindow else {
return .err(
code: "not_found",
message: "No CmuxMainWindow for window_id",
data: ["window_id": windowId.uuidString]
)
}

func rectPayload(_ rect: NSRect) -> [String: Double] {
[
"x": Double(rect.origin.x),
"y": Double(rect.origin.y),
"width": Double(rect.width),
"height": Double(rect.height),
]
}

let before = window.frame
CmuxMainWindow.applyOffscreenRecoveryIfNeeded(window)
let after = window.frame
return .ok([
"changed": !before.equalTo(after),
"before": rectPayload(before),
"after": rectPayload(after),
])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Isolate the debug handler from the production controller.

v2DebugWindowRecoverAfterDisplayChange is a debugger-only socket seam with no production caller. Move its implementation to a dedicated debug source file or folder. Keep only minimal routing glue in Sources/TerminalController.swift if required.

As per path instructions, genuinely debug-only facilities must be isolated from production Sources/**/*.swift files.

🤖 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/TerminalController.swift` around lines 10660 - 10689, Move the
debug-only v2DebugWindowRecoverAfterDisplayChange handler out of
TerminalController.swift into the project’s dedicated debug source location,
preserving its parameter validation, window lookup, recovery call, and response
payload. Remove the production implementation and retain only minimal routing
glue in TerminalController.swift if the socket dispatch requires it.

Source: Path instructions

Comment on lines +10664 to +10668
guard let window = AppDelegate.shared?.windowForMainWindowId(windowId) as? CmuxMainWindow else {
return .err(
code: "not_found",
message: "No CmuxMainWindow for window_id",
data: ["window_id": windowId.uuidString]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a neutral not-found message.

The v2 error body exposes the internal type name "No CmuxMainWindow for window_id". Return a product-neutral message such as "Main window not found".

As per coding guidelines, API error bodies must not expose implementation details.

🤖 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/TerminalController.swift` around lines 10664 - 10668, Update the
not-found error returned by the window lookup guard in the relevant
TerminalController flow to use a product-neutral message such as “Main window
not found” instead of exposing the internal CmuxMainWindow type name; preserve
the existing error code and window_id data.

Source: Coding guidelines

@austinywang
austinywang force-pushed the issue-9696-display-disconnect-refit branch from ee3d144 to 8485b81 Compare August 6, 2026 06:36
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 2

🤖 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/App/MultiMonitorWindowGeometry.swift`:
- Around line 56-65: Update the frame-sizing logic in the visible-frame
adjustment function to cap the effective minimum size at the display’s available
size (visibleFrame minus both insets) before applying result.size. Preserve the
declared minimum when it fits, but ensure result.width and result.height never
exceed the available visible dimensions so the subsequent origin clamping can
keep the entire window visible.

In `@Sources/TerminalController.swift`:
- Line 10701: Update the frame handling around framePayload so a supplied frame
that parses to nil returns the invalid_params error instead of falling back to
window.frame; retain window.frame only when the frame parameter is absent, and
preserve the existing valid-frame behavior.
🪄 Autofix

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 Plus

Run ID: 9f76240e-303e-4380-9107-0ddefef6a890

📥 Commits

Reviewing files that changed from the base of the PR and between 79a3f64 and 8485b81.

📒 Files selected for processing (9)
  • Sources/App/CmuxMainWindow.swift
  • Sources/App/MultiMonitorWindowGeometry.swift
  • Sources/App/SettingsWindowGeometry.swift
  • Sources/AppDelegate.swift
  • Sources/TerminalController+DebugMethodNames.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CmuxMainWindowConstrainFrameTests.swift
  • cmuxTests/SettingsWindowPresenterTests.swift

Comment on lines +56 to +65
let maxVisibleSize = NSSize(
width: max(minimumSize.width, visibleFrame.width - 2 * inset),
height: max(minimumSize.height, visibleFrame.height - 2 * inset)
)
result.size.width = min(result.size.width, maxVisibleSize.width)
result.size.height = min(result.size.height, maxVisibleSize.height)
let minX = visibleFrame.minX + inset
let minY = visibleFrame.minY + inset
let maxX = max(minX, visibleFrame.maxX - inset - result.width)
let maxY = max(minY, visibleFrame.maxY - inset - result.height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clamp the frame to the available visible size.

When minimumSize exceeds visibleFrame - 2 * inset, Line 57 retains a size that cannot fit on the display. Lines 64-65 can move only the origin. The recovered window remains clipped on the right or top edge.

Bound the effective minimum size to the available visible size before setting the frame. Preserve the declared minimum only when the target display can contain it.

🤖 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/App/MultiMonitorWindowGeometry.swift` around lines 56 - 65, Update
the frame-sizing logic in the visible-frame adjustment function to cap the
effective minimum size at the display’s available size (visibleFrame minus both
insets) before applying result.size. Preserve the declared minimum when it fits,
but ensure result.width and result.height never exceed the available visible
dimensions so the subsequent origin clamping can keep the entire window visible.

]
}

let before = framePayload(params["frame"]) ?? window.frame

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an invalid supplied frame.

If frame is present but framePayload returns nil, this line silently uses window.frame and returns success. Reject the malformed parameter with invalid_params instead.

Proposed fix
-        let before = framePayload(params["frame"]) ?? window.frame
+        let before: NSRect
+        if let rawFrame = params["frame"], !(rawFrame is NSNull) {
+            guard let parsedFrame = framePayload(rawFrame) else {
+                return .err(code: "invalid_params", message: "Invalid frame", data: nil)
+            }
+            before = parsedFrame
+        } else {
+            before = window.frame
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let before = framePayload(params["frame"]) ?? window.frame
let before: NSRect
if let rawFrame = params["frame"], !(rawFrame is NSNull) {
guard let parsedFrame = framePayload(rawFrame) else {
return .err(code: "invalid_params", message: "Invalid frame", data: nil)
}
before = parsedFrame
} else {
before = window.frame
}
🤖 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/TerminalController.swift` at line 10701, Update the frame handling
around framePayload so a supplied frame that parses to nil returns the
invalid_params error instead of falling back to window.frame; retain
window.frame only when the frame parameter is absent, and preserve the existing
valid-frame behavior.

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

4 participants