Skip to content

fix(pi-tui): make the viewport anchor follow above-viewport content shifts#1364

Open
liruifengv wants to merge 1 commit into
mainfrom
fix/pi-tui-above-viewport-shift
Open

fix(pi-tui): make the viewport anchor follow above-viewport content shifts#1364
liruifengv wants to merge 1 commit into
mainfrom
fix/pi-tui-above-viewport-shift

Conversation

@liruifengv

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue; follow-up to #1353, the problem is explained below.

Problem

The viewport anchor introduced in #1353 pins a buffer row index. But when content above the viewport changes length — a finished agent row collapsing, steps merging into a summary line, a spinner line disappearing — every line below the change shifts, while the pinned window keeps showing the same row indices. Two symptoms, observed live during long streaming turns:

  • Upward creep: the window suddenly shows content k lines further along; the screen appears to keep jumping up, and the blank area under the input box keeps growing.
  • Permanent row loss: the k rows that slid above the window top were never scrolled into scrollback (scrollback holds older bytes at those indices), so they exist nowhere — scrolling back shows gaps.

Every above-viewport net shrink swallowed that many rows, repeatedly, for the whole turn (debug logs showed single frames shrinking by 9–20 lines).

What changed

doRender now scores two hypotheses for a frame whose diff starts above the viewport and whose length changed, by counting mismatched window rows under each alignment:

  • window stayed put (the previous behavior), vs
  • window content shifted by the length delta.

When the shift explains the frame strictly better, the anchor moves with the content:

  • Pure shift (zero mismatches after alignment): the screen already shows exactly that content — re-anchor and update cursor bookkeeping with no painting at all. This is the common streaming case.
  • Shift plus local in-window changes (spinner/timer rows ticking in the same frame): move the anchor with the shift, then repaint the window there.

Scrollback stays exactly-once: committed content ends precisely where the shifted anchor begins, so later growth commits rows in content order — no loss, no duplication. Frames better explained by the pinned hypothesis (appends, tail deletions, oscillation) take the existing paths unchanged.

Validation

  • New e2e case07-above-viewport-shift (in the e2e/ rendering-bug ledger): the visible window must not move when above-viewport lines are removed, the shift must apply even when a live status row changes in the same frame, and later growth must commit each row exactly once. Fails on the previous revision, passes now.
  • The stale-content unit test now asserts the follow-the-content window; its old expectation had baked in the swallowed-row behavior (a visible row silently vanished).
  • Full pi-tui suite (745 tests) and repo-wide tests pass; typecheck clean.

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.

…hifts

The anchor pins a buffer row index, but an above-viewport length change
shifts the content living at every index below it. The pinned window
then suddenly showed content further along (visible upward creep), and
the rows that slid above the window top were lost: never committed to
scrollback, which holds older bytes at those indices. During streaming,
every above-viewport net shrink (a finished agent row collapsing, a
merged step, a spinner line disappearing) permanently swallowed that
many rows, and the blank area under the input box kept growing.

doRender now scores two hypotheses for such frames — window stayed put
vs window content shifted by the length delta — and when the shift
explains the frame strictly better, moves the anchor with the content:
a pure shift re-anchors with no painting at all (the screen already
shows exactly that content), and a shift with local in-window changes
(spinner/timer rows) repaints the window at the shifted anchor.
Commit order stays continuous, so the exactly-once scrollback invariant
is preserved: no loss, no duplication.

Covered by e2e case07 (above-viewport shift), which fails on the
previous revision; the stale-content unit test now asserts the
follow-the-content window instead of the old swallowed-row behavior.
@changeset-bot

changeset-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8d631b0

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 4, 2026

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

commit: 8d631b0

@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: 8d631b063f

ℹ️ 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 on lines +1594 to +1596
const lengthDelta = newLines.length - this.previousLines.length;
const shiftedTop = prevViewportTop + lengthDelta;
if (lengthDelta !== 0 && shiftedTop >= 0) {

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 Handle net-zero above-viewport shifts

When a single streaming frame both removes rows above the viewport and appends the same number of rows near the tail, the total lengthDelta is 0, so this guard skips the new shift-following path entirely. The later clamped diff then repaints the shifted tail in place without scrolling the old top viewport rows into scrollback, recreating the row-loss bug for frames where a status/activity block collapses while new output arrives. The shift amount needs to be derived from the above-viewport edit/alignment, not only from the net buffer length.

Useful? React with 👍 / 👎.

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