From 1ad75a3602f790858af1db46ca5980337fdc4a76 Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Thu, 9 Apr 2026 22:58:47 +0800 Subject: [PATCH] fix: prevent duplicate AI message when switching conversations during stream Two bugs caused the same AI response to appear twice: 1. handleSelectConversation would fetch conversation history even while a stream was in progress (isLoading). If the fetch resolved mid-stream, fetchConversation overwrote the conversation messages unconditionally, writing the server-persisted AI message (with a real UUID) alongside the still-streaming client-side message (with a temp ID), producing a duplicate. Fix: guard the fetch with !isLoading, and add a second check inside the .then callback so setConversations only writes history if the messages array is still empty (protects against slow-resolving network requests). 2. onDone in streamChat could theoretically be called a second time if the ReadableStream completed without a "done" SSE event. Fix: call reader.cancel() immediately after onDone so the while loop exits cleanly. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- frontend/components/gemini/GeminiChat.tsx | 14 ++++++++++---- frontend/lib/api.ts | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/components/gemini/GeminiChat.tsx b/frontend/components/gemini/GeminiChat.tsx index e20c561..403f0da 100644 --- a/frontend/components/gemini/GeminiChat.tsx +++ b/frontend/components/gemini/GeminiChat.tsx @@ -185,9 +185,9 @@ export function GeminiChat() { setActiveConvId(id); setInputValue(""); - // Load messages if not yet loaded + // Load messages if not yet loaded and no stream is in progress const conv = conversations.find((c) => c.id === id); - if (conv && conv.messages.length === 0) { + if (conv && conv.messages.length === 0 && !isLoading) { fetchConversation(id) .then((detail) => { const msgs: Message[] = detail.messages.map((m) => ({ @@ -196,8 +196,14 @@ export function GeminiChat() { content: m.content, timestamp: new Date(m.created_at), })); + // Only write history if the conversation still has no messages + // (guards against a race where streaming already populated it) setConversations((prev) => - prev.map((c) => (c.id === id ? { ...c, messages: msgs } : c)) + prev.map((c) => + c.id === id && c.messages.length === 0 + ? { ...c, messages: msgs } + : c + ) ); }) .catch(() => { @@ -205,7 +211,7 @@ export function GeminiChat() { }); } }, - [conversations] + [conversations, isLoading] ); const handleRenameConversation = useCallback( diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index fa85041..f3d7743 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -209,6 +209,7 @@ export function streamChat( onEvent(event); if (event.type === "done") { onDone(); + reader.cancel(); return; } } catch { @@ -216,6 +217,7 @@ export function streamChat( } } } + // Only call onDone here if the stream ended without a "done" event onDone(); }) .catch((err) => {