Skip to content

m14 drags: each terminal follows its pane in every frame of a divider drag (delete the drag hold) - #36328

Merged
lawrencecchen merged 8 commits into
feat-cmux-nextfrom
fix/m14-drags-live-terminals
Nov 5, 2026
Merged

lawrencecchen merged 8 commits into
feat-cmux-nextfrom
fix/m14-drags-live-terminals

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

cmux-ad m14 and m18 (Lawrence's P0, "resizing splits/columns, still not animating the terminals"): in every frame of a divider or column-edge drag, and of the spring its release starts, each terminal takes that frame's pane size and its text re-wraps at that size. The program in it gets its new size when the drag lands.

Why

  • The drag hold (Debug tunable terminalDrag.holdSurface, RESIZE VIDEOS A3, on by default) kept each terminal's surface at its drag-start size for the whole drag: cut where the pane was narrower, the terminal background beside it where the pane was wider.
  • At the release spring's start, restAt moved the hold to the final size, so the release spring showed the final grid, not the pane's size.
  • pixel-perfect's trace (STATUS 13764): 157-159 held ticks in every col-drag-2 film; the surface was up to 150 pt off its pane. That was so with motion.frameOwner.terminals on or off.

Fix

  • Delete TerminalDragTunables.holdsSurfaceDuringDrag (terminalDrag.holdSurface) and its path (TerminalSurfaceView+PaneMotion.swift).
  • At the drag's first frame each terminal leaves a spring hold the drag caught (a split, a close or a press still landing: endPaneMotion()).
  • From there the surface takes its pane's size in every frame through the placement every resize uses (TerminalHostView.placeSurface and the frame owner's place(_:anchor:)), with the present rule of motion.frameOwner.terminals:
    • off: the bounds change at once, and Ghostty's frame of that size lands 1-4 refreshes later;
    • on: ready-or-keep, through TerminalFramePresenter (frame-owner-s4's T-KEYS changes "keep").
  • The reflow preview (Ghostty's render-only re-wrap, no SIGWINCH) re-wraps the text at each size. During a drag no final grid is reported, so TerminalGridPolicy.previewsViewGrid stays true.
  • The release spring of a drag holds no final size and reports no final grid at its start (paneMotionWillRest and joinPaneMotion skip restAt while the drag is followed). A final grid at the start would set geometry.final and turn the preview off for the spring.
  • The PTY still gets one grid per drag, when the release lands, as before. frame-owner-s4's T-KEYS PR puts a 25 ms coalescer (Ghostty.app's rule) in the common report path; drags then use it with no change here.
  • stopFollowingResizeGesture() has no drag hold to release any more.

No present-mode override for drags: the drag follows the switch, which T-KEYS turns on by default (m18).

Changelog

  • Improved: While you drag a divider or a column edge, each terminal resizes with its pane and wraps its text to the new width in every frame.

Frames

  • Frames: every terminal in the panes a divider or column-edge drag resizes, in every drag frame and every frame of the release spring.
    • Before: the surface kept its drag-start size, cut or beside the terminal background, until the release spring, which held the final size.
    • After: the surface has its pane's size in every frame and its text re-wraps there. With motion.frameOwner.terminals off, Ghostty's frame of each size shows 1-4 refreshes after the bounds (top-left, not stretched); with it on, the frame owner's ready-or-keep.
  • Frames: a drag that catches a split's or a close's spring: the terminals in it leave the spring's final-size hold at the drag's first frame.
  • No content stops following its pane.

Tests

Red first (2399b3523bf, tests only, on feat 9ac955e274c), then the fix (96ad37a03a3); a second red/green pair for Codex round 1's blocker (ca64d25e72e test, b78df0c4f55 fix). The head merges feat (216a6d14b74).

Run Ref Step Result
red, non-display 2399b3523bf 7c4ee94d75b6ddb8d88e290d DividerDragTerminalSteadyTests FAIL: p1's surface took 1 width in 30 drag steps (widths.count → 1), "drag step 1: p1's surface (600.0, 800.0) in a (592.0, 800.0) area" ...; the swap test: the surface held at 600 pt. DividerDragTerminalResizeCountTests passes.
red, display (gui-session-v1) 2399b3523bf 58d8d7065b215146f8919a23 TerminalDividerDragFrameTests FAIL, both switch values: "new sizes in drag frames [0, 0]" of 20; frames behind their pane [20, 20] (off), [21, 21] (on)
green, 36 suites (first fix) 96ad37a03a3 807d9ee21deb7bfe0713d1ff 35 suites pass; SplitSpringMotionTests.fiveRapidSplitsAreOneMotion fails on its 8.33 ms frame-time bound (28.1 ms), a timing test with no drag, the same test that failed at #35940's head (2ff367f37c3d6070185f8309, 11.66 ms)
red: a surface swapped in mid-drag (Codex round 1) ca64d25e72e 4de6e21ebb4e7fb530560fff aSurfaceSwappedInMidDragFollowsTheDrag FAIL: fresh.isFollowingResizeGesture → false
green, 36 suites 216a6d14b74 5cf38b4cf9c34221b74af108 ci-step m14d2-green exited 0: 36 suites, 187 tests
green, display (gui-session-v1) 216a6d14b74 dd5500c490fe9bed76d6b35e ci-step m14d2-display-216a6d14b74 exited 0. TerminalDividerDragFrameTests: switch off, new sizes in drag frames [20, 20] of 20, 0 frames behind their pane; switch on, [17, 17] of 20, 20 frames behind (the presenter's keep path: each size shows one commit later; T-KEYS deletes "keep"); misplaced 0, unsized 0, stretched 0, PTY grids [1, 1]
app build 216a6d14b74 bdb41a9fe9c381e4682715ac (tag m14-drags-v1) done

Observed / Not run

  • Observed: the fleet runs above, the prepush gate, check-no-godfiles (TerminalHostView.swift kept at its base line count), check-motion, the package conventions lint.
  • Codex round 1 at b72c7e2d3fa: FIX. (1) blocker: a surface swapped in mid-drag did not follow the drag; fixed red-first (ca64d25e72e, b78df0c4f55): the host follows the drag and a surface joins its host's drag. (5) should-fix: the display test pins the switch before each frame; fixed. (4) The PTY count is asserted by DividerDragTerminalResizeCountTests (one grid per drag, none mid-drag; unchanged and green); this PR keeps that rule until T-KEYS' 25 ms coalescer. (2) A forced report (a terminal shown mid-drag) still sends at once, as before this PR. (3) Not verified: a drag that catches a running key-resize hold (FRAME OWNER S3 followedHold). With the switch off the host's follow rule may keep the key hold's size until that hold ends; no test here covers a drag during a key spring.
  • Known effect, not this PR: with motion.frameOwner.terminals on (the default from perf: resize perf paths on by default (cmux-ad m19, no switches) #36311), a drag shows each size one commit late, through the presenter's "keep" (display step dd5500c490fe9bed76d6b35e: 17 of 20 drag frames take a new size, 20 frames one commit behind the pane). frame-owner-s4's T-KEYS PR deletes "keep". With the switch off: 20 of 20, 0 behind.
  • Observed (pixel-perfect film, reported to the RESIZE OWNER; tables in STATUS 14353): px-col-drag-2-sw-terminals at 96ad37a03a3 against base feat 9ac955e: held terminal frames 155-161 at the base, 0 at the head. One terminal edge frame per film at the release (p0's high side cut by 102 px or more). No other new red.
  • Observed at head 9d5718797f84a02bed13ebd4b57d527e84e449d5 (after the feat merge): launch smoke PASS on macOS 14.8.7 and 15.7.7 (os-matrix step 5d88e49aad86d5579008326d, build dc809e30886d6b7643c3ce94, 0 crashes, 0 hangs); focused suites 8b429b07ee626b091b4c0cfc exit 0 (10 suites, 51 tests).
  • Not run: films (after merge), a tagged dogfood build.

…rame of a divider drag (red)

cmux-ad m14 (Lawrence's P0) and m18: in every frame of a divider drag and of its release spring, each
terminal takes that frame's pane size (its text re-wrapped by the reflow preview), never the drag-start
size. On feat the drag hold (terminalDrag.holdSurface, default on) keeps each surface at its drag-start
size through the drag, and the release spring holds its final size.

- DividerDragTerminalSteadyTests (rewritten to the m14 contract; switch motion.frameOwner.terminals off):
  every drag step and release frame, each surface's size is its host area's; a drag that catches a
  split's spring follows from its first frame; a surface swapped in mid-drag takes its pane's size.
- TerminalDividerDragFrameTests (display; switch off and on): per committed frame, top-left on its area,
  a size its pane had, a new size in at least half of the 20 drag frames; after the landing, the pane's
  size and the PTY's newest grid the one the view fits.

Sidebar-Look: no visual change
…llows its pane in every drag frame

cmux-ad m14 (Lawrence's P0, "resizing splits/columns, still not animating the terminals") and m18 T-DRAGS.
The drag hold (terminalDrag.holdSurface, RESIZE VIDEOS A3, default on) kept each terminal's surface at its
drag-start size for the whole drag, clipped or beside the terminal background, and the release spring
held its final size (pixel-perfect's trace, STATUS 13764: 157-159 held ticks per col-drag-2 film).

- Delete TerminalDragTunables.holdsSurfaceDuringDrag (`terminalDrag.holdSurface`) and its path.
- A drag's first frame ends a spring's hold it caught (endPaneMotion); from there the surface takes its
  pane's size in every frame through the placement every resize uses (TerminalHostView.placeSurface,
  the frame owner's place(_:anchor:)), with motion.frameOwner.terminals' present rule. The reflow preview
  re-wraps the text at each size (no final grid is reported during the gesture, so it stays on).
- The release spring of a drag holds no final size and reports no final grid at its start
  (paneMotionWillRest and joinPaneMotion skip restAt while the drag is followed): the surface keeps
  following its pane, and the grid goes when the release lands, as before (one PTY resize per drag).
  The m18 25 ms coalescer replaces that hold in frame-owner-s4's T-KEYS report path.
- stopFollowingResizeGesture() has no hold to release.

Sidebar-Look: no visual change
The What's New highlight for 96ad37a (cmux-ad m14 drags): each terminal resizes with its pane and
re-wraps its text in every frame of a divider or column-edge drag.

Sidebar-Look: no visual change
… (red at b72c7e2)

Codex review of b72c7e2 (blocker): the drag was told only to the surfaces shown at its start
(ScreenContentView.resizeGestureMoved collects the followers once). A fresh surface a replay installs
mid-drag (TerminalSession.swapSurface -> TerminalHostView.install) did not follow it: its release spring
held its final size (restAt) and it reported a grid on every drag frame.

aSurfaceSwappedInMidDragFollowsTheDrag (replaces aSurfaceSwappedInMidDragTakesItsPanesSize): the fresh
surface follows the drag under way, takes its pane's size in every drag step and release frame, and stops
following when the release lands.

Sidebar-Look: no visual change
…e swapped in mid-drag follows it too

Codex review of b72c7e2:
- (blocker) The drag is told to the TerminalHostView, which outlives a surface swap
  (PaneResizeGestureFollowing moves from TerminalSurfaceView to the host). The host tells the surface it
  shows (followResizeGesture), and a surface joins its new host's drag when it moves into it
  (viewWillMove(toSuperview:)); leaving its host, it stops. The host forgets a drag when it leaves its
  window (no end may reach it then), as the surface already did.
- (should-fix) TerminalDividerDragFrameTests pins motion.frameOwner.terminals before each frame: the
  store is process-wide and another suite may set it at an await.
- (nit) Its comment said a drag does not read the switch; a drag takes the switch's present rule.

Sidebar-Look: no visual change
… count (god-file rule)

b78df0c added the host's followsResizeGesture (2 lines) to a file already over its 400-line limit on the base
(558 lines); two doc comments it touches each take one line now.

Sidebar-Look: no visual change
@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 9d57187: 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=pr36328 -f ref=fix/m14-drags-live-terminals -f platform=macos -f backend_mode=local

Default changes

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

  • tunable-source.CmuxNextDesign/Tunables/TerminalDragTunables.swift:all: "cab3d3153edd27ef2c43eb3abd1a3819800e7847845fef5e2100863692ffbbd4" -> "060460dd240b8038982e2f9778e3393893e00fed4957d1f2cd3a7b281f72ada6"
  • tunable-source.CmuxNextDesign/Tunables/TerminalDragTunables.swift:holdsSurfaceDuringDrag: "7f939fb347a62f071f4887444a385f8e40c6898667371343891e8dceb5fef395" -> (none)
  • tunable.terminalDrag.holdSurface: false -> (none)

Signed off on this commit by review (claude-review-subagent, started by lead).

Merge receipt: merged as 058a07e4ae11 with these defaults changed, signed off by review (claude-review-subagent, started by lead).

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

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Base finding, not this PR: SplitSpringMotionTests.fiveRapidSplitsAreOneMotion (an 8.33 ms frame-time bound, no drag, no terminal) failed in step 807d9ee21deb7bfe0713d1ff at 96ad37a03a3 (28.1 ms). The same test failed at #35940's head 588c9c01929 (step 2ff367f37c3d6070185f8309, 11.66 ms), a PR that does not touch it. It passed at this PR's head 216a6d14b74 (step 5cf38b4cf9c34221b74af108, 36 suites green).

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-subagent): SHIP at 216a6d1

Independent review subagent (correctness first, read-only, no build), relayed by the lead at 11:00Z. No blocker. Nits for a follow-up PR (not pushed here, a push voids this receipt):

  1. DividerDragTerminalSteadyTests and TerminalDividerDragFrameTests send .ended at the last .moved point, so the new guard !isFollowingResizeGesture in paneMotionWillRest (TerminalSurfaceView+PaneMotion.swift:25) has no red/green test of its own; release a few points away or on a snapping column edge.
  2. Rename …AndResizesItsProgramOnce or assert resizes == [1, 1].
  3. RestingPlacement.swift:64: the doc of PaneResizeGestureFollowing still says a follower keeps its drag-start size.
  4. bench-terminal-resize.py:22, :240 and its test :20 still say holdSurface is on by default.
  5. Clear followsResizeGesture in viewWillMove(toWindow:) when newWindow !== window.

@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 5, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-subagent): SHIP at 216a6d1

Independent review subagent, posted by the perf lead because the owner stopped on an API overload. No blocker. Follow-up nits (a later PR, not this head): the release-spring guard in paneMotionWillRest needs a red/green test (release away from the last move); rename or assert the TerminalDividerDragFrameTests resize count; update the PaneResizeGestureFollowing doc (RestingPlacement.swift:64) and the stale holdSurface text in bench-terminal-resize.py; clear followsResizeGesture on any window change.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Check classification at head 216a6d14b74 (none from this PR):

  • cmux-next swift test (run 41996591952): WindowCleanupCoverageTests.everySuiteThatOpensAWindowClosesWhatItLeaves names AutomationsSignInTests, WindowViewGeometryTests, WindowFrameBrightnessGridTests, AgentPageFlickerLogTests, UpdatedCardFirstFrameTests, WebProjectionTests (none touched here), and HoverCardMachineColorTests 2 color asserts (not touched here). Base findings.
  • build / release-compile (hq run 41996596581): infrastructure, xcodebuild -resolvePackageDependencies got HTTP 504 downloading Sparkle 2.10.0. Rerun the same SHA.

…gs-live-terminals

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/TerminalDragTunables.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextTerminal/TerminalHostView+PaneMotion.swift
#	Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DividerDragTerminalSteadyTests.swift
@cursor

cursor Bot commented Nov 5, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

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

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

@lawrencecchen lawrencecchen removed 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 5, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-subagent, started by lead): SHIP at 9d57187

Scope: only the merge of feat fe07c0ab50a (#36311) into this PR. The SHIP at 216a6d14b74 covers the rest.

Method

  • I compared the PR-only diff at the old head (git diff 16afd28be6f4 216a6d14b74) with the PR-only diff at the new head (git diff fe07c0ab50a 9d5718797f84). The two diffs touch the same 8 files. Their added lines are identical, byte for byte. Only the removed lines and the hunk context are different, because the base is now perf: resize perf paths on by default (cmux-ad m19, no switches) #36311.
  • For the 6 files with no conflict, the feat delta (16afd28..fe07c0a) and the merge delta (216a6d1..9d57187) are identical.
  • No other file differs between feat fe07c0ab50a and the head.

#36311 lines: none lost

#36328 lines: none lost

Tests

terminalDrag.holdSurface references at the head

  • Code: none. The tunable, its descriptor in all, and its reader in TerminalSurfaceView+PaneMotion.swift are gone.
  • TerminalHostView.holdSurface(size:anchor:) and the calls to it in TerminalSurfaceCommitSizeTests remain. This is the spring hold, not the tunable.
  • Script: bench-terminal-resize.py:244-249 drag_holds_terminals() still asks for the key. With the key gone, debug.tunables returns an error, and the function returns False, as its docstring says. Its docstring at :245-246 still says "on by default since P0 RESIZE VIDEOS A3b: a divider drag keeps terminal text in place and reflows once when it lands #32006". That text comes from feat and is outdated, but it does not block.
  • Comments: terminal_resize_verdict.py:11, test_bench_terminal_resize.py:20,165, and the m14 history comments in the PR's own files. These are history only.
  • schemas/defaults/cmux-next.json:1598 still has holdSurface: true. Feat also still has frameOwner.terminals: false there. The bot generates this file, so I do not ask for a hand edit.

One PTY grid per drag with framesOwnedByCommit = true

  • In the drag, place() goes to requestWantedFrame(), then to updateSurfaceSize(). updateSurfaceSize() holds the grid while isFollowingResizeGesture is true (TerminalSurfaceView.swift:478).
  • A frame that lands goes through landing. It does not send a report (:363).
  • paneMotionWillRest and joinPaneMotion skip restAt during a drag, so the release spring does not report a final grid at its start. The grid goes once, at the landing (followResizeGesture(false), or the spring settles).
  • 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) #36199's surfaceFollowsArea needs followedHold (a key hold only), so a drag does not use it.
  • Proof:
    • The display run at 216a6d14b74 (step dd5500c490fe9bed76d6b35e, switch on) gave PTY grids [1, 1], with each newest grid equal to the grid the view fits.
    • At this head, DividerDragTerminalResizeCountTests passed with the switch default now true (step 8b429b07ee626b091b4c0cfc, suite 3/10).

Observed / Not run

@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 5, 2026
@lawrencecchen
lawrencecchen merged commit 058a07e into feat-cmux-next Nov 5, 2026
306 of 315 checks passed
@lawrencecchen
lawrencecchen deleted the fix/m14-drags-live-terminals branch November 5, 2026 12:19
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
Conflicts, both sides kept:
- TerminalDragTunables: feat (#36328, m14) deleted terminalDrag.holdSurface; this branch (m26) deleted
  motion.frameOwner.terminals. Neither switch is left (all: []).
- TerminalHostView+PaneMotion: feat's drag-following extension (PaneResizeGestureFollowing on the host) plus
  this branch's switch-free key-resize doc.
- TerminalHostView: releaseSurfaceHold doc names a caught drag and a followed key press.
Tests feat added with the deleted switch pinned (DividerDragTerminalSteadyTests, TerminalDividerDragFrameTests)
no longer pin it; the drag frame test checks the always-on rule (no stretched frame).

Sidebar-Look: no visual change

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 9d571879 Deployed Nov 5, 2026 by lawrencecchen via cmux-tui tree preflight #115837
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