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) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-09 22:58:47 +08:00
co-authored by Claude Sonnet 4.6
parent d86d9b8b32
commit 1ad75a3602
2 changed files with 12 additions and 4 deletions
+10 -4
View File
@@ -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(
+2
View File
@@ -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) => {