The `viewportRefresh()` helper (added by 498e9c2cc, kept across the
erase-after-content refactor in cdbf07a24) claimed to repaint only the
visible viewport but actually wrote every line in `newLines` from the top
of the screen. On terminals without alt-screen — which is OMP's default
on the main screen — that pushed `newLines.length - height` rows into
scrollback on every refresh.
With `PI_DEBUG_REDRAW=1` on a 24-row terminal and a 52-line transcript
the path fired roughly every 30 ms while streaming markdown:
[..] fullRender: firstChanged < viewportTop (5 < 28) — viewport
refresh (prev=52, new=52, height=24)
[..] same, 33 ms later
[..] same, 35 ms later
...
Users saw historical prompt frames (`╰ ㅁㄴ`, `╰ 바ㅇㄱㅁ`, `╰ 바방금 ...`)
stacking up in scrollback even though the visible viewport rendered
correctly. The Korean IME bracketed-paste / display fix from 37ada6c54
is genuinely independent — the residue was always the viewportRefresh
path quietly pushing whole-transcript snapshots into scrollback.
This change:
* Anchors the repaint window to the last `height` content rows. The
`\x1b[H` + line-write loop now never exceeds the visible area, so
the repaint itself can never scroll.
* Pre-scrolls only the delta when the bottom-anchored visible window
slid forward since the last render. The differential-append path
already does the same — losing scrolled-off content silently would
break `Ctrl+B [` / mouse-wheel scrollback during streaming.
* Truncates lines wider than `width` (matching the differential
path's pattern, including the `LINE_TERMINATOR`/`SEGMENT_RESET`
re-append). Wrap-around would otherwise push repaint content past
the bottom row and re-introduce the same unwanted scroll.
* Enriches the `PI_DEBUG_REDRAW` log line with `visibleStart..end`
and `visibleCount/newLines.length` so future repeats of this kind
of regression are obvious in `~/.omp/agent/omp-debug.log`.
Tests:
* New `render-regressions.test.ts` case (scrollback integrity):
height-6 terminal, 30-line transcript, 30 above-viewport mutations.
Asserts (1) scrollback growth stays under 50 rows (pre-fix was
~720), and (2) above-viewport marker values that only ever existed
above the visible window never appear in scrollback.
* Tightened the existing `viewportRefresh` case in
`render-no-2k-flash.test.ts`: now also asserts the captured frames
do NOT contain the above-viewport mutation token and DO contain
the visible-window's bottom line.
* Re-passes the pre-existing `retains append history when offscreen
header changes during overflow growth` test which had been failing
against both the broken viewportRefresh (received 17 lines per
marker) and a naive non-scrolling fix (received 0). The new
pre-scroll branch puts it back at exactly 1.
Full TUI suite: 393/393 pass (was 392/393 before, with the one failure
being the offscreen-header test described above).