Skip to content

fix(tui): prevent viewport jump when thinking finalizes above viewport#1141

Open
starSumi wants to merge 3 commits into
MoonshotAI:mainfrom
starSumi:fix/thinking-viewport-scroll-jump
Open

fix(tui): prevent viewport jump when thinking finalizes above viewport#1141
starSumi wants to merge 3 commits into
MoonshotAI:mainfrom
starSumi:fix/thinking-viewport-scroll-jump

Conversation

@starSumi

Copy link
Copy Markdown

Summary

When ThinkingComponent transitions from live to finalized above the viewport, its line count changes (4→3), triggering pi-tui's destructive fullRender(true) which clears the screen and jumps to the top.

Fix: Stable transition mechanism — finalize() keeps live-format line count, compact() defers the reduction to a safe render cycle when assistant content also changes.

Root Cause

finalize() → render lines: 4→3
→ pi-tui: firstChanged < prevViewportTop → fullRender(true) → \x1b[2J → jump to top

Solution

thinking.ts — Stable mode

  • finalize() enters stable mode: stops spinner, keeps same line shape as live
  • compact() defers actual line-count reduction, returns boolean
  • render() uses mode === 'live' || stableMode for identical output

streaming-ui.ts — Deferred compaction

  • onThinkingEnd()finalize() (stable, not compact)
  • compactThinkingIfPending() called at onStreamingTextStart() — both changes in same render cycle
  • Fallback in finalizeTurn(), cleanup in resetLiveText()

thinking.test.ts — Updated + 3 new tests

  • Stable mode line count preservation, compact return value, expand/collapse after compact

Why it works

Phase Lines pi-tui path
Live 4 (stable) Differential ✓
Thinking ends 4 (stable) Differential ✓
Assistant starts 3 (compact) + new content fullRender masked by content below

Testing

  • TypeScript compiles clean
  • All 8 tests pass
  • Backward compatible (finalized-mode constructor unchanged)

Related

When the ThinkingComponent transitions from 'live' to 'finalized' mode
while above the visible viewport, its rendered line count changes (4→3
lines). This triggers pi-tui's `firstChanged < prevViewportTop` branch,
which falls back to a destructive fullRender (clear-screen), causing the
terminal to jump to the top.

Introduce a stable transition mechanism:
- `finalize()` now enters 'stable mode' that keeps the same line count
  as live mode (spinner replaced by static bullet, same content region)
- `compact()` defers the actual line-count reduction to a safe window
- StreamingUIController calls compactThinkingIfPending() at the start
  of assistant text streaming, so both changes land in the same render
  cycle and the fullRender is masked by the new content below

Fixes MoonshotAI#981

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 004c55e

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: 3cfd3a6df6

ℹ️ 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 +601 to +605
if (child instanceof ThinkingComponent) {
if ((child as ThinkingComponent).compact()) {
this.host.state.ui.requestRender();
}
break;

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 Compact the pending thinking block, not the first one

When transcriptContainer already contains an older ThinkingComponent (for example after a previous turn or replayed message), this loop stops on that older finalized block; compact() returns false, but _pendingThinkingCompact has already been cleared and the loop still breaks. The just-finalized stable-mode block later in the transcript is then never compacted, leaving it in the live-shaped thought view instead of the finalized preview.

Useful? React with 👍 / 👎.

if (state.appState.streamingPhase === 'idle') return;
this.host.deferUserMessages = false;
// Last-chance compact in case no assistant text triggered it
this.compactThinkingIfPending();

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 Move last-chance compaction after finalizing thinking

For a turn that ends while thinking is still active and no assistant text has started, _pendingThinkingCompact is still false at this point, so this call is a no-op; finalizeLiveTextBuffers() below then calls onThinkingEnd() and sets the pending flag after the last-chance compact has already passed. That leaves no-assistant thinking blocks in stable/live-shaped rendering after turn completion and leaks the pending compact into a later event.

Useful? React with 👍 / 👎.

Two bugs caught by Codex automated review:

1. compactThinkingIfPending() found the FIRST ThinkingComponent in
   transcript, which could be an older block from a previous turn.
   Now walks in reverse to find the most recent stable-mode instance.

2. Last-chance compaction in finalizeTurn() was called BEFORE
   finalizeLiveTextBuffers(), so _pendingThinkingCompact was still
   false when thinking was still active. Moved after finalizeLiveTextBuffers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

终端流式输出时偶尔自动滚动到顶部(pi-tui fullRender 触发清屏)

1 participant