修复Agent消息在响应结束后显示两条

This commit is contained in:
xintaofei
2026-03-10 20:02:59 +08:00
parent 91636ada7f
commit 11a5484b79
3 changed files with 49 additions and 2 deletions

View File

@@ -145,6 +145,7 @@ const ConversationTabView = memo(function ConversationTabView({
const [draftAgentType, setDraftAgentType] = useState<AgentType>(agentType)
const selectedAgent = conversationId != null ? agentType : draftAgentType
const [modeId, setModeId] = useState<string | null>(null)
const [sendSignal, setSendSignal] = useState(0)
const [agentsLoaded, setAgentsLoaded] = useState(false)
const [usableAgentCount, setUsableAgentCount] = useState(0)
const [agentConnectError, setAgentConnectError] = useState<string | null>(
@@ -433,6 +434,7 @@ const ConversationTabView = memo(function ConversationTabView({
optimisticTurn,
optimisticTurn.id
)
setSendSignal((prev) => prev + 1)
setSyncState(effectiveConversationId, "awaiting_persist")
if (connStatus === "connected") {
@@ -577,6 +579,7 @@ const ConversationTabView = memo(function ConversationTabView({
conversationId={effectiveConversationId}
connStatus={connStatus}
isActive={isActive}
sendSignal={sendSignal}
/>
)

View File

@@ -1,6 +1,6 @@
"use client"
import { memo, useCallback, useEffect, useMemo } from "react"
import { memo, useCallback, useEffect, useMemo, useRef } from "react"
import { useDbMessageDetail } from "@/hooks/use-db-message-detail"
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
import { ContentPartsRenderer } from "./content-parts-renderer"
@@ -29,11 +29,13 @@ import {
} from "@/lib/agent-plan"
import type { ConnectionStatus } from "@/lib/types"
import { VirtualizedMessageThread } from "@/components/message/virtualized-message-thread"
import { useStickToBottomContext } from "use-stick-to-bottom"
interface MessageListViewProps {
conversationId: number
connStatus?: ConnectionStatus | null
isActive?: boolean
sendSignal?: number
}
interface ResolvedMessageGroup extends MessageGroup {
@@ -169,10 +171,38 @@ const PendingTypingIndicator = memo(function PendingTypingIndicator() {
)
})
const AutoScrollOnSend = memo(function AutoScrollOnSend({
signal,
enabled,
}: {
signal: number
enabled: boolean
}) {
const { scrollToBottom } = useStickToBottomContext()
const lastSignalRef = useRef(signal)
useEffect(() => {
if (!enabled) return
if (signal === lastSignalRef.current) return
lastSignalRef.current = signal
scrollToBottom()
const rafId = requestAnimationFrame(() => {
scrollToBottom()
})
return () => {
cancelAnimationFrame(rafId)
}
}, [enabled, scrollToBottom, signal])
return null
})
export function MessageListView({
conversationId,
connStatus,
isActive = true,
sendSignal = 0,
}: MessageListViewProps) {
const t = useTranslations("Folder.chat.messageList")
const sharedT = useTranslations("Folder.chat.shared")
@@ -339,6 +369,7 @@ export function MessageListView({
className="flex-1 min-h-0"
resize={shouldUseSmoothResize ? "smooth" : undefined}
>
<AutoScrollOnSend signal={sendSignal} enabled={isActive} />
<VirtualizedMessageThread
items={threadItems}
getItemKey={(item) => item.key}

View File

@@ -221,6 +221,9 @@ function reduceHydrateDetail(
const current = state.byConversationId.get(conversationId)
const nextExternalId = detail.summary.external_id ?? null
const acceptSnapshot = shouldAcceptPersistedSnapshot(current, detail)
const prevPersistedTurnCount = current?.persistedTurns.length ?? 0
const prevPersistedMessageCount = current?.persistedMessageCount ?? 0
const prevPersistedUpdatedAt = current?.persistedUpdatedAt ?? null
const optimisticTurns = current?.optimisticTurns ?? []
const persistedTurns = acceptSnapshot
? detail.turns
@@ -234,11 +237,20 @@ function reduceHydrateDetail(
const shouldDropOptimistic =
optimisticTurns.length > 0 &&
persistedTurns.length >= (current?.persistedTurns.length ?? 0) + 1
const nextUpdatedAt = detail.summary.updated_at ?? null
const hasPersistedAdvance =
acceptSnapshot &&
(detail.turns.length > prevPersistedTurnCount ||
detail.summary.message_count > prevPersistedMessageCount ||
(nextUpdatedAt !== null &&
(prevPersistedUpdatedAt === null ||
nextUpdatedAt > prevPersistedUpdatedAt)))
const nextSession: ConversationRuntimeSession = {
...(current ?? createEmptySession(conversationId)),
externalId: nextExternalId,
persistedTurns,
liveMessage: hasPersistedAdvance ? null : (current?.liveMessage ?? null),
optimisticTurns: shouldDropOptimistic ? [] : optimisticTurns,
syncState: shouldDropOptimistic ? "idle" : (current?.syncState ?? "idle"),
activeTurnToken: shouldDropOptimistic
@@ -370,6 +382,7 @@ function reducer(
const preferFromSnapshot =
from.persistedTurns.length >= to.persistedTurns.length
const mergedLiveMessage = to.liveMessage ?? from.liveMessage
const merged: ConversationRuntimeSession = {
...to,
@@ -379,7 +392,7 @@ function reducer(
? from.persistedTurns
: to.persistedTurns,
optimisticTurns: [...from.optimisticTurns, ...to.optimisticTurns],
liveMessage: to.liveMessage ?? from.liveMessage,
liveMessage: mergedLiveMessage,
syncState: to.syncState !== "idle" ? to.syncState : from.syncState,
activeTurnToken: to.activeTurnToken ?? from.activeTurnToken,
lastHydratedAt: