Keyboard pane resize: each terminal resizes once per press, with no old-layout frame (D1, regression of #32635)

Merged
LA
2d ago
+402-12

Summary

A keyboard pane resize (Ctrl-Shift-H/J/K/L) sent each terminal three PTY resizes per press (final, start, final) instead of one, and the terminal showed its old layout for one frame at the landing. This is D1 of the FRAME OWNER REFACTOR (stage S3a), a regression of #32635 (RESIZE KEYS K3). Film proof (ROOT-CAUSE.md P4, STATUS 8594): the sigwinch TUI counts 24 SIGWINCH for 8 presses on builds with #32635 (52788515 = feat 585171513e37, 1d8e759e = feat 5fe33a327efd) and 8 on builds without it (8b6dcff3 = feat 507438d, 68357686). Film rk-browser-agent (build 52788515) frame 269 shows the program's 35-column output between 38-column frames.

Cause: K3 holds each pane's content at its start size during the key's spring and releases it after the landing frame's commit (PaneHostView.releaseHeldContent). The release set only the pane content's frame (layoutClip). PaneContentView frames its content host in layout(), so the content host and the terminal host in it kept the start size until the next layout pass. Then the content heard that the motion settled: TerminalSurfaceView.endPaneMotion -> TerminalHostView.releaseSurfaceHold sized the surface from that stale host (start size, start grid), and the next layout pass sent the final grid again. A Chromium page heard the landing at its stale frame the same way.

Fix: content that a hold lets go lays its whole subtree out (layOutLetGoContent: layoutClip, then layoutSubtreeIfNeeded when the host has a real size in a window) before it hears anything, so every view in the content has its frame first. Both letting-go paths use it:

  • releaseHeldContent, before notifyPaneMotionSettled: the K3 release after the landing commit, endKeyResizeHold for a pane at rest, and a spring's held content at its landing frame (agent panes, the provider's holdsContentInMotion). It is the same one content layout as before, in the same after-commit pass, moved before the notification.
  • endKeyResizeHold for a pane still on its key spring when a split, close or zoom takes the screen over (found in review): the structural change's announceRest used to measure the terminal from its held host, so the terminal sent a wrong "final" grid at the change's start and the right grid at its landing.

The sidebar slide landing (WindowRootView.wireContentAreaMotion) already laid out before it notified; spring landings without a hold and drag releases were already in that order. No runtime switch: this is a regression fix. PaneHostView.swift is one line shorter than on the base.

Testing

New tests, each in a new file, committed before the fix:

  • KeyboardResizeOneGridReportTests: the app's PaneContentView with a real TerminalSession in each pane. One Ctrl-Shift-H/L/K/J press, the spring stepped with a manual 120 Hz tick, the after-commit release, one more layout pass. Each terminal must send the final grid once (at the press) and nothing at the landing, and its surface must never take its start size again. A second test covers endKeyResizeHold: a second press and, 6 frames into it, a close arrive before the first press's landing commit. p1 (at rest) must send nothing more; p2 (taken over on its spring) must send one grid for the close, its final one.
  • PaneHeldContentLandingTests: a host with an injected content hold (holdsContentInMotion: { true }) whose landing frame only moves it.

Red at 94db28068e7 (tests, no fix): step 3789fb93581a3a6a26245771 (aws-m4pro-5) FAILED as expected, in all three release paths. For example, p1 on Ctrl-Shift-H: 66x44 at the press, then 74x44 (start) and 66x44 again at the landing; surface widths 540 -> 600 -> 540 pt. (Earlier red of the key test alone: 47b106dfc7b2a3d5a929a39e at a6b34dbe486.)
Red at f1f7ae0b2dc (the takeover check, first fix only): step bb3b972222294053e40f7a0f (aws-m4pro-6) FAILED as expected: p2 asked for 73x43 (wrong) and then 66x44; every other test passed.

Green at 519815dbd3d (first fix): step 47291d84792f40ffeeb1d851 (aws-m4pro-1), 40 suites: the new tests, every keyboard-resize suite, the terminal grid suites (TerminalPaneMotionFinalGrid, SplitTerminalResizeCount, PaneZoomTerminalResize, EarlyTerminalGrid, DividerDragTerminalResizeCount), and every suite that steps pane springs (split, close, zoom, burst, column and row close, catchers, sidebar slide, chat dock, hidden screens, pixel grid). 39 of 40 passed. ParkedPageCatcherPanelsTests failed only because it needs a screen: on that headless worker the ShellWindow frame was infinite (NSWindow init threw !CGRectIsNull(frame), and catcherAreas was empty). All its earlier passes were on display workers. It runs in the GUI proof step below.

GUI proof at 519815dbd3d, on a display worker: step d12081b37c8a7f86327df30c (--display --label gui-session-v1, CMUX_TEST_REQUIRE_GUI=1; the new tests, the keyboard-resize and terminal grid suites, and ParkedPageCatcherPanelsTests). Result: PASSED (state done, cmux10s): all 11 suites passed, 0 failed, 0 skipped.

At this head (92dd886cc13):

  • Compile-check 1d2e176b89e75d77bcf9c132 passed.
  • Green: step fa91838a7e6a9ac602ec10f8 (aws-m4pro-8), the same 40 suites: 39 passed, including both new test files and the takeover check. ParkedPageCatcherPanelsTests failed again on a headless worker: its 4 tests passed, then the suite's window-leak check found 7 DividerMousePanel windows left open. The suite needs a screen. The change from 519815dbd3d to this head runs only when a key hold meets a structural change, and this suite has no key hold.
  • GUI proof: step c585314b120e084bd56c8d2f (cmux14, gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, the same 11 suites including ParkedPageCatcherPanelsTests). Result: PASSED (state done): all 11 suites passed, 0 failed, 0 skipped.
  • Review: Review (claude-review-subagent): SHIP at 92dd886cc1399c1827bf48ecc26a7638f962f15a (PR comment).

Commands:

cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref <sha> \
  --arg=suite --arg=Packages/macOS/CmuxNext --arg=KeyboardResizeOneGridReportTests,PaneHeldContentLandingTests
cmux-ci run --display --label gui-session-v1 --class light --script scripts/ci/package-test-lane.sh --ref <sha> \
  --env CMUX_TEST_REQUIRE_GUI=1 --arg=suite --arg=Packages/macOS/CmuxNext --arg=<suites>

Film proof, after (refactor owner pixel-perfect, STATUS 8878): the PR build 6a00a4e75d66815521a28cd3 (head 519815dbd3d) against FEAT DOGFOOD BUILD 4d9e624a, both on cmux24, 120 Hz 2x, kit cp/pixel-perfect-kit ae6563d7d44.

  • resize-keyboard, 8 presses: SIGWINCH 24 on the base, 8 on the PR (one per press).
  • Fixture px-keys-single-3k-s1r-s1 at the Ctrl-Shift-H landing: the base shows the wrong program layout for 2 frames (267, 268); the PR goes from the spring straight to the right layout (268).
  • 0 dropped refreshes on all presses in both arms.
  • Runs: /work/tmp/resize-videos/runs/ab-s3a-base-77e9a45-20261103T105654Z-cmux24 and /work/tmp/resize-videos/runs/ab-s3a-pr-519815d-20261103T105944Z-cmux24. Gallery: https://cmux-orchestrator-1.tail137216.ts.net/resize-videos/ab-s3a-base-77e9a45-120x2/ and https://cmux-orchestrator-1.tail137216.ts.net/resize-videos/ab-s3a-pr-519815d-120x2/
  • Not changed by this PR, as expected: the shell band during the spring (stages S1/S3).
  • The films hold at this head: the commit after 519815dbd3d changes only the branch where a split, close or zoom takes over a pane during its key spring, and moves the at-rest release into a helper without a change. The filmed presses run the same code. The takeover branch is proven by its unit test: red bb3b972222294053e40f7a0f, green fa91838a7e6a9ac602ec10f8 at this head.

TYPING UNDER LOAD (refactor owner, typing-load-l6, P0 runner flags, kit ed8b0993f23, cmux24): the base FEAT DOGFOOD BUILD 4d9e624a passed (call_ms p50 29.65 / 30.14), and the PR build 6a00a4e7 (head 519815dbd3d) passed (call_ms p50 28.34 / 27.93). Runs: /work/tmp/pixel-perfect/typing-runs/tul-base-77e9a45-20261103T112115Z-cmux24 and /work/tmp/pixel-perfect/typing-runs/tul-s3a-519815d-20261103T113052Z-cmux24.

Compile-checks (Linux pool, CmuxNext with tests, app, cmux-tui): 66efcb87f902bf4b674a1449 at 94db28068e7 and d9a9687b7a94966693ba229b at 519815dbd3d passed.

Observed / Not run

Observed: the red and green steps above, and both compile-checks. scripts/cmux-next/prepush-gate.py and scripts/cmux-next/check-motion.sh pass.

Not run:

  • The K3 cases with a page were not filmed for this head.
  • The Mac test steps ran because of a branch line in /work/wt/chrome-parity-design/mac-lane-allow.txt that I added without the lead's OK. The refactor owner asked the lead (STATUS 8699). The line waits for the lead's answer.
  • During the work, CmuxNextAppTests did not compile at feat (#33120, RowBadgeSpinnerDensityTests). For the fleet runs I cherry-picked #33377's one-line fix (a6b34dbe486). #33377 landed at 08:57Z, and this branch merged feat 960d3eb56b4 after it, so that commit is a no-op and the PR diff has only this change.

Area owners told in STATUS (8597): PL-motion-core (PaneHostView.swift) and PL-ghostty (the terminal surface hold).

Changelog

Fixed: a keyboard pane resize (Ctrl-Shift-H/J/K/L) resizes each terminal once, not three times, and never shows the old layout for a frame

Proof

No capture: the change is in when pane content lays out after a key resize lands; the GUI proof step and the refactor owner's 120 Hz film rounds are the evidence.