Repository navigation
fix(windows): Move to New Window never opens a window exactly on one already open - #30011
Conversation
…at do not cover each other (red) v2.2.6-opt gate, mw-restart-three-windows (handoff/v224-gate-triage.md): two moves of a workspace to a new window from window 1 opened both new windows at the same frame [438, 242, 1100, 720]; the second hid the first. This test runs the real action (moveWorkspaceToNewWindow) twice from one window and checks that no two windows share a frame origin. Red on the base: MovedWindowPlacement places from the source frame alone, so both moves give one frame. (cherry picked from commit c49bca9)
…already open; it cascades on from it v2.2.6-opt gate, mw-restart-three-windows (handoff/v224-gate-triage.md): two moves of a workspace to a new window from one window placed both new windows from the source frame alone (MovedWindowPlacement: beside it, else cascaded 28 pt), so the second opened exactly on the first and hid it completely; the app then correctly paused the hidden window's terminal, and the restored session showed one window where a person had two. MovedWindowPlacement.frame(beside:on:avoiding:) takes the frames of the windows open now: from a spot a window already takes (same top-left), it steps on by the cascade step, down and right, back to the screen's top-left when it would leave the screen, as AppKit's cascadeTopLeft(from:) does; it stops after as many steps as there are windows. WindowManager.openMovedWindow passes the visible windows' frames. Chrome opens each new window offset from the last one (WindowSizer::AdjustBoundsToBeVisibleOnDisplay / the cascade in GetSavedWindowBounds). Test: MoveToNewWindowTests.twoMovesFromOneWindowOpenTwoWindowsThatDoNotCoverEachOther (red at c49bca9, green here; fleet steps below) and the existing theNewWindowOpensBesideTheOldOne. (cherry picked from commit 27a3b5d)
…me, also one not ordered in yet Green 8889f1c9 (27a3b5d) still failed twoMovesFromOneWindowOpenTwoWindowsThatDoNotCoverEachOther (MoveToNewWindowTests.swift:162: three windows, two origins); the other 7 tests passed. openMovedWindow passed only windows with isVisible to frame(beside:on:avoiding:); the window the first move opened was not visible yet (a moved window orders in later; in a test process or behind, never), so the second move did not avoid it. Every open controller's window frame counts now. Test: MoveToNewWindowTests (red 158a3a38 at c49bca9 failed on its assertion; green: fleet step below). (cherry picked from commit d899ecc)
|
Review (claude-review-subagent): FIX at dc8712d Read-only review of cba94d5..dc8712d (3 files). Nothing compiled or run on the review machine. Correctness
Checked and OK
Evidence state
|
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowManager+Membership.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowManager+Membership.swift:341">
P2: The occupancy check in `MovedWindowPlacement.frame(beside:on:avoiding:)` matches on top edge (`maxY`), not origin. An open window with the same origin but a different height is not treated as occupied, so the new window can land on its exact origin. Compare `minY` instead of `maxY`, or check both edges, so the placement matches the origin the regression test asserts.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| func openMovedWindow(_ ids: [String], from origin: MoveOrigin, select: Bool) { | ||
| let source = origin.window.flatMap(controller(for:)) | ||
| let frame = source?.window.map { MovedWindowPlacement().frame(beside: $0.frame, on: ($0.screen ?? NSScreen.main)?.visibleFrame ?? .infinite) } | ||
| let frame = source?.window.map { MovedWindowPlacement().frame(beside: $0.frame, on: ($0.screen ?? NSScreen.main)?.visibleFrame ?? .infinite, avoiding: controllers.compactMap { $0.window?.frame }) } |
There was a problem hiding this comment.
P2: The occupancy check in MovedWindowPlacement.frame(beside:on:avoiding:) matches on top edge (maxY), not origin. An open window with the same origin but a different height is not treated as occupied, so the new window can land on its exact origin. Compare minY instead of maxY, or check both edges, so the placement matches the origin the regression test asserts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Windows/WindowManager+Membership.swift, line 341:
<comment>The occupancy check in `MovedWindowPlacement.frame(beside:on:avoiding:)` matches on top edge (`maxY`), not origin. An open window with the same origin but a different height is not treated as occupied, so the new window can land on its exact origin. Compare `minY` instead of `maxY`, or check both edges, so the placement matches the origin the regression test asserts.</comment>
<file context>
@@ -332,13 +332,13 @@ extension WindowManager {
func openMovedWindow(_ ids: [String], from origin: MoveOrigin, select: Bool) {
let source = origin.window.flatMap(controller(for:))
- let frame = source?.window.map { MovedWindowPlacement().frame(beside: $0.frame, on: ($0.screen ?? NSScreen.main)?.visibleFrame ?? .infinite) }
+ let frame = source?.window.map { MovedWindowPlacement().frame(beside: $0.frame, on: ($0.screen ?? NSScreen.main)?.visibleFrame ?? .infinite, avoiding: controllers.compactMap { $0.window?.frame }) }
let whole = source.map { Set(ids) == Set(registry.members(of: $0.state.id)) } == true
if whole, let source, let frame { source.window?.setFrame(frame, display: true); recordSaver.stateDidChange(source.state) }
</file context>
… a spot of their own (red) Review of #30011 (FIX at dc8712d): when the source window fills its screen or nearly fills it, the moved-window cascade has no room on the screen. Each step left the screen and jumped back to the screen's top-left, which was the spot a window already took, so the loop ran out of steps and returned a taken spot. The second move from a zoomed window still opened exactly on the first (1512x944 screen, zoomed source: both at (28, -28); source (20, 20, 1480, 900): the third move on the second at (28, 16)). threeMovesFromOneWindowEachGetASpotOfTheirOwn calls MovedWindowPlacement.frame(beside:on:avoiding:) three times from a zoomed source, a near-full source and the gate's 1100x720 source, and checks that no two windows share a top-left. It needs no app or daemon. Red at this commit for the first two arguments.
…a taken spot, never back onto it Review of #30011 (FIX at dc8712d): from a taken spot, a cascade step that left the screen jumped back to the screen's top-left. For a source that fills or nearly fills its screen, that spot was already taken, so the loop returned a taken spot and the second move from a zoomed window still opened exactly on the first. frame(beside:on:avoiding:) now jumps to the top-left only when that spot is free; otherwise it steps on down and right, partly off screen. A window partly off screen is better than one hidden under another, and the base already placed a near-full source's first move partly off screen. Each turn checks a new spot (a step moves down and right; a jump is taken only when free and ends the loop), so occupied.count turns always reach a free spot. The doc comment now says `occupied` is every open window, as the call site passes. Test: MoveToNewWindowTests.threeMovesFromOneWindowEachGetASpotOfTheirOwn (red at the parent commit, green here) and twoMovesFromOneWindowOpenTwoWindowsThatDoNotCoverEachOther.
|
Review (claude-review-subagent-2): SHIP at 1079922 Scope: the whole PR, Correctness: no blocking defect found.
Not blocking. Do these before
Notes (no change required):
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Not verified: macOS jobs did not run: compile and app tests were skipped on CI passes on Written by Shipped in:
|
|
Found 8 test failures on Blacksmith runners: Failures
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Two Move to New Window actions from one window opened both new windows at the same frame, so the second one hid the first completely. The app then paused the hidden window's terminal, and after a restart the session showed one window where the person had two (v2.2.6-opt gate journey
mw-restart-three-windows: both windows at[438, 242, 1100, 720]).MovedWindowPlacementplaced the new window from the source frame alone (beside it, else cascaded 28 pt), so every move from the same window got the same answer.frame(beside:on:avoiding:)now takes the frames of the windows already open. From a spot a window already takes (same top-left), it steps on by the cascade step, down and right, as AppKit'scascadeTopLeft(from:)does. A step that would leave the screen goes back to the screen's top-left only when that spot is free; otherwise it steps on, partly off screen. A zoomed or near-full source has no free spot on its screen, and a window partly off screen is better than one hidden under another (review finding on dc8712d).WindowManager.openMovedWindowpasses every controller's window frame, including a window a move just opened that is not ordered in yet. Chrome offsets each new window from the last one in the same way (WindowSizer, theGetSavedWindowBoundscascade).Ported from
cp/fix-moved-window-cascade(perf train 321: c49bca9, 27a3b5d, d899ecc), plus the zoomed-source fix found in review. On feat the bug is present:openMovedWindowcalledframe(beside:on:)with no other windows. The App change is net zero lines.Testing
MoveToNewWindowTests.twoMovesFromOneWindowOpenTwoWindowsThatDoNotCoverEachOtherruns the real action (moveWorkspaceToNewWindow) twice from one window on a same-tree daemon and checks that no two windows share a frame origin.MoveToNewWindowTests.threeMovesFromOneWindowEachGetASpotOfTheirOwncalls the placement three times from a zoomed source, a near-full source and the gate's 1100x720 source, and checks that no two windows share a top-left.Fleet jobs (
package-test-lane.sh suite Packages/macOS/CmuxNext MoveToNewWindowTests):22fd224f6c49ba07065294e7, ok. Its log shows the changed files compiled.e5203dcf8f07b53eb1f87a3fon aws-m4pro-3. FAILED as expected:twoMovesFromOneWindowOpenTwoWindowsThatDoNotCoverEachOtherat MoveToNewWindowTests.swift:162,(origins.count → 2) == (frames.count → 3); the other 7 tests passed.0095c9ba236049b6ffc95432on aws-m4pro-1. FAILED as expected at MoveToNewWindowTests.swift:190: zoomed source(spots.count → 2) == (open.count → 4), near-full source(3) == (4); the 1100x720 case and the other 8 tests passed.ec225222ef8848ba784d7bacon aws-m4pro-6. PASSED: "Test run with 9 tests in 1 suite passed", both new tests included (the zoomed test with its 3 cases).Observed / Not run
mw-restart-three-windowsjourney on a feat build.Changelog
Fixed: Moving two workspaces to new windows from the same window no longer opens the second window exactly on top of the first
Proof
No capture: window placement only; the regression tests check the frames.