fix(web): dedupe optimistic user message against snapshot resync (#1620)

* fix(web): dedupe optimistic user message against snapshot resync

* fix(web): match snapshot messages by identity
This commit is contained in:
qer 2026-07-13 21:54:08 +08:00 committed by GitHub
parent 3c0e368cbd
commit e91a616f21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 54 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
web: Fix duplicate user message bubbles after a session snapshot resync.

View file

@ -18,9 +18,23 @@ export function mergeSnapshotMessages(
const earliestSnapshotMs = Date.parse(snapshot[0]!.createdAt);
if (Number.isNaN(earliestSnapshotMs)) return snapshot;
// The optimistic bubble keeps its client-side id to avoid remounting, while
// submitPrompt stamps the authoritative v2 user-message id into promptId.
// Match that identity against the snapshot instead of guessing from content:
// repeated prompts are distinct messages even when their text/media is equal.
const snapshotIds = new Set(snapshot.map((m) => m.id));
const snapshotUserIds = new Set(snapshot.filter((m) => m.role === 'user').map((m) => m.id));
const older = loaded.filter((message) => {
const createdAtMs = Date.parse(message.createdAt);
return !Number.isNaN(createdAtMs) && createdAtMs < earliestSnapshotMs;
if (Number.isNaN(createdAtMs) || createdAtMs >= earliestSnapshotMs) return false;
if (snapshotIds.has(message.id)) return false;
if (
message.role === 'user' &&
message.promptId !== undefined &&
snapshotUserIds.has(message.promptId)
) return false;
return true;
});
return older.length > 0 ? [...older, ...snapshot] : snapshot;

View file

@ -534,6 +534,40 @@ describe('mergeSnapshotMessages', () => {
expect(mergeSnapshotMessages([], snapshot)).toBe(snapshot);
expect(mergeSnapshotMessages(snapshot, [])).toEqual([]);
});
function optimisticUser(id: string, createdAt: string, text: string, promptId: string): AppMessage {
return {
id,
sessionId: 's1',
role: 'user',
content: [{ type: 'text', text }],
createdAt,
promptId,
metadata: { 'kimiWeb.optimisticUserMessage': true },
};
}
function realUser(id: string, createdAt: string, text: string): AppMessage {
return {
id,
sessionId: 's1',
role: 'user',
content: [{ type: 'text', text }],
createdAt,
};
}
it('drops an optimistic user message when its promptId is the snapshot message id', () => {
const loaded = [optimisticUser('msg_opt_1', '2026-01-02T23:59:59.000Z', 'hello', 'msg_9')];
const snapshot = [realUser('msg_9', '2026-01-03T00:00:00.000Z', 'hello')];
expect(mergeSnapshotMessages(loaded, snapshot).map((m) => m.id)).toEqual(['msg_9']);
});
it('keeps an optimistic user message when a different snapshot message repeats its content', () => {
const loaded = [optimisticUser('msg_opt_1', '2026-01-02T23:59:59.000Z', 'hello', 'msg_8')];
const snapshot = [realUser('msg_9', '2026-01-03T00:00:00.000Z', 'hello')];
expect(mergeSnapshotMessages(loaded, snapshot).map((m) => m.id)).toEqual(['msg_opt_1', 'msg_9']);
});
});
describe('mergeSnapshotSubagents', () => {