Merge pull request #1279 from pikasTech/fix/issue-1278-trace-tool-command-preview

fix(web): Trace 工具行展示命令摘要
This commit is contained in:
Lyon
2026-06-15 20:51:30 +08:00
committed by GitHub
4 changed files with 40 additions and 1 deletions
+3
View File
@@ -4,6 +4,7 @@ export interface TraceEventRow {
tone: "ok" | "blocked" | "warn" | "source"; tone: "ok" | "blocked" | "warn" | "source";
header: string; header: string;
body: string | null; body: string | null;
preview?: string | null;
terminal?: boolean; terminal?: boolean;
bodyFormat?: "markdown" | "text"; bodyFormat?: "markdown" | "text";
} }
@@ -101,6 +102,7 @@ function isToolTraceRow(row: TraceEventRow): boolean {
} }
function traceToolSummary(row: TraceEventRow): string { function traceToolSummary(row: TraceEventRow): string {
if (row.preview) return compactTraceOneLine(row.preview, 90);
const firstLine = traceMarkdownPreviewLines(row.body).find((line) => !/^(stdout|stderr|exitCode|rowId|terminal):/iu.test(line.trim())); const firstLine = traceMarkdownPreviewLines(row.body).find((line) => !/^(stdout|stderr|exitCode|rowId|terminal):/iu.test(line.trim()));
return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 90); return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 90);
} }
@@ -132,6 +134,7 @@ function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent): Tr
tone: traceEventTone(event), tone: traceEventTone(event),
header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(), header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
body, body,
preview: command ? compactTraceOneLine(command, 2000) : null,
bodyFormat: "text" bodyFormat: "text"
}; };
} }
+4
View File
@@ -107,6 +107,10 @@ assertIncludes(messageTraceDebugSource, "trace-meta-panel", "Trace identity and
assertIncludes(appSource, "scrollbar-width: none;", "Platform sidebar must scroll without a visible scrollbar"); assertIncludes(appSource, "scrollbar-width: none;", "Platform sidebar must scroll without a visible scrollbar");
assertIncludes(appSource, "border-collapse: collapse;", "Markdown tables must use GitHub-like bordered table styling"); assertIncludes(appSource, "border-collapse: collapse;", "Markdown tables must use GitHub-like bordered table styling");
assertIncludes(appSource, "grid-template-columns: minmax(0, 1fr);", "Trace rows must give the readable body the full content width"); assertIncludes(appSource, "grid-template-columns: minmax(0, 1fr);", "Trace rows must give the readable body the full content width");
assertIncludes(traceTimelineSource, "toolCommandPreview", "Trace tool summaries must expose command previews in the collapsed single-line row");
assertIncludes(traceTimelineSource, "trace-tool-command-preview", "Trace tool command preview must render inside the collapsed summary");
assertIncludes(traceTimelineSource, "row.preview", "Trace tool command preview must use request-side row.preview instead of response body text");
assertIncludes(appSource, "text-overflow: ellipsis;", "Trace tool command preview must keep long commands visually clipped on one line");
assertIncludes(appSource, "const primaryAction = computed", "Workbench composer must derive a single primary send/cancel/steer action"); assertIncludes(appSource, "const primaryAction = computed", "Workbench composer must derive a single primary send/cancel/steer action");
assertIncludes(appSource, "primaryAction.value === \"cancel\"", "Composer primary button must switch to cancel while a turn is running and the draft is empty"); assertIncludes(appSource, "primaryAction.value === \"cancel\"", "Composer primary button must switch to cancel while a turn is running and the draft is empty");
assertIncludes(appSource, "primaryAction.value === \"steer\"", "Composer primary button must switch to steer while a turn is running and the draft has text"); assertIncludes(appSource, "primaryAction.value === \"steer\"", "Composer primary button must switch to steer while a turn is running and the draft has text");
@@ -13,6 +13,7 @@ const expanded = ref(false);
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []); const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length); const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[])); const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[]));
const toolPreviewLimit = 140;
watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => { watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => {
if (!following.value || !expanded.value) return; if (!following.value || !expanded.value) return;
@@ -70,6 +71,18 @@ function rowIsTool(row: TraceEventRow): boolean {
return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header); return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header);
} }
function toolCommandPreview(row: TraceEventRow): string | null {
if (!rowIsTool(row)) return null;
const preview = singleLinePreview(row.preview);
if (!preview) return null;
return preview.length > toolPreviewLimit ? `${preview.slice(0, toolPreviewLimit - 3).trimEnd()}...` : preview;
}
function singleLinePreview(value: unknown): string | null {
const preview = String(value ?? "").replace(/\u0000/gu, "").replace(/\r\n|\r|\n/gu, " ").replace(/\s+/gu, " ").trim();
return preview || null;
}
function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unknown> { function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unknown> {
return trace && typeof trace === "object" ? trace as Record<string, unknown> : {}; return trace && typeof trace === "object" ? trace as Record<string, unknown> : {};
} }
@@ -87,7 +100,10 @@ function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unkn
<ol ref="listRef" @scroll="onScroll"> <ol ref="listRef" @scroll="onScroll">
<li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'"> <li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'">
<details v-if="rowIsTool(row)" class="trace-tool-details"> <details v-if="rowIsTool(row)" class="trace-tool-details">
<summary><code>{{ row.header }}</code></summary> <summary>
<code>{{ row.header }}</code>
<span v-if="toolCommandPreview(row)" class="trace-tool-command-preview" :title="row.preview ?? undefined">{{ toolCommandPreview(row) }}</span>
</summary>
<MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" /> <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> <pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
<span v-else class="trace-row-body muted">已记录</span> <span v-else class="trace-row-body muted">已记录</span>
@@ -1073,8 +1073,10 @@
min-width: 0; min-width: 0;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
overflow: hidden;
cursor: pointer; cursor: pointer;
list-style: none; list-style: none;
white-space: nowrap;
} }
.trace-tool-details > summary::before { .trace-tool-details > summary::before {
@@ -1094,11 +1096,25 @@
.trace-tool-details > summary code { .trace-tool-details > summary code {
min-width: 0; min-width: 0;
max-width: 42%;
flex: 0 1 auto;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.trace-tool-command-preview {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
color: #334155;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.trace-timeline li { .trace-timeline li {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);