Merge pull request #1279 from pikasTech/fix/issue-1278-trace-tool-command-preview
fix(web): Trace 工具行展示命令摘要
This commit is contained in:
@@ -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"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user