Skip to content

P0 RESIZE KEYS K4: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up move a row edge one exact step - #32471

Merged
cmux-bot[bot] merged 7 commits into
feat-cmux-nextfrom
fix/resize-keys-rows-4
Nov 2, 2026
Merged

cmux-bot[bot] merged 7 commits into
feat-cmux-nextfrom
fix/resize-keys-rows-4

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

P0 RESIZE KEYS K4 (cmux-ad 2026-11-02, "pixel perfect ctrl shift hjkl"). In a column with two or more rows (rows-v1, New Row), Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up on a row's pane did nothing (films rk-rows on feat 507438d and on the dogfood source: 4 presses, 0 motion). PaneResize.change looked for a vertical split in the pane's own row tree, then for a column dock band, and found neither.

Fix: with no vertical split in the pane's row, a vertical press moves the edge below that row (above it for the last row) one step, 5 % of the column height. ShownResizeRange.rowHeights(moving:in:by:) trades whole permille between the two rows (scaled as a filled stack places them), the exact inverse of RowStackGeometry.compute, so every press moves the edge (height + gap) * 5 % points; each row keeps 100 permille and its tree's minimum height, and a press there changes nothing. The press sends one setRowHeights intent, as a row drag's release. PaneResize.press (CmuxNextBridge) now holds the press for the App handler (app_lines -8).

Based on feat-cmux-next 4729158, after #32421 (K1) landed. Commits cherry-picked from the earlier stacked branch fix/resize-keys-rows-3.

Testing

Red first, on the fleet (scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext ...), compile-checked first:
On the earlier branch (same code on the K1 branch):

  • Red 33c8684 (test + stub): step 4c7110360ce6031586c0c741 FAILED (KeyboardResizeRowsTests: the first press moved nothing).
  • First fix 81d2674: step 4695da174e153e514523a0b2 FAILED (each press 1.85 pt over the step: it reused the drag math without the row gap). Replaced by the third commit.
  • Fix 0ffb845: step b3ff1df9b1269bdae00d4ce6 PASSED: KeyboardResizeRowsTests 2/2, KeyboardResizeShownLimitTests 2/2, PaneResizeGestureTests 4/4.
    At this PR's commits:
  • compile-check 5507dfe5e6c7103b60fb1c4e at d5d5c04 PASSED. Red 1caf8f8: step 00b1b53e90539096f0dcfea1 FAILED as expected (the first press moved nothing).
  • Review FIX FIRST at d5d5c04 (comment 6583295487): a row stored below its minimum stepped from the stored value (Down moved the edge up to 168 pt UP); rows off with [1000, 1000] sent heights the model drops; a band dock with rows lost its up/down extent.
    • Red 92e30db (new KeyboardResizeRowsEdgeCasesTests, code unchanged): step c76c7d3ce9744b135cc465fd FAILED (4 of 5 tests).
    • Fix 5d85619 (step from the shown permille, stay in 100...1000, scrolling stack stops at the filling sum, band docks keep their extent); test fix 76e9d24 (two edge cases pressed a pane in its own split and a dock already at its widest: test faults, see STATUS 7291).
    • Never-merge red 8e255ff (= 92e30db + the test fix, the code before 5d85619): compile-check 2fe13ddd21f6138b91e048e8 PASSED; step b599ecb3f1e640255c355d85 FAILED as expected (4 of 5 tests, 9 issues).
    • Head 76e9d24: compile-check 435bc457d32371945d989106 PASSED (CmuxNext with tests, app); green + GUI proof (display worker gui-session-v1, CMUX_TEST_REQUIRE_GUI=1) at the exact head 76e9d24: step 3d9875597a2de2ee67df63d9 PASSED: KeyboardResizeRowsEdgeCasesTests 5/5, KeyboardResizeRowsTests 2/2, KeyboardResizeShownLimitTests 2/2, PaneResizeGestureTests 4/4.

Observed / Not run

  • Observed: the red and green above; the before films rk-rows (feat and dogfood).
  • Not run: an after-fix film of rk-rows on a build of this head.

No capture: keyboard resize motion is filmed by the RESIZE KEYS kit (cp/resize-keys-kit).

Changelog

  • Fixed: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up resize a pane that sits in a row of a column.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up so a vertical press on a pane in a rows-v1 column moves the neighboring row edge by exactly one step. Previously these presses did nothing because PaneResize.change only looked for a vertical split or dock band in the pane's row.

  • PaneResize.press (in CmuxNextBridge) now handles the new .rowHeights change and writes one setRowHeights intent, replacing the split-ratio and column-width handling in PaneHandlers.
  • ShownResizeRange.rowHeights(moving:in:by:) trades whole permille between the two rows as the exact inverse of row placement, so each press moves the edge (height + gap) * 5% toward the press; a press at a row minimum refuses quietly instead of moving.

Edge cases

  • The press steps from the shown row height, so a row stored below its panes' minimum never moves the edge the wrong way.
  • Presses never send heights outside 100...1000 (which the model would drop), and a scrolling stack never jumps more than one step when it fills the column.
  • A top or bottom dock band keeps resizing with up/down as before rows.

Written for commit 9c778d1. Summary will update on new commits.

View guided diff Turn on auto-fix

…e row edge one whole step (red)

RESIZE KEYS K4 (cmux-ad 2026-11-02, P0; film rk-rows on feat 507438d and the dogfood source): in a column of two rows
(rows-v1, New Row) Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up on a row's pane moved nothing: PaneResize.change looked for a
vertical split in the row's own tree, then a dock band, and found neither. PaneResize.press (CmuxNextBridge) now holds
the press for the App handler (the change inside the shown range, the model write), with the new Change.rowHeights;
ShownResizeRange.rowHeights(moving:in:by:) is a stub here (never returned): the test fails.

(cherry picked from commit 33c8684)
…ow move its row edge one step

RESIZE KEYS K4 (film rk-rows): in a rows-v1 column a vertical key press on a row's pane found no vertical split and
did nothing. With no vertical split in the pane's own row, the press now moves the edge below that row (above it for
the last row) by one step, 5 % of the column height, through RowResize, the same math as a row edge drag (each row
keeps its minimum; a press at a minimum changes nothing), and sends one setRowHeights intent, as a drag's release.

(cherry picked from commit 81d2674)
… moves the row edge exactly one step

The first K4 fix reused the row drag's math (RowResize.heights), which maps a pointer's pixels to permille over the
pair's height without the gap between the rows; RowStackGeometry places a row at (height + gap) * p / 1000 - gap, so
each press moved the edge 1.85 pt more than the declared step (green step 4695da17, KeyboardResizeRowsTests:51, 4 of
4 presses). A press now adds and removes whole permille (scaled as a filled stack places them): the exact inverse of the
placement, one (height + gap) * 5 % step per press, snapped once to the pixel grid. The row minimums (100 permille and
the tree's minimum height) stop it; a press there changes nothing.

(cherry picked from commit 0ffb845)
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 76e9d24: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr32471 -f ref=fix/resize-keys-rows-4 -f platform=macos -f backend_mode=local

Default changes

This pull request changes 3 defaults at 9c778d19039e (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Handlers/PaneHandlers.swift:bindSizing: "681f80e202a88a9c9d46048376eb1755a8b26252ad63685e14d5b4fe0f0c9d7d" -> "da035f7f89179ad430377c929d1c002c6e8a8d4b51f4511c3ca6dff3596424ab"
  • tunable-source.CmuxNextBridge/PaneResize.swift:change: "3e48b09b3862c5a443c6631586ff58dba1f3e49b7ac0a78e240b82ea5639af86" -> "ee023fb4e6d2c209a6851f5ce95f9157de251bbbad9f5485204f7fd4b9a24af8"
  • tunable-source.CmuxNextBridge/PaneResize.swift:press: (none) -> "a6096a0af03d6c4430d44f3b0d50a7ac7b19260cc1180739a81d48142cf0c37b"

Signed off on this commit by review (independent subagent).

Merge receipt: merged as 2fdcbbae7e44 with these defaults changed, signed off by review (independent subagent).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): FIX FIRST at d5d5c04

The normal path is correct: in a filled stack (sum <= 1000) each press trades round(0.05 * sum) permille within the pair. The sum stays the same, so the edge moves (H + gap) * 0.05 within pixel rounding. A scrolling stack moves the upper row alone by 50 permille. But two states that users can reach break the contract in the PR body ("never a model change that moves nothing", one whole step, no heights the model refuses). I checked both by porting RowStackGeometry.compute and rowHeights(moving:in:by:) to a script (H = 841, gap = 8, scale 2).

Findings

  1. ShownResizeRange.swift:66: a row whose stored height is below its tree's minimum makes a dead press or moves the edge the wrong way. next steps from the stored rows[upper].height, not from the height the screen shows. compute shows such a row at its minimum, so the stored and shown values differ. K1's PaneResize.stepped clamps current into the shown range first for this reason ("a value past what the screen can show counts as the shown limit, so the press back is a whole step"). The row path skips that clamp. Simulated results:

    • Upper row stored [100, 900], its tree needs 150 pt. Down gives [186, 814] and the edge moves 0.0 pt (the model changes, nothing moves). Up gives the same [186, 814]: it changes the model and moves nothing, and the next press is nil.
    • Lower row stored [600, 400], its tree needs 500 pt. Down gives [402, 598] and the edge moves -168 pt (up).

    How to reach it: drag a row edge to a row's minimum in a tall window, then shrink the window (or move it to a smaller display). Splitting a pane inside a small row also does it. A row at 100 permille (newRow with matchCurrent from a small row, or another client's set-row-heights) is below one pane's minimum in most windows.

    Fix: clamp first, as stepped does. Use let current = min(max(rows[upper].height, low), high), then next = min(max(current + step, low), high) and guard next != current. In the scrolling branch, use current = max(rows[upper].height, floor(rows[upper])).

  2. ShownResizeRange.swift:64: with rows turned off (fits, O3) and a sum above 1000, the trade sends heights the model refuses. low and high keep the pair inside the tree minimums but not inside LayoutRow.heightRange. [1000, 1000] is the normal state after New Row on a full-height row, with newRowHeight = row.height. With rows turned off, the scale is 2 and the step is 100: Down gives [1100, 900] and Up gives [900, 1100]. LayoutModel.setRowHeights drops both (heights.allSatisfy(heightRange.contains)). press still returns true, so every press does nothing and shows no refusal. Fix: use low = max(floor(upper), pair - LayoutRow.heightRange.upperBound) and high = min(pair - floor(lower), LayoutRow.heightRange.upperBound). A press with no in-range trade is then nil and gets the quiet refusal. You could also renormalize the column to 1000 and send fit: true, but that also changes how the rows scroll when rows are turned on again, so the clamp is the safer fix.

Lower severity (fix with the above or as a follow-up)

  1. ShownResizeRange.swift:71: a scrolling press that takes the sum below 1000 is not a whole step. [520, 500] Up gives [470, 500], sum 970. The stack switches to fill and the edge moves -30.0 pt, not -42.45. At sum == 1000 both modes place rows identically, so clamp next >= 1000 - (sum - rows[upper].height) while sum > 1000. That press is then a part step to the mode boundary, and the next press trades a whole step.
  2. PaneResize.swift:35: in a top or bottom docked column with rows, Up and Down no longer resize the dock. Docked columns hold rows (ScreenGeometry.addDock, "G5"). The rows branch now runs before the band-dock branch, so from any row pane Up and Down move only row edges. At a row minimum the press is refused even though the dock edge can still move. Before this PR the same press moved the dock's inner edge. If you want this change, write it in the PR body. If not, fall back to the dock branch when rowHeights returns nil, or for the row next to the dock's inner edge.
  3. Tests: KeyboardResizeRowsTests covers only a filled 500/500 two-row column. Add the cases that fail above: a stored height below the minimum with the lower row (expect motion in the press direction, or nil), rows off at [1000, 1000] (expect nil or in-range heights), a scrolling [1000, 1000], and a three-row middle edge. press itself (the fit flag and the model guard) has no test.

What I checked and found sound

  • change() still looks for nearestSplit in the row's own tree (column.tree(containing:) returns the row root when hasRows), so a vertical split in a row wins. A single-row column (rows.count == 1, so hasRows is false) uses the old path. For the last row, upper = index - 1 moves the edge above it.
  • The filled math is the exact inverse of compute: sum uses max(h, 1), the same factor, and only row upper changes the edge's y, so the last row's fill to the bottom does not move the edge. Simulated [500, 500]: +42.50 pt for each press (want 42.45). Scrolling [1000, 1000]: -42.50 pt for each press.
  • fit is true only for a sum of exactly 1000. The reducer set_row_heights checks the same row set, the 100...1000 range and the fit sum, so the in-range output from the normal path is accepted.
  • The test is meaningful. Red commit 1caf8f8 has the change() branch with rowHeights stubbed to nil, so #require(edge) fails. It has no sleeps. shownResizeRange reads the cached baseGeometry, so the press does no layout pass. press is @MainActor and only touches main-actor model and view objects, so its isolation is sound. In the App target, PaneHandlers goes from 11 lines to 4.

Fleet

  • The compile-check 5507dfe5e6c7103b60fb1c4e is done.
  • The red step 00b1b53e90539096f0dcfea1 and the green + GUI step 7f60310de81b14f177432189 were still running when I posted this, so I make no claim about their results.

… refused heights, never jump, and leave band docks alone (red)

Review of #32471 (FIX FIRST at d5d5c04, comment 6583295487): a row stored below the height its panes need
showed at that minimum, but the press stepped from the stored value: Down moved the edge up to 168 pt UP, or changed
the model and moved nothing. With rows off and heights [1000, 1000] (New Row on a full row) a press sent 1100 / 900,
which the model drops (no motion, no refusal). A scrolling stack brought to 1000 jumped by less than a step, and a
top or bottom dock with rows lost its up/down dock resize.
…de heights the model takes; band docks keep their up/down resize

Review of #32471 (FIX FIRST at d5d5c04): the press now starts from the upper row's shown permille (a row stored
below its panes' minimum shows at that minimum), moves one step toward the press only (never the wrong way; nil when
nothing would move, so the key refuses quietly), keeps both rows in 100...1000 (setRowHeights takes nothing else: rows
off with [1000, 1000] now refuses instead of sending 1100/900), and in a scrolling stack stops at the sum that fills
the column (a part step there, then the filled trade). A top or bottom dock with rows keeps up/down on its band extent.
…s own, and a band dock below its widest extent

Green + GUI step dc6882a5 at 5d85619: two edge-case tests were wrong, not the code. In the upper-row test p1 sat
in a vertical split inside its row, so the press (correctly) moved that split; p1 now sits beside the three-pane
stack. The band dock had width 1.0, its widest, so Down was (correctly) refused; it is 0.4 now.
@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
the short nightly decide gate must use the pooled Linux route with a Blacksmith fallbac
k/
the short nightly decide gate must use the pooled Linux route with a Blacksmith fallbac
k
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): SHIP at 76e9d24

I read the diff 4729158bf95..76e9d242628, the fix d5d5c04fcf0..5d856196742, the test fix 76e9d24, RowStackGeometry.compute, RowResize.heights, LayoutModel.setRowHeights and LayoutRootView.shownResizeRange. I read the fleet results myself (below). I did not build or run anything.

The earlier FIX FIRST items

  1. Row stored below its minimum: resolved. ShownResizeRange.swift:65 steps from shownUpper = max(stored, floor), and stepped (:68-72) returns nil unless next is strictly past shownUpper in the press direction. I checked each bound by hand:
    • Fill, Down: next <= high <= 1000, and the lower row gets pair - next >= floor(lower) >= 100.
    • Fill, Up: next >= low >= max(floor(upper), pair - 1000), so both rows stay in 100...1000.
    • When the upper row shows at its floor, Up is nil. When the lower row is past its floor, Down is nil, and Up still moves one whole step from where the edge shows. The red log shows the old code moved that edge 174 pt.
  2. Rows off, [1000, 1000]: resolved. pair - range.upperBound and min(..., range.upperBound) (:76-77) keep every output in LayoutRow.heightRange. With scale 2, both directions return nil and get the quiet refusal. This is the nil option from the first review. Note that a drag (RowResize.heights) in this state also makes out-of-range shares that setRowHeights drops, so the key and the drag agree.
  3. Scrolling part step: code resolved, test does not catch it (see note A). low = max(floor, 1000 - rest) (:84) stops the press at sum 1000. At that sum the fill and scroll placements are the same, so the next press is a whole fill step.
  4. Band dock: resolved by choice, and the PR body says so. PaneResize.swift:35 skips the rows branch for isBand docks, so up and down resize the dock as before rows. Row edges inside a top or bottom dock have no key resize. They had none before this PR either, so this is no regression.
  5. Tests: partly done. The PR adds tests for an upper and a lower row below the minimum, rows off at [1000, 1000], a scrolling stack and a band dock. A three-row middle edge test and a press/fit test are not added (note B).

The 76e9d24 test changes are honest

  • Upper-row test. The old row stack(["p1", "q1", "q2"]) put p1 in the vertical split s-p1 inside its own row. change() takes nearestSplit in column.tree(containing:) (the row root) before the rows branch, so Up and Down correctly moved that split. The new row split("h", .horizontal, p1, stack(q1, q2, q3)) gives p1 no vertical ancestor, and the row still needs three panes of height.

  • Band dock test. At width: 1 a top dock is at widthRange.upperBound, so Down is correctly nil. At 0.4, Down gives .columnWidth.

  • The red still catches the defects. git diff 8e255ff2e21 92e30dbe5f0 touches only the test file. git diff 8e255ff2e21 76e9d242628 touches only the two source files. So the never-merge red is "the head's tests on the old code". Step b599ecb3f1e640255c355d85 (ref 8e255ff, state failed) recorded 9 issues in 4 tests:

    • the band dock gave a non-columnWidth change;
    • the upper row: Up sent [329, 671], and Down was off by 43.15 pt;
    • rows off: 4 out-of-range sends;
    • the lower row: Down moved the edge up, and Up was off by 174.35 pt.

    aScrollingStackNeverJumpsMoreThanOneStep passed on the red. The PR body says "4 of 5" correctly.

  • Green. Step 3d9875597a2de2ee67df63d9 (ref 76e9d24, labels gui-session-v1, exit 0): EdgeCases 5/5, KeyboardResizeRowsTests 2/2, KeyboardResizeShownLimitTests 2/2, PaneResizeGestureTests 4/4. Compile-check 435bc457d32371945d989106 at the same SHA (--package CmuxNext --app): done.

Row math against RowStackGeometry.compute

  • fills (!rowsEnabled || sum <= 1000) matches compute's fits || sum <= 1000, because ScreenGeometry.layoutContent passes fits: !style.rowsEnabled. The sum uses max(h, 1) in both places.
  • In a filled stack, the trade keeps the sum. Only row upper changes the edge's y, so each press moves (H + gap) * round(0.05 * sum) / sum, which is one step within pixel rounding. Rows above upper that show at their minimum add a constant offset. The last row's fill to the bottom does not move the edge.
  • In a scrolling stack, scale is 1 and only the upper row changes, so each press moves (H + gap) * 0.05.
  • press sends fit only when the sum is exactly 1000. setRowHeights checks the same range, the same row set and the same fit sum, so a non-nil change is never dropped.

No regression to split, column or dock presses

The new branch runs only for axis == .vertical && column.hasRows && !isBand. Before this PR, that case reached guard axis == .horizontal else { return nil }. The split, column-width and band-dock paths are byte-identical. PaneResize.press copies the old PaneHandlers body. The case "no screen" still returns without a refusal. The new .rowHeights case is the only addition, and KeyboardResizeShownLimitTests handles it exhaustively.

Rules

  • The tests have no sleeps.
  • press is @MainActor and touches only main-actor model and view objects. rowHeights is a pure function on a nonisolated struct, and its nested functions capture only lets.
  • shownResizeRange reads the cached view.baseGeometry, so the key path does no layout pass.

Notes (not blocking; follow-ups)

  • A. KeyboardResizeRowsEdgeCasesTests.swift:92-103 does not test fix 3. It passed on the never-merge red. An Up press that crosses into fill mode always moves less than a step, because the new sum is below 1000 and the scale-up makes the placed permille larger. A check of <= step + 1 can never fail for that defect, and [700, 700] reaches exactly 1000 anyway. Fix: start at [520, 500], press Up once, and expect heights [500, 500] (sum 1000) and an edge move of (H + gap) * 20 / 1000. The old code sends [470, 500] and moves 30 pt.
  • B. Add a three-row middle-edge test and a press test with a fit of true and of false, as the first review asked.
  • C. ShownResizeRange.swift:57-58: floor can be up to 0.5 permille below the true minimum. The cause: RowStackGeometry.permille(forPixelHeight:) rounds to the nearest permille. When a row is stored at exactly floor + 1, Up sends floor, and the edge can stay on the same pixel: a dead 1-permille press. I ported the formulas (H = 841, gap = 8, scale 2): 58 of the minimum heights from 64 to 400 pt (in 0.25 pt steps) give no motion. A drag that clamps to the minimum gives floor + 1 for rows with a minimum near 460 pt, so this case is narrow. Fix: compute the floor with .rounded(.up) from (points + gap) / (H + gap) * 1000 * scale, not from the rounded permille. The same applies to floor(rows[upper + 1]) in high.

Checks on this head

  • defaults guard is red because it waits for a non-self Review (...): SHIP at <head> comment. It flags the tunable-source hashes of PaneHandlers.bindSizing, PaneResize.change and PaneResize.press. This comment is that receipt.
  • nightly-guards / workflow-guard-tests / release-notary failed with a JSONDecodeError in a workflow guard script. This PR touches no workflow or release file. I did not check the base for the same red.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Base finding, not this PR: nightly-guards / workflow-guard-tests / release-notary fails with a JSONDecodeError in a workflow guard script; it is red on unrelated open PRs at the same time (#32518, #32515, #32514, #32512, #32487, #32472). This PR touches no workflow or release file. Review follow-ups A (scrolling test that catches fix 3), B (three-row and press/fit tests) and C (row minimum rounded up, a 1-permille dead press) go in a follow-up PR.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 2, 2026
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
… placement, so no row key press is dead next to it

Review note C of #32471: the floor came from RowStackGeometry.permille(forPixelHeight:), which rounds to the nearest
permille, so it could be up to half a permille below the real minimum; a row stored one permille above that floor took
an Up press that moved no pixel. The floor now rounds up from (minimum + gap) / (height + gap) * 1000 (times the fill
scale). Notes A and B need no code change: the tests pin the behavior #32471 already has.
@cursor

cursor Bot commented Nov 2, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 19:06 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 19:06 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 19:07 Inactive
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): SHIP at 9c778d1

9c778d1 is a merge commit. Its parents are the reviewed head 76e9d24 (SHIP above) and feat-cmux-next 069f8c0, and it brings in 54 commits. git diff 76e9d242628 9c778d19039 is empty for every file of this PR: PaneResize.swift, ShownResizeRange.swift, PaneHandlers.swift, KeyboardResizeRowsTests.swift, KeyboardResizeRowsEdgeCasesTests.swift and KeyboardResizeShownLimitTests.swift. git log 76e9d242628..9c778d19039 names no commit that touches those files, LayoutModel+Rows.swift or RowStackGeometry.swift. The same diff is empty for all of Sources/CmuxNextLayout and Sources/CmuxNextBridge, so the row geometry, setRowHeights, shownResizeRange and the split, column and dock paths are the same code I reviewed. No new code outside the PR switches over PaneResize.Change, so the added .rowHeights case cannot make a new switch non-exhaustive. The earlier review and its non-blocking notes A to C apply unchanged. I did not build this merge or run its tests, and I make no claim about CI on it.

@cmux-bot
cmux-bot Bot merged commit 2fdcbba into feat-cmux-next Nov 2, 2026
163 of 169 checks passed
@cmux-bot
cmux-bot Bot deleted the fix/resize-keys-rows-4 branch November 2, 2026 19:17
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…ey press (review notes A, B, C of #32471) (#32550)

* test(next panes): row key presses stop exactly at the filling sum, move a middle row's lower edge, and take no dead press one permille above a minimum (red)

Follow-ups A, B, C of the re-review of #32471 (SHIP at 76e9d24, comment 6585271874). A: the scrolling test of
#32471 could not fail for the part step; [520, 500] must give [500, 500]. B: a three-row case. C: the row minimum in
permille is rounded to the nearest, so a row stored one above it can take an Up press that moves no pixel.

(cherry picked from commit ecf6522)

* test(next panes): the dead-press test sweeps the minimum content height, so it fails on a floor rounded to the nearest permille (red)

The first version (ecf6522, red step e7a1ebcb) passed on the old code: its few pane stacks never hit a minimum that
sits just under a half permille. A copy of RowStackGeometry finds 56 such content heights between 64 and 400 pt at a
857 pt column (for example 64.75 pt: floor 114 rounded, 115 real; stored 115, Up sends 114 and no pixel moves).

(cherry picked from commit 3f9caa0)

* fix(next panes): a row's minimum in permille rounds up from the exact placement, so no row key press is dead next to it

Review note C of #32471: the floor came from RowStackGeometry.permille(forPixelHeight:), which rounds to the nearest
permille, so it could be up to half a permille below the real minimum; a row stored one permille above that floor took
an Up press that moved no pixel. The floor now rounds up from (minimum + gap) / (height + gap) * 1000 (times the fill
scale). Notes A and B need no code change: the tests pin the behavior #32471 already has.

(cherry picked from commit f83ca3b)
(cherry picked from commit ff9dbbe)

* test(next panes): at 1x and 2x, row Up presses never die and end with the row at its minimum (red)

Review of #32550 (FIX FIRST at 77abc43, comment 6587348634): the floor rounded up from the exact placement can
show one device pixel above the minimum (Up refused while one more pixel could move), and at 1x dead presses remain.
ShownResizeRange takes the backing scale (LayoutRootView passes the screen's reconciled scale); the floor does not use
it yet here, so the test fails.

* fix(next panes): a row's floor is the largest stored permille that still shows at the row minimum on the screen's pixel grid

Review of #32550 (FIX FIRST at 77abc43): rounded up from the exact placement, the floor could show one device
pixel above the minimum (Up refused while one more pixel could move), and at 1x dead presses remained. The floor now
replays RowStackGeometry.compute's placement on the screen's backing scale (ShownResizeRange.scale): the largest stored
permille whose placed height, rounded to a pixel, is at most the minimum rounded up to one. A few steps from the exact
estimate, no layout pass.

* fix(next panes): a row key press checks only its own direction's bound, so Up reaches the row minimum where both minimums do not fit

Green + GUI step 7d3cdbc4 at 15df074 (KeyboardResizeRowsPixelFloorTests): with tall minimums (both rows need about
426 pt of an 857 pt column) the Up press was refused 0.5 to 2.5 pt above the minimum. Cause: stepped() refused every
press when low > high, but high only bounds Down (it keeps the lower row at its minimum); Up only grows the lower row.
Not a rounding order: the pixel floor is exact.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order).

This PR raised swift_logic_lines by +69 at its merge commit 2fdcbba: scripts/cmux-next/swift-logic-lines.py before and after the merge, with the same table (the tip's, c6be943). The largest declarations of the rise:

  • struct ShownResizeRange in Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Geometry/ShownResizeRange.swift: +50 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)
  • enum PaneResize in Packages/macOS/CmuxNext/Sources/CmuxNextBridge/PaneResize.swift: +21 lines counted as Swift logic (its row counts it as logic)

plans/cmux-next/layer-ownership.md says Rust owns every state, list and rule and Swift is the platform shell. Please move the new logic of this PR in a follow-up: to Rust (the crate that owns the area; the owner lists in STATUS 12147-12165 name the target per area), or at least out of the app into its package. If a declaration above is platform code (AppKit, Core Animation, OS calls, strings, tokens, DEBUG tooling), reply here and the table gets a PLATFORM row with your reason instead.

The swift-logic row is a report, not a gate: nothing blocks on this comment.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 9c778d19 Deployed Nov 2, 2026 by cmux-bot[bot] via wait for same-key tree publisher #102347
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant