From 876579fa73c7ac9f7d6f0946e8e323fbf0fd45ed Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 15 Jul 2026 01:12:47 +0100 Subject: [PATCH] fix: prevent duplicate chat row keys --- ui/src/pages/chat/chat-thread.test.ts | 26 ++++++++++++++++++++++++++ ui/src/pages/chat/chat-thread.ts | 11 +++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/chat/chat-thread.test.ts b/ui/src/pages/chat/chat-thread.test.ts index 780705dcf27e..3d47c7037a85 100644 --- a/ui/src/pages/chat/chat-thread.test.ts +++ b/ui/src/pages/chat/chat-thread.test.ts @@ -407,6 +407,32 @@ describe("buildCachedChatItems row identity", () => { expect(appended.key).toBe(initial.key); }); + it("does not reclaim a group key naturally owned by another reordered group", () => { + resetChatThreadState(); + const first = { + __openclaw: { id: "first", seq: 1 }, + role: "user", + senderLabel: "same", + content: "First", + timestamp: 1, + }; + const second = { + __openclaw: { id: "second", seq: 2 }, + role: "user", + senderLabel: "same", + content: "Second", + timestamp: 2, + }; + expect(messageGroups({ messages: [first, second] })).toHaveLength(1); + + first.senderLabel = "different"; + first.timestamp = 3; + const regrouped = messageGroups({ messages: [first, second] }); + + expect(regrouped).toHaveLength(2); + expect(new Set(regrouped.map((group) => group.key)).size).toBe(regrouped.length); + }); + it("keeps a projected-sibling group stable after an unrelated prepend", () => { resetChatThreadState(); const siblings = [ diff --git a/ui/src/pages/chat/chat-thread.ts b/ui/src/pages/chat/chat-thread.ts index 94ebd8692f52..c2052cf2ea20 100644 --- a/ui/src/pages/chat/chat-thread.ts +++ b/ui/src/pages/chat/chat-thread.ts @@ -1544,6 +1544,9 @@ function stabilizeChatItems( previousGroupByMessageKey.set(message.key, item); } } + const nextNaturalGroupKeys = new Set( + next.filter((item) => item.kind === "group").map((item) => item.key), + ); const claimedGroupKeys = new Set(); const reconciled = next.map((item) => { if (item.kind !== "group") { @@ -1583,6 +1586,9 @@ function stabilizeChatItems( if (!best) { return item; } + if (best.group.key !== item.key && nextNaturalGroupKeys.has(best.group.key)) { + return item; + } claimedGroupKeys.add(best.group.key); return item.key === best.group.key ? item : { ...item, key: best.group.key }; }); @@ -1659,10 +1665,7 @@ function updateCachedLiveStream( if (item.key !== expectedKey || input.stream === null) { return false; } - const text = trimAccumulatedStreamPrefix( - sanitizeStreamText(input.stream), - accumulatedPrefix, - ); + const text = trimAccumulatedStreamPrefix(sanitizeStreamText(input.stream), accumulatedPrefix); if (text.length === 0 || stripHeartbeatTokenForDisplay(text).shouldSkip) { return false; }