fix(tui): stop thinking spinner leaking past turn end on empty deltas#97
Conversation
When a provider emits an empty thinking delta (e.g. Anthropic signature_delta -> think: ""), a ThinkingComponent was created with a running spinner but thinkingDraft stayed empty. Subsequent calls to flushThinkingToTranscript guarded on thinkingDraft.length and returned early without calling onThinkingEnd(), leaking the spinner past the turn end. Two fixes: - onThinkingUpdate: skip component creation for empty text when no existing component needs updating (source prevention). - flushThinkingToTranscript: finalize any orphaned component even when thinkingDraft is empty (defensive cleanup).
🦋 Changeset detectedLatest commit: 88894bd 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 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds safeguards in the TUI thinking-stream rendering to prevent “thinking” spinners/components from leaking past turn end (notably when providers emit empty thinking deltas), and adds regression tests for these scenarios.
Changes:
- Added tests covering empty thinking deltas and orphaned thinking component finalization on turn end.
- Prevent creation of a ThinkingComponent when the streamed thinking text is empty and no component exists.
- Ensure any existing active ThinkingComponent is finalized on flush even when
thinkingDraftis empty.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts | Adds regression tests for empty thinking deltas and for cleaning up orphaned thinking components at turn end. |
| apps/kimi-code/src/tui/kimi-tui.ts | Implements guards to avoid creating empty thinking components and to finalize leaked components when flushing thinking to transcript. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private flushThinkingToTranscript(nextMode: LivePaneState['mode'] = 'idle'): void { | ||
| this.flushStreamingUiUpdatesNow(); | ||
| if (this.state.thinkingDraft.length === 0) { | ||
| // A live ThinkingComponent may still exist with a running spinner | ||
| // (e.g. created by an empty thinking delta). Finalize it here so | ||
| // the spinner does not leak past the thinking phase. | ||
| if (this.state.activeThinkingComponent !== undefined) { | ||
| this.onThinkingEnd(); | ||
| } | ||
| this.patchLivePane({ mode: nextMode }); | ||
| return; | ||
| } |
|
|
||
| // flushThinkingToTranscript must finalize the component even when | ||
| // thinkingDraft is empty, so the spinner does not outlive the turn. | ||
| expect(driver.state.activeThinkingComponent).toBeUndefined(); |
Related Issue
Resolve #96
Problem
When an AI provider emits an empty thinking delta (e.g. Anthropic
signature_delta→think: ""), the TUI creates aThinkingComponentwith a running spinner but no text. When the thinking phase ends,flushThinkingToTranscriptsees an emptythinkingDraftand returns early without stopping the spinner, causing it to animate indefinitely.What changed
onThinkingUpdate: skip component creation when text is empty and no existing component needs updatingflushThinkingToTranscript: finalize any orphanedThinkingComponenteven whenthinkingDraftis emptyChecklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.