0ba8a5ff0e
The clear-terminal branch of renderInteractiveFrame writes raw `output` (no trailing newline) via a direct stdout.write, but then called `writer.sync(outputToRender)`. `outputToRender` appends "\n" for non-fullscreen frames, so on a fullscreen→non-fullscreen transition (a fullscreen frame shrinking below the viewport) sync recorded a state that didn't match the screen: with a declared cursor (useCursor) it placed the persistent caret one row too high (buildCursorSuffix with hasTrailingNewline=true, basing the caret on row `visibleLineCount` instead of the real `visibleLineCount - 1`), and it recorded previousLineCount off by one so the next frame's erase was eraseLines(N+1) (G46 residue). This is the fullscreen→non-fullscreen sibling of #198. Fix: sync the SAME string just written (`output`). `outputToRender === output` whenever the frame is fullscreen or screen-reader, so steady-state fullscreen and SR are byte-for-byte unchanged (G17: an empty SR frame still syncs "" → zero lines). Leaving-fullscreen, overflowing, and unmount-clear all write raw `output`, so syncing `output` is consistent for every clear sub-case. TDD: a new integration test mounts a fullscreen TTY frame with a declared cursor, shrinks it below the viewport, and asserts the emitted caret row and the following frame's erase count. Red before the fix (cursorUp(3)/eraseLines(4)), green after (cursorUp(2)/eraseLines(3)). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>