Skip to content

fix(pi-tui): skip destructive redraw for in-place changes above the viewport - #3197

Open
jiangshibiao wants to merge 2 commits into
MoonshotAI:mainfrom
jiangshibiao:fix/pi-tui-above-viewport-inplace
Open

fix(pi-tui): skip destructive redraw for in-place changes above the viewport#3197
jiangshibiao wants to merge 2 commits into
MoonshotAI:mainfrom
jiangshibiao:fix/pi-tui-above-viewport-inplace

Conversation

@jiangshibiao

@jiangshibiao jiangshibiao commented Aug 24, 2026

Copy link
Copy Markdown

Related Issue

Resolve #981

Fixes #1487, fixes #1705, fixes #1805, fixes #2008, fixes #2167, fixes #2168, fixes #2212, fixes #1213 — all reports of the same root cause (an above-viewport change triggering a destructive full redraw that yanks scroll position during streaming), verified against the same code path. Also covers the scroll-position portions of #2743 and #1261. Partially addresses #2098 and #2296 — in-place ticks are covered, frames that insert or collapse rows above the viewport still full-redraw (see "Known limitations"). Earlier reports #1117 #1284 #1180 #2097 were closed without the mechanism being fixed; the problem persists through 0.38.0 (the latest release at the time of writing).

Problem

During streaming, any change to rows above the visible viewport — a thinking block switching from live spinner to finalized text, or a status/elapsed-time line ticking — takes the firstChanged < prevViewportTop branch in pi-tui's differential renderer and falls back to fullRender(true). That emits ESC[2J ESC[H ESC[3J, clearing the screen and scrollback and reprinting the whole buffer, which yanks the user's scroll position (widely reported as "jumps to the top") and can fire several times per second while a status line ticks.

What changed

In packages/pi-tui/src/tui-main-screen.ts, an in-place change above the viewport (same line count, no kitty image lines in the changed range) no longer triggers the destructive redraw:

  • Change entirely above the viewport: commit the frame caches without painting — those rows are already committed to scrollback, so repainting them cannot change what the terminal shows.
  • Change spanning the viewport boundary: clamp the repaint range to the viewport top and continue on the normal differential path.
  • Line-count changes or kitty-image involvement keep the existing destructive full redraw.
  • The lowest skipped row is tracked (aboveViewportStaleTop); the Termux height-increase path (the only height-change path that avoids fullRender) invalidates and repaints re-exposed rows.

This is deliberately narrow. The fork's earlier broad viewport/scrollback patches were reverted in 23daf0f after accumulating blank-screen / duplicated-scrollback / lost-row edge cases; this change never re-anchors the viewport and never scroll-commits, leaving upstream differential behavior untouched everywhere else. The divergence is registered in packages/pi-tui/AGENTS.md (item 9) with guarding tests.

Tests: new "TUI above-viewport in-place changes" suite in packages/pi-tui/test/tui-render.test.ts (skip-paint, boundary clamp, Termux re-exposure, layout-shift fallback). Full pi-tui suite passes (978 tests); tsc --noEmit is clean. Verified locally against a built CLI: scrolling up during streaming no longer jumps when a thinking block finalizes or a status line ticks.

Known limitations / related work:

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve). — I am aware none of the linked issues carries an /approve yet; linking the canonical reports so maintainers can pick one, and happy to adjust per guidance.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset. (Changeset included: patch for @moonshot-ai/kimi-code.)
  • Ran gen-docs skill, or this PR needs no doc update. (Internal rendering change; packages/pi-tui/AGENTS.md divergence list updated, no user docs affected.)

…iewport

Lines already committed to scrollback cannot change what the terminal
shows, yet any above-viewport change fell back to fullRender(true),
issuing ESC[2J/ESC[H/ESC[3J and reprinting the whole buffer. During
streaming (thinking blocks finalizing, status lines ticking) this
cleared scrollback and yanked the user's scroll position to the top.

Skip the paint for in-place above-viewport changes (same line count,
no kitty images in the range), clamp boundary-spanning changes to the
viewport, and track the lowest skipped row so a Termux height increase
can invalidate re-exposed cache entries. Layout shifts keep the
destructive redraw.
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c97f407

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d6ac69715

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/pi-tui/src/tui-main-screen.ts Outdated
this.hardwareCursorRow = state.hardwareCursorRow;
this.maxLinesRendered = state.maxLinesRendered;
this.previousViewportTop = state.previousViewportTop;
this.aboveViewportStaleTop = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve stale-row state across render-state restoration

When captureRenderState() is called after an above-viewport repaint was skipped and that state is restored before a Termux height increase, this assignment discards the only record of the stale scrollback row. The restored line cache already contains the current text, so resizing to expose that row produces no diff and no full render, leaving the terminal showing the old content indefinitely. Include aboveViewportStaleTop in TuiMainScreenRenderState and restore it instead of clearing it.

AGENTS.md reference: packages/pi-tui/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c97f407aboveViewportStaleTop is now carried in TuiMainScreenRenderState so it survives captureRenderState/restoreRenderState, with a regression test covering the skip → capture/restore → Termux height-increase sequence (full pi-tui suite: 979 tests pass).

… restore

The above-viewport in-place skip path records the lowest stale row in
aboveViewportStaleTop so a later Termux height increase can invalidate
and repaint re-exposed rows. restoreRenderState discarded that marker,
so a capture/restore round-trip between the skipped repaint and the
resize left the re-exposed row showing stale content with no diff and
no full render to correct it. Carry the marker in
TuiMainScreenRenderState instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment