Skip to content

RESIZE KEYS K4 follow-up: a row's minimum rounds up, so no dead row key press (review notes A, B, C of #32471) - #32550

Merged
lawrencecchen merged 7 commits into
feat-cmux-nextfrom
fix/resize-keys-rows-followup-3
Nov 2, 2026
Merged

lawrencecchen merged 7 commits into
feat-cmux-nextfrom
fix/resize-keys-rows-followup-3

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-ups of the RESIZE KEYS K4 review (#32471, re-review SHIP at 76e9d24, comment 6585271874, notes A, B, C):

  • C (code, final form after review): a row's minimum height in permille 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 (Ctrl-Shift-K / Ctrl-Cmd-Up) that changed the model and moved no pixel: a dead press. ShownResizeRange.rowHeights now takes the largest stored permille that still shows at the minimum on the screen's pixel grid (the backing scale comes from LayoutRootView), and each press direction checks only its own bound.
  • A (test): a scrolling stack [520, 500] pressed Up stops exactly at [500, 500] (the P0 RESIZE KEYS K4: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up move a row edge one exact step #32471 test could not fail for this).
  • B (test): the middle of three rows moves its lower edge; the top row stays.

Testing

On the fleet (scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext ...), compile-checked first. Same three commits as on the earlier branch (fix/resize-keys-rows-followup-2), cherry-picked onto feat after #32471 landed:

  • First red ecf6522: step e7a1ebcb854157ef99fd6a3e PASSED: no red (its few pane stacks never hit a minimum just under a half permille). The test was rewritten.
  • Red 3f9caa0 (test C sweeps the minimum content height 64...400 pt by 0.25): step 0238a2a684d3c6729caf3137 FAILED as expected: 54 dead presses. A and B pass on the old code (they pin P0 RESIZE KEYS K4: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up move a row edge one exact step #32471's behavior).
  • Fix ff9dbbe: green + GUI proof (display worker gui-session-v1, CMUX_TEST_REQUIRE_GUI=1) step ec668ceba17c41f19174e264 PASSED: KeyboardResizeRowsFollowupTests 3/3, KeyboardResizeRowsEdgeCasesTests 5/5, KeyboardResizeRowsTests 2/2, KeyboardResizeShownLimitTests 2/2.
  • This head 77abc43: compile-check runs; GUI proof at this exact head runs before ready-to-land.

Review FIX FIRST at 77abc43 (comment 6587348634): the floor rounded up from the exact placement could show one device pixel above the minimum, and dead presses remained at 1x. Then:

  • Red aead116 (KeyboardResizeRowsPixelFloorTests at 1x and 2x; ShownResizeRange takes the backing scale): step 1165ac076f27889b2fc1771f (aws-m4pro-4) FAILED as expected (dead presses at 1x, rows stopping above the minimum at 1x and 2x).
  • Fix 15df074 (floor = the largest stored permille that still shows at the minimum on the pixel grid): green + GUI step 7d3cdbc426944307db777996 (cmux14) FAILED: a real fault: Up was refused 0.5-2.5 pt above a tall row minimum where the two rows' minimums do not both fit (stepped() refused every press when low > high, but high bounds Down only).
  • Fix 429fea2 (each direction checks only its own bound): compile-check d1c80cab9116595a31aa0175 PASSED; green + GUI proof (display worker gui-session-v1, CMUX_TEST_REQUIRE_GUI=1) at the exact head 429fea2: step 307a58a0d37a11f3b30f3c21 on cmux-austin-mini-0 PASSED, 18 test cases, none skipped (these suites need no daemon): PixelFloor (1x, 2x), Followup 3/3, RowsEdgeCases 5/5, Rows 2/2, ShownLimit 2/2. Five identical submissions coalesced into this one step; the suites have no timing, so a repeat adds no evidence.

Observed / Not run

  • Observed: the steps above.
  • Not run: a film; the dead press needs a row stored exactly one permille above its minimum, which a person reaches only by a drag in narrow cases.

No capture: geometry rounding only.

Changelog

  • Fixed: Ctrl-Shift-K / Ctrl-Cmd-Up next to a row's minimum height no longer takes a key press that moves nothing.

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


Summary by cubic

Fixes the dead row-resize key press next to a row's minimum so Ctrl-Shift-K / Ctrl-Cmd-Up always moves an edge or is refused at 1x and 2x.

ShownResizeRange now takes the screen's backing scale and derives the floor as the largest stored permille that still shows at the row's minimum on the pixel grid, replaying RowStackGeometry's placement. This replaces both the nearest-permille rounding (up to half a permille too low, causing dead presses) and the exact-placement rounding up (which could show one pixel above the minimum and still dead-press at 1x). Up presses now stop only at the upper row's minimum, so Up still moves the edge where both rows' minimums cannot fit — the lower row only grows.

  • Adds tests for review notes A, B, C of P0 RESIZE KEYS K4: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up move a row edge one exact step #32471: a scrolling stack stops exactly where it fills the column, a middle row moves only its lower edge, and a sweep of minimum content heights catches previously dead presses.
  • Adds a 1x/2x pixel-floor test: from any stored height just above the minimum, Up presses either move the edge or are refused and end with the row at its minimum.

Written for commit 94e5501. Summary will update on new commits.

View guided diff Turn on auto-fix

…ve 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)
…ht, 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)
… 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)
@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 429fea2: 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=pr32550 -f ref=fix/resize-keys-rows-followup-3 -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): FIX FIRST at 77abc43

I read the diff 2fdcbbae7e44..77abc43df418 (3 commits), RowStackGeometry.compute/pixelHeight/permille(forPixelHeight:), SplitGeometry.roundToPixel/ceilToPixel, ScreenGeometry.layoutContent, DockStripGeometry.place, ScreenContentView.reconcile, LayoutRootView.shownResizeRange and PaneResize.change. I read the two fleet steps myself. I did not build or run the Swift code. To check the rounding I ported the compute formulas to a short Python script (two filled rows, sum 1000, minimum heights 64...400 pt in 0.25 pt steps). The counts below come from that port, not from the app.

Finding (blocking): the floor is exact in continuous permille, not on the pixel grid

Whether a press moves the edge depends on max(roundToPixel(pixelHeight(p)), ceilToPixel(minimum)). It does not depend on pixelHeight(p) >= minimum. ceil((m + gap) / (H + gap) * 1000 * scale) is the smallest permille whose unrounded height reaches the minimum. That permille can still show one device pixel above ceilToPixel(minimum). In that case floor - 1 shows at the minimum, and the press to it would move the edge one pixel. The new comment says "a permille below this shows at the minimum, so a press there would move nothing". The first half is true. The second half is false whenever shown(floor) > ceilToPixel(minimum).

Worked example (H 857, gap 0, scale 2, row minimum 100.5 pt, so ceilToPixel = 100.5):

  • exact = 100.5 / 857 * 1000 = 117.27. The new floor is 118. The base floor (nearest) is 117.
  • p = 118: 101.126 pt, which rounds to 101.0. p = 117: 100.269 pt, which rounds to 100.5 (max with 100.5 gives 100.5).
  • On the base, Up from 118 sends 117 and the edge moves 0.5 pt to the minimum. With this PR, Up at 118 returns nil. The edge stops one pixel above the minimum, and the press is refused while a pixel could still move.

Port counts, as (dead presses F+1 -> F, refusals one pixel above the shown minimum), base -> head:

scale H gap base head
2 857 0 52, 127 0, 701
2 841 8 56, 132 0, 722
2 450 0 374, 0 150, 299
1 857 0 597, 0 283, 208
1 841 8 684, 0 308, 173

(n ≈ 1250 to 1345 minimum heights per row.) So:

  1. At 2x with a column taller than about 500 pt, the PR removes the dead presses. But for about 45% of minimum heights (574 of 1254 at H 857), the press now stops one pixel short where the base reached the minimum. This is a behavior change that the PR does not describe.
  2. At 1x (any non-Retina display: baseGeometry uses the real scale) and in short columns at 2x, dead presses remain. They are fewer than on the base, but they are not gone. Example at 1x, H 857, minimum 100.25: floor 117 shows 101 (clamped from 100.269 -> 100) and 118 shows 101 (101.126 -> 101), so Up from 118 is a dead press. The changelog line ("no longer takes a key press that moves nothing") is not true there.

Suggested fix: compute the floor on the pixel grid at the screen's scale. Use the largest permille whose placed height still shows at ceilToPixel(minimum), which is ceil((ceilToPixel(m, s) + 0.5 / s + gap) / (H + gap) * 1000 * sumScale - 1e-9) - 1, at least 100. Then every press from above that floor moves the edge, and the floor is exactly the shown minimum. In my port, this gives 0 dead presses and 0 one-pixel-short refusals for every row above at both scales. ShownResizeRange does not have the scale today (ScreenGeometry does not store it). Pass reconciledScale through, or store it on ScreenGeometry. This floor can be up to one pixel below the continuous minimum (the row then shows clamped to ceilToPixel). Check the lower row (high) and the last-row fill tolerance (<= available + 0.5) with it before using the same function for both rows. Extend test C with scale 1, a short viewport, and a second assertion: when a press at the floor returns nil, the upper edge already equals the edge at the minimum.

What I checked and found sound

  • Scale factor. scale = sum / 1000 for filled stacks matches compute's factor = 1000 / sum (stored p places at p * 1000 / sum). Scrolling stacks use 1. fills matches compute's fits || sum <= 1000. The - 1e-9 only keeps an exact integer from rounding up by float noise, which is harmless.
  • Division. The old permille(forPixelHeight:) guarded denominator > 0. The new code does not, but stripHeight = max(1, ...) (DockStripGeometry.swift:219) and stripGap = max(0, ...), so H + gap >= 1. Int(...) cannot trap.
  • Lower row in high. pair - floor(lower) keeps the lower row at a permille whose unrounded height is at least its minimum, which is not worse than the base (the base could be half a permille under). The same pixel-grid comment applies: Down can be refused one pixel before the lower row reaches its shown minimum.
  • No other code change. The diff touches only floor(_:). The 1000 clamp from permille(forPixelHeight:) is gone. A floor above 1000 * scale gives low > high -> nil, which is the same result as the base.
  • Tests. Test C is a deterministic loop (1345 iterations, no sleeps, no async). It ran in 0.073 s green and 0.093 s red, and checked > 0 keeps it from passing vacuously. Tests A and B pin P0 RESIZE KEYS K4: Ctrl-Shift-J/K and Ctrl-Cmd-Down/Up move a row edge one exact step #32471 behavior and passed on the red, as the body says.
  • Fleet steps. Red 0238a2a684d3c6729caf3137 (ref 3f9caa0, aws-m4pro-8): failed, aRowOnePermilleAboveItsMinimumNeverTakesADeadPress with 54 issues (for example, "content height 64.75: Up sent [114, 886] and the edge stayed at 93.0"). The other two tests passed. Its ShownResizeRange.swift blob (6b0348f309ca) is the base blob, and its test blob (e2fbbb8ee42d) is this head's. Green ec668ceba17c41f19174e264 (ref ff9dbbe, gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, exit 0): Followup 3/3, EdgeCases 5/5, Rows 2/2, ShownLimit 2/2. Its source and test blobs equal this head's. I did not see a step at 77abc43 itself.

… 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.
…ill 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.
…d, 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.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): SHIP at 429fea2

I read the diff feat-cmux-next...429fea28b2a (6 commits; source: ShownResizeRange.swift, LayoutRootView.swift), RowStackGeometry.compute/pixelHeight, SplitGeometry.roundToPixel/ceilToPixel, ScreenGeometry.layoutContent, ScreenContentView.reconcile, DockStripGeometry and PaneResize.change/press. I did not build or run Swift. I ported compute and rowHeights (head floor, the base nearest-permille floor and the 77abc floor, old and new stepped) to Python with Swift's rounding (ties away from zero). All counts below come from that port, not from the app.

What I checked

  1. The floor is exact on the pixel grid. The search finds the largest stored permille whose placed height, rounded to a pixel at self.scale, is at most ceilToPixel(minimum). shown is monotone, so the two loops end at that value. The two scales are used correctly: the local scale (sum / 1000) converts stored to placed permille, self.scale is the pixel grid. Two-row filled stacks, H in {857, 841, 450, 300, 1100, 1300, 856.5}, gap {0, 8}, 1x and 2x, minimum 64...400 pt by 0.25, starts floor-2...floor+3 and 500: Up always ends at exactly ceilToPixel(minimum) (0 stops above it; base and 77abc had hundreds). Scrolling stacks use the same placement (factor 1), so the same holds there.
  2. Direction-only bounds. Down's next is at most high, so the lower row keeps >= floor(lower) and stays <= 1000. Up's next is at least low >= pair - 1000, and next < shownUpper can only be true when next < rows[upper].height, so the lower row only grows. With low > high, Down cannot pass high > shownUpper (that would need a lower row stored above 1000), so it returns nil. Port, 580,751 presses where both minimums do not fit: 0 out of range, 0 dead, 0 wrong direction, 0 Down presses that shrink the lower row under its minimum, 0 Up stops above the minimum. Random sweep, 78,074 presses (2 to 4 rows; sum 1000, sum below 1000 and scrolling; H 150...1400; gap 0/4/8; 1x/2x): 0 out of range, 0 wrong direction, 0 sum changes in filled stacks, 0 rows above the edge moved, 0 upper rows under their minimum.
  3. Dead presses. Up: 0 at every H/gap/scale above, with one exception. At 1x, when the floor is the model bound 100 (the minimum is lower than 100 permille's height), the part step 101 -> 100 can move nothing (H 857, gap 8: 57 presses). All 57 are that case, and the base does the same. Short columns at 2x (450, 300) have 0 dead Up presses now (base: 364 and 465).
  4. Scale. reconcile sets reconciledScale = scale on the line just before baseGeometry = ScreenGeometry.compute(..., scale: scale), so the two always match. The fallback view.scale is used only before the first reconcile. A canvas has no row stacks, so rowHeights returns nil there. LayoutRootView.shownResizeRange is the only production caller.
  5. Tests and fleet. Both new suites are plain loops with no sleeps and no async. I read the logs myself. Red 1165ac076f27889b2fc1771f (ref aead116, aws-m4pro-4) failed. aead1169c6f adds only the scale plumbing to the source, so the floor is still 77abc's. The failures: 1x dead presses (for example "min 97.25 start 121: [120, 880] kept 98.0"), 189 stops above the minimum at 1x and 816 at 2x. 7d3cdbc426944307db777996 (ref 15df074) failed with 9 stops above the minimum at each scale, which is the low > high case that 429fea2 fixes. The test covers it because both rows share the swept minimum. Green 307a58a0d37a11f3b30f3c21 (ref = this head, cmux-austin-mini-0, gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, exit 0): PixelFloor (1x, 2x), Followup 3/3, RowsEdgeCases 5/5, Rows 2/2, ShownLimit 2/2 passed.

Non-blocking notes (follow-ups, not regressions in the PR's claim)

  • Fill-factor float form. shown uses Double(stored) / scale, but compute uses Double(height) * (1000 / Double(sum)). When the sum is not 1000, these can differ by one ulp at a half-pixel tie: 395 of 2.07 M (sum, H, gap, scale, permille) samples round to a different pixel. In the port this gives 33 Up stops one pixel above the minimum out of about 54k stops with sums 777 to 998 (example: sum 900, H 450, 1x, minimum 64.5: stops at 131 showing 66, where 130 shows 65). Fix: compute shown with the same expression as compute, Double(stored) * (1000 / Double(sum)).
  • Lost denominator guard. The old permille(forPixelHeight:) guarded columnHeight + gap > 0. The new exact divides by stack.frame.height + stack.gap and then calls Int(exact.rounded(.down)), which traps on inf/NaN. stripGap is 0 by default, and side-dock frames use max(0, ...) heights (DockStripGeometry line 240), not max(1, ...). I found no realistic way to reach a 0-height row stack with a key press (it needs a zero-height viewport, or the viewport-.zero init geometry on a screen that was never reconciled). A one-line guard stack.frame.height + stack.gap > 0 or an exact.isFinite check closes it.
  • Pixel quantization that remains (it also exists on the base, and it comes from compute). One permille can be more than one pixel, so a row edge cannot always land on the minimum pixel. (a) At Down's stop, the lower (last, filled-to-the-bottom) row can show 0.5 pt under its minimum. That is one device pixel at 2x, inside compute's own available + 0.5 tolerance: 526 of 1345 minimums at H 857 2x (base 451, 77abc about 0). (b) In tall columns, where one permille is more than 2 px (H 1100 to 1300 at 2x, and any H above 1000 at 1x), holding a row at its minimum can leave a filled stack 1 pt taller than the column, so it scrolls 1 pt (H 1300, 2x: 250 of 1345, base 41; at 1x: 332, base 478). (c) At 1x, Down can still take a dead part step where a tie rounds both rows up (H 450, gap 0: 160, base 880). The root cause for all three is in RowStackGeometry.compute: a fixed 0.5 pt tolerance and a held minimum with no redistribution. It is out of scope here. The changelog line speaks only about Up, and Up is correct.
  • Nits: rename the local scale to fillScale (two scales in one function are easy to confuse). Drop the scale: CGFloat = 2 default on the public init, so that a new caller cannot silently get 2x. The new tests cover Up on two filled rows at one viewport only. A Down sweep and a sum-below-1000 case would pin the notes above.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 22:43 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 22:43 Inactive
@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
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 22:43 Inactive
@lawrencecchen
lawrencecchen merged commit 9d7e696 into feat-cmux-next Nov 2, 2026
193 of 201 checks passed
@lawrencecchen
lawrencecchen deleted the fix/resize-keys-rows-followup-3 branch November 2, 2026 23:02

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 94e55017 Deployed Nov 2, 2026 by cmux-bot[bot] via wait for same-key tree publisher #102841
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