diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index cb6607ec..f0a01490 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -76,7 +76,10 @@ export function traceDisplayRows(trace: Record = {}, events: Tr if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(trace, lastAssistantRow, completionEvent); else rows.push(traceCompletionSummaryRow(trace, completionEvent)); } - return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event)); + if (rows.length > 0) return rows; + if (events.length === 0) return []; + if (traceNoiseEventCount(events) === events.length) return [traceNoiseSummaryRow(trace, events)]; + return events.filter((event) => !isNoisyTraceEvent(event)).map((event) => traceDisplayRow(trace, event)); } export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string { @@ -350,6 +353,20 @@ function traceCompletionSummaryRow(trace: Record, event: TraceE }; } +function traceNoiseSummaryRow(trace: Record, events: TraceEvent[]): TraceEventRow { + const lastEvent = events.at(-1); + const status = nonEmptyString(trace.status ?? trace.traceStatus ?? lastEvent?.status) ?? "running"; + const lastLabel = lastEvent ? readableTraceLabel(lastEvent) : "未观测"; + return { + rowId: "trace-noise-summary", + seq: numberOrNull(lastEvent?.seq), + tone: lastEvent ? traceEventTone(lastEvent) : "source", + header: `${traceClock(lastEvent?.createdAt)} Trace ${status},等待可读事件`, + body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}。`, + bodyFormat: "text" + }; +} + function traceDisplayRow(trace: Record, event: TraceEvent): TraceEventRow { const label = readableTraceLabel(event); return { diff --git a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts index 0dd95ee6..048639b2 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts @@ -3,6 +3,7 @@ import test from "node:test"; import type { AgentRunProvenance, ChatMessage, TraceEvent } from "../src/types/index.ts"; import { canCancelMessage, canReplayTrace, canRetryMessage, messageTraceId, renderSafeMarkdown, traceEventBody, traceEventLabel, traceIdentityText, visibleTraceEvents } from "../src/components/workbench/message-rendering.ts"; +import { traceDisplayRows, traceNoiseEventCount } from "../../../tools/src/hwlab-cli/trace-renderer.ts"; test("R1 markdown rendering keeps structure and strips unsafe HTML", () => { const html = renderSafeMarkdown("# 标题\n\n- 项目\n\n\n\n[link](https://example.com)"); @@ -48,6 +49,20 @@ test("R1 trace rendering hides chunk noise without dropping meaningful events", assert.equal(visibleTraceEvents(events, false).length, 3); }); +test("R1 shared trace renderer collapses pure AgentRun backend noise", () => { + const events = Array.from({ length: 12 }, (_, index) => ({ + label: index % 3 === 0 ? "agentrun:backend:run-created" : index % 3 === 1 ? "agentrun:backend:command-created" : "agentrun:backend:runner-job-created", + status: "running", + createdAt: "2026-06-15T02:00:00.000Z" + })); + const rows = traceDisplayRows({ traceId: "trc_noise", status: "running" }, events); + assert.equal(traceNoiseEventCount(events), 12); + assert.equal(rows.length, 1); + assert.equal(rows[0]?.rowId, "trace-noise-summary"); + assert.match(rows[0]?.body ?? "", /已隐藏 12 条 AgentRun backend 状态事件/u); + assert.doesNotMatch(rows[0]?.body ?? "", /run-created\ncommand-created/u); +}); + test("R1 status summary keeps the restored 23-row floor", () => { const message = agentMessage({ status: "failed", diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue index 17882873..9ceaeb39 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue @@ -3,21 +3,35 @@ import { computed, nextTick, ref, watch } from "vue"; import type { RunnerTrace, TraceEvent } from "@/types"; import { isTerminalStatus } from "@/composables/useTraceSubscription"; import { firstNonEmptyString } from "@/utils"; -import { traceEventBody, traceEventLabel, traceEventMeta, visibleTraceEvents } from "@/components/workbench/message-rendering"; +import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue"; +import { traceEventBody, traceEventLabel, traceEventMeta } from "@/components/workbench/message-rendering"; +import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts"; const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string }>(); const listRef = ref(null); const following = ref(true); const expanded = ref(false); -const compact = ref(true); +const rawMode = ref(false); const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []); const eventCount = computed(() => props.trace?.eventCount ?? events.value.length); -const visibleEvents = computed(() => visibleTraceEvents(events.value, compact.value)); -const noiseCount = computed(() => Math.max(0, events.value.length - visibleEvents.value.length)); +const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record[])); +const noiseCount = computed(() => traceNoiseEventCount(events.value as Record[])); const lastEvent = computed(() => events.value.at(-1) ?? null); -const lastEventLabel = computed(() => firstNonEmptyString(props.trace?.lastEventLabel, lastEvent.value?.label, lastEvent.value?.type, lastEvent.value?.kind) ?? "未观测"); +const lastReadableRow = computed(() => readableRows.value.at(-1) ?? null); +const lastEventLabel = computed(() => firstNonEmptyString(lastReadableRow.value?.header, props.trace?.lastEventLabel, lastEvent.value?.label, lastEvent.value?.type, lastEvent.value?.kind) ?? "未观测"); +const summaryItems = computed(() => { + const agentRun = recordValue(props.trace?.agentRun); + return [ + { label: "status", value: firstNonEmptyString(props.trace?.status, props.trace?.traceStatus) }, + { label: "session", value: props.trace?.sessionId }, + { label: "thread", value: props.trace?.threadId }, + { label: "run", value: firstNonEmptyString(agentRun?.runId, props.trace?.runId) }, + { label: "command", value: firstNonEmptyString(agentRun?.commandId, props.trace?.commandId) }, + { label: "waiting", value: props.trace?.waitingFor } + ].filter((item): item is { label: string; value: string } => Boolean(item.value)); +}); watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => { if (!following.value || !expanded.value) return; @@ -50,6 +64,18 @@ function eventKey(event: TraceEvent, index: number): string { return `${event.ts ?? "no-ts"}-${event.label ?? event.type ?? event.kind ?? "event"}-${index}`; } +function rowKey(row: TraceEventRow, index: number): string { + return `${row.rowId}-${row.seq ?? "no-seq"}-${index}`; +} + +function traceRecord(trace: RunnerTrace | null | undefined): Record { + return trace && typeof trace === "object" ? trace as Record : {}; +} + +function recordValue(value: unknown): Record | null { + return value && typeof value === "object" ? value as Record : null; +} + diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index ad79dd44..f43ff79c 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -752,6 +752,33 @@ flex-wrap: wrap; } +.trace-summary-strip { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.trace-summary-chip { + display: inline-flex; + min-width: 0; + max-width: 100%; + align-items: center; + gap: 6px; + border: 1px solid #d8e1eb; + border-radius: 8px; + background: white; + color: #334155; + padding: 5px 8px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; +} + +.trace-summary-chip b { + color: #64748b; + font-family: inherit; + text-transform: uppercase; +} + .trace-toolbar strong, .trace-id { min-width: 0; @@ -811,12 +838,46 @@ .trace-timeline li { display: grid; - grid-template-columns: minmax(96px, 160px) minmax(0, 1fr) auto; + grid-template-columns: minmax(180px, 280px) minmax(0, 1fr) auto; gap: 8px; color: #475569; font-size: 12px; } +.trace-timeline li[data-terminal="true"] { + border-left: 3px solid #22c55e; + padding-left: 8px; +} + +.trace-render-row .trace-row-body { + min-width: 0; + margin: 0; + white-space: pre-wrap; +} + +.trace-render-row .trace-row-markdown { + display: grid; + gap: 6px; + white-space: normal; +} + +.trace-render-row .muted { + color: #94a3b8; +} + +.trace-render-row pre.trace-row-body { + max-height: 120px; + overflow: auto; + border: 1px solid #d8e1eb; + border-radius: 8px; + background: white; + color: #334155; + padding: 8px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; + line-height: 1.45; +} + .trace-timeline li span, .trace-timeline li small { min-width: 0;