Skip to content

fix(pi-tui): repaint viewport in place when content collapses above it#1315

Merged
liruifengv merged 10 commits into
mainfrom
fix/pi-tui-collapse-repaint
Jul 2, 2026
Merged

fix(pi-tui): repaint viewport in place when content collapses above it#1315
liruifengv merged 10 commits into
mainfrom
fix/pi-tui-collapse-repaint

Conversation

@liruifengv

@liruifengv liruifengv commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue; the problem is explained below.

Problem

The vendored pi-tui fork clamps the differential render to the visible viewport when a change occurs above it, instead of doing a destructive full redraw (which clears scrollback and yanks the user's scroll position — on Windows Terminal it jumps to the absolute top, microsoft/terminal#20370).

Removing those destructive redraws exposed two problems on content shrink:

  1. Blank screen: when content shrinks past the viewport top while a line above the viewport also changes, the clamped firstChanged lands in the deleted tail region. The render loop runs empty while the cursor sits at the clamped row, the deleted-line clearing writes \r\n past the screen bottom, and the terminal scrolls without the viewport anchor being updated. Every subsequent render lands on the wrong rows: the screen goes blank, the input box disappears, and the TUI never recovers.
  2. UI hover: previousViewportTop only ever grows during normal rendering. Upstream's frequent destructive redraws re-anchored the content bottom to the screen bottom as a side effect; without them, any shrink (e.g. collapsing expanded tool output) left the UI hovering mid-screen above an ever-growing band of dead rows.

What changed

  • Viewport re-anchoring (pi-tui): whenever a change leaves previousViewportTop above max(0, newLines - height), repaint the visible viewport in place with the tail of the new content and re-anchor. The input area stays glued to the screen bottom (content shorter than the screen top-anchors, matching a fresh session), no ESC[2J/ESC[3J is emitted, so scrollback and the user's scroll position are preserved — the original scroll-yank fix does not regress. This subsumes the earlier collapse-specific repaint; that branch is now a defensive destructive fallback. Kitty images in the repaint window fall back to a destructive full render, and a multi-row image straddling the viewport top is included in the image-delete range.
  • Deleted-line clearing clamped to the screen bottom (pi-tui): the tail-clear loop can no longer scroll the terminal untracked.
  • Session resets and the ctrl+o expansion toggle force a destructive full render (kimi-code): /new, /clear, and session switches previously got a clean screen only as a side effect of destructive redraws on shrink; they now explicitly request a full render (requestRender(true)). The ctrl+o toggle does the same: expanding shifts content above the viewport, and the clamped differential render would stack a duplicate copy of the transcript in scrollback on every toggle. Both are deliberate user actions, while compaction and collapse during streaming keep scrollback intact.
  • Regression tests (xterm-headless virtual terminal): blank-screen collapse with self-healing, input box re-anchored to the screen bottom after a shrink, content taller than the screen, scroll position preserved while scrolled into scrollback during a collapse, and kitty image deletion for a block straddling the viewport top. Three existing renderer tests that encoded the old destructive-redraw/hover behavior were updated to assert the re-anchored behavior. VirtualTerminal gained scrollViewport / getScrollPosition / scrollToBottom test helpers.

Verified in a live terminal with PI_DEBUG_REDRAW=1: expanding then collapsing long output, streaming shrink cycles, and starting a fresh session all hit the intended paths with no blank screen and no dead rows below the input box.

Full pi-tui suite: 709 tests pass. kimi-code suite: 2177 tests pass. tsc --noEmit clean in both packages.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

When content shrinks past the viewport top while a line above the
viewport also changes, the clamped differential path left the render
loop empty, cleared deleted lines past the screen bottom, and desynced
the cursor anchor — leaving the viewport blank with the input box gone
until a full redraw.

Repaint the visible viewport in place for that case (no ESC[3J, so the
scrollback and the user's scroll position are preserved), and clamp
deleted-line clearing to the screen bottom so it can never scroll
untracked.
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5aaecf1

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

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/kimi-code Patch
@moonshot-ai/pi-tui 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

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@5aaecf1
npx https://pkg.pr.new/@moonshot-ai/kimi-code@5aaecf1

commit: 5aaecf1

@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: bad60a9213

ℹ️ 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.ts Outdated
The collapse repaint in pi-tui intentionally preserves scrollback, so
/new, /clear, and session switches no longer got a clean screen as a
side effect of the destructive full redraw — the previous session's
text stayed above the welcome banner.

Session resets want a pristine screen, so force a destructive full
render explicitly instead of relying on the renderer's shrink
behavior.
…pse repaint

A multi-row kitty image can start above prevViewportTop while its
reserved rows are still visible. The collapse repaint's image-delete
range started at prevViewportTop and missed the image line carrying the
id, leaving a stale overlay that also dropped out of
previousKittyImageIds tracking. Widen the range to include such a
straddling block.
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 18b2164c5e

ℹ️ 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".

…e screen bottom

previousViewportTop only ever grows during normal rendering, so after a
shrink the content bottom could hover above the screen bottom, leaving
dead rows that nothing repaints. Upstream masked this by frequently
doing destructive full redraws, which re-anchored as a side effect; the
fork removed those redraws without replacing the re-anchoring.

Generalize the collapse repaint into a re-anchor check at the top of
the differential path: whenever prevViewportTop exceeds
max(0, newLines - height), repaint the visible viewport in place with
the tail of the new content. The input area snaps back to the screen
bottom, scrollback and the user's scroll position stay intact (no
ESC[3J), and the previous collapse branch becomes a defensive
destructive fallback.

Update the three renderer tests that encoded the old behavior
(destructive redraw on shrink, viewport hover after clamped shrink) to
assert the re-anchored behavior instead.
Expanding tool output shifts content above the viewport; the clamped
differential render paints the shifted content through the screen,
stacking a duplicate copy below the stale one in scrollback on every
toggle. The toggle is a deliberate user action (like /clear), so do a
destructive full render instead: scrollback holds exactly one copy and
the expanded output stays readable by scrolling up.
@liruifengv liruifengv merged commit b40bb71 into main Jul 2, 2026
9 checks passed
@liruifengv liruifengv deleted the fix/pi-tui-collapse-repaint branch July 2, 2026 12:52
@github-actions github-actions Bot mentioned this pull request Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant