From f74fe371dafec6b377eb4abe8a36b3d04a41cfc0 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Wed, 11 Mar 2026 13:06:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E5=87=BA=E7=8E=B0=E5=93=8D=E5=BA=94=E4=B8=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/conversation-detail-panel.tsx | 5 +++++ src/components/message/message-list-view.tsx | 9 +++++++-- src/contexts/conversation-runtime-context.tsx | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index 97aa744..1e86015 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -277,6 +277,11 @@ const ConversationTabView = memo(function ConversationTabView({ async (refreshConversationId: number) => { try { const refreshed = await refreshDetailCache(refreshConversationId) + // Skip ACK during prompting to avoid clearing liveMessage / + // resetting syncState while streaming. The useEffect with the + // connStatus === "prompting" guard will handle it naturally + // once prompting ends. + if (prevStatusRef.current === "prompting") return acknowledgePersistedDetail(refreshConversationId, refreshed) } catch (error) { setSyncState(refreshConversationId, "failed") diff --git a/src/components/message/message-list-view.tsx b/src/components/message/message-list-view.tsx index 824c6b6..b9cbf70 100644 --- a/src/components/message/message-list-view.tsx +++ b/src/components/message/message-list-view.tsx @@ -165,6 +165,8 @@ export function MessageListView({ [sharedT] ) + const sessionSyncState = session?.syncState ?? "idle" + const { threadItems, nonStreamingAdapted } = useMemo(() => { const allTurns = timelineTurns.map((item) => item.turn) const allAdapted = adaptMessageTurns(allTurns, adapterText) @@ -206,12 +208,15 @@ export function MessageListView({ } const lastPhase = timelineTurns[timelineTurns.length - 1]?.phase ?? null - if (connStatus === "prompting" && lastPhase === "optimistic") { + if ( + lastPhase === "optimistic" && + (connStatus === "prompting" || sessionSyncState === "awaiting_persist") + ) { items.push({ key: "pending-typing", kind: "typing" }) } return { threadItems: items, nonStreamingAdapted: nonStreaming } - }, [adapterText, connStatus, timelineTurns]) + }, [adapterText, connStatus, sessionSyncState, timelineTurns]) const historicalPlanEntries = useMemo( () => extractLatestPlanEntriesFromMessages(nonStreamingAdapted), diff --git a/src/contexts/conversation-runtime-context.tsx b/src/contexts/conversation-runtime-context.tsx index dba9d0c..245be44 100644 --- a/src/contexts/conversation-runtime-context.tsx +++ b/src/contexts/conversation-runtime-context.tsx @@ -250,7 +250,10 @@ function reduceHydrateDetail( ...(current ?? createEmptySession(conversationId)), externalId: nextExternalId, persistedTurns, - liveMessage: hasPersistedAdvance ? null : (current?.liveMessage ?? null), + liveMessage: + hasPersistedAdvance && current?.syncState !== "awaiting_persist" + ? null + : (current?.liveMessage ?? null), optimisticTurns: shouldDropOptimistic ? [] : optimisticTurns, syncState: shouldDropOptimistic ? "idle" : (current?.syncState ?? "idle"), activeTurnToken: shouldDropOptimistic