mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(web): hide the sending moon once the first reply token streams
sendingBySession only cleared on turn end / error, so the in-flight moon lingered beside the streaming reply for the whole turn. Clear it on the first assistantDelta (thinking/text token) or messageUpdated (a turn that opens with a tool use), matching the moon's intended 'before the reply starts' semantics.
This commit is contained in:
parent
176d3d874c
commit
980ff9d48a
2 changed files with 128 additions and 0 deletions
|
|
@ -509,6 +509,21 @@ function connectEventsIfNeeded(): void {
|
|||
// is kept, only a marker line records the compaction).
|
||||
applyEvent(appEvent, meta.sessionId, meta.seq);
|
||||
|
||||
// The "sending" moon is an in-flight placeholder for the dead air BEFORE
|
||||
// the reply streams. Clear it the instant the assistant produces anything
|
||||
// — the first thinking/text token (assistantDelta) or a tool-use the turn
|
||||
// opens with (messageUpdated) — so the moon yields to the live stream
|
||||
// instead of lingering beside it until the turn ends.
|
||||
if (
|
||||
(appEvent.type === 'assistantDelta' || appEvent.type === 'messageUpdated') &&
|
||||
rawState.sendingBySession[appEvent.sessionId]
|
||||
) {
|
||||
rawState.sendingBySession = {
|
||||
...rawState.sendingBySession,
|
||||
[appEvent.sessionId]: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Turn-end cleanup for the session the event belongs to — including
|
||||
// sessions running in the background (see onSessionIdle).
|
||||
if (
|
||||
|
|
|
|||
113
apps/kimi-web/test/sending-moon.test.ts
Normal file
113
apps/kimi-web/test/sending-moon.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { AppSession, KimiEventHandlers, KimiWebApi } from '../src/api/types';
|
||||
|
||||
const now = '2026-06-11T00:00:00.000Z';
|
||||
|
||||
function session(id: string): AppSession {
|
||||
return {
|
||||
id,
|
||||
title: id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: 'idle',
|
||||
cwd: '/repo',
|
||||
model: 'kimi-test',
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheCreationTokens: 0,
|
||||
totalCostUsd: 0,
|
||||
contextTokens: 0,
|
||||
contextLimit: 128_000,
|
||||
turnCount: 0,
|
||||
},
|
||||
messageCount: 0,
|
||||
lastSeq: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal('WebSocket', class WebSocket {});
|
||||
|
||||
let handlers: KimiEventHandlers | undefined;
|
||||
const eventConn = {
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
bindNextPromptId: vi.fn(),
|
||||
seedSnapshot: vi.fn(),
|
||||
abort: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
const created = session('sess_1');
|
||||
const api = {
|
||||
createSession: vi.fn(async () => created),
|
||||
getSessionSnapshot: vi.fn(async () => ({
|
||||
asOfSeq: 0,
|
||||
epoch: 'ep_test',
|
||||
session: created,
|
||||
messages: [],
|
||||
hasMoreMessages: false,
|
||||
inFlightTurn: null,
|
||||
pendingApprovals: [],
|
||||
pendingQuestions: [],
|
||||
})),
|
||||
submitPrompt: vi.fn(async () => ({ promptId: 'pr_1', userMessageId: 'msg_real' })),
|
||||
listTasks: vi.fn(async () => []),
|
||||
getGitStatus: vi.fn(async () => ({ branch: 'main', ahead: 0, behind: 0, entries: {} })),
|
||||
getSessionStatus: vi.fn(async () => ({
|
||||
model: 'kimi-test',
|
||||
thinkingLevel: 'high',
|
||||
permission: 'manual',
|
||||
planMode: false,
|
||||
contextTokens: 0,
|
||||
maxContextTokens: 128_000,
|
||||
contextUsage: 0,
|
||||
})),
|
||||
connectEvents: vi.fn((next: KimiEventHandlers) => {
|
||||
handlers = next;
|
||||
return eventConn;
|
||||
}),
|
||||
getFileUrl: vi.fn((fileId: string) => `/files/${fileId}`),
|
||||
} as unknown as KimiWebApi;
|
||||
|
||||
vi.doMock('../src/api', () => ({ getKimiWebApi: () => api }));
|
||||
const { useKimiWebClient } = await import('../src/composables/useKimiWebClient');
|
||||
return {
|
||||
client: useKimiWebClient(),
|
||||
getHandlers: () => {
|
||||
if (!handlers) throw new Error('connectEvents not called');
|
||||
return handlers;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('sending moon placeholder', () => {
|
||||
it('clears on the first streamed token instead of lingering until turn end', async () => {
|
||||
const { client, getHandlers } = await setup();
|
||||
await client.createSession('/repo');
|
||||
await client.sendPrompt('hello');
|
||||
expect(client.isSending.value).toBe(true);
|
||||
|
||||
// First token of the reply arrives.
|
||||
getHandlers().onEvent(
|
||||
{
|
||||
type: 'assistantDelta',
|
||||
sessionId: 'sess_1',
|
||||
messageId: 'm1',
|
||||
contentIndex: 0,
|
||||
delta: { text: 'Hi' },
|
||||
},
|
||||
{ sessionId: 'sess_1', seq: 5 },
|
||||
);
|
||||
|
||||
expect(client.isSending.value).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue