ProcessTerminal.stop()still restores the raw-mode state captured bystart(), but now treatsEIO,EPIPE, andENOTCONNfrom the teardown-timesetRawMode()call as a dead terminal instead of crashing the exiting CLI.- Unexpected raw-mode restoration errors still propagate so shutdown does not hide unrelated defects.
test/terminal.test.tscovers successful restoration, the dead-terminalEIOregression, and unexpected-error propagation.
- An SSH or PTY peer can disappear after input draining but before raw-mode restoration. Node/Bun then throws a
synchronous stdin ioctl error, which bypasses the coding-agent's stdout/stderr error handlers and replaces the
requested exit with an uncaught
setRawMode failed with errno: 5stack.
- Raw-mode ownership and restoration are private
ProcessTerminallifecycle responsibilities. Extensions receive neither the saved raw-mode state nor a teardown hook around the stdin ioctl.
- LOW:
packages/tui/src/terminal.tsaround the terminal error classifier andProcessTerminal.stop()raw-mode restoration. - LOW:
packages/tui/test/terminal.test.tsaround lifecycle coverage.
- Cursor restoration and visibility bytes now stay inside each synchronized
render frame instead of being written after
FRAME_END. - The editor stops drawing its inverse-video fake cursor when the hardware
cursor is visible; it still emits
CURSOR_MARKERfor IME placement. - The renderer also removes a colocated inverse-video cursor after
CURSOR_MARKER, covering focused single-lineInputconsumers and both inverse-off (CSI 27 m) and full-reset (CSI 0 m) terminators without discarding full-reset semantics. - Runtime cursor-mode toggles defer visibility changes to the replacement frame, and shutdown no longer blanks content beneath a hardware cursor.
- With
showHardwareCursor: true, animated Working updates briefly published the real cursor on the loader row before a second write returned it to the editor, producing rapid flicker. - The visible hardware cursor and fake cursor were both drawn at the editor insertion point, making Korean IME composition look duplicated. The same ownership conflict affected search, selector, login, and extension inputs.
- This cannot be implemented as an extension: cursor-marker extraction, synchronized-frame boundaries, and final ANSI cursor writes are renderer invariants below the extension API.
- HIGH:
tui.tssynchronized render exits and cursor positioning. - LOW:
components/editor.tscursor rendering.
tui.tsreusesnormalizeTerminalOutputresults across frames through a per-instance memo keyed by the raw line string. Full normalization passes swap in a fresh map holding only the lines used by the current frame, so the memo never outgrows the transcript it mirrors (whose normalized strings it shares by reference). Unchanged lines now keep their string identity across frames, which also restores O(1) reference-equality diff compares that fresh normalization allocations previously defeated. Image lines keep bypassing normalization unchanged.mux.tsviewportRenderEnabled()now defaults on;PI_TUI_VIEWPORT_RENDER=0opts out of viewport-bounded normalize+diff and1still forces it on. Output byte-equivalence between the bounded and full paths is pinned bytest/viewport-render.test.ts(streaming, offscreen line-count changes, offscreen in-place mutations).scripts/perf-trend-local.shpins the two baseline frame-cost lanes toPI_TUI_VIEWPORT_RENDER=0so their historical meaning (unbounded full pass) survives the default flip.bench/frame-cost.ts300-frame p50 on Apple M5 Max, stable components: 100k-line transcript 16.20ms -> 1.97ms (new default; 8.2x) and 16.20ms -> 12.34ms with bounding opted out (memo only); 30k lines 4.51ms -> 1.66ms; 10k lines 2.23ms -> 1.34ms. Emitted bytes per frame stay identical (131) across all lanes.- Coverage:
test/viewport-render.test.tsproves the unset-flag default bounds normalization, the opted-out full pass renormalizes only new content after the first frame, and byte-identical writes across both paths;test/mux.test.tspins the default-on/opt-out switch semantics.
The normalize/diff pipeline is private render state inside TUI.doRender() (previousLines, previousRawLines,
viewport offsets). No component or extension seam can deduplicate normalization work or change the bounded-path
default without owning that state.
- MEDIUM:
tui.tsnormalizeLine()/applyLineResets()bodies and the render-state field block. - LOW:
mux.tsviewportRenderEnabled(),test/mux.test.ts,test/viewport-render.test.ts,scripts/perf-trend-local.shbench lanes.
- Bare
/no longer lists everyskill:<name>command, and partial/skillinput exposes oneskill:namespace hint instead of flooding the palette with every child skill. /skill:and case variants such as/SKILL:open the full skill namespace, while/followed by a skill's full name or leading letters finds matching child skills directly.
- The shared
skill:prefix flooded the root slash-command overview and obscured the smaller set of general commands, while filtering every child also left/skillas a discoverability dead end.
The shared autocomplete provider owns slash-command filtering before coding-agent extensions receive input, so an extension cannot change which registered skill commands appear for each typed prefix.
- LOW:
slash-command-autocomplete.tsskill filtering and its focused autocomplete regression test.
components/markdown.tsregisters bounded Marked block and inline tokenizers for$...$,$$...$$,\(...\), and\[...\]math. Dollar delimiters require non-word outer boundaries, and bracket/parenthesis candidates stop at inline-code or competing opener boundaries. Currency, shell variables, code spans, and malformed delimiters remain literal, including partial streamed currency/shell pairs and math-like text after an unclosed inline-code opener.- The dependency-free
components/latex.tsconverter uses a balanced parser for nested fractions, roots, text wrappers, symbols, and Unicode sub/superscripts. Formula length and nesting budgets fall back to the original text instead of partially converting or repeatedly rescanning untrusted input. A leading combining mark receives a dotted-circle anchor so terminal cell width agrees with the differential renderer. - TeX epsilon/phi variants and escaped script markers stay distinct, complete command names prevent prefix corruption, and unknown commands remain readable. Display formulas inherit their surrounding style context.
- Coverage:
test/markdown.test.tsproves ordinary-text boundaries, nested/budgeted conversion, streamed partial currency/shell and inline-code frames, CJK wide cells, inherited styles, malformed preservation, and focusedVirtualTerminalcell widths including a column-zero combining mark.
The Markdown component owns tokenization before extension-facing coding-agent UI hooks run. Rendering formulas
consistently in assistant messages, nested Markdown structures, and every direct TUI consumer requires the parser seam.
- MEDIUM:
components/markdown.tsparser construction and custom token branches. - LOW:
components/latex.tssymbol/script conversion tables andtest/markdown.test.tsLaTeX cases.
tui.tsnow formats the full over-wide render diagnostic only when strict mode needs it or before the first release-mode crash dump. Later over-wide release frames still truncate safely, but no longer map every rendered line throughvisibleWidth()after the one-shot dump has already been written.__renderDiagnosticStats()exposes diagnostic line-scan counts only underPI_TUI_TEST_SEAMS=1.test/render-contract.test.tsproves the first over-wide release frame scans diagnostic input and a second frame neither writes nor rescans the transcript.
The existing overWideCrashDumpWritten guard covered only the filesystem write. Building crashData happened before
that guard, so an animated row could rescan a large resumed transcript on every frame even though no second dump was
possible. Before the companion coding-agent throttle, a 34 MB session's 32 ms Working shimmer turned that
diagnostic work into a continuous CPU loop.
- LOW:
tui.tsaround release-mode over-wide truncation and crash diagnostics. - LOW:
test/render-contract.test.tsover-wide release behavior.
components/editor.tssetText()no longer unconditionally clears the large-paste registry. It now prunes only entries whose markers do not appear in the new text (and resets numbering when the registry empties). Markers that survive a programmaticgetText()→setText()round-trip stay live: they remain atomic segments and still expand to the full pasted body on submit and ingetExpandedText().- Pruning matches the exact canonical marker string reconstructed from the stored body via the shared
formatPasteMarker()helper (also used at insert time), so arbitrary new text that merely looks like a live marker ([paste #1 +5 lines]with a mismatched suffix) cannot accidentally revive a registry entry and expand to unrelated content. - Provenance check:
setText()retains an entry only if its canonical marker appears in BOTH the previous and the new text (a genuine carried-over round-trip). Stale registry entries — kill-line/word-delete remove marker text without touching the registry, intentionally, so yank can restore a killed marker — can no longer be revived by replacement text that coincidentally contains their exact marker. Explicit cross-instance transfers usesetPasteState(), which skips the provenance check by design. - New
getPasteState()/setPasteState()onEditorplus optionalgetPasteState?/setPasteState?on theEditorComponentinterface (exportedEditorPasteState): snapshots the registry for transfer between editor instances.setPasteState()raises the paste counter above transferred ids (no collisions) and prunes entries whose markers are absent from the current text. The interface documents the paired contract: implement both together — callers treat an editor withsetPasteStatebut nogetPasteStateas paste-unaware, because it could not re-export collapsed markers on a later hand-off. - The submit/
getExpandedText()expansion logic is extracted as the exportedexpandPasteMarkers(text, state)helper so consumers holding a paste snapshot (e.g. an editor hand-off where the source lacksgetExpandedText) can expand markers without duplicating the marker grammar. - Expansion and atomic segmentation are both canonical-exact and therefore consistent: only the exact marker string produced at paste time expands or merges into an atomic segment. Same-id text with a different suffix (e.g. a literal
[paste #1 +5 lines]while entry #1 stores 12 lines) stays literal and is not treated atomically. Previously expansion was suffix-lenient and segmentation was id-based, so a coincidental same-id literal could be replaced by the stored body at submit. - Previously any
setTextround-trip (dialog save/restore, queued-message restore, editor hand-off) orphaned live markers into dead literal text, so submitting sent the literal[paste #1 +18 lines]placeholder to the model instead of the pasted content. - Tests:
test/editor.test.ts"Paste marker atomic behavior" — round-trip preservation, queued-restore combination, selective/exact pruning, coincidental-marker rejection, cross-instance transfer, counter collision safety, and numbering reset.
The paste registry and marker segmentation are Editor-private state; consumers only see getText()/setText()/getExpandedText() and cannot preserve the registry across a round-trip themselves. Cross-instance transfer needs a first-class snapshot API for the same reason.
- LOW:
components/editor.tssetText(),prunePastes(),formatPasteMarker(),getPasteState()/setPasteState(), and the handlePaste marker-insertion line. - LOW:
editor-component.tsoptional paste-state methods;index.tsEditorPasteStateexport. - LOW:
test/editor.test.tspaste marker suite.
terminal-image.ts:detectCapabilitiesno longer hard-disables images under tmux. It probes the effective#{allow-passthrough}value for the current pane (plus#{client_termname}) viatmux display-message -p; when passthrough ison/alland the outer terminal implements the Kitty graphics protocol (kitty/Ghostty/WezTerm viaclient_termnameor leaked env hints), capabilities becomeimages: "kitty", tmuxPassthrough: true. Both probes are dependency-injectable for tests.terminal-image.ts: new exportedwrapTmuxPassthrough(sequence)wraps a sequence in a tmux DCS envelope (ESC Ptmux; … ESC \with every payload ESC doubled).encodeKittywraps each APC chunk individually anddeleteKittyImage/deleteAllKittyImageswrap their delete commands whentmuxPassthroughis active.terminal-image.ts: Kitty Unicode placeholder placement for split-safe tmux rendering. Direct passthrough placement draws at the outer terminal's cursor and breaks in split panes, so placeholder-capable outer terminals (kitty, Ghostty) getkittyUnicodePlaceholders: true:encodeKittygains avirtualoption (U=1virtual placement),buildKittyPlaceholderRowemits U+10EEEE cells with row/column (and id high-byte) diacritics plus the image id in the 24-bit foreground color, andrenderImagereturns per-rowlines(first line carries the wrapped transmission). Placeholder cells are plain 1-column text, so tmux clips/scrolls/moves them with the pane. WezTerm (no placeholder support) stays on direct placement;PI_TUI_TMUX_KITTY_PLACEMENT=placeholder|directoverrides the heuristic. TheImagecomponent usesresult.lineswhen present instead of one sequence line plus empty padding rows.terminal-image.ts: the tmux probe also reportsclient_cell_width/client_cell_height; when tmux images are enabled the detected cell size is adopted viasetCellDimensionsbecause tmux never answers theCSI 16 tcell-size query (verified against tmux 3.6), keeping image aspect ratios correct.terminal-image.ts/index.ts:outerKittyGraphicsMode(clientTermname)is exported so the coding-agent startup guidance can decide whether recommendingallow-passthroughis useful for the attached terminal.utils.ts:extractAnsiCodelearned DCS sequences (ESC P … ST), skipping doubled-ESC pairs so the escaped inner ST does not terminate the envelope early. Wrapped image lines therefore keepvisibleWidth === 0and stay compatible with the TUI's Kitty image-line bookkeeping (id/row extraction intui.tsusesindexOf("\x1b_G"), which still matches inside the doubled-ESC payload).
Image capability detection and Kitty sequence emission are terminal-image.ts internals consumed by the
Image component and the TUI renderer's image deletion/diff paths; extensions cannot re-wrap sequences the
renderer emits.
- MEDIUM:
terminal-image.tstmux branch ofdetectCapabilities,encodeKittychunk assembly, andrenderImagekitty branches. - LOW:
utils.tsextractAnsiCodeescape-sequence branches;components/image.tskitty line assembly. - LOW:
index.tsterminal-image export list;test/terminal-image.test.tstmux capability tests.
autocomplete.tsreopens slash suggestions for a/skill:token after a completed, known leading skill command and offers only skill commands there. Completion inserts the selected second skill command with its leading slash and trailing space.- Other slash commands remain leading-only, and skill suggestions do not appear after prose or an unknown leading skill. This keeps the autocomplete contract aligned with the executable leading-run parser in coding-agent.
The shared autocomplete provider owns the suggestion and insertion decisions that editor consumers use before the coding-agent session receives a prompt.
- LOW:
autocomplete.tsslash-command suggestion and completion branches. This fork-local diff is deliberately minimal because the file is shared with upstream pi.
packages/tui/src/tui.ts: the static 16ms render throttle (MIN_RENDER_INTERVAL_MS) is now an instance field#minRenderIntervalMs(default 16ms — behavior unchanged for existing callers) plussetMaxRenderFps(fps): fps is clamped to 30-120 and stored asMath.floor(1000 / fps)(120fps ⇒ 8ms interval).packages/tui/src/index.ts: exportsgetGraphemeSegmenterandgetWordSegmenterfromutils.tsso consumers (smooth-streaming reveal in coding-agent) share the singleIntl.Segmenterinstances.- Tests:
packages/tui/test/render-fps-cap.test.ts(mocked-timer throttle-delay assertions) andpackages/tui/test/segmenter-exports.test.ts(root re-export identity).
The render throttle is TUI-private scheduler state; extensions and components can request renders but cannot
safely replace the minimum frame interval. The segmenters already existed as module singletons in utils.ts — only
the package-root export surface was missing.
- LOW:
packages/tui/src/tui.tsaround the scheduler field declarations andscheduleRender(). - LOW:
packages/tui/src/index.tsaround theutils.tsre-export list.
packages/tui/src/terminal.ts(+index.tsexport):ProcessTerminalacceptsonExternalStdoutWrite. While started,process.stdout.writeis patched so writes not issued by the terminal itself are forwarded to the handler instead of reaching the screen; the terminal's own output goes through the captured raw writer. External writes previously interleaved with frames, scrolled the viewport, and permanently desynchronized differential rendering. Passthrough restores onstop(), and a throwing handler falls back to raw stdout so output is never lost.packages/tui/src/terminal.ts:setTitlestrips C0/C1 control characters before emitting OSC 0 — an embedded BEL/ESC in session, tool, or extension titles terminated the sequence early and dumped the remainder as raw output.packages/tui/src/tui.ts:renderRequestedandinputRenderPendingare reset in bothstop()andstart(). A render requested within the pending window (nextTick or the 16ms throttle) or while stopped leftrenderRequestedset, so every plainrequestRender()after restart silently no-oped until a keypress.
- stdout ownership, OSC emission, and render-scheduling flags are
ProcessTerminal/TUIinternals; components and extensions cannot patch process streams or reset private scheduler state safely.
- MEDIUM:
packages/tui/src/terminal.tsaroundstart()/stop()stream handling andsetTitle. - LOW:
packages/tui/src/tui.tsstop()/start()scheduling-state resets. - LOW:
packages/tui/test/external-stdout-guard.test.ts,packages/tui/test/terminal.test.ts.
packages/tui/src/tui.ts: added multiplexer-aware full-render policy, bounded mux viewport repaint, opt-in viewport-bounded normalize/diff, scroll-then-diff for bounded concurrent mutations, cursor visibility write coalescing, SGR reset-after-clear coverage, and release-mode render-failure containment.packages/tui/src/utils.ts: replaced the width cache with a two-generation cache and added the measured SGR coalescing utility/report path; runtime SGR coalescing remains unwired because the measured byte reduction was below the adoption gate.
These behaviors depend on TUI's private render state: previous and raw line snapshots, viewport offsets,
terminal dimensions, cursor bookkeeping, synchronized output framing, mux detection, image-row handling, and
row-clear invariants. Components and extensions can reduce churn or request renders, but they cannot safely
replace the renderer's terminal-byte decisions or update its internal cursor/viewport state.
- HIGH:
packages/tui/src/tui.tsarounddoRender(),fullRender(),renderViewportInsertScroll(),renderScrollbackReplay(),positionHardwareCursor(), and render-error diagnostic handling. - MEDIUM:
packages/tui/src/utils.tsaround width caching, terminal-output normalization, and ANSI parsing helpers. - LOW:
packages/tui/test/tui-render.test.tsflicker-budget and scrollback assertions when upstream changes renderer byte expectations.
- In
packages/tui/src/tui.ts, every frame write is bracketed byTUI.FRAME_BEGIN(DECSET 2026+DECRST 7) andTUI.FRAME_END(DECSET 7+DECRST 2026) instead of bare synchronized-output markers. - New regression:
packages/tui/test/regression-wrap-desync-ghost-line.test.ts.
- Differential rendering tracks the cursor with relative moves only. When the terminal draws a row wider than
visibleWidth()measured (East-Asian-ambiguous glyphs, emoji newer than the terminal's Unicode tables, decomposed Hangul jamo), the row physically wraps, the cursor drifts one row down, and every later single-row diff (e.g. the loader seconds tick) paints one row too low — leaving a stale, partially overwritten ghost line such asWorking (0s • esc to interrupt)above the fresh one. With autowrap off during the frame, over-wide rows clip at the last column and the drift cannot happen. Autowrap is restored at frame end so the shell never observes the disabled state, even after a crash between frames.
- MEDIUM: every
let buffer = "\x1b[?2026h"/buffer += "\x1b[?2026l"site inTUI.doRender(),fullRender(),renderViewportInsertScroll(), andrenderScrollbackReplay()— upstream edits to those literals will conflict with theFRAME_BEGIN/FRAME_ENDconstants.
packages/tui/src/components/loader.tssupportsmessageFormatterwith an independent message animation interval.- Senpi's normal TUI depends on this for
Working (Xs • esc to interrupt)shimmer; a loader that only animates the indicator frame is not compatible with the forked CLI.
The loader is instantiated by InteractiveMode during streaming. Extensions can replace the indicator options, but a
globally installed CLI must ship a TUI runtime whose Loader honors messageFormatter.
- HIGH:
packages/tui/src/components/loader.tsaroundLoaderIndicatorOptions,setIndicator(),restartAnimation(), andupdateDisplay(). - HIGH: package/release wiring that decides whether
@code-yeongyu/senpibundles this forked TUI runtime or installs upstream npm@earendil-works/pi-tui.
- In
packages/tui/src/tui.tsTUI.doRender(), structural changes that begin above the previous viewport now replay the latest canonical transcript from the top of the visible viewport when the visible rows would otherwise be unchanged. - In
packages/tui/test/tui-render.test.ts, the Ctrl+O regression now checks the latest xterm scrollback suffix for multiple offscreen expanded blocks, not only the visible tail viewport.
- Terminal scrollback rows above the visible viewport cannot be rewritten in place. The earlier fork-only differential remap updated
previousLineswithout writing a new canonical transcript, so older collapsed tool/read blocks stayed visually collapsed while the bottom block appeared updated. A full screen clear fixed the stale scrollback but reintroduced visible flicker, so the replay now avoids bothESC[2JandESC[3Jand validates the newest canonical suffix instead of trying to delete historical rows.
- HIGH:
TUI.doRender()around thefirstChanged < prevViewportTopbranch, because this preserves the fork's no-viewport-clear behavior while adding a scrollback-only replay path. - LOW:
packages/tui/test/tui-render.test.tsunderTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), content shrinkage that starts above the current viewport now remaps the viewport to the new bottom and uses the existing in-place viewport repaint path instead of forcingfullRender(true). - In
packages/tui/test/tui-render.test.ts, regressions now cover a direct above-viewport collapse and repeated Ctrl+O-equivalent expand/collapse toggles.
- Ctrl+O toggles every expandable chat item. When expanded tool output collapses above the visible rows, the old shrink branch cleared the screen and scrollback (
ESC[2J/ESC[3J]), which produced a visible TUI flash even when the final visible tail rows were unchanged.
- MEDIUM:
TUI.doRender()around thefirstChanged < prevViewportTopremap branch, because this fork already carries upstream-divergent differential repaint logic there. - LOW:
packages/tui/test/tui-render.test.tsunderTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), streaming inserts that move the viewport down while leaving a stable bottom suffix now use a scroll-region update for the changed viewport prefix, then paint only the newly inserted rows. - The fast path skips image rows and overlays, preserving the existing safer repaint paths for cases where terminal-owned image placement or overlay composition makes scroll-region edits risky.
- In
packages/tui/test/tui-render.test.ts, an expanded-output regression now asserts repeated appends avoid viewport/scrollback clears, keep DECSET 2026 balanced, preserve the final viewport, and avoid repainting stable tail rows every tick.
The decision depends on internal renderer state: previous and next viewport slices, line-count delta, stable suffix detection, image-line detection, hardware cursor bookkeeping, and synchronized terminal writes. Components and extensions can reduce churn, but cannot safely emit scroll-region edits or update TUI's private viewport/cursor state.
packages/tui/src/tui.tsnear the viewport remap and differential render branches indoRender().packages/tui/test/tui-render.test.tsinTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), above-viewport growth that remapsviewportTopnow repaints only the visible viewport rows in place under synchronized output instead of falling back to a post-init full replay path. - The repaint path deletes only kitty images in the previously visible viewport slice before rewriting rows, preserving image cleanup without clearing scrollback.
- In
packages/tui/test/tui-render.test.ts, the above-viewport expansion regression now also asserts no raw\x1b[2J/\x1b[3Jappears and verifies visible expanded rows are repainted while DECSET 2026 remains balanced.
The decision point depends on internal renderer bookkeeping (prevViewportTop, viewportTop, hardwareCursorRow, kitty image ID tracking, and synchronized write boundaries). Extensions/components can trigger renders but cannot replace this internal fallback behavior or safely rewrite only viewport rows at this stage.
packages/tui/src/tui.tsaround thefirstChanged < prevViewportTopbranch insidedoRender()(viewport remap handling and fallback path).packages/tui/test/tui-render.test.tsinTUI viewport remap for above-viewport growthassertions.
- Tighten
TUI.doRender()fallback paths so streaming updates can stay on the differential renderer instead of clearing the full screen when unchanged visible viewport rows are stable. - Keep synchronized output (
DECSET 2026) balanced around every differential write path. - Add flicker-budget regression tests for synthetic streaming workloads in
packages/tui/test/tui-render.test.ts.
The fallback decisions live inside TUI.doRender() and depend on private renderer state: previousLines, viewport offsets, terminal dimensions, cursor row tracking, and the line-diff window. Extension hooks and components can request renders, but they cannot override the internal decision to call fullRender(true) or wrap terminal writes with synchronized output.
Component-level caching is added in coding-agent components because high-frequency assistant/tool updates rebuild render trees during streaming. External extensions can register alternate renderers, but they cannot memoize the built-in assistant and tool execution components without replacing core interactive-mode rendering.
packages/tui/src/tui.ts:TUI.doRender()fallback branches around width/height changes,clearOnShrink, deleted-line handling, viewport-shift handling, and synchronized output writes.packages/tui/src/tui.ts:fullRenderpaths andfullRedrawCountaccounting.packages/coding-agent/src/modes/interactive/components/assistant-message.ts: assistant streaming render cache.packages/coding-agent/src/modes/interactive/components/tool-execution.ts: tool execution streaming render cache.packages/coding-agent/src/modes/interactive/interactive-mode.ts: streaming render request audit comments nearmessage_updateandtool_execution_update.
flicker budget under streaminginpackages/tui/test/tui-render.test.tsverifies:- full clear sequence count stays at the initial render only,
- ANSI escape bytes remain below the content-byte budget,
- every
DECSET 2026begin has a matching end, - no
fullRender(true)equivalent clear occurs after the init phase.