Skip to content

refactor(render): remove the resize-settle hold and sweep transition - #353

Merged
forketyfork merged 1 commit into
mainfrom
refactor/remove-resize-settle-hold
Aug 10, 2026
Merged

refactor(render): remove the resize-settle hold and sweep transition#353
forketyfork merged 1 commit into
mainfrom
refactor/remove-resize-settle-hold

Conversation

@forketyfork

@forketyfork forketyfork commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Solution

Every terminal resize used to start a resize-settle hold: the renderer froze the pre-resize cached texture until the session's output stayed silent for resize_settle_quiet_ms (400 ms), then revealed the new layout with a resize_settle_transition_ms (1400 ms) diagonal sweep. This PR deletes that subsystem and renders the post-SIGWINCH repaint live, the way Ghostty does.

Why it had to go

It fired when nothing was resized. A debug log captured while scrolling a Claude transcript in full view shows the hold re-engaging twice on plain scroll output:

08:14:42  resize settle hold started, 122x16 -> 224x40
08:14:43  released after 433ms (quiet)          -> sweep starts
08:14:43  re-armed mid-sweep by a 2048-byte chunk, rearm 1/2
08:14:44  released after 1154ms                 -> second sweep
08:14:45  re-armed mid-sweep by a 2048-byte chunk, rearm 2/2
08:14:46  released after 1114ms                 -> third sweep

Each re-arm redisplayed the stale grid-sized (122x16) texture stretched over the full-screen rect — the "pixelated expanded view" — then swept again. One view toggle produced ~5 s of frozen-and-sweeping content. It stopped only because resize_settle_max_rearms (2) was exhausted.

The re-arm predicate was "a drain call of ≥2 KB within the 1400 ms sweep or 3000 ms after it." Chunk size cannot distinguish an agent redrawing after a resize from a user scrolling, and in Full view wheel events are forwarded to the app as SGR mouse escapes whenever it has mouse tracking on (Claude does), so scrolling reliably trips it.

It was expensive even when working correctly. Every non-re-armed release in that log landed at 433–483 ms — the 400 ms silence floor plus a few ms. Claude's entire repaint completes in ~30 ms, then the code spends 400 ms waiting for silence it already had and 1400 ms animating. That is ~1.8 s of mandatory animation per grid↔full toggle to hide something already finished.

The VT layer never needed help. applyTerminalResize in app/layout.zig mirrors Ghostty's Termio.resize call for call — same Terminal.resize, same pixel-size update, same synchronized_output clear, same DEC 2048 in-band report. Ghostty carries no hold, freeze, or sweep anywhere in Termio.zig, Surface.zig, or renderer/, and resizes long-history sessions instantly. The multi-second repaint the feature was built for is codex-specific; an agent that leaves garbage mid-resize and converges once settled is exhibiting its own behavior, which the terminal should show rather than conceal.

What was removed

  • session/state.zig — nine tuning constants, the ResizeSettleRelease enum, six SessionState fields, eight methods, the re-arm hook in the PTY drain loop, and eleven tests. quietDurationMs stays; DEC 2026 synchronized output still uses it.
  • render/renderer.zigresizeSettleHoldsCache, maybeBeginSettleTransition, renderSettleTransition, renderResizeSettleHold, releaseTransitionTextures, the three transition_* cache fields, the settle branch in the expand/collapse path, and anyDirty's now_ms parameter.
  • gfx/shimmer.zigdrawSweepReveal with its revealMargin and sweep_strip_h helpers; sweepGeometry no longer takes a margin now that only the cyclic wait shimmer calls it. The quit-teardown overlay's shimmer.draw is untouched.
  • app/layout.zig / app/runtime.zigapplyTerminalResize drops now_ms, cascading through both layout wrappers, twelve call sites, and a dead RuntimeScaleChangeContext.now field.

Deliberately kept: the DEC 2026 synchronized-output hold (synchronizedOutputHoldsCache). It honors an explicit \e[?2026h from the application rather than guessing from silence, and Ghostty implements it too.

Tests

The two applyTerminalResize tests that asserted on settle state now assert the opposite guarantee — that a resize bumps render_epoch so new content publishes immediately with nothing able to defer it. Verified non-tautological: commenting out the markDirty() call makes it fail; restoring it makes it pass.

zig build, zig build test (319 tests), just lint, and zig fmt --check all pass.

docs/ARCHITECTURE.md now records why Architect deliberately has no such mechanism, including the cost and the reason the re-arm heuristic is unfixable in principle, so this does not get reinvented.

No linked issue — this came out of a debugging session rather than a filed report.

Test plan

Rendering behavior can't be checked by CI, so these need a human at the window:

  • Open a session with a long Claude conversation history, switch grid → full: content should appear immediately, with no frozen or pixelated intermediate frame and no diagonal sweep.
  • In that full view, scroll up and down through the transcript repeatedly: scrolling should stay smooth and never trigger a pixelated upscaled frame or a sweep (this is the reported bug).
  • Toggle grid ↔ full rapidly several times: each toggle should be instant, with no animation backlog.
  • Repeat with a codex session that has a long transcript. Expect codex's own paced repaint to be visible now, converging on the correct state once it settles — same as Ghostty. Confirm this reads as acceptable rather than broken.
  • Resize the Architect window itself (drag a corner) with an agent running: repaint should track the drag and settle correctly.
  • Confirm the quit-teardown overlay still shimmers — it shares gfx/shimmer.zig, which this PR modifies.
  • Confirm an app using DEC 2026 (codex reflow) still lands as one atomic frame rather than a top-to-bottom rescroll; that hold was intentionally preserved.

Issue: The sweeping resize animation fired constantly with no resize
involved — scrolling a Claude transcript in full view replayed a pixelated,
upscaled frame followed by the sweep, twice, before stopping. Ghostty
resizes the same long-history sessions instantly with no such machinery,
which called the whole hold/freeze/settle approach into question.

Solution: The hold froze the pre-resize texture until output stayed silent
for 400 ms, then swept the new layout in over 1400 ms — roughly 1.8 s of
animation per grid/full toggle to conceal a repaint that Claude finishes in
about 30 ms. Its re-arm heuristic treated any 2 KB drain within ~4.4 s of a
resize as a further repaint wave, which ordinary scroll output trips, since
chunk size cannot distinguish an agent redrawing from a user scrolling.
Architect's VT resize already mirrors Ghostty's Termio.resize call for call,
so the repaint now renders live exactly as it does there; an agent that
leaves garbage mid-resize and converges once settled is exhibiting its own
behavior, not something the terminal should hide.
@forketyfork
forketyfork marked this pull request as ready for review August 10, 2026 07:18
@forketyfork
forketyfork merged commit 61406ca into main Aug 10, 2026
4 checks passed
@forketyfork
forketyfork deleted the refactor/remove-resize-settle-hold branch August 10, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant