From 54733817206c5caf9939a15342cd346001562a83 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 25 Feb 2026 15:20:38 +0200 Subject: [PATCH] fix: prevent chat messages from duplicating or disappearing (#507) - Tighten message deduplication and turn-structure checks in chat rendering - Avoid stale cached turn grouping data during streaming and incremental updates - Keep message order stable so headers and grouped messages render consistently --- .../ui/src/components/chat/MessageList.tsx | 31 ++++++++++++------- .../chat/contexts/TurnGroupingContext.tsx | 7 +++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/chat/MessageList.tsx b/packages/ui/src/components/chat/MessageList.tsx index 3c511f7e..e9abf99b 100644 --- a/packages/ui/src/components/chat/MessageList.tsx +++ b/packages/ui/src/components/chat/MessageList.tsx @@ -57,6 +57,10 @@ const hasSameTurnStructure = (prev: ChatMessageEntry[], next: ChatMessageEntry[] const prevMessage = prev[index]; const nextMessage = next[index]; + if (prevMessage !== nextMessage) { + return false; + } + if (prevMessage.info.id !== nextMessage.info.id) { return false; } @@ -466,19 +470,24 @@ const MessageList: React.FC = ({ }, [permissions, questions, onMessageContentChange]); const baseDisplayMessages = React.useMemo(() => { - const seenIds = new Set(); + const seenIdsFromTail = new Set(); const nextNormalizedCache = new Map(); - const normalizedMessages = messages - .filter((message) => { - const messageId = message.info?.id; - if (typeof messageId === 'string') { - if (seenIds.has(messageId)) { - return false; - } - seenIds.add(messageId); + + const dedupedMessages: ChatMessageEntry[] = []; + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + const messageId = message.info?.id; + if (typeof messageId === 'string') { + if (seenIdsFromTail.has(messageId)) { + continue; } - return true; - }) + seenIdsFromTail.add(messageId); + } + dedupedMessages.push(message); + } + dedupedMessages.reverse(); + + const normalizedMessages = dedupedMessages .map((message, index) => { const messageId = typeof message.info?.id === 'string' && message.info.id.length > 0 ? message.info.id diff --git a/packages/ui/src/components/chat/contexts/TurnGroupingContext.tsx b/packages/ui/src/components/chat/contexts/TurnGroupingContext.tsx index f1671f09..9af96fd8 100644 --- a/packages/ui/src/components/chat/contexts/TurnGroupingContext.tsx +++ b/packages/ui/src/components/chat/contexts/TurnGroupingContext.tsx @@ -519,6 +519,9 @@ const hasSameTurnStructure = (prev: ChatMessageEntry[], next: ChatMessageEntry[] } for (let index = 0; index < prev.length; index += 1) { + if (prev[index] !== next[index]) { + return false; + } if (prev[index]?.info?.id !== next[index]?.info?.id) { return false; } @@ -543,6 +546,9 @@ const isAppendOnlyChange = (prev: ChatMessageEntry[], next: ChatMessageEntry[]): } for (let index = 0; index < prev.length; index += 1) { + if (prev[index] !== next[index]) { + return false; + } if (prev[index]?.info?.id !== next[index]?.info?.id) { return false; } @@ -658,6 +664,7 @@ export const TurnGroupingProvider: React.FC = ({ mess const cached = staticCacheRef.current; if ( cached && + hasSameTurnStructure(cached.messages, messages) && cached.structureKey === structureKey && cached.defaultActivityExpanded === defaultActivityExpanded && cached.showTextJustificationActivity === showTextJustificationActivity