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

View File

@@ -1,6 +1,6 @@
"use client" "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 { useDbMessageDetail } from "@/hooks/use-db-message-detail"
import { useConversationRuntime } from "@/contexts/conversation-runtime-context" import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
import { ContentPartsRenderer } from "./content-parts-renderer" import { ContentPartsRenderer } from "./content-parts-renderer"
@@ -29,11 +29,13 @@ import {
} from "@/lib/agent-plan" } from "@/lib/agent-plan"
import type { ConnectionStatus } from "@/lib/types" import type { ConnectionStatus } from "@/lib/types"
import { VirtualizedMessageThread } from "@/components/message/virtualized-message-thread" import { VirtualizedMessageThread } from "@/components/message/virtualized-message-thread"
import { useStickToBottomContext } from "use-stick-to-bottom"
interface MessageListViewProps { interface MessageListViewProps {
conversationId: number conversationId: number
connStatus?: ConnectionStatus | null connStatus?: ConnectionStatus | null
isActive?: boolean isActive?: boolean
sendSignal?: number
} }
interface ResolvedMessageGroup extends MessageGroup { 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({ export function MessageListView({
conversationId, conversationId,
connStatus, connStatus,
isActive = true, isActive = true,
sendSignal = 0,
}: MessageListViewProps) { }: MessageListViewProps) {
const t = useTranslations("Folder.chat.messageList") const t = useTranslations("Folder.chat.messageList")
const sharedT = useTranslations("Folder.chat.shared") const sharedT = useTranslations("Folder.chat.shared")
@@ -339,6 +369,7 @@ export function MessageListView({
className="flex-1 min-h-0" className="flex-1 min-h-0"
resize={shouldUseSmoothResize ? "smooth" : undefined} resize={shouldUseSmoothResize ? "smooth" : undefined}
> >
<AutoScrollOnSend signal={sendSignal} enabled={isActive} />
<VirtualizedMessageThread <VirtualizedMessageThread
items={threadItems} items={threadItems}
getItemKey={(item) => item.key} getItemKey={(item) => item.key}

View File

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