fix(tui): prevent viewport jump when thinking finalizes above viewport#1141
fix(tui): prevent viewport jump when thinking finalizes above viewport#1141starSumi wants to merge 3 commits into
Conversation
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 detectedLatest commit: 004c55e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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>
There was a problem hiding this comment.
💡 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".
| if (child instanceof ThinkingComponent) { | ||
| if ((child as ThinkingComponent).compact()) { | ||
| this.host.state.ui.requestRender(); | ||
| } | ||
| break; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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>
Summary
When
ThinkingComponenttransitions from live to finalized above the viewport, its line count changes (4→3), triggering pi-tui's destructivefullRender(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
Solution
thinking.ts— Stable modefinalize()enters stable mode: stops spinner, keeps same line shape as livecompact()defers actual line-count reduction, returns booleanrender()usesmode === 'live' || stableModefor identical outputstreaming-ui.ts— Deferred compactiononThinkingEnd()→finalize()(stable, not compact)compactThinkingIfPending()called atonStreamingTextStart()— both changes in same render cyclefinalizeTurn(), cleanup inresetLiveText()thinking.test.ts— Updated + 3 new testsWhy it works
Testing
Related