mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(tui): keep waiting spinner during encrypted reasoning streams (#1256)
Empty (encrypted/redacted) thinking deltas no longer switch out of waiting mode, which previously stopped the moon spinner while no thinking component was ever created, leaving a blank spinner-less gap until the first real text/tool token.
This commit is contained in:
parent
ef61f4369b
commit
0cc02ac67d
3 changed files with 66 additions and 0 deletions
5
.changeset/keep-waiting-spinner-empty-reasoning.md
Normal file
5
.changeset/keep-waiting-spinner-empty-reasoning.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Keep the waiting spinner visible while encrypted reasoning streams, fixing a blank spinner-less gap before the first response text appears.
|
||||
|
|
@ -438,6 +438,14 @@ export class SessionEventHandler {
|
|||
|
||||
private handleThinkingDelta(event: ThinkingDeltaEvent): void {
|
||||
const { state, streamingUI } = this.host;
|
||||
// Encrypted / redacted reasoning (e.g. Kimi over the Anthropic-compatible
|
||||
// protocol) streams thinking deltas whose visible text is empty — only an
|
||||
// opaque signature rides along. Such deltas carry nothing to render, so
|
||||
// switching into the `thinking` pane mode here would stop the "waiting"
|
||||
// moon spinner while no ThinkingComponent is ever created (it needs visible
|
||||
// text), leaving a blank, spinner-less gap until the first real text/tool
|
||||
// token arrives. Keep the moon up until actual thinking text shows up.
|
||||
if (event.delta.length === 0 && !streamingUI.hasThinkingDraft()) return;
|
||||
streamingUI.appendThinkingDelta(event.delta);
|
||||
this.host.patchLivePane({ mode: 'idle' });
|
||||
if (state.appState.streamingPhase !== 'thinking') {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { ApprovalPanelComponent } from '#/tui/components/dialogs/approval-panel';
|
||||
import { KIMI_CODE_PLUGIN_MARKETPLACE_URL } from '#/constant/app';
|
||||
import { MOON_SPINNER_FRAMES } from '#/tui/constant/rendering';
|
||||
import {
|
||||
AgentSwarmProgressComponent,
|
||||
agentSwarmGridHeightForTerminalRows,
|
||||
|
|
@ -4528,6 +4529,58 @@ command = "vim"
|
|||
expect(driver.streamingUI.hasActiveThinkingComponent()).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the waiting moon spinner while reasoning streams only empty (encrypted) thinking deltas', async () => {
|
||||
const { driver } = await makeDriver();
|
||||
|
||||
// Turn begins -> waiting mode shows the moon spinner.
|
||||
driver.sessionEventHandler.handleEvent(
|
||||
{
|
||||
type: 'turn.started',
|
||||
agentId: 'main',
|
||||
sessionId: 'ses-1',
|
||||
turnId: 1,
|
||||
} as Event,
|
||||
vi.fn(),
|
||||
);
|
||||
expect(driver.state.appState.streamingPhase).toBe('waiting');
|
||||
expect(driver.state.livePane.mode).toBe('waiting');
|
||||
|
||||
// Encrypted reasoning: thinking.delta events whose visible text is empty.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
driver.sessionEventHandler.handleEvent(
|
||||
{
|
||||
type: 'thinking.delta',
|
||||
agentId: 'main',
|
||||
sessionId: 'ses-1',
|
||||
delta: '',
|
||||
} as Event,
|
||||
vi.fn(),
|
||||
);
|
||||
}
|
||||
|
||||
// The moon must stay up: still waiting, no orphan thinking component, and
|
||||
// the activity pane still renders a moon frame (no blank, spinner-less gap).
|
||||
expect(driver.state.appState.streamingPhase).toBe('waiting');
|
||||
expect(driver.state.livePane.mode).toBe('waiting');
|
||||
expect(driver.streamingUI.hasActiveThinkingComponent()).toBe(false);
|
||||
const activity = stripSgr(renderActivity(driver));
|
||||
expect(MOON_SPINNER_FRAMES.some((frame) => activity.includes(frame))).toBe(true);
|
||||
|
||||
// Real thinking text finally arrives -> transition into thinking mode.
|
||||
driver.sessionEventHandler.handleEvent(
|
||||
{
|
||||
type: 'thinking.delta',
|
||||
agentId: 'main',
|
||||
sessionId: 'ses-1',
|
||||
delta: 'actual reasoning',
|
||||
} as Event,
|
||||
vi.fn(),
|
||||
);
|
||||
driver.streamingUI.flushNow();
|
||||
expect(driver.state.appState.streamingPhase).toBe('thinking');
|
||||
expect(driver.streamingUI.hasActiveThinkingComponent()).toBe(true);
|
||||
});
|
||||
|
||||
it('finalizes an orphaned thinking component on turn end', async () => {
|
||||
const { driver } = await makeDriver();
|
||||
driver.state.appState.streamingPhase = 'thinking';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue