Merge pull request #2715 from pikasTech/fix/workbench-lazy-tool-bodies
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success

fix: 延迟渲染 Workbench 工具输出
This commit is contained in:
Lyon
2026-07-21 08:13:14 +08:00
committed by GitHub
2 changed files with 66 additions and 4 deletions
@@ -269,6 +269,13 @@ describe("TraceTimeline sequence authority", () => {
expect(completedSummary.get("code").text()).toMatch(/^\d{2}:\d{2}:\d{2}$/u);
expect(completedSummary.get(".trace-tool-command-preview").text()).toBe("bun test tool-state");
expect(completedSummary.text()).not.toMatch(/\b(?:ok|fail|run|running)\b|total=|duration=|exit=|/u);
expect(completedDetails.text()).not.toContain("1 test passed");
completedDetails.element.open = true;
await completedDetails.trigger("toggle");
await nextTick();
expect(completedDetails.text()).toContain("1 test passed");
wrapper.unmount();
});
@@ -307,6 +314,51 @@ describe("TraceTimeline sequence authority", () => {
expect(wrapper.text()).toContain("共 1000 events");
wrapper.unmount();
});
it("does not mount folded tool output bodies for a dense live Trace", async () => {
const events = Array.from({ length: 120 }, (_, index) => ({
sourceEventId: `evt_tool_dense_${index + 1}`,
runId: "run_tool_dense",
sourceSeq: index + 1,
source: "agentrun.kafka",
type: "tool_call",
label: "agentrun:tool:completed",
status: "completed",
toolName: "commandExecution",
itemId: `tool_dense_${index + 1}`,
command: `rg dense-${index + 1}`,
stdoutSummary: `dense output ${index + 1}`,
exitCode: 0
}));
const wrapper = mount(TraceTimeline, {
props: {
autoExpanded: true,
trace: {
traceId: "trc_tool_dense",
eventSource: "hwlab-kafka-sse",
status: "running",
eventCount: events.length,
events
}
}
});
await nextTick();
const tools = wrapper.findAll(".trace-tool-details");
expect(tools).toHaveLength(120);
expect(wrapper.findAll(".trace-tool-details .trace-row-body")).toHaveLength(0);
const lastTool = tools.at(-1);
expect(lastTool).toBeDefined();
if (!lastTool) throw new Error("last tool row was not rendered");
lastTool.element.open = true;
await lastTool.trigger("toggle");
await nextTick();
expect(wrapper.findAll(".trace-tool-details .trace-row-body")).toHaveLength(1);
expect(lastTool.text()).toContain("dense output 120");
wrapper.unmount();
});
});
function liveTrace(status: "running" | "completed") {
@@ -22,6 +22,7 @@ const TRACE_RENDER_ROW_WINDOW = 120;
const TRACE_RENDER_EVENT_WINDOW = TRACE_RENDER_ROW_WINDOW * 2;
const listRef = ref<HTMLOListElement | null>(null);
const expanded = ref(false);
const expandedToolRowIds = ref(new Set<string>());
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useWorkbenchBottomFollowScroll(listRef, { threshold: 24 });
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
@@ -104,6 +105,13 @@ function onDisclosureToggle(event: Event): void {
writeStoredExpanded(target.open);
}
function onToolDisclosureToggle(row: TraceEventRow, event: Event): void {
const target = event.currentTarget;
if (!(target instanceof HTMLDetailsElement)) return;
if (target.open) expandedToolRowIds.value.add(row.rowId);
else expandedToolRowIds.value.delete(row.rowId);
}
function setExpandedProgrammatically(value: boolean): void {
if (expanded.value === value) return;
programmaticToggleTarget = value;
@@ -175,14 +183,16 @@ function firstNonEmptyString(...values: unknown[]): string | null {
<p v-if="hiddenRowCount > 0 || inputEventWindowed" class="trace-window-notice">显示最近 {{ renderedRows.length }} 行,共 {{ eventCount }} events</p>
<ol v-if="renderedRows.length > 0" ref="listRef" @scroll="onScroll">
<li v-for="(row, index) in renderedRows" :key="rowKey(row, index)" class="trace-render-row" data-testid="trace-render-row" :data-trace-id="trace.traceId || undefined" :data-event-seq="row.seq ?? undefined" :data-event-seq-authority="row.sequenceAuthority" :data-projected-seq="row.sequenceAuthority === 'projected' ? row.seq ?? undefined : undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-tool-state="row.toolState ?? undefined" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="hiddenRowCount + index + 1">
<details v-if="rowIsTool(row)" class="trace-tool-details" :aria-busy="row.toolState === 'running' ? 'true' : undefined">
<details v-if="rowIsTool(row)" class="trace-tool-details" :aria-busy="row.toolState === 'running' ? 'true' : undefined" @toggle="onToolDisclosureToggle(row, $event)">
<summary :aria-label="traceToolAccessibilityLabel(row)" :title="traceToolAccessibilityLabel(row)" aria-live="polite" aria-atomic="true">
<code>{{ row.header }}</code>
<span v-if="toolCommandPreview(row)" class="trace-tool-command-preview">{{ toolCommandPreview(row) }}</span>
</summary>
<MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" />
<pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
<span v-else class="trace-row-body muted">已记录</span>
<template v-if="expandedToolRowIds.has(row.rowId)">
<MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" />
<pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
<span v-else class="trace-row-body muted">已记录</span>
</template>
</details>
<template v-else>
<code>{{ row.header }}</code>