Frame owner S4 (switch on): a key resize draws each terminal at its pane's size with its prompt in view (m14, no low-prompt hold)

Merged
LA
1d ago
+406-113

Summary

With motion.frameOwner.terminals on, a key resize (Ctrl-Shift-HJKL) now draws every terminal at its pane's size in every frame, with its prompt in view. This is the last terminal failure of cmux-ad m14 in key springs that pixel-perfect's films show (F2, films jjk-3kz: 75 frames ON, 77 OFF), plus the shrink case H3. The program's grid still changes once per press.

  • F2 (a pane that grows, prompt low). restAt held the surface at the press's final size on the pane's bottom edge (the low prompt's bottom hold, #35470 and #35914). The surface was taller than its pane, so its top rows were cut for the whole spring.
  • H3 (a pane that shrinks, prompt low). TerminalGridPolicy.previewsViewGrid turned Ghostty's reflow preview off while a reported final grid did not fit the view (the old m6 rule). The pane drew the owner's grid from its top-left, so its last rows, the prompt among them, were cut until the daemon announced the new grid. After that it drew the new grid with blank rows under it.

The fix keeps the reflow preview on in every frame of such a spring. Ghostty's ReflowPreview reflows a copy of the live terminal to the grid the view fits with Terminal.resize. That resize keeps the cursor row in view: rows that shrink lose the blank rows below the cursor, then the top rows go to history; rows that grow pull history down while the cursor is on the last row. The preview is render-only, so the PTY grid does not change.

One decision now drives the frame, the hold and the preview: TerminalHostView.surfaceFollowsArea (the host follows its pane through a key hold, and the switch is on).

  • followedSurfaceFrame gives the surface its area's frame, whatever hold it has. The low-prompt branch from #35914 is removed: with the switch on it had no key-spring use left. The switch-off path keeps its own bottom hold (hold.producerFrame and restAt's motionAnchor).
  • restAt holds nothing for a following host and lets go of an earlier hold whatever the anchor. Before, G1 let go only for .top. The final grid still goes once per press (reportFinalGrid).
  • updateReflowPreview passes the decision to TerminalGridPolicy.previewsViewGrid(followingPane:). The policy stays a pure value type; the old rule applies to every view that does not follow its pane.

Unchanged: the switch off (F2 stays until the deletion PR removes that path), and split, close, zoom and divider-release springs (no following host; m16 (a) owns them). A divider drag is not affected either, whatever state the switch has: a drag hands no follow (followedHold stays nil), so the preview keeps the old rule, and no final grid is reported during a drag, so that rule keeps the preview on as before. The drag gesture (TerminalSurfaceView+PaneMotion.swift 94-117) and TerminalSurfaceView+FrameOwner.swift are not touched. Owner of CmuxNextTerminal and CmuxNextTerminalGeometry: PL-ghostty (session cmux-b6). The two perf-hot functions touched (followedSurfaceFrame, updateReflowPreview) stay O(1) with no main-thread wait: they add one view cast and one switch read per frame.

Frames: terminals, switch on, a key resize. Before: a terminal whose prompt is in its lower half stays at the press's final size on its pane's bottom edge while its pane grows, taller than the pane, with its top rows cut until the landing. While its pane shrinks, it shows its old rows from the top, with the prompt cut off at the bottom until the daemon answers, then the new rows with a blank band under them. After: every terminal has its pane's size in every frame and shows the rows that size fits, with the prompt on the last row. Rows go up into history as the pane shrinks and come back as it grows. The program gets one new size per press, as before. Switch off: no change.

The join question

A split or close spring held a surface at its final size, then a key press comes before the landing. With the switch on, does the followed surface always get its area's frame?

  • A pane still on the split or close spring when the key comes starts no key hold. ScreenContentView.applyPresentation starts one only for a host not already on a layout spring (startsKeyHold = springing && holdsContentForKeyResize && !host.isOnLayoutSpring), and PaneHostView.isOnLayoutSpring.didSet runs only on a change. Its terminal host never follows, so its surface keeps the structural hold, which restAt moves to the key press's final size, until that spring lands. endPaneMotion then lets it go. This is the same with the switch on or off. New test: PaneContentPolicyTests.aKeyPressDuringACloseSpringFollowsOnlyThePanesThatWereAtRest.
  • A host that does follow now always gives its surface the area's frame, by construction. followedSurfaceFrame no longer reads surfaceHold when surfaceFollowsArea, and restAt lets go of any hold at each press. Before this PR that was not so: a .bottom hold taller than the area (a press's, or a join's set before its area handed it the follow) kept the surface on the bottom edge. TerminalSurfaceCommitSizeTests.aFollowedSurfaceTakesItsAreasFrameWhateverItsHold covers it.

Testing

Regression test (display suite, a window that draws, real Ghostty surfaces): TerminalKeyResizeFrameTests.aLowPromptTerminalDrawsTheGridItsPaneFitsInEveryFrameOfAKeySpring(presses:). The rig is p1 over p2, each with history and its prompt on its last row. Each terminal has an IO that announces each grid it is asked for, as the daemon does (AnnouncingTerminalIO, MANUAL_MIRROR), and the switch is on. The presses are Ctrl-Shift-J on p1, Ctrl-Shift-K, and J J K two frames apart. Each frame's sizes land with their own frames before the next frame. In every frame, each surface must have its area's frame, and the grid Ghostty drew (ghostty_surface_drawn_grid) must be the grid the shown frame fits (ghostty_surface_fit_grid). After the landing, each terminal must rest at its area with the owner's grid drawn and its prompt on its last row, and its program must have got at most one grid per press.

Red at the test commit b3c8edd85ac (both runs on the fleet):

  • Display (cmux-ci run --display --label gui-session-v1 --class light --script scripts/ci/package-test-lane.sh --ref b3c8edd85ac954394dd0dc5adf329183e993b68d --arg=suite --arg=Packages/macOS/CmuxNext --arg=TerminalKeyResizeFrameTests,TerminalFrameOwnerMainThreadTests,TerminalSurfaceCommitSizeTests): step 9674722a52c4724a7f728e7a on cmux9s-Mac-mini, failed:
    • TerminalKeyResizeFrameTests.aLowPromptTerminalDrawsTheGridItsPaneFitsInEveryFrameOfAKeySpring failed in all 3 cases, for the hold and for the drawn grid, with every frame's size shown with its own frame ("frame not shown 0"):
      • J: F2 in 13 frames, "frame 1 p1: surface (0.0, 0.0, 1000.0, 301.0), its area (0.0, 0.0, 1000.0, 273.0)" through the area at 299 pt; H3 in 10 frames, "frame 1 p2: drew 124x13, its frame fits 124x15" through "fits 124x14". PTY grids per terminal [1, 1].
      • K: the same with the panes swapped. F2 is on p2 (its pane grows up, the 301 pt surface on the bottom edge), 13 frames; H3 is on p1, "drew 124x13, its frame fits 124x15", 10 frames.
      • J J K: F2 on p1 in 7 frames (the second J's hold, 331 pt, in a 282 pt area); H3 on p2 in 5 frames ("drew 124x12, its frame fits 124x14"). PTY grids [3, 3].
      • The suite's other two tests passed.
    • TerminalSurfaceCommitSizeTests.aFollowedSurfaceTakesItsAreasFrameWhateverItsHold failed for the hold: the taller hold waited for a frame, and the surface landed at 600x340 in a 600x270 area.
    • TerminalSurfaceCommitSizeTests.aReduceMotionKeyResizeNeverHoldsTheMainThreadForTheTerminals also failed there (2 presenter fallbacks, presented 8 < 10, two layer/frame mismatches). That is not this change: the commit changes tests only, and this test is not among them. It is a timing test (frames later than the keep limit) on a slow host (this step built for about an hour). It passed at the fix commit on cmux10s (below).
    • TerminalFrameOwnerMainThreadTests passed.
  • Non-display (the same lane without --display, suites PaneContentPolicyTests, TabStripFollowsPaneTickTests, KeyResizeFirstTickPinTests, PaneGroundCoversPaneTests, KeyboardResizeContentHoldTests, KeyboardResizeReflowAfterLandingTests, TerminalPaneMotionFinalGridTests, TerminalGridPolicyHoldTests): step 8a1b915d3aa7ef69fa469769, failed in PaneContentPolicyTests only, on the four changed switch-on tests, for the hold:
    • aPromptOnTheLastRowStaysInViewInAPaneAKeyGrowsDown(switchOn: true): (taller → 14) == 0; frames 1-18 "p1's surface (1188.0, 411.0) in a (1188.0, 374.0) area" and "p1's surface keeps a hold" through frame 25.
    • aLowPromptPressOnTheOtherAxisKeepsTheSurfaceOnItsArea: "p4's surface (0.0, 0.0, 624.0, 411.0), its area x 0.0 top 373.0 size (624.0, 373.0)" and "keeps a hold" in every frame.
    • aLowPromptGrowDownTwiceThenUpKeepsTheSurfaceOnItsArea: "the K press's first frame: p1's surface 451.0 pt tall, top 451.0, in a 394.0 pt area", and "keeps a hold" through frame 19.
    • aReversePressAfterALowPromptGrowsDownFollowsItsPane(switchOn: true): (held → 4) == 0.
    • The switch-off arms and the other 7 suites passed.

Green at the fix commit ac1e0c79c6e, and at the head c9888ee0cc9 for the join test:

  • Non-display, the same 8 suites at ac1e0c79c6ece72765d5c3436c72647faa44c662: step a81f55e586617ab5218c7d4f, all passed (PaneContentPolicyTests 19 tests, the 4 changed ones among them; TerminalGridPolicyHoldTests 7, with the new case).
  • Display, the same 3 suites at ac1e0c79c6ece72765d5c3436c72647faa44c662: step cd828014760da40d8c2b13de on cmux10s-Mac-mini, all 3 suites passed and every test ran:
    • The new test showed no frame off its area, no drawn grid off and no frame not shown in all 3 cases (J, K, J J K), with PTY grids per terminal [1, 1], [1, 1] and [3, 3].
    • The existing m6 test reported "misplaced 0, unsized 0, stretched 0", PTY grids [1, 1].
    • TerminalSurfaceCommitSizeTests passed, 7 of 7.
    • TerminalFrameOwnerMainThreadTests passed.
  • PaneContentPolicyTests at the head c9888ee0cc9a5ddd48058d6387fdf110a105d809 (adds the join test): step 955ffc1b3958526bcb35fb50, 20 tests passed.
  • Display (measurement step on a perf mini, --display lane, label gui-session-v1) at the head c9888ee0cc9a5ddd48058d6387fdf110a105d809: step f0fc19fcda4ba87fdc633dec on cmux17-perf, exit 0. Suites: KeyResizeFirstTickPinTests, KeyboardResizeContentHoldTests, KeyboardResizeReflowAfterLandingTests, PaneContentPolicyTests, PaneGroundCoversPaneTests, TabStripFollowsPaneTickTests, TerminalFrameOwnerMainThreadTests, TerminalGridPolicyHoldTests, TerminalKeyResizeFrameTests, TerminalPaneMotionFinalGridTests, TerminalSurfaceCommitSizeTests (all CmuxNextAppTests). Added by the perf lead.

Existing tests whose switch-on expectation changed. In each, m14 replaces F2: with the switch on, the surface has its area's frame and holds nothing. No switch-off assertion changed.

  • PaneContentPolicyTests.aPromptOnTheLastRowRidesTheBottomOfAPaneAKeyGrowsDown is now aPromptOnTheLastRowStaysInViewInAPaneAKeyGrowsDown. Its switch-on arm expected the surface taller than its pane in more than 3 frames (the bottom hold). It now expects that in no frame, and the surface at its pane's size with no hold. The switch-off arm is unchanged.
  • aLowPromptBottomHoldAfterAPressOnTheOtherAxisKeepsItsPanesWidth is now aLowPromptPressOnTheOtherAxisKeepsTheSurfaceOnItsArea. It required a bottom hold to happen and checked only the leading edge and width. It now checks the whole frame (leading edge, width, top, height) and that no hold is kept.
  • aLowPromptBottomHoldNeverLeavesABandAboveTheTerminal is now aLowPromptGrowDownTwiceThenUpKeepsTheSurfaceOnItsArea. It required a bottom hold after the K press and checked the surface was not shorter than its area. It now checks the area's height on its top edge and that no hold is kept.
  • aReversePressAfterALowPromptGrowsDownFollowsItsPane: its switch-on arm required the grow-down press to hold the surface taller than its pane. It now expects no such frame. The switch-off arm still requires the hold.
  • TerminalSurfaceCommitSizeTests.aKeptFrameOfALowPromptHoldNotTallerThanItsAreaStaysTopLeft is now aFollowedSurfaceTakesItsAreasFrameWhateverItsHold. A followed surface given a bottom hold taller than its area moved to the bottom edge and asked for that size. It now keeps the area's frame and asks for no other size. The first half (a hold not taller than its area, its kept frame top-left) is unchanged.
  • TerminalGridPolicyHoldTests: previewsViewGrid is now previewsViewGrid(followingPane:), with a new case for a following view.

TerminalKeyResizeFrameTests.PaneScreen gains axis: (as the unmerged R1 branch fix/frame-owner-s4-r1-reduce-motion names it) and announcesGrids:. Whichever lands second merges the two init parameter lists.

Observed / Not run

Observed: the fleet runs above, at the named commits: the red results, the green results and the join test.

Not run:

  • No performance A/B and no pixel-perfect film; frame-owner-s4 runs those.
  • No tagged dogfood build and no macOS 14 or 15 run.
  • The test reads the grid Ghostty drew, not the frame's pixels. "Prompt in view" in each frame follows from that grid being the one the view fits, plus Terminal.resize's cursor rule. The test checks the cursor's row directly only at rest.

Not changed and still open: a parked tab shown during a key hold calls restAt before its area hands it the follow (one layout pass later). It can still ask Ghostty for one frame at the press's final size (F3 of #35470's review; pre-existing, not F2).

Changelog

none

Proof

No capture: motion.frameOwner.terminals is off by default, so the default app draws nothing new. frame-owner-s4 films the switch-on path with pixel-perfect.