Skip to content

Frame owner S4 (m18/m26): key resizes show each terminal at its pane size in every frame (drawn ahead, never stretched); PTY size coalesced 25 ms; no key hold; switch deleted - #36478

Merged
lawrencecchen merged 35 commits into
feat-cmux-nextfrom
fix/frame-owner-s4-t-keys
Nov 6, 2026

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Frame owner S4, T-KEYS (cmux-ad m18 to m26). DRAFT until the head's fleet runs are green.

In a key resize (Ctrl-Shift-H/J/K/L), every frame shows each terminal at its pane's size, with its text re-wrapped at that size (Ghostty's render-only reflow preview). A frame is never stretched or cropped. The PTY size follows at most every 25 ms (Ghostty.app's rule).

How it works at head 5840aea:

  • Bounds only with their frame (rule 1). TerminalFramePresenter keeps ready-or-keep. A terminal's bounds change only in the commit that presents a frame Ghostty drew at those bounds. The pass runs before Core Animation's commit (order 1_999_999), never draws and never waits. The keepLimit fallback (bounds without their frame, which stretched it) is deleted. Long keeps (more than 2 refreshes) are counted in Stats.longKeeps as data.
  • Draw ahead in key springs (rule 2, the Chromium model). ScreenContentView.step steps a copy of each moving pane's spring once more, by MotionClock.nextFrameInterval: the display link's refresh period, 8.33 ms at 120 Hz or 16.7 ms at 60 Hz. The size change goes down PaneHostView -> PaneContentAreaView -> PaneMotionHoldFollowing.drawAhead(nextHostSize:) -> TerminalHostView. After this turn's pass, the surface gives Ghostty that size, render only (TerminalSurfaceView.applyAhead, no grid report). The renderer draws it during the refresh, and the next pass presents it with its bounds. A size drawn ahead stays until the pane's key hold ends: the final placement takes it, or dropAhead gives Ghostty the view's size back (B1, diag bf2b674a: a 2-tick timeout dropped it while the spring's next step was 16.6 ms away, so that refresh waited for its frame again).
  • No next size (rule 3). Divider drags, Reduce Motion jumps and hidden panes draw nothing ahead. Their last bounds and frame stay, clipped or beside the terminal's ground, until the new frame is drawn.
  • No off-main stretch (rule 4). The fork's offMainDecisionLocked (IOSurfaceLayer.zig:292 at pin 12ec155e1) sends a frame whose size differs from the shown frame through main, not to the layer. On main, embedderDrops drops it while the bounds do not have its size.
  • PTY size coalesced. TerminalGridReportCoalescer (TerminalGridReportCoalescer: PTY grid at most once per 25 ms, the newest (Ghostty's min_ms rule) #36553, on feat) is in the one report path, TerminalSurfaceView.updateSurfaceSize. In a key resize, the first new grid opens a 25 ms window and the newest grid goes when it ends: at the first display refresh at or after 25 ms (the window presenter ticks it), or at its DemandTimer, whichever comes first (B3, diag bf2b674a: the timer alone fired up to 124 ms late on a loaded runner). The PTY gets only the grid of the committed bounds (shownGrid), never a size drawn ahead. A hide, a close or a surface swap cancels a waiting grid; shown again, the view sends its grid only if the PTY lacks it. Forced reports go at once. Reports at rest go at once unless the PTY has the grid. The landing flushes. Split and close final grids go through reportNow.
  • No key hold, no switch. motion.frameOwner.terminals is deleted (m19/m21). Every surface presents through its window's commit from creation (ownFramesByCommit). The renderer-present path and the switch-off key hold are gone. terminalDrag.holdSurface is unchanged: frame-owner-s2 owns it, and perf: resize perf paths on by default (cmux-ad m19, no switches) #36311 turned it off.

Drags: #36631 (frame-owner-s2, on feat) owns the drag grid path (TerminalGridReporter.drag(_:shownNow:) and shown(_:live:)). This PR keeps it and only feeds it the committed bounds' grid. Feat's stale guard (review F1 of #35470) stays in holding:.

Split, close and zoom springs: unchanged. The final grid goes at the start, the final-size hold stays, and the grid is held to the landing.

Ghostty fork: this uses the API at pin 850bb9849 (#36577, merged from feat): ghostty_surface_set_size per size and ghostty_surface_set_reflow_preview. No new fork call.

Owners told: PL-ghostty (cmux-b6, CmuxNextTerminal), PL-motion-core (CmuxNextLayout motion), Performance Lead (MotionClock).

Frames:

  • Terminal, key resize, before: with the switch off (the old default), the surface kept one size per press, so the text froze, then jumped. With it on, a size whose frame was late kept the old bounds and frame for up to 6 refreshes, and then took its bounds without the frame (stretched).
  • Terminal, key resize, after: from the spring's second frame on, in every frame the surface has its pane's size and shows the frame drawn at it, with the text re-wrapped at that size. The first frame keeps the last frame for one refresh, because nothing was drawn ahead before the press. Under load, a late frame keeps the last bounds and frame (counted), never stretched.
  • No re-wrap (Ghostty's preview does not run) on the alternate screen, in a view scrolled into history, with Kitty images on screen, or in a view whose grid the owner does not apply. There the program's grid is drawn top-left, cropped or padded by the terminal's own ground to the pane edge (pixel-perfect ruled this passes m14).
  • Terminal, Reduce Motion jump and divider drag: the last frame stays at most until the new one is drawn (m18's 2-refresh allowance).
  • Split, close, zoom: unchanged.

Known limit (review N3 of #36199): selection and mouse hit tests use the live grid while the reflow preview draws.

Testing

Red before the fix (fleet scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext):

  • 42273d31 at 65249a4 (display): TerminalWindowFrameSyncTests red, 3 issues (old bounds kept in committed turns).
  • 10848c24 at 65249a4 (non-display): red as expected (TabStripFollowsPaneTickTests x4, KeyboardResizeContentHoldTests, KeyboardResizeReflowAfterLandingTests).
  • f5645d6c at 10abcfd (display; the head before draw ahead): stretched frames. TerminalKeyResizeFrameTests:101 ("layer 547x572, frame 549x572", 44 frames), TerminalSurfaceCommitSizeTests:48 and :217/:220. These are the red tests of rule 1.

Green at the head: at b6a7e1e: non-display 3ac57da9, display 516187f9, red control 3e648297 (fix/frame-owner-s4-t-keys-noahead-nomerge, draw ahead off: TerminalWindowFrameSyncTests cases 2 and 4 must fail). Earlier f3a717ae at 372ff9e failed to compile (one test lost its @Test(arguments:), fixed in b6a7e1e).

Changed tests: each asserted behavior that m18 or m26 removes. The commits name each one.

  • TerminalWindowFrameSyncTests (new; from draft test(next): m14 window frame sync: every committed frame shows each terminal at its pane size (red) #36158, a35e159, with bd7ae3a's messages): the fence/deadline case and its seam are dropped (no fence on m18's path). Check (b) (the shown frame equals the layer's size) is strict in every committed turn. Case 2 paces one refresh per spring frame, with output in both terminals, and requires (a) (the layer equals its pane's area) from the spring's second frame on, and drawnAhead > 0. Case 1 (Reduce Motion jump) allows a keep for 2 refreshes.
  • One grid per press -> 25 ms rule: KeyboardResizeContentHoldTests, KeyboardResizeReflowAfterLandingTests, TerminalKeyResizeFrameTests (aKeyResize..., aTerminalWhoseFrameWaitsAtTheLanding...), PaneContentPolicyTests (key press, held key, press and reverse, join).
  • Switch-off cases removed with the path (no code is left to test): KeyboardResizeContentHoldTests and KeyboardResizeReflowAfterLandingTests theProducerFollowsItsPaneWithTheSwitchOffToo, and the switchOn: false arms in PaneContentPolicyTests, PaneGroundCoversPaneTests, KeyResizeFirstTickPinTests, TabStripFollowsPaneTickTests and TerminalLaunchFrameKeeperTests. Their switch-on assertions stay.
  • TerminalSurfaceCommitSizeTests: feat's ready-or-keep assertions, with fallbacks changed to longKeeps (data).
  • Kept: KeyboardResizeContentHoldTests 34-71 and TabStripFollowsPaneTickTests 195-216 test generic held content (pages, chats: PaneHostView's K3 hold, B-PAGES under m18), not terminals.

Head 5b3c0af / 5840aea: steps

The head merges feat twice more:

Gate 3b69fa25 at 5b3c0af (display): all 10 suites pass.

  • TerminalWindowFrameSyncTests (5). Key spring: 27 frames; bounds (a) and frame (b) fail in 0 turns; keeps 0, longKeeps 0, drawnAhead 38. Main thread: awake max 7.00 ms, blocked max 4.67 ms (limits 33.3 / 16.7).
  • TerminalKeyResizeFrameTests (3). Low prompt: off area 0, drawn grid off 0, PTY grids [1, 1] in all 3 cases.
  • TerminalFrameOwnerMainThreadTests, TerminalSurfaceCommitSizeTests (7), DividerDragTerminalResizeCountTests, TerminalDragShownGridTests, TerminalGridReportCoalescerTests (10), TerminalHiddenResizeTests, CmuxNextAppTests.PaneMotionPointerAreasTests (2).
  • TerminalDragCancelledSizeGridTests ("p1 fits 60x33, its program's newest grid 60x33").
    Non-display 0d4bc7f7: the target compiles; its suites pass. Launch smoke bff71f4e (build 4baa84fa, tag t-keys-5b3c0af-v1, LOCAL backend): macOS 14.8.7 and 15.7.7 PASS, 0 crashes, 0 hangs, 0 faults.
    5840aea adds one test only (TerminalHiddenResizeTests.aGridKeptForTheShowNeverGoesAfterANewerGrid, frame-owner-s2's rule). It has no red, because the code already meets the rule: 27ff8556 passed without the proposed fix. Steps at 5840aea: non-display 6a33b4de, display 990efb42, build 0f0233d6: pending.

Base finding: BrowserPaneMotionPointerAreasTests aborts the test process (signal 6) at feat 5445597 (d54c116e) and at ed3a5ee (34251351) alike. It is not caused by this PR (see the PR comment).

B5, one red per check. Each step runs the head's test files on old product code, on a never-merge branch.

  • (a) bounds = drawn size in every commit: 01a4393b, fix/frame-owner-s4-t-keys-b5a2-nomerge (head ed3a5ee with draw-ahead off). REAL red: TerminalWindowFrameSyncTests:125 found.bounds ("turn 830 (+18.07 ms) p1 (its new size waits for its frame): layer 452x572 px, pane area 458x572 px", 49 turns) and :113 waitedAfterFirst; then the final drawnAhead > 0 (:131). Frame (b) is 0: no stretch. All other cases pass.
  • (b) the main thread is never blocked: no red. b04e6676 (product 65249a4) fails only in setup (:312 waited > 0: that product has no frame-owner wait in keys). The earlier blocked reds (42273d31 21.18 ms, 25.08 ms at f9855be) were runner stalls in a passing product. Like case 4, this check guards a rule no commit broke.
  • (c) every PTY grid is a shown size, at most every 25 ms: eaacc239, product 11324e0. REAL red: TerminalWindowFrameSyncTests:260 unshownView ("p1 turn 3306 (+68.49 ms): view grid 58x33, shown 57x33"). Everything else passed.
  • (d) the low-prompt grid fits its frame: 0a8a81d3, product b3c8edd (before the low-prompt fix ac1e0c7). REAL red: TerminalKeyResizeFrameTests:278 off ("frame 1 p1: surface (0, 0, 1000, 301), its area (0, 0, 1000, 273)"; 26, 26 and 14 frames). The undrawn check was 0 there, so off carries the red. At 10abcfd (85d62100) (d) passed, because the fix is older than this PR.
  • Other reds in the old-product steps (85d62100, 0a8a81d3, b04e6676):
    • real: TerminalKeyResizeFrameTests:100 misplaced, :101 unsized, :102 stretched (rule a), :107; TerminalWindowFrameSyncTests:264 late (rule c);
    • setup-only, NOT proof: TerminalWindowFrameSyncTests:75, :109, :312 and TerminalKeyResizeFrameTests:149.
  • Steps that did not compile, so they are not evidence: 402684d2 (the PaneMotionPointerAreasTests red), f5a030ed (no coalescer type in 65249a4).

Shims on the old-product branches. Each is a test hook or a constant, with no product behavior:

  • TerminalFramePresenter.presentedHook (DEBUG): the B4 read point.
  • TerminalFramePresenter.Stats.drawnAhead: always 0, where the product has no draw-ahead.
  • TerminalGridReportCoalescer.defaultInterval: the 25 ms constant, where the product has no coalescer.
    The head's two test files apply unchanged.

Case 4 (the spring's last frames and the landing show their own frames with the new text) has no red: it guards a bug that no commit has. With draw-ahead off it passes (3e648297, 85d62100), because without draw-ahead the landing's frame is asked at the landing and presented with it. It catches a regression in which the landing drops the frame drawn ahead for it (a repeated set_size at the same size).

Test changes in this round: none to an assertion.

  • B2: TurnRecorder (test support) splits each turn's time into the span before the commit and the commit, each with its off-CPU time.
  • B4 option (ii): the low-prompt test reads Ghostty's drawn grid in the presenter's pass, right after the present and before the next size goes ahead (TerminalFramePresenter.presentedHook, DEBUG and bench builds). Read later, Ghostty's last draw is the frame drawn ahead, not the frame on screen.
  • PaneMotionPointerAreasTests: the deleted switch's off arm goes; the case runs once with frames owned by the commit.

Head f2fa5c4: T-KEYS review fixes (review of 5840aea)

Merges feat 7ca85cc (with #36985, a drag's mouse-up sends its newest grid; no conflict).

  • P1 Fix find focus and align search overlay #1, draw-ahead stopped after one late frame: the landing asked for the size wanted at it (landed -> requestWantedFrame) before that turn's size drawn ahead went to Ghostty, so each later frame was drawn a refresh late. Now the landing leaves the next refresh's size to applyAheads (drawsAheadAfterLanding).
  • P1 Disambiguate Sentry environment between production and dev #2, a grid a hide dropped was lost when the view showed inside a key spring: resendShownGrid(atRest:) sends it at rest, coalesced in a key spring or a drag, else at the landing.
  • P2 Release v1.13.0 #3, a frame drawn ahead presented without its bounds after a hide/show: a hide drops the size drawn ahead (dropAheadForHide, rule 3).
  • TerminalDragShownGridTests:83 (990efb42, 2 of 38 grids): the test sampled shown sizes only after the commits it drove; a size the presenter's own refresh turn showed inside the drag's 12 ms pace was missed. It now also records each present in the pass (presentedHook). The red did not repeat: 3/3 pass at 5840aea (df16ac19) and 3/3 at feat 7ca85cc (fffb6022).

Red / green (fleet package-test-lane.sh suite Packages/macOS/CmuxNext, display):

  • Red, P1 Disambiguate Sentry environment between production and dev #2 at 5840aea (df16ac19): TerminalHiddenResizeTests aGridAHideDropsReachesThePTYWhenTheViewShowsDuringASpringAndLands: after.last nil != G1 86x27. Green at 87f7e6d (4ac1f0b4) and twice more at aa792cc (03b121b2): the PTY gets [86x27].
  • P1 Fix find focus and align search overlay #1 and P2 Release v1.13.0 #3 have no red: the b697e21 cases 2b and 2c pass on the old product too, and a case 2d that held frames with mode 2026 held none on the fleet (c547fa88: SETUP longKeeps 0), so dcbe397 takes it out. These two fixes come from reading the code.
  • 4ac1f0b4 at 87f7e6d (21 frame-owner suites, display): 20 pass, TerminalDragShownGridTests 3/3. PaneContentPolicyTests fails 3 cases on the display worker only (surface 591 in a 585 area at frame 2; join test sent.last 78x44, final 88x44); it passes without a display at aa792cc (419bbc2e). The same 3 cases fail the same way at the old product on a display worker (a88e4f85 at 5840aea + tests), so the review fixes did not cause them. The cases came with this PR's S4 change (372ff9e) and expect each surface at its area in each frame, which S4 keeps only when Ghostty draws in time: open, for the merge queue.
  • 03b121b2 at aa792cc: TerminalWindowFrameSyncTests x2 (7 of 8 pass; only the removed case 2d), TerminalHiddenResizeTests and TerminalDragShownGridTests pass.
  • Build 9889ffdca5c6 (tag t-keys-v1, local backend) at 87f7e6d; dcbe397 changes only a test after it. Launch smoke: macOS 14.8.7 PASS (dc1b3711), 15.7.7 PASS on attempt 2 (dbf3f382; attempt 1 design_on/design_off overlay check, a known VM flake). 0 crashes, 0 hangs.
  • Merged feat again at f2fa5c4 (0e0885a): a doc conflict in bench-terminal-resize.py, and bench-terminal-resize: pane check samples each window's frames; an unsampled terminal fails #37049's pane check read TerminalFramePresenter.Stats.fallbacks, which S4 took out (S4 never shows bounds without their frame). follow_presenter now reports long_keeps; test_bench_terminal_resize passes. Build 973bb957 (tag t-keys-v2) at f2fa5c4; launch smoke macOS 14.8.7 PASS, 15.7.7 PASS (bbf0af0e), 0 crashes, 0 hangs. Focused suites at f2fa5c4 (t-keys-merge-f2fa5c4): still running at the merge (lead order m43).
  • Not run: the key-resize bench and films.

Observed / Not run

Observed:

Not run yet:

  • The steps at 5840aea above (pending).
  • The launch smoke at 5840aea (build 0f0233d6): pending. At 5b3c0af and ed3a5ee it passed (bff71f4e, bf9b23ac). Builds use the LOCAL backend: this agent's VM cannot reach cmux-dev-backend-1, so cloud, sign-in and pairing do not work in them; the smoke does not need them.
  • Films and the bench A/B: frame-owner-s4 runs them.
  • The perf-hot benches named by the hook (applyAheads, frameTicked, placeSurface): not run.

Changelog

  • Changed: In a keyboard pane resize, each terminal re-wraps its text at the pane's size in every frame, never stretched, and resizes its program at most every 25 ms, as Ghostty.app does.

Proof

No capture: films by pixel-perfect follow.


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


Summary by cubic

During a keyboard pane resize, each terminal now re-wraps its text at the pane's size in every frame, and its program gets the newest grid at most once per 25 ms (Ghostty.app's min_ms rule), flushed at the landing. The PTY only receives grids the pane actually showed.

  • A terminal's bounds change only in the commit that presents a frame drawn at them; during a key spring each terminal draws the next refresh's pane size ahead, so no frame is ever stretched. A drawn-ahead size is kept until its hold ends instead of being dropped by a refresh timeout.
  • A frame that lands late leaves the next refresh's size to the draw-ahead step instead of asking for the wanted size first, so draw-ahead survives a late landing.
  • Grid-report windows end at the first display refresh at or after 25 ms (ticked by the window's presenter), with a timer as backstop; a waiting grid is canceled when the view hides, its session closes, or a surface swap detaches it. A hide also drops the size drawn ahead. Shown again, the view sends only a grid the PTY lacks, as a grid at rest during a key spring or drag, else at the landing; a grid a hidden view keeps for its show never goes after a newer grid.
  • All grid reports route through feat's TerminalGridReporter (gridReports): key springs go coalesced, a divider drag sends its grid as it moves (frame-owner-s2 owns the drag grid path), and feat's stale in-flight guard stays in the holding: rule.
  • The key hold and the motion.frameOwner.terminals switch are deleted.

Testing

  • TerminalWindowFrameSyncTests checks the shown frame equals the layer's size in every committed turn; case 5 requires the PTY to get only grids the pane showed, at most every 25 ms. New cases cover draw-ahead at the app's cadence and hide/show inside a key spring.
  • TerminalGridReportCoalescerTests (9 tests) cover the refresh-ended window with a late timer, the timer-only fallback, and both sources ending a window once; TerminalHiddenResizeTests covers the hide path's resend-only-if-the-PTY-lacks-it rule, a dropped grid that reaches the PTY when the view shows during a spring, and the guard that a kept-for-show grid never goes after a newer grid.
  • TerminalDragShownGridTests now records each present in the presenter's pass, so sizes shown during the drag's pace count as shown.

Written for commit 87f7e6d. Summary will update on new commits.

View guided diff Turn on auto-fix

…e fits in every frame of a key spring (red)

cmux-ad m14 with motion.frameOwner.terminals on: in every frame each terminal
shows pixels drawn at its pane's size, with the prompt in view. Two failures
remain for a terminal whose prompt is low (pixel-perfect films jjk-3kz):
- F2: a pane a press grows keeps its surface at the final size on its bottom
  edge (the low prompt's bottom hold), taller than its pane: its top rows are cut;
- H3: a pane that shrinks draws the owner's grid from its top-left (the reflow
  preview is off while the final grid does not fit), so its prompt is cut until
  the owner announces the new grid.

TerminalKeyResizeFrameTests (display): p1 over p2 with low prompts and an IO
that announces each grid it is asked for, as the daemon does (MANUAL_MIRROR).
J, K, and J J K on p1. After each frame's sizes land: each surface has its
area's frame, and the grid Ghostty drew is the grid the shown frame fits.

Switch-on expectations of F2's hold change to m14's (the surface has its
area's frame and holds nothing):
- PaneContentPolicyTests: aPromptOnTheLastRowRidesTheBottomOfAPaneAKeyGrowsDown
  (now ...StaysInViewInAPaneAKeyGrowsDown; the switch-off arm is unchanged),
  aLowPromptBottomHoldAfterAPressOnTheOtherAxisKeepsItsPanesWidth (now
  aLowPromptPressOnTheOtherAxisKeepsTheSurfaceOnItsArea),
  aLowPromptBottomHoldNeverLeavesABandAboveTheTerminal (now
  aLowPromptGrowDownTwiceThenUpKeepsTheSurfaceOnItsArea), and the switch-on arm
  of aReversePressAfterALowPromptGrowsDownFollowsItsPane;
- TerminalSurfaceCommitSizeTests: aKeptFrameOfALowPromptHoldNotTallerThanItsAreaStaysTopLeft
  (now aFollowedSurfaceTakesItsAreasFrameWhateverItsHold): a hold taller than
  the area no longer moves the surface to the bottom edge.
…erminal's reflow preview at its pane's grid; no low-prompt bottom hold

cmux-ad m14 (switch motion.frameOwner.terminals on): in every frame each
terminal shows pixels drawn at its pane's size, with its prompt in view.

F2: restAt held a low prompt's surface at the press's final size on its
pane's bottom edge (#35470, #35914): taller than its pane, its top rows cut
for the whole spring. H3: TerminalGridPolicy.previewsViewGrid turned the
reflow preview off while a reported final grid did not fit the view (the old
m6 rule), so a shrinking pane drew the owner's grid from its top-left, its
prompt cut until the owner announced the new grid.

One decision now drives the frame, the hold and the preview:
TerminalHostView.surfaceFollowsArea (the host follows its pane through a key
hold with the switch on).
- followedSurfaceFrame gives the surface its area's frame, whatever hold it
  has; the low-prompt branch of #35914 had no key-spring use left and is gone.
- restAt holds nothing for a following host and lets go of an earlier hold
  (a join's), whatever the anchor (G1 released only for .top). The final grid
  still goes once per press (reportFinalGrid).
- updateReflowPreview passes it to previewsViewGrid(followingPane:), which
  keeps the preview on: Ghostty's ReflowPreview reflows a copy of the terminal
  to the view's grid with Terminal.resize, which keeps the cursor row in view.

Switch off, and splits, closes, zooms and divider releases (no following
host): unchanged. TerminalGridPolicyHoldTests covers the new parameter.
…ly the panes that were at rest

The join question of c1, with motion.frameOwner.terminals on: a close spring
holds a terminal at its final size and a key press comes before it lands.
ScreenContentView.applyPresentation starts a key hold only for a pane not on a
layout spring, so the pane still on the close spring does not follow: its
surface keeps the close's hold (moved to the key press's final size) until the
spring lands. The pane at rest when the key came follows the key hold and has
its area's frame in every frame. Both rest at their areas after the landing.
…ws each terminal at its pane's size (red)

From draft #36158 (test/frame-owner-s4-m14-window-sync-red, a35e159 +
bd7ae3a's messages): cases 1-3 of TerminalWindowFrameSyncTests. The
fence/deadline case and its seam protocol are dropped (the fence is off
m18's path).

m18 (Lead, STATUS 13889): no keep and no main-thread wait, so a frame
Ghostty has not drawn by its commit lands late: its new bounds go in that
commit with the last frame, top-left. The presenter now counts those
(Stats.lateLandings, lateLandings(of:), isLate(_:); at the base only a
keep-limit fallback lands late). Check (a), the layer is its pane's area,
holds in every committed turn; check (b), the shown frame is the layer's
size, holds in every turn except while that terminal's frame is late;
each case prints the late turns and the longest late streak (ms and 120 Hz
refreshes), and every terminal must show its own frame at rest. Cases 1
and 3 require a pass of the presenter (a new size went through it)
instead of a present or a waiting turn: with no keep both can be zero.

Red at this commit: (a) fails in the turns ready-or-keep keeps a
terminal's old bounds.
…tch on or off, no grid at the press, no keep (red)

m18 (Lead, STATUS 13889) replaces m6's 'pixels per frame, program size per
press' and ready-or-keep:
- KeyboardResizeContentHoldTests: the terminal case no longer expects one
  grid per terminal at the press; it expects no grid in the key's turn and
  each PTY's last grid = its view's grid at rest (the coalesced reports end
  at the landing). The switch-off case (one surface size per press) becomes
  'the producer follows its pane with the switch off too': m18 deletes the
  key hold in both states, the switch only picks who presents.
- KeyboardResizeReflowAfterLandingTests: 'final grid by the first frame
  tick' becomes 'no grid by the first frame tick, PTY ends at the view's
  grid'; the switch-off case follows its pane.
- TabStripFollowsPaneTickTests (terminal case): the surface is on its area
  in every frame in both switch states (it took one size per press off).
- TerminalSurfaceCommitSizeTests: a new size's bounds are in the commit of
  its layout (never kept); a turn whose frame is not the layer's size is
  allowed only while the presenter has that frame late; the kept-frame
  test becomes 'a followed surface takes its new area in the turn it
  changes'; fallback counts become late landings.
- TerminalKeyResizeFrameTests.showsItsOwnFrame reads the presenter instead
  of the deleted in-flight size.

Kept as they are: KeyboardResizeContentHoldTests lines 34-71 and
TabStripFollowsPaneTickTests 195-216 test generic held content (pages,
chats: PaneHostView's K3 hold, B-PAGES under m18), not terminals.
… size in every frame; no key hold, no keep; switch on

cmux-ad m18 (Lead, STATUS 13889).
- No key hold: TerminalHostView.surfaceFollowsArea no longer reads the
  switch. A host that follows its pane through a key resize gives its
  surface the area in every frame with motion.frameOwner.terminals on or
  off (off = the kill switch: Ghostty's renderer presents). restAt for a
  following host reports no final grid and holds nothing; the view's grid
  goes to the PTY as for any spring (held to the landing here; coalesced in
  the next commit). Split, close and zoom springs keep their final grid at
  the start and their final-size hold.
- No keep: TerminalSurfaceView.place(_:) sets the view's frame at once (any
  layout, AppKit's display cycle included), so ghostty_surface_set_size gets
  the pane's pixel size in that turn and the reflow preview re-wraps the
  text at it. The presenter's pass presents a frame Ghostty drew by the
  pass with those bounds in the same commit; one not drawn yet is a late
  landing (Stats.lateLandings, isLate, lateLandings(of:)): the last frame
  shows top-left in the new bounds and the watch presents the new frame in
  the first commit after it is drawn. Deleted: keepLimit, the fallback, the
  keep counter, the in-flight and wanted sizes, keepOrigin and
  anchoredFrame, and the group present (groupTicks, groupWaits): with no
  keep every request lands in its first pass, so a group could only delay a
  ready frame. A held display scale goes to the layer with its frame or at
  the pass.
- TerminalDragTunables.framesOwnedByCommit defaults to true (kept as the
  kill switch). holdsSurfaceDuringDrag is frame-owner-s2's and unchanged.

Green for the red tests of the two commits before.
…s window, the newest (Ghostty's min_ms rule)

T-KEYS (frame-owner-s4, STATUS 13889): a key resize or divider drag changes a
terminal's fit grid on almost every frame, and each report resized the PTY and
sent SIGWINCH. The coalescer follows Ghostty.app's src/termio/Thread.zig
(min_ms 25): the first new grid opens a 25 ms DemandTimer window, later grids
replace the waiting one, the fire sends the newest once; a grid equal to the
last sent one sends nothing. flush() ends the window at once (gesture end),
reportNow(_:) is the forced report, cancel() drops the waiting grid.
Not wired yet: the T-KEYS key-path change calls it from the grid-report path.

Tests: TerminalGridReportCoalescerTests (manual clock), run on the fleet.
… sends to the newest grid in order (no Duration-keyed dictionary)
…5 ms, ending at the landing's grid

Wires TerminalGridReportCoalescer (the two commits before, from
fix/frame-owner-s4-t-keys-coalescer) into the one grid report path,
TerminalSurfaceView.updateSurfaceSize:
- live (the host follows its pane through a key resize, or
  isFollowingResizeGesture): viewSized(grid), the first change opens a
  25 ms window and the newest goes when it ends (Ghostty.app Thread.zig
  min_ms). The view's grid is no longer held to the landing for a key
  resize; a split's, close's or zoom's spring still holds it.
- forced (a new owner): reportNow. At rest: viewSized then flush, so the
  report goes now unless the PTY already has it.
- endPaneMotion flushes: the landing's grid goes at the landing.
- reportFinalGrid (split, close) goes through reportNow too, so the
  coalescer's last-sent grid is always the PTY's.
- sendGridReport(_:) reads the cell size when it sends; gridReports counts
  sends. isGridReportPending (held or in a window) settles
  TerminalSizeSample. The reflow preview stays on while a grid waits.
Drags: frame-owner-s2's drag PR drops isFollowingResizeGesture from the
holding: argument; its reports then go through the same coalescer with no
call of its own.

Tests of m6's 'final grid by the press's first frame' move with it:
PaneContentPolicyTests.grids flushes the window before it reads (a waiting
grid counts as asked for); the held-key cases allow one grid per read plus
the landing, last = final; the press-and-reverse case ends at the start
grid; TerminalKeyResizeFrameTests bounds the grids per terminal by the
25 ms windows of the press and checks each PTY ends at its view's grid.
…ze case in both switch states, to the m18 contract

m18 deletes the key hold in both states of motion.frameOwner.terminals and
sends the view's grid coalesced (25 ms) instead of one final grid at the
press. Each test below asserted that removed behavior; each is rewritten
(none dropped) so it fails if the hold or the press-time grid comes back:
- aKeyPressShowsEachSurfaceAtItsPanesSizeAndSendsOneGrid ->
  ...AndItsPTYEndsAtItsFinalGrid(direction:switchOn:): now also runs with
  the switch off; no grid in the key's turn; PTY ends at the final grid.
  Replaces aKeyPressSizesEachSurfaceOnceWithTheSwitchOff (asserted one
  surface size per press, the key hold).
- aHeldKeyShowsEachSurfaceAtItsPanesSizeAndSendsAtMostOneGridPerPress ->
  ...AndItsPTYEndsAtTheFinalGrid(switchOn:): one grid per read plus the
  landing, last = final. Replaces aHeldKeySizesEachSurfaceOnlyInAPressTurn
  WithTheSwitchOff (surface sized only in press turns: the key hold).
- aPressAndItsReverseInOneHoldSendEachGridInItsTurn ->
  aPressAndItsReverseInOneHoldEndsAtTheStartGrid(switchOn:): surface on its
  pane every frame, rests at the start size, PTY ends at the start grid.
  Replaces aPressAndItsReverseRestsAtTheStartSizeWithTheSwitchOff.
- aParkedTerminalShownDuringAKeyHoldFollowsFromItsFirstFrame: the switch-
  off arm now requires the area's size in every frame (was <= 1 new size).
- aPromptOnTheLastRowStaysInViewInAPaneAKeyGrowsDown and
  aReversePressAfterALowPromptGrowsDownFollowsItsPane: the switch-off arm
  now requires no bottom hold (was: the hold must exist).
- aTerminalThatJoinsAKeyHoldGetsOneGridPerPressThroughASecondPress: each
  read gets at most one grid, the last is the final grid, none after it
  (was: the second press's final grid at its first frame, none at landing).
- aLowPromptPressOnTheOtherAxisKeepsTheSurfaceOnItsArea and
  aLowPromptGrowDownTwiceThenUpKeepsTheSurfaceOnItsArea: a setup check
  that the prompt is in the lower half (review N2 of #36199).
- grids(_:_:) ends the coalescer's window before it reads.
… the switch-off section removal took with it

00471ab removed the ...WithTheSwitchOff section and the grids(_:_:)
helper that sat inside it; the other suites call it (compile error at
step 2dfd615d48e54adee09a2e9d). Restored unchanged (it flushes the
coalescer's window before it reads).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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

Review f2fa5c4: 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=pr36478 -f ref=fix/frame-owner-s4-t-keys -f platform=macos -f backend_mode=local

web and size checks: green

Default changes

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

  • tunable-source.CmuxNextDesign/Tunables/TerminalDragTunables.swift:framesOwnedByCommit: "5f48e64f539b1f89ac72b6b47b40ce85cd32ee12b09a74891ddc841485ac8114" -> (none)
  • tunable-source.CmuxNextDesign/Tunables/TerminalDragTunables.swift:section: "284d6a1f8bd7243ae3295a8bd9eb50aae5c7e459dd5eed51572d2f56c6eefa98" -> (none)
  • tunable.motion.frameOwner.terminals: true -> (none)

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at f2fa5c47a39c0b08ed8f22f4d38d7c4ce78cac63, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @austinywang or @teamleaderleo (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Merge receipt: merged as 76865dd55e51 with these defaults changed, WITHOUT a SHIP receipt or an approving call owner review on its head.

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

…he 25 ms coalescer rule; a tab shown mid key hold follows from its first frame

m18 replaces 'one grid per press' with TerminalGridReportCoalescer's rule
(at most one grid per 25 ms window of the press plus the landing's flush;
the last grid is the view's grid at rest):
- TerminalKeyResizeFrameTests.aTerminalWhoseFrameWaitsAtTheLandingGetsOne
  GridForThePress -> aTerminalWhoseFrameIsLateAtTheLandingEndsAtTheLandings
  Grid: the mode-2026 terminal's frame is waiting or late at the landing
  (setup check, read after the frame's commit), grids bounded by the 25 ms
  windows, PTY ends at the view's grid (step f5645d6c failed its setup
  require: with no keep nothing waits).
- PaneContentPolicyTests.aTerminalThatJoinsAKeyHoldGetsOneGridPerPress
  ThroughASecondPress: grids bounded by the 25 ms windows from the press,
  last = final, none after it; the reads no longer flush the window.
Code (measured at step d8948371, 2 grids in one read): a parked tab shown
mid key hold joined (restAt) before its area handed it the follow, so it
sent a final grid outside the coalescer and then coalesced ones.
TerminalHostView.joinsAreaKeyHold: restAt treats it as following.
…wner-s4-t-keys

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/TerminalDragTunables.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextTerminal/TerminalHostView+PaneMotion.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextTerminal/TerminalSurfaceView+PaneMotion.swift
#	Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PaneContentPolicyTests.swift
#	Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TerminalKeyResizeFrameTests.swift
#	Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TerminalSurfaceCommitSizeTests.swift
#	plans/cmux-next/ghostty-integration-DESIGN.md
…; key springs draw the next refresh's size ahead; motion.frameOwner.terminals deleted

Measured at 10abcfd (step f5645d6c): c102749 removed the keep and
let the layer take its new bounds before Ghostty drew that size, so the old
frame showed stretched in the new bounds (TerminalKeyResizeFrameTests:101
"frame 1 p2: layer 547x572, frame 549x572", TerminalSurfaceCommitSizeTests
:48, :217, :220). The resize owner chose draw ahead (m26); a synchronous
main-thread draw is refused.

The contract, the Chromium model (the browser compositor shows a new size
only with a frame the renderer submitted for it, and the renderer gets the
size before the compositor needs its frame):
1. A terminal's bounds change only in the commit that presents a frame
   drawn at those bounds. TerminalFramePresenter's ready-or-keep is back
   (feat's pass at order 1_999_999, the group present included: with keeps
   a group lands its two terminals together). Its keepLimit fallback, which
   set the bounds without the frame (a stretch), is deleted; long keeps are
   counted as data (Stats.longKeeps, more than 2 refreshes).
2. Key springs draw ahead: ScreenContentView.step steps a copy of each
   moving pane's spring once more, by MotionClock.nextFrameInterval (the
   display link's refresh period: 8.33 ms at 120 Hz, 16.7 ms at 60 Hz), and
   hands the size change down PaneHostView -> PaneContentAreaView ->
   PaneMotionHoldFollowing.drawAhead(nextHostSize:) -> TerminalHostView.
   After this turn's pass the surface gives Ghostty that size, render only
   (TerminalSurfaceView.applyAhead: no grid report; the reflow preview
   re-wraps at it); one whose frame still waits gets it when that frame
   lands. The next refresh's place() finds the size drawn ahead, and the
   pass presents it with its bounds. An ahead size no layout takes within 2
   refreshes goes back (dropAhead). set_size is not repeated at the size
   Ghostty already has (it would drop the drawn frame).
3. A divider drag, a Reduce Motion jump and hidden panes draw nothing ahead:
   the last bounds and frame stay, clipped or beside the terminal ground,
   until the new frame is drawn.
4. The pass never draws and never waits. Off-main presents of a frame drawn
   ahead cannot reach the old bounds: the fork's offMainDecisionLocked
   routes a frame whose size differs from the shown frame through main
   (IOSurfaceLayer.zig:292, pin 12ec155e1), and embedderDrops drops it there
   while the bounds do not have its size.

motion.frameOwner.terminals is deleted (m19/m21: no switches): every
surface presents through its window's commit from creation
(ownFramesByCommit); the renderer-present path and the switch-off key hold
are gone. holdsSurfaceDuringDrag is unchanged (frame-owner-s2's; #36311
set it off). No code or test writes the key any more, which also removes
the cross-suite TunableStore reset (step f933b493).

Tests:
- TerminalWindowFrameSyncTests: (b) strict in every committed turn (no
  stretch, no excuse); case 2 paces one refresh per spring frame with
  output in both terminals and requires (a) from the spring's second frame
  on (frames kept = 0 after the first commit) and drawnAhead > 0; case 1
  (Reduce Motion jump, rule 3) allows a keep for 2 refreshes.
- TerminalSurfaceCommitSizeTests back to feat's assertions; fallback
  counts become longKeeps (data).
- Switch-off cases removed with the path:
  KeyboardResizeContentHoldTests.theProducerFollowsItsPaneWithTheSwitchOffToo,
  KeyboardResizeReflowAfterLandingTests.theProducerFollowsItsPaneWithTheSwitchOffToo(direction:),
  and the switchOn: false arm of PaneContentPolicyTests (aKeyPressShows...,
  aHeldKeyShows..., aPressAndItsReverse..., aParkedTerminalShown...,
  aPromptOnTheLastRow..., aReversePressAfterALowPrompt...,
  aTerminalThatJoinsAKeyHold...), PaneGroundCoversPaneTests (2 cases),
  KeyResizeFirstTickPinTests (2), TabStripFollowsPaneTickTests (1) and
  TerminalLaunchFrameKeeperTests (2). Their switch-on assertions stay.
  PaneGroundCoversPaneTests.ownFrames/releaseFrames are deleted.
@lawrencecchen lawrencecchen changed the title Frame owner S4 (T-KEYS): a key resize re-wraps each terminal at its pane size in every frame; PTY size coalesced 25 ms; no key hold Frame owner S4 (m18/m26): key resizes show each terminal at its pane size in every frame (drawn ahead, never stretched); PTY size coalesced 25 ms; no key hold; switch deleted Nov 5, 2026
…ed; a key spring's last frames and landing show their own frames with the new text

Resize owner's two tests (TerminalWindowFrameSyncTests):
- Case 2 (paced key spring, output every frame): no committed turn after
  the spring's first frame has a terminal waiting for its frame. A frame
  drawn at S(n+1) in refresh n must be the one commit n+1 presents with
  bounds S(n+1); a discarded ahead frame makes every spring frame wait.
- Case 4 (spring end): the last 3 spring frames and the turns up to one
  refresh after the landing show each terminal's own frame at its bounds
  and its pane's area, with nothing waiting; output written after the
  landing shows in a newer frame of the rest size.

The fork holds the frame (Metal.zig embedderShowsKeptFrame keeps it in
'fresh'; hostFrame(show:) presents it with presentFresh at the host size);
only a host-size change drops a kept frame, even back to an equal size,
which setSurfacePixels never makes. Red evidence: these run at a
no-draw-ahead copy of this head (fix/frame-owner-s4-t-keys-noahead-nomerge,
applyAhead returns at once).
@blacksmith-sh

This comment has been minimized.

…vesNoHoleWhereThePaneWas's columns arguments

372ff9e's switch-off cleanup removed '@test(arguments: [false, true])'
from every test, also this one, whose parameter is columns:, not
switchOn: (compile error at step f3a717ae).
…ds its pane showed, at most every 25 ms (red)

Resize owner's film finding (STATUS 14461): the PTY must get only sizes the
pane SHOWED, through the 25 ms coalescer. At b6a7e1e updateSurfaceSize
reads the grid from ghostty_surface_size after set_size(inFlight ?? ahead
?? bounds), so a call while a size is drawn ahead (the owner's announcement
arriving between the pass and the next frame) puts the ahead grid into
geometry and the coalescer: a grid the pane may never show (a press that
retargets the spring).

TerminalWindowFrameSyncTests case 5,
aHeldKeyResizeGivesThePTYOnlyGridsThePaneShowedAtMostEvery25ms: 3 presses 4
frames apart, paced one refresh per frame, output in both terminals, an
owner that announces each grid (AnnouncingTerminalIO now records each
report and its main-thread time in resizeOnMain). Checks (a) in every
committed turn the view's grid (fittedGrid) is the grid its committed layer
bounds fit; (b) every PTY grid is one a committed turn showed; (c) grids at
least 25 ms apart before the landing; (d) a changed shown grid reaches the
PTY within 25 ms + 2 refreshes + 8 ms; (e) the last grid is the rest grid.
The recorder samples shownGrid and viewGrid per terminal per turn; the
spring helper takes presses/framesBetweenPresses (default one press).
…ver a size drawn ahead

updateSurfaceSize gave geometry and the coalescer the grid of
ghostty_surface_size after set_size(inFlight ?? ahead ?? bounds). A call
while a size was asked and not shown, or drawn ahead for the next refresh
(the owner's announcement arriving between the pass and the next frame),
sent that grid: one a press that retargets the spring may never show.

Now the grid comes from the view's committed bounds
(TerminalSurfaceView.shownGrid: ghostty_surface_fit_grid of their pixel
size, or ghostty_surface_size's grid when Ghostty has the bounds' size).
The presenter's landing (frameDidLand -> updateSurfaceSize) gives the
coalescer each shown grid as the bounds commit. The inFlight and ahead
sizes still go to set_size only (Ghostty's render, its reflow preview).
The model's grid follows the same shown grid. Drags are unchanged (the
holding: rule stays; frame-owner-s2's PR moves drags to the coalescer).

Green for TerminalWindowFrameSyncTests case 5 (11324e0, red step
19cd7b71).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Base finding (does not block this PR): BrowserPaneMotionPointerAreasTests.aKeyResizeWithABrowserPaneCostsAppKitNoCursorRectWork aborts the test process with signal 6 ("freed pointer was not the last allocation") at feat-cmux-next 5445597 too.

  • At feat 5445597, run alone with the exact filter CmuxNextAppTests.BrowserPaneMotionPointerAreasTests: step d54c116e, the same abort.
  • In this PR's gate c0594a02 (ed3a5ee), the filter PaneMotionPointerAreasTests also matched this browser suite. PaneMotionPointerAreasTests itself passed (both tests), and then the process aborted in the browser test.
  • This PR does not change browser or CEF code. The same test alone at ed3a5ee: step 34251351, pending.

… cancelled size's drag grid reaches the program)

#36478 lands after #36912 and resolves the conflict (frame-owner-s4 order, frame-owner-s2's rule):
- TerminalGridReporter: feat's init(view:clock:), awaitingShown and shownCancelled, plus the B3 `ticks:` beside
  `clock:` in its coalescer.
- shownCancelled now takes the grid of the bounds the view TAKES at the cancel, never the stored awaitingShown grid
  (with T-KEYS's committed-bounds input that is the older grid). Both presenter cancel sites in
  TerminalSurfaceView+FrameOwner.swift (place()'s hidden or windowless branch, releaseFrameOwnership) call it after their
  frame write, through reportGridAfterCancel(): shown, the view's fittedGrid goes (coalesced while a drag lives, else
  as a grid at rest, nothing when the PTY has it); not shown, the grid goes when the view shows (resendsShownGrid).
- git grep of the 8 symbols #36478 deletes: no code hit.
…s after a newer grid (frame-owner-s2's rule)

Point 2 of frame-owner-s2's check on 5b3c0af. A hide drops a waiting grid and keeps one for the show; then a
spring's final grid, or a forced report, goes while hidden; then the view shows. The PTY's last grid must be the view's
fit, and the show sends at most one grid.

No red: the code already meets the rule. Step 27ff8556 (non-display, 5b3c0af + this test, without the proposed
124b857535e) passed both arms: final arm hidden [40x12], shown [86x27], fit 86x27; forced arm hidden [], shown [86x27].
The show's resend sends fittedGrid after the show's own updateSurfaceSize, so it is never an older grid; a forced report
does not go while hidden. 124b857535e is dropped; this test stays as a guard.
@blacksmith-sh

This comment has been minimized.

…, a hidden grid in a spring, hide/show in a spring (red)

- P1 #1 TerminalWindowFrameSyncTests.atTheAppsCadenceAKeySpringShowsEachTerminalAtItsPaneSizeFromItsThirdFrame: one
  commit per refresh after the step and its layout, then a sleep to the next refresh with no extra main turns; check (a)
  from the spring's third frame. The paced case's extra commits hid a draw-ahead that stops after one late frame.
- P1 #2 TerminalHiddenResizeTests.aGridAHideDropsReachesThePTYWhenTheViewShowsDuringASpringAndLands: G1 waits during a
  key spring (PTY at G0), hide, show while on the spring, land: the PTY must end at G1.
- P2 #3 TerminalWindowFrameSyncTests.aHideAndShowInsideAKeySpringNeverShowsAFrameDrawnAheadInTheOldBounds: p2 hidden
  and shown inside a key spring; check (b) in every committed turn.
Red on the fleet at this commit (never-merge branch) before the fix.
…, a hidden grid reaches the PTY, a hide drops the size drawn ahead

- P1 #1: a frame that landed late in a key spring asked for the size wanted now before this turn's size drawn ahead
  went to Ghostty (`landed` -> `requestWantedFrame` before `applyAheads`), so every later frame was drawn a refresh
  late. Now the landing leaves the next refresh's size to `applyAheads` (`drawsAheadAfterLanding`); the next layout
  takes it, as Chromium gives the renderer the size before the compositor needs its frame.
- P1 #2: a grid a hide dropped (`cancelGridReport`) was lost when the view showed inside a key spring: the show's
  resend ran only at rest and cleared its flag. `resendShownGrid(atRest:)` sends it at rest, coalesced in a key
  spring or a drag, else at the landing (`endPaneMotion`, a drag's end).
- P2 #3: a hide kept the size drawn ahead, so the show's present showed that frame in the old bounds. A hide drops it
  (`dropAheadForHide`); shown, Ghostty gets the view's size back.
- TerminalDragShownGridTests (fleet 990efb42, 2 of 38 grids "before any commit showed it"): the test sampled shown
  sizes only after the commits it drove; sizes the presenter's own refresh turns showed during the drag's 12 ms pace
  were missed. It now also records each present in the pass (`presentedHook`), with the bounds the pass gave it.
…ring costs its own refreshes, not the rest of the spring

The app-cadence case (b697e21) passed on the old product at fleet df16ac19 (no frame came late on that runner: keeps 0),
so it is no red for P1 #1. Case 2d holds both programs' frames with synchronized output (mode 2026, kept across a locked
grid's resize) for spring frames 2 to 5, so a frame lands late with a newer size wanted; check (a) from frame 11 on.
…fleet (no red)

Fleet c547fa88 at 153afb9 (old product): its SETUP failed, longKeeps 0: synchronized output did not hold the
frames of a resize, so no frame came late and the case proves nothing. Takes back aa792cc only (a test).
@lawrencecchen
lawrencecchen marked this pull request as ready for review November 6, 2026 08:58
# Conflicts:
#	scripts/cmux-next/bench-terminal-resize.py
…cks (S4 has none)

#37049 on feat reads TerminalFramePresenter.Stats.fallbacks, which S4 m18 (c102749) took out: S4 never
shows bounds without their frame. follow_presenter now reports long_keeps (passes that kept a late frame past
longKeepTicks refreshes). test_bench_terminal_resize passes locally (python unittest).
@blacksmith-sh

blacksmith-sh Bot commented Nov 6, 2026

Copy link
Copy Markdown

Found 4 test failures on Blacksmith runners:

Failures

Test View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs

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

@lawrencecchen
lawrencecchen merged commit 76865dd into feat-cmux-next Nov 6, 2026
177 of 185 checks passed
@lawrencecchen
lawrencecchen deleted the fix/frame-owner-s4-t-keys branch November 6, 2026 09:17
lawrencecchen added a commit that referenced this pull request Nov 6, 2026
…its rest as it starts (#37079)

* test(layout): the retarget test reopens the close on its way, not at the new rest's place

aRetargetToASmallerRestKeepsTheLargerPageUntilTheLanding has failed on every tip Swift suite run
since #36677 (a8bcdff, first red at f1965d7, run 42089396624; the PR's own Swift suite was
skipped): it retargeted the close back to 200 pt while the host still stood at 200 pt, its place and
size at the new rest. A host there is at rest and holds nothing (contentFrame), so the chat laid out
at 184 pt. An early reopen comes while the pane is on its way to the wider rest, as the open-then-close
case of openCloseAndInterruptedPaneMotionNeverShowsBlankOrReflowsPerFrame already does (300 pt): the
test now presents that frame first, and keeps every expectation (384 pt through the retarget and its
frames, 184 pt at the landing).

* test(layout): a split taking a key hold over lays held content out once, at the split's rest, on its first frame (red)

#36677 changed this test to expect a layout at the key spring's rest, which
RESIZE KEYS K3 forbids for held content (the key hold keeps the start size;
#36478 removed it for terminals only). The test now expects no layout during the
key hold, one at the split's rest on the split's first frame, and none at the landing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(layout): a split taking a key hold over lays held content out at its rest as it starts

After endKeyResizeHold the held size was still the key hold's start size, and the
split's announceRest kept the larger of it and the split's rest (#26118), so a chat
that shrinks kept its start width until the landing. The first rest after a
takeover now replaces the held size; later retargets keep the larger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — f2fa5c47 Deployed Nov 6, 2026 by lawrencecchen via namespace autofix #15909
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant