fix(terminal): prevent resize from bricking the parser mid inline-image decode
Problem
Terminals were crashing with:
Error: improper continuation due to previous async handler, giving up parsing
at ur.parse (addon-webgl…) ← EscapeSequenceParser.parse
at _r.flushSync (…) ← WriteBuffer.flushSync
at vr.resize (…) ← CoreTerminal.resize
at l.fit (terminal-parking…) ← FitAddon.fit
at measureAndResize (terminal-runtime…)
at updateRuntimeAppearance (…)
at TerminalRuntimeRegistryImpl.updateAppearance (…)
at commitHookEffectListMount (react…)
This isn't a recoverable error: when it throws, xterm sets the parser to FAIL, so every subsequent write throws too — the terminal is bricked until reload.
Root cause
@xterm/addon-image registers async parser handlers for inline images. iTerm2 (OSC 1337) and Kitty (APC) decode via createImageBitmap(...).then(...), which keeps the escape-sequence parser paused while the bitmap decodes. (SIXEL's handler is synchronous and never triggers this.)
During that paused window, any resize → FitAddon.fit() → terminal.resize(), and xterm's CoreTerminal.resize() unconditionally calls WriteBuffer.flushSync(), which re-enters parser.parse() synchronously. The parser detects an improper continuation and throws + permanently fails. flushSync is documented as "unreliable with async parser handlers."
The reported trace is the mount path: right after createRuntime replays the serialized scrollback (which can contain an inline image), the mount effect fires updateAppearance → fit while that image is still decoding.
Fix
Never resize while a write is still being parsed.
parser-idle-gate.ts(new, xterm-free pure logic):wrapWritedecorates the terminal'swriteto count in-flight writes — xterm fires a write's callback only after that chunk and its async handlers fully parse, sopending === 0means the parser is back inGROUND.runWhenParserIdleruns the resize immediately when nothing is pending, or parks it (latest-wins) and flushes from the settling write's callback on a microtask, so the eventualflushSyncnever runs while a handler is paused — nor re-entrantly inside xterm's own write loop.terminal-runtime.ts: install the wrap once increateRuntime(before any write), so every write — live firehose, buffer restore, status notices — is counted and nothing can bypass it.measureAndResizegates the fit on the runtime's gate.terminal-runtime-registry.ts: drop the now-redundant post-fit dimension check (dimensions can settle asynchronously; theonResizecallback covers it).- v1 terminal (
helpers.ts,v1-terminal-cache.ts,useTerminalLifecycle.ts): same gating for the legacy terminal, which loads the sameImageAddon— wrapxterm.writeat creation, route every fit (attach, ResizeObserver, font-settle, appearance, post-attach backend sync) through the gate, and cancel parked work on detach/dispose.updateAppearancenow reports dimension changes via callback instead of a synchronous return, since a gated fit can run after the call returns.
Full inline-image support (iTerm2/Kitty/SIXEL) is retained.
Testing
parser-idle-gate.test.ts: runs-when-idle, defers-until-drain, runs-once, coalesces, preserves caller callback.- Mutation-checked: forcing a synchronous resize (the bricking behavior) fails the defer/coalesce tests.
bun run typecheckand Biome clean.
Summary by cubic
Prevents terminal crashes when a resize lands during inline image decoding by deferring fits until the parser is idle. This removes “improper continuation” errors and keeps inline images (iTerm2/Kitty/SIXEL) working.
- Bug Fixes
- Gate resizes on parser idleness and cancel parked work on detach/dispose; wrap
terminal.writeto count in‑flight parses from@xterm/addon-imageand deferfit()until write callbacks unwind (latest‑wins, microtask flush, re‑park when a new write arrives before the flush). - Route all fits through the gate in
measureAndResizeand the v1 cache; only callonResizewhen size changes, preserve scroll position; remove redundant post‑fit dimension check in the registry and the extra host‑visibility guard beforemeasureAndResize; drop dead v1lastCols/lastRows; font‑settle refits now report via the gated callback. - Add unit tests for the gate (runs‑when‑idle, defers, coalesces, cancels, preserves callbacks, keeps gate closed until callback unwinds, re‑parks on drain→microtask races).
- Gate resizes on parser idleness and cancel parked work on detach/dispose; wrap
Summary by CodeRabbit
- New Features
- Improved terminal resizing and refitting by deferring fit/refresh and coordinating it with terminal write activity until the terminal parser is idle.
- Coalesces multiple idle-gated resize requests so only the latest queued action runs, including during rapid updates.
- Bug Fixes
- Prevents resizing/fit during in-flight terminal writes (including nested write scenarios) to reduce flicker and inconsistent updates.
- Ensures callbacks for resize notifications fire only when dimensions actually change, and pending work is cancelled on detach/dispose.
- Tests
- Added automated coverage for idle-gated execution, coalescing, cancellation, and write-drain timing.