From 6c220a9aef5139eb84aedb7c5c0827830661b663 Mon Sep 17 00:00:00 2001 From: Willem Jiang Date: Thu, 7 May 2026 17:31:48 +0800 Subject: [PATCH] fix(chat): prevent first user message from being swallowed in new conversations (#2731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(chat): prevent first user message from being swallowed in new conversations The optimistic message clearing effect cleared too eagerly — any stream message (including AI messages from messages-tuple events) triggered the clear before the server's human message had arrived via values events. For new threads this caused the user's first prompt to disappear permanently. Only clear optimistic messages once the server's human message has been confirmed to arrive in thread.messages, not just when any message arrives. Fixes #2730 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- frontend/src/core/threads/hooks.ts | 34 +++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index ef605d832..249ea366b 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -286,6 +286,13 @@ export function useThreadStream({ const summarizedRef = useRef>(null); // Track message count before sending so we know when server has responded const prevMsgCountRef = useRef(thread.messages.length); + // Track human message count before sending to prevent clearing optimistic + // messages before the server's human message arrives (e.g. when AI messages + // from "messages-tuple" events arrive before the input human message from + // "values" events). + const prevHumanMsgCountRef = useRef( + thread.messages.filter((m) => m.type === "human").length, + ); summarizedRef.current ??= new Set(); @@ -294,14 +301,28 @@ export function useThreadStream({ useEffect(() => { startedRef.current = false; sendInFlightRef.current = false; + prevMsgCountRef.current = thread.messages.length; + prevHumanMsgCountRef.current = thread.messages.filter( + (m) => m.type === "human", + ).length; }, [threadId]); - // Clear optimistic when server messages arrive (count increases) + // Clear optimistic when server messages arrive. + // For messages with a human optimistic message, wait until the server's + // human message has arrived to avoid clearing before the input message + // appears in the stream (the input message may arrive via "values" events + // after individual "messages-tuple" events for AI messages). useEffect(() => { - if ( - optimisticMessages.length > 0 && - thread.messages.length > prevMsgCountRef.current - ) { + if (optimisticMessages.length === 0) return; + + const hasHumanOptimistic = optimisticMessages.some( + (m) => m.type === "human", + ); + const newHumanMsgArrived = + thread.messages.filter((m) => m.type === "human").length > + prevHumanMsgCountRef.current; + + if (!hasHumanOptimistic || newHumanMsgArrived) { setOptimisticMessages([]); } }, [thread.messages.length, optimisticMessages.length]); @@ -322,6 +343,9 @@ export function useThreadStream({ // Capture current count before showing optimistic messages prevMsgCountRef.current = thread.messages.length; + prevHumanMsgCountRef.current = thread.messages.filter( + (m) => m.type === "human", + ).length; // Build optimistic files list with uploading status const optimisticFiles: FileInMessage[] = (message.files ?? []).map(