diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 051102ec..a8423653 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -19,6 +19,8 @@ export interface TraceDisplayRowsOptions { export function traceDisplayRows(trace: Record = {}, events: TraceEvent[] = [], options: TraceDisplayRowsOptions = {}): TraceEventRow[] { const orderedEvents = traceEventsForDisplay(events); const effectiveTrace = traceWithInferredStart(trace, orderedEvents); + const finalResponseText = traceFinalResponseText(effectiveTrace); + const hasCompletionEvent = orderedEvents.some(isCompletionTraceEvent); const rows: TraceEventRow[] = []; const renderedSourceEvents = new Set(); const renderedToolIdentities = new Set(); @@ -47,7 +49,12 @@ export function traceDisplayRows(trace: Record = {}, events: Tr continue; } if (isTerminalAssistantTraceEvent(event)) { - rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: true }, options)); + if (finalResponseText) { + if (hasCompletionEvent) continue; + rows.push(traceFinalResponseRow(event, finalResponseText, options)); + } else { + rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: true }, options)); + } continue; } if (isAssistantTraceEvent(event)) { @@ -55,7 +62,6 @@ export function traceDisplayRows(trace: Record = {}, events: Tr continue; } if (isCompletionTraceEvent(event)) { - const finalResponseText = traceFinalResponseText(effectiveTrace); rows.push(finalResponseText ? traceFinalResponseRow(event, finalResponseText, options) : traceCompletionSummaryRow(effectiveTrace, event, options)); continue; } diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue index 882ec4f9..17af8559 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue @@ -7,7 +7,7 @@ import { useBottomFollowScroll } from "@/composables/useBottomFollowScroll"; import { formatDisplayClock } from "@/config/runtime"; import { traceDisplayRows, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts"; -const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null; sealedFinalText?: string }>(); +const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null; hideSealedFinalResponse?: boolean }>(); const listRef = ref(null); const expanded = ref(false); @@ -16,7 +16,7 @@ const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottom const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []); const eventCount = computed(() => props.trace?.eventCount ?? events.value.length); const rawReadableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record[], { formatClock: formatDisplayClock })); -const readableRows = computed(() => filterReadableRows(rawReadableRows.value, props.sealedFinalText)); +const readableRows = computed(() => filterReadableRows(rawReadableRows.value, props.hideSealedFinalResponse === true)); const projectionDiagnosticLabel = computed(() => { const projection = props.trace?.projection ?? null; const health = projection?.projectionHealth ?? props.trace?.projectionHealth; @@ -83,39 +83,15 @@ function rowIsTool(row: TraceEventRow): boolean { return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header); } -function filterReadableRows(rows: TraceEventRow[], sealedFinalText: string | undefined): TraceEventRow[] { - const finalText = normalizeComparableText(sealedFinalText); - if (!finalText || finalText.length < 24) return rows; - return rows.filter((row) => !assistantRowDuplicatesFinal(row, finalText)); +function filterReadableRows(rows: TraceEventRow[], hideSealedFinalResponse: boolean): TraceEventRow[] { + if (!hideSealedFinalResponse) return rows; + return rows.filter((row) => !traceRowDuplicatesSealedFinal(row)); } -function assistantRowDuplicatesFinal(row: TraceEventRow, finalText: string): boolean { +function traceRowDuplicatesSealedFinal(row: TraceEventRow): boolean { if (rowIsTool(row)) return false; - const rowTextRaw = [row.header, row.preview, row.body].filter(Boolean).join("\n"); - if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) return false; - const rowText = normalizeComparableText(rowTextRaw); - if (!rowText || rowText.length < 24) return true; - return finalText.includes(rowText) || rowText.includes(finalText) || longestSharedSubstringLength(finalText, rowText) >= Math.min(80, Math.floor(Math.min(finalText.length, rowText.length) * 0.7)); -} - -function normalizeComparableText(value: unknown): string { - return String(value ?? "").replace(/\s+/gu, " ").trim(); -} - -function longestSharedSubstringLength(a: string, b: string): number { - const left = a.length <= b.length ? a : b; - const right = a.length <= b.length ? b : a; - const max = Math.min(left.length, 320); - let best = 0; - for (let start = 0; start < max; start += 1) { - for (let end = Math.min(max, start + 180); end > start + best; end -= 1) { - if (right.includes(left.slice(start, end))) { - best = end - start; - break; - } - } - } - return best; + if (row.rowId.startsWith("trace-final-response:")) return true; + return row.terminal === true && /(?:助手最终消息|assistant\s+(?:final\s+)?message)/iu.test(String(row.header ?? "")); } function toolCommandPreview(row: TraceEventRow): string | null { diff --git a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue index da9108db..1a955430 100644 --- a/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue +++ b/web/hwlab-cloud-web/src/components/workbench/ConversationPanel.vue @@ -277,14 +277,25 @@ function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | n function sealedDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null { const key = messageDurationFloorKey(message); + const runningFloor = displayedDurationFloorMs.get(key); + const sealedFloor = sealedDurationMs.get(key); + const previous = maxDefinedDurationMs(runningFloor, sealedFloor); displayedDurationFloorMs.delete(key); - const previous = sealedDurationMs.get(key); if (durationMs === null) return previous ?? null; - const next = previous === undefined ? durationMs : Math.max(previous, durationMs); + const next = previous === null ? durationMs : Math.max(previous, durationMs); if (next !== previous) sealedDurationMs.set(key, next); return next; } +function maxDefinedDurationMs(...values: Array): number | null { + let maximum: number | null = null; + for (const value of values) { + if (typeof value !== "number" || !Number.isFinite(value)) continue; + maximum = maximum === null ? value : Math.max(maximum, value); + } + return maximum; +} + function messageDurationFloorKey(message: ChatMessage): string { return firstNonEmptyString(message.id, message.runnerTrace?.traceId, message.traceId) ?? "unknown-agent-message"; } @@ -403,7 +414,7 @@ function formatDuration(ms: number): string { - +