diff --git a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue index c94de355..219fd3a1 100644 --- a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue +++ b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue @@ -27,6 +27,7 @@ const panelRef = ref(null); const { following, keepBottomAfterUpdate, onScroll: onPanelScroll, scrollToBottom } = useBottomFollowScroll(panelRef, { threshold: 56 }); const detailMessageId = ref(null); const displayedDurationFloorMs = new Map(); +const sealedDurationMs = new Map(); const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages); const hasRunningAgentMessages = computed(() => visibleMessages.value.some(isRunningMessage)); const { nowMs } = useWorkbenchNowTicker(hasRunningAgentMessages); @@ -62,6 +63,9 @@ watch(visibleMessages, (messages) => { for (const key of Array.from(displayedDurationFloorMs.keys())) { if (!visibleKeys.has(key)) displayedDurationFloorMs.delete(key); } + for (const key of Array.from(sealedDurationMs.keys())) { + if (!visibleKeys.has(key)) sealedDurationMs.delete(key); + } }, { immediate: true }); function openDetails(message: ChatMessage): void { @@ -256,13 +260,14 @@ function messageDurationMeta(message: ChatMessage): { text: string; label: strin if (message.role !== "agent") return null; const timing = messageTimingForDisplay(message); const rawDurationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : terminalMessageDurationMs(message, timing); - if (rawDurationMs === null) return null; const durationMs = isRunningMessage(message) ? monotonicDisplayDurationMs(message, rawDurationMs) : sealedDisplayDurationMs(message, rawDurationMs); + if (durationMs === null) return null; const value = formatDuration(durationMs); return { text: `耗时 ${value}`, label: `总耗时:${value}` }; } -function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number): number { +function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null { + if (durationMs === null) return null; const key = messageDurationFloorKey(message); const previous = displayedDurationFloorMs.get(key); const next = previous === undefined ? durationMs : Math.max(previous, durationMs); @@ -270,8 +275,13 @@ function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number): n return next; } -function sealedDisplayDurationMs(message: ChatMessage, durationMs: number): number { - displayedDurationFloorMs.delete(messageDurationFloorKey(message)); +function sealedDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null { + const key = messageDurationFloorKey(message); + displayedDurationFloorMs.delete(key); + const previous = sealedDurationMs.get(key); + if (previous !== undefined) return previous; + if (durationMs === null) return null; + sealedDurationMs.set(key, durationMs); return durationMs; } @@ -302,10 +312,11 @@ function terminalMessageDurationMs(message: ChatMessage, timing: ChatMessage["ti } function elapsedTerminalDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null { + const messageRecord = message as Record; const trace = message.runnerTrace as Record | null | undefined; const traceTiming = trace?.timing as Record | null | undefined; const startedAt = timestampMs(timing?.startedAt ?? message.startedAt ?? traceTiming?.startedAt ?? trace?.startedAt); - const endedAt = timestampMs(timing?.finishedAt ?? message.finishedAt ?? traceTiming?.finishedAt ?? trace?.finishedAt ?? timing?.lastEventAt ?? message.lastEventAt ?? traceTiming?.lastEventAt ?? trace?.lastEventAt); + const endedAt = timestampMs(timing?.finishedAt ?? message.finishedAt ?? messageRecord.completedAt ?? traceTiming?.finishedAt ?? trace?.finishedAt ?? traceTiming?.completedAt ?? trace?.completedAt); if (startedAt === null || endedAt === null || endedAt < startedAt) return null; return Math.trunc(endedAt - startedAt); }