fix: deduplicate messages and UI cards when streamSubgraphs is true

With streamSubgraphs:true, subgraph values events push messages that
the parent Supervisor later merges, causing the same message.id to
appear multiple times in thread.messages. Similarly, the same card_id
can appear in both subgraph and parent values snapshots.

- Add deduplicateMessages() at module level: keeps the last (most
  up-to-date) copy per message.id, eliminating duplicate AI messages
  caused by subgraph-then-parent merge events.
- Apply deduplicateUiItems() to the full allUi array before per-message
  card matching, so loading→done card pairs pushed by subgraphs and
  parent are collapsed globally before orphan attribution runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-13 15:36:40 +08:00
co-authored by Claude Sonnet 4.6
parent 09a0021007
commit e9808cc44f
+21 -5
View File
@@ -73,6 +73,16 @@ function deduplicateUiItems(items: UIMsgLocal[]): UIMsgLocal[] {
});
}
// ─── Message deduplication by id ────────────────────────────────────────────
// With streamSubgraphs:true, subgraph values events push messages that the
// parent graph later merges. The same message.id can appear multiple times in
// thread.messages. Keep only the last (most up-to-date) copy per id.
function deduplicateMessages(msgs: Message[]): Message[] {
const seen = new Map<string, number>();
msgs.forEach((m, i) => { if (m.id) seen.set(m.id, i); });
return msgs.filter((m, i) => !m.id || seen.get(m.id) === i);
}
// ─── Tool groups ────────────────────────────────────────────────────────────
const TOOL_GROUPS = [
{ key: "knowledge", label: "知识库", icon: BookOpen, tools: ["kb_search"] },
@@ -551,12 +561,13 @@ function App() {
if (thread.messages.length > 0) {
lastMessagesRef.current = thread.messages;
}
const activeMessages: Message[] =
const activeMessages: Message[] = deduplicateMessages(
thread.messages.length > 0
? thread.messages
: lastMessagesRef.current.length > 0
? lastMessagesRef.current
: historicalMessages;
: historicalMessages
);
// ── Collect completed tool call IDs ─────────────────────────────────────
const completedToolIds = new Set(
@@ -677,11 +688,16 @@ function App() {
)}
{activeMessages.map((message, idx) => {
// Render UI cards attached to this message
// Render UI cards attached to this message.
// Apply deduplicateUiItems to the full ui array first so that cards
// pushed by both subgraph and parent-graph values events (same card_id)
// are collapsed before any per-message matching logic runs.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allUi: UIMsgLocal[] = thread.messages.length > 0
const allUi: UIMsgLocal[] = deduplicateUiItems(
thread.messages.length > 0
? ((thread.values as any)?.ui ?? [])
: historicalUi;
: historicalUi
);
// Match UI items by message_id; for subgraph messages where IDs
// may not align, fallback: distribute orphaned cards by tool name.
let matchedUi = allUi.filter(