openclaw - ✅(Solved) Fix [Bug]: TUI long messages vanish — fullRender(true) clears terminal scrollback (\x1b[3J) [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78017Fetched 2026-05-06 06:17:52
View on GitHub
Comments
2
Participants
2
Timeline
11
Reactions
2
Author
Timeline (top)
mentioned ×4subscribed ×4commented ×2cross-referenced ×1

Long assistant messages in openclaw tui appear briefly, then disappear from the screen entirely (including from terminal scrollback) once the conversation continues. Shorter messages render normally. Only reproduced via the TUI so far.

Root Cause

Likely root cause

Fix Action

Fixed

PR fix notes

PR #4204: fix(tui): preserve scrollback on content-driven full redraws

Description (problem / solution / changelog)

Summary

Stops pi-tui from wiping the terminal's scrollback buffer on content-driven full redraws. Previously, fullRender(true) always emitted \x1b[2J\x1b[H\x1b[3J, which clears scrollback as well as the visible screen. Because pi-tui deliberately renders in normal-screen mode (not alt-screen) so that conversation history stays scrollable, that scrollback wipe was actively destructive.

Why

firstChanged < prevViewportTop triggers fullRender(true) whenever any line above the previous viewport changes. During long streaming markdown the renderer re-flows the entire message body on every chunk; once the message has grown past the visible area, even a small reshuffle (paragraph boundary moving, code-fence opening, list re-indent) can put firstChanged above prevViewportTop. Each time that fired, the user lost:

  • Any pre-existing shell history that was in the terminal's scrollback before pi-tui started.
  • Any TUI lines that had naturally scrolled out during prior renders.

Width changes are different — wrapping changes mean the existing scrollback is visually misaligned at the new width, so clearing it there is defensible. But for content-driven redraws there's no need to throw it away.

Change

Split the clear parameter on the local fullRender helper:

const fullRender = (clear: boolean | "scrollback"): void => {
    if (clear === "scrollback") {
        buffer += "\x1b[2J\x1b[H\x1b[3J"; // clear screen + home + clear scrollback
    } else if (clear === true) {
        buffer += "\x1b[2J\x1b[H";        // clear screen + home, preserve scrollback
    }
    // when not wiping scrollback, write only the bottom `height` lines so we
    // don't push duplicate copies into scrollback that prior renders already
    // pushed there
    const startLine = clear === true ? Math.max(0, newLines.length - height) : 0;
    for (let i = startLine; i < newLines.length; i++) { /* ... */ }
    // ...
};

Call sites:

TriggerBeforeAfter
widthChangedfullRender(true)fullRender("scrollback")
heightChanged (non-Termux)fullRender(true)fullRender(true) (preserves scrollback)
clearOnShrinkfullRender(true)fullRender(true) (preserves scrollback)
targetRow < prevViewportTopfullRender(true)fullRender(true) (preserves scrollback)
extraLines > heightfullRender(true)fullRender(true) (preserves scrollback)
firstChanged < prevViewportTopfullRender(true)fullRender(true) (preserves scrollback)

Only widthChanged keeps the scrollback wipe.

Edge case considered

Writing all of newLines after \x1b[2J\x1b[H (without \x1b[3J) would push newLines.length - height lines into scrollback via the natural-scroll path, duplicating content that earlier differential renders already pushed there. The patch addresses this by writing only the bottom height lines on clear === true. The viewport ends up populated with the latest content; scrollback retains whatever the prior renders left there. That history may be slightly stale relative to the current state of an above-viewport line, but it's strictly better than losing it entirely — and the differential renderer already accepts some scrollback drift (it never updates lines that have scrolled out).

Tests

Adds two regression tests in packages/tui/test/tui-render.test.ts:

  1. preserves shell scrollback across a content-driven full redraw — pre-populates the virtual terminal's scrollback with shell-style lines, runs the TUI long enough to hit the above-viewport path, and asserts the pre-existing lines are still in getScrollBuffer(). Fails on main (not ok), passes with this patch.
  2. clears scrollback on width change (wrapping invalidates prior render) — sanity check that the widthChanged branch still triggers a full redraw.

Existing 554 tests continue to pass (npm test from packages/tui).

Reported downstream

Filed as openclaw/openclaw#78017 — long messages "vanish" in the OpenClaw TUI after streaming. The OpenClaw TUI consumes pi-tui directly, so the fix lands at this layer.

Risk

Low. The fix only changes which escape sequences are emitted on the full-redraw paths and how many lines are written; the line-tracking state (previousLines, previousViewportTop, hardwareCursorRow, cursorRow) is updated identically to before, so subsequent differential renders continue to operate on the same coordinate system.

Changed files

  • packages/tui/src/tui.ts (modified, +37/-9)
  • packages/tui/test/tui-render.test.ts (modified, +77/-0)

Code Example

\x1b[?2026h          (begin sync)
\x1b[2J\x1b[H\x1b[3J (clear screen + home + clear scrollback)
... rewrites only current `newLines` ...
\x1b[?2026l

---

PI_DEBUG_REDRAW=1 openclaw tui
# tail -f ~/.pi/agent/pi-debug.log

---

fullRender: firstChanged < viewportTop (<n> < <m>) (prev=<X>, new=<Y>, height=<H>)
RAW_BUFFERClick to expand / collapse

Summary

Long assistant messages in openclaw tui appear briefly, then disappear from the screen entirely (including from terminal scrollback) once the conversation continues. Shorter messages render normally. Only reproduced via the TUI so far.

Environment

  • OpenClaw: 2026.5.4 (325df3e)
  • Interface: openclaw tui
  • pi-tui: bundled at node_modules/@mariozechner/pi-tui/dist/tui.js
  • OS: macOS (Darwin 25.3.0, arm64)

Symptom

  • Send/receive a long assistant message that overflows the visible viewport.
  • The full message is briefly visible while/just after streaming.
  • After the next render (subsequent message, status/footer change, focus event, etc.), the message vanishes — and is also gone from terminal scrollback (cannot scroll up to recover it).
  • Short messages are unaffected.

Likely root cause

pi-tui uses normal-screen rendering plus a differential-redraw path that occasionally falls back to fullRender(true). fullRender(true) issues:

\x1b[?2026h          (begin sync)
\x1b[2J\x1b[H\x1b[3J (clear screen + home + clear scrollback)
... rewrites only current `newLines` ...
\x1b[?2026l

The third escape (\x1b[3J) wipes the terminal’s scrollback buffer. After that, only the bottom terminal_height lines of newLines remain visible — anything that scrolled into scrollback during prior renders is gone.

There are three triggers for fullRender(true) in pi-tui/dist/tui.js:

  1. widthChanged — line ~743
  2. heightChanged (non-Termux) — line ~751
  3. firstChanged < prevViewportTop — line ~842

(3) is the suspected culprit for this report. While a long assistant message streams, AssistantMessageComponentHyperlinkMarkdownMarkdown.setText() invalidates and re-renders the entire markdown body on each chunk. As the body grows past the viewport, any changed line in the early portion of the rendered buffer (e.g., a line that re-flows due to a paragraph boundary shift, a blockquote close, or a code-fence open) sets firstChanged to a position above prevViewportTop. That triggers fullRender(true), clearing scrollback and leaving only the last height lines visible.

Code references (paths within bundled openclaw install):

  • node_modules/@mariozechner/pi-tui/dist/tui.js
    • \x1b[2J\x1b[H\x1b[3J in fullRender (within doRender)
    • if (firstChanged < prevViewportTop) { fullRender(true); return; }
  • dist/tui-CD7AaN2H.js
    • AssistantMessageComponent.setTextHyperlinkMarkdown.setTextMarkdown.setTextinvalidate()

Why it’s surprising

pi-tui deliberately uses normal-screen + scrollback (not alt-screen) so that conversation history remains scrollable in the user’s terminal. \x1b[3J in the fullRender path defeats that goal whenever a non-resize trigger fires — the user permanently loses the rendered content above the viewport.

Suggested fixes

Pick one or combine:

  1. Drop \x1b[3J from the firstChanged < prevViewportTop path. Width/height changes plausibly justify clearing scrollback (wrapping changes), but a content-change full-redraw doesn’t. For path (3), a redraw of just the visible viewport is sufficient.
  2. Stabilize early lines during streaming. When only the trailing portion of a streaming markdown changes, ensure earlier lines are byte-identical to the previous render so firstChanged lands inside the viewport.
  3. Treat scroll-pinned content above viewport as immutable. If firstChanged < prevViewportTop, accept that scrollback is authoritative for those lines and only redraw from prevViewportTop downward.

Repro

  1. Start openclaw tui in any terminal that supports \x1b[3J (iTerm2, Terminal.app, kitty, alacritty, Ghostty all do).
  2. Ask a question that produces an assistant reply longer than the visible terminal height (e.g., “explain X in detail with examples and a code block”).
  3. Wait for the reply to finish streaming, then send any follow-up — or trigger any event that re-renders (resize back-and-forth, focus change, status update).
  4. Try to scroll up in the terminal. The long message is gone.

Diagnostic capture

For maintainers reproducing locally:

PI_DEBUG_REDRAW=1 openclaw tui
# tail -f ~/.pi/agent/pi-debug.log

Each fullRender logs its reason (first render, terminal width changed, terminal height changed, firstChanged < viewportTop, clearOnShrink, extraLines > height, deleted lines moved viewport up). The expected log line for this bug is:

fullRender: firstChanged < viewportTop (<n> < <m>) (prev=<X>, new=<Y>, height=<H>)

Related

  • #44130 — TUI scroll-jump / auto-scroll behavior (related auto-scroll family)
  • #11269 (closed) — original auto-scroll-to-top regression

Impact

UX bug — content the user already saw cannot be recovered, including via the terminal’s native scrollback. Long-form responses (explanations, code review output, plan documents) are the most affected.

extent analysis

TL;DR

The issue can be fixed by modifying the fullRender function in pi-tui to avoid clearing the terminal's scrollback buffer when firstChanged < prevViewportTop.

Guidance

  • Identify the fullRender function in pi-tui/dist/tui.js and remove the \x1b[3J escape sequence that clears the scrollback buffer.
  • Alternatively, stabilize early lines during streaming by ensuring earlier lines are byte-identical to the previous render, so firstChanged lands inside the viewport.
  • To verify the fix, reproduce the issue using the provided steps and check if the long message remains in the terminal's scrollback buffer after re-rendering.

Example

No code snippet is provided as the issue is specific to the pi-tui library and requires modification of its internal code.

Notes

The fix may require modifications to the pi-tui library, which could have unintended consequences. It's essential to thoroughly test the changes to ensure they do not introduce new issues.

Recommendation

Apply the workaround by modifying the fullRender function to avoid clearing the scrollback buffer when firstChanged < prevViewportTop, as this is the most direct solution to the problem.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING