agent响应结束后异步反显token信息
This commit is contained in:
@@ -142,6 +142,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
appendOptimisticTurn,
|
appendOptimisticTurn,
|
||||||
completeTurn,
|
completeTurn,
|
||||||
refetchDetail,
|
refetchDetail,
|
||||||
|
syncTurnMetadata,
|
||||||
removeConversation,
|
removeConversation,
|
||||||
setExternalId,
|
setExternalId,
|
||||||
setLiveMessage,
|
setLiveMessage,
|
||||||
@@ -191,6 +192,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
const createConversationPendingRef = useRef(false)
|
const createConversationPendingRef = useRef(false)
|
||||||
const externalIdSavedRef = useRef(false)
|
const externalIdSavedRef = useRef(false)
|
||||||
const sessionIdRef = useRef<string | null>(null)
|
const sessionIdRef = useRef<string | null>(null)
|
||||||
|
const syncCancelRef = useRef<(() => void) | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dbConvIdRef.current = dbConversationId
|
dbConvIdRef.current = dbConversationId
|
||||||
@@ -293,9 +295,21 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
// Turn completed — promote liveMessage + optimisticTurns to localTurns
|
// Turn completed — promote liveMessage + optimisticTurns to localTurns
|
||||||
completeTurn(effectiveConversationId)
|
completeTurn(effectiveConversationId)
|
||||||
|
|
||||||
|
// Cancel previous metadata sync (handles rapid consecutive turns)
|
||||||
|
syncCancelRef.current?.()
|
||||||
|
syncCancelRef.current = null
|
||||||
|
|
||||||
const persistedId = dbConvIdRef.current
|
const persistedId = dbConvIdRef.current
|
||||||
if (!persistedId) return
|
if (!persistedId) return
|
||||||
|
|
||||||
|
// Async patch metadata (usage, duration_ms, model, session_stats)
|
||||||
|
if (persistedId > 0) {
|
||||||
|
syncCancelRef.current = syncTurnMetadata(
|
||||||
|
persistedId,
|
||||||
|
effectiveConversationId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (connStatus !== "disconnected" && connStatus !== "error") {
|
if (connStatus !== "disconnected" && connStatus !== "error") {
|
||||||
updateConversationStatus(persistedId, "pending_review")
|
updateConversationStatus(persistedId, "pending_review")
|
||||||
.then(() => refreshConversations())
|
.then(() => refreshConversations())
|
||||||
@@ -303,7 +317,13 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
console.error("[ConversationTabView] update status:", e)
|
console.error("[ConversationTabView] update status:", e)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [completeTurn, connStatus, effectiveConversationId, refreshConversations])
|
}, [
|
||||||
|
completeTurn,
|
||||||
|
connStatus,
|
||||||
|
effectiveConversationId,
|
||||||
|
refreshConversations,
|
||||||
|
syncTurnMetadata,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only sync non-null liveMessage updates to state. When conn.liveMessage
|
// Only sync non-null liveMessage updates to state. When conn.liveMessage
|
||||||
@@ -415,6 +435,7 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
mountedRef.current = true
|
mountedRef.current = true
|
||||||
return () => {
|
return () => {
|
||||||
mountedRef.current = false
|
mountedRef.current = false
|
||||||
|
syncCancelRef.current?.()
|
||||||
if (connStatusRef.current === "prompting") {
|
if (connStatusRef.current === "prompting") {
|
||||||
// Agent still responding — mark for deferred cleanup
|
// Agent still responding — mark for deferred cleanup
|
||||||
setPendingCleanup(effectiveConversationId, true)
|
setPendingCleanup(effectiveConversationId, true)
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import type { LiveMessage } from "@/contexts/acp-connections-context"
|
import type { LiveMessage } from "@/contexts/acp-connections-context"
|
||||||
import { getFolderConversation } from "@/lib/tauri"
|
import { getFolderConversation } from "@/lib/tauri"
|
||||||
import type { DbConversationDetail, MessageTurn } from "@/lib/types"
|
import type {
|
||||||
|
DbConversationDetail,
|
||||||
|
MessageTurn,
|
||||||
|
SessionStats,
|
||||||
|
TurnUsage,
|
||||||
|
} from "@/lib/types"
|
||||||
import { inferLiveToolName } from "@/lib/tool-call-normalization"
|
import { inferLiveToolName } from "@/lib/tool-call-normalization"
|
||||||
|
|
||||||
export type ConversationSyncState = "idle" | "awaiting_persist"
|
export type ConversationSyncState = "idle" | "awaiting_persist"
|
||||||
@@ -108,6 +113,17 @@ type Action =
|
|||||||
conversationId: number
|
conversationId: number
|
||||||
pendingCleanup: boolean
|
pendingCleanup: boolean
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "PATCH_TURN_METADATA"
|
||||||
|
conversationId: number
|
||||||
|
turnPatches: Array<{
|
||||||
|
index: number
|
||||||
|
usage?: TurnUsage | null
|
||||||
|
duration_ms?: number | null
|
||||||
|
model?: string | null
|
||||||
|
}>
|
||||||
|
sessionStats?: SessionStats | null
|
||||||
|
}
|
||||||
| { type: "REMOVE_CONVERSATION"; conversationId: number }
|
| { type: "REMOVE_CONVERSATION"; conversationId: number }
|
||||||
| { type: "RESET" }
|
| { type: "RESET" }
|
||||||
|
|
||||||
@@ -431,6 +447,47 @@ function reducer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "PATCH_TURN_METADATA": {
|
||||||
|
const current = state.byConversationId.get(action.conversationId)
|
||||||
|
if (!current || current.localTurns.length === 0) return state
|
||||||
|
|
||||||
|
const patchedTurns = [...current.localTurns]
|
||||||
|
let changed = false
|
||||||
|
for (const patch of action.turnPatches) {
|
||||||
|
const turn = patchedTurns[patch.index]
|
||||||
|
if (!turn) continue
|
||||||
|
const newUsage = turn.usage ?? patch.usage
|
||||||
|
const newDuration = turn.duration_ms ?? patch.duration_ms
|
||||||
|
const newModel = turn.model ?? patch.model
|
||||||
|
if (
|
||||||
|
newUsage !== turn.usage ||
|
||||||
|
newDuration !== turn.duration_ms ||
|
||||||
|
newModel !== turn.model
|
||||||
|
) {
|
||||||
|
patchedTurns[patch.index] = {
|
||||||
|
...turn,
|
||||||
|
usage: newUsage,
|
||||||
|
duration_ms: newDuration,
|
||||||
|
model: newModel,
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed && !action.sessionStats) return state
|
||||||
|
|
||||||
|
const patchedDetail =
|
||||||
|
current.detail && action.sessionStats
|
||||||
|
? { ...current.detail, session_stats: action.sessionStats }
|
||||||
|
: current.detail
|
||||||
|
|
||||||
|
return updateSessionInState(state, action.conversationId, () => ({
|
||||||
|
...current,
|
||||||
|
localTurns: changed ? patchedTurns : current.localTurns,
|
||||||
|
detail: patchedDetail,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
case "SET_PENDING_CLEANUP":
|
case "SET_PENDING_CLEANUP":
|
||||||
return updateSessionInState(state, action.conversationId, (current) => ({
|
return updateSessionInState(state, action.conversationId, (current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -478,6 +535,10 @@ interface ConversationRuntimeContextValue {
|
|||||||
conversationId: number,
|
conversationId: number,
|
||||||
syncState: ConversationSyncState
|
syncState: ConversationSyncState
|
||||||
) => void
|
) => void
|
||||||
|
syncTurnMetadata: (
|
||||||
|
dbConversationId: number,
|
||||||
|
runtimeConversationId?: number
|
||||||
|
) => () => void
|
||||||
migrateConversation: (
|
migrateConversation: (
|
||||||
fromConversationId: number,
|
fromConversationId: number,
|
||||||
toConversationId: number
|
toConversationId: number
|
||||||
@@ -607,6 +668,99 @@ export function ConversationRuntimeProvider({
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const syncTurnMetadata = useCallback(
|
||||||
|
(
|
||||||
|
dbConversationId: number,
|
||||||
|
runtimeConversationId?: number
|
||||||
|
): (() => void) => {
|
||||||
|
const runtimeId = runtimeConversationId ?? dbConversationId
|
||||||
|
let cancelled = false
|
||||||
|
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const trySync = (attempt: number) => {
|
||||||
|
const delay = attempt === 0 ? 1500 : 3000
|
||||||
|
timerId = setTimeout(() => {
|
||||||
|
if (cancelled) return
|
||||||
|
const session =
|
||||||
|
stateRef.current.byConversationId.get(runtimeId)
|
||||||
|
if (!session || session.localTurns.length === 0) return
|
||||||
|
if (session.syncState === "awaiting_persist") return
|
||||||
|
|
||||||
|
getFolderConversation(dbConversationId)
|
||||||
|
.then((parsed) => {
|
||||||
|
if (cancelled) return
|
||||||
|
const cur =
|
||||||
|
stateRef.current.byConversationId.get(runtimeId)
|
||||||
|
if (!cur || cur.localTurns.length === 0) return
|
||||||
|
if (cur.syncState === "awaiting_persist") return
|
||||||
|
|
||||||
|
const localAssistantIndices: number[] = []
|
||||||
|
for (let i = 0; i < cur.localTurns.length; i++) {
|
||||||
|
if (cur.localTurns[i].role === "assistant") {
|
||||||
|
localAssistantIndices.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedAssistantTurns = parsed.turns.filter(
|
||||||
|
(t) => t.role === "assistant"
|
||||||
|
)
|
||||||
|
|
||||||
|
const offset =
|
||||||
|
parsedAssistantTurns.length - localAssistantIndices.length
|
||||||
|
const patches: Array<{
|
||||||
|
index: number
|
||||||
|
usage?: TurnUsage | null
|
||||||
|
duration_ms?: number | null
|
||||||
|
model?: string | null
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
for (let i = 0; i < localAssistantIndices.length; i++) {
|
||||||
|
const parsedIdx = offset + i
|
||||||
|
if (
|
||||||
|
parsedIdx < 0 ||
|
||||||
|
parsedIdx >= parsedAssistantTurns.length
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
const pt = parsedAssistantTurns[parsedIdx]
|
||||||
|
if (!pt.usage && !pt.duration_ms && !pt.model) continue
|
||||||
|
patches.push({
|
||||||
|
index: localAssistantIndices[i],
|
||||||
|
usage: pt.usage,
|
||||||
|
duration_ms: pt.duration_ms,
|
||||||
|
model: pt.model,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patches.length > 0 || parsed.session_stats) {
|
||||||
|
dispatch({
|
||||||
|
type: "PATCH_TURN_METADATA",
|
||||||
|
conversationId: runtimeId,
|
||||||
|
turnPatches: patches,
|
||||||
|
sessionStats: parsed.session_stats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestPatch = patches[patches.length - 1]
|
||||||
|
if (!latestPatch?.usage && attempt < 1) {
|
||||||
|
trySync(attempt + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Silent — localTurns content remains visible
|
||||||
|
})
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
trySync(0)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
if (timerId) clearTimeout(timerId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const completeTurn = useCallback((conversationId: number) => {
|
const completeTurn = useCallback((conversationId: number) => {
|
||||||
dispatch({ type: "COMPLETE_TURN", conversationId })
|
dispatch({ type: "COMPLETE_TURN", conversationId })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -677,6 +831,7 @@ export function ConversationRuntimeProvider({
|
|||||||
getTimelineTurns,
|
getTimelineTurns,
|
||||||
fetchDetail,
|
fetchDetail,
|
||||||
refetchDetail,
|
refetchDetail,
|
||||||
|
syncTurnMetadata,
|
||||||
completeTurn,
|
completeTurn,
|
||||||
appendOptimisticTurn,
|
appendOptimisticTurn,
|
||||||
setLiveMessage,
|
setLiveMessage,
|
||||||
@@ -693,6 +848,7 @@ export function ConversationRuntimeProvider({
|
|||||||
getTimelineTurns,
|
getTimelineTurns,
|
||||||
fetchDetail,
|
fetchDetail,
|
||||||
refetchDetail,
|
refetchDetail,
|
||||||
|
syncTurnMetadata,
|
||||||
completeTurn,
|
completeTurn,
|
||||||
appendOptimisticTurn,
|
appendOptimisticTurn,
|
||||||
setLiveMessage,
|
setLiveMessage,
|
||||||
|
|||||||
Reference in New Issue
Block a user