fix(runtime): give screen-reader mode a dedicated write path (Ink parity, G59) (#62)

Ink's onRender screen-reader branch (ink.tsx:573-625) writes the wrapped SR
transcript with a RAW stdout.write using manual ansiEscapes.eraseLines(prev) +
(inline static, if any) + the wrapped output, sets lastOutput/lastOutputToRender/
lastOutputHeight, and RETURNS before the normal interactive frame path. It emits
NO clearTerminal, does NOT accumulate/replay fullStaticOutput, does NOT go
through log-update, and does NOT hide the cursor.

vue-tui routed SR frames through renderInteractiveFrame, so a tall/overflowing
SR transcript (outputHeight >= viewportRows, then previousOutputHeight >
viewportRows) hit the clearTerminal branch — wiping the SR user's scrollback,
replaying accumulated fullStaticOutput, and the mount-time hide left the cursor
hidden.

This adds a dedicated `if (isScreenReaderEnabled) { ... return; }` branch in
commit(), before fullStaticOutput accumulation (now gated off for SR) and before
renderInteractiveFrame, mirroring Ink's SR branch byte-for-byte (eraseLines +
inline static + wrapped output, lastOutputToRender = wrapped output with no
trailing "\n", height = split count). It never clears the terminal, never
replays static, never uses the log-update writer, and the mount-time cursor-hide
is now skipped for SR mode. The normal (non-SR) interactive path is unchanged —
clearTerminal-on-tall-frame still applies there. G17/G46 SR behavior preserved.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-30 14:30:02 +08:00
committed by GitHub
parent c8cbacfbb8
commit 8140c663ce
3 changed files with 217 additions and 5 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ Non-obvious calls made while fixing gaps, recorded for review in the final repor
| G56 | app-exit-instances-animation-sr | exit() misclassifies cross-realm Error objects (no [object Error] fallback) → resolves waitUntilExit() instead of rejecting — sweep-7 LOW | P3 | todo | — | — |
| G57 | app-exit-instances-animation-sr | useAnimation reset() while isActive=false zeros frame/time/delta; Ink keeps the frozen (paused) values — sweep-7 LOW | P3 | todo | — | — |
| G58 | static-newline-spacer | Standalone `<Transform>` with DIRECT text/Newline children (not wrapped in `<Text>`) renders nothing — text silently dropped; Ink's Transform IS an ink-text host so bare-string children render inline (MEDIUM, sweep-8; canonical README pattern) | P1 | pr-open | `fix/parity-standalone-transform-text` | #61 |
| G59 | render-lifecycle-reconciler | Interactive SR frames route through renderInteractiveFrame → a tall/overflowing SR frame emits clearTerminal + replays fullStaticOutput + hides cursor; Ink's dedicated SR branch never clears/accumulates/hides (MEDIUM, sweep-8) | P1 | todo | — | — |
| G59 | render-lifecycle-reconciler | Interactive SR frames route through renderInteractiveFrame → a tall/overflowing SR frame emits clearTerminal + replays fullStaticOutput + hides cursor; Ink's dedicated SR branch never clears/accumulates/hides (MEDIUM, sweep-8) | P1 | pr-open | `fix/parity-sr-dedicated-write-path` | #62 |
| G60 | box-layout-border | String-typed dimension/position values (bare numeric string e.g. width="50") treated as POINT not PERCENT; Ink applies all string dims as percent — sweep-8 LOW | P3 | todo | — | — |
| G61 | stdout-stderr-stdin-size-cursor | Bracketed-paste disable write during stdin dispose() lacks Ink's destroyed/writableEnded (canWriteToStdout) guard — sweep-8 LOW | P3 | todo | — | — |
| G62 | render-lifecycle-reconciler | resolveExit()/teardown() writable-stream checks omit the writableLength fallback + stdout.writable flag that Ink's getWritableStreamState uses — sweep-8 LOW | P3 | todo | — | — |