fix: tighten web trace reading

This commit is contained in:
lyon
2026-06-15 14:44:47 +08:00
parent 62216341e1
commit 53f8d94471
6 changed files with 195 additions and 92 deletions
+8
View File
@@ -1616,7 +1616,10 @@ test("hwlab-cli client agent trace can render with the Web trace row path", asyn
assert.equal(result.payload.body.sourceEventCount, 3);
assert.ok(result.payload.body.renderedRowCount >= 1);
assert.ok(result.payload.body.rows.some((row: any) => //u.test(row.header)));
assert.equal(JSON.stringify(result.payload.body.rows).includes("request accepted"), false);
assert.equal(JSON.stringify(result.payload.body.rows).includes("request:accepted"), false);
assert.equal(JSON.stringify(result.payload.body.rows).includes("session:reused"), false);
assert.equal(result.payload.body.rows[0].body, "当前有两个 HWPOD 可用。");
assert.equal(result.payload.traceStatus, "completed");
assert.equal(result.payload.rendered.traceStatus, "completed");
assert.equal(result.payload.rendered.render, "web");
@@ -1710,6 +1713,8 @@ test("hwlab-cli Web trace render does not treat AgentRun result-ready as final a
assert.equal(text.includes("助手最终消息"), false);
assert.equal(text.includes("助手消息"), false);
assert.match(text, //u);
assert.match(text, / 00:00:02/u);
assert.equal(text.includes("总耗时 00:00:00"), false);
assert.equal(text.includes("AgentRun result is ready for HWLAB short-connection polling"), false);
});
@@ -1785,6 +1790,9 @@ test("hwlab-cli Web trace render keeps AgentRun middle assistant message", async
assert.match(text, / HWPOD /u);
assert.match(text, / 2 HWPOD /u);
assert.match(text, //u);
assert.match(text, / 00:00:03/u);
assert.equal(text.includes("agentrun:request:accepted"), false);
assert.equal(text.includes("总耗时 00:00:00"), false);
});
test("hwlab-cli Web trace render keeps the full final-response body without truncating long agent summaries", async () => {
+33 -54
View File
@@ -11,12 +11,11 @@ export interface TraceEventRow {
type TraceEvent = Record<string, unknown>;
export function traceDisplayRows(trace: Record<string, unknown> = {}, events: TraceEvent[] = []): TraceEventRow[] {
const effectiveTrace = traceWithInferredStart(trace, events);
const rows: TraceEventRow[] = [];
const renderedSourceEvents = new Set<string>();
const renderedToolIdentities = new Set<string>();
const assistantRows: AssistantRowState[] = [];
let requestRendered = false;
let setupRendered = false;
let lastAssistantRowIndex = -1;
let completionEvent: TraceEvent | null = null;
for (let index = 0; index < events.length; index += 1) {
@@ -34,30 +33,22 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
if (renderedToolIdentities.has(identity)) continue;
renderedToolIdentities.add(identity);
}
rows.push(traceToolCallRow(trace, event));
rows.push(traceToolCallRow(effectiveTrace, event));
continue;
}
if (isRequestTraceEvent(event)) {
if (!requestRendered) {
rows.push(traceRequestSummaryRow(event));
requestRendered = true;
}
continue;
}
if (isSetupTraceEvent(event)) {
if (!setupRendered) {
rows.push(traceSetupSummaryRow(event));
setupRendered = true;
}
continue;
}
if (isTerminalAssistantTraceEvent(event)) {
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, trace, event, { terminal: true });
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: true });
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
continue;
}
if (isAssistantTraceEvent(event)) {
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, trace, event, { terminal: false });
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: false });
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
continue;
}
@@ -65,21 +56,21 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
completionEvent = event;
continue;
}
rows.push(traceDisplayRow(trace, event));
rows.push(traceDisplayRow(effectiveTrace, event));
}
if (completionEvent) {
const finalResponseText = traceFinalResponseText(trace);
const finalResponseText = traceFinalResponseText(effectiveTrace);
if (finalResponseText) {
lastAssistantRowIndex = upsertAuthoritativeFinalResponseRow(rows, assistantRows, completionEvent, finalResponseText);
}
const lastAssistantRow = lastAssistantRowIndex >= 0 ? rows[lastAssistantRowIndex] : undefined;
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(trace, lastAssistantRow, completionEvent);
else rows.push(traceCompletionSummaryRow(trace, completionEvent));
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(effectiveTrace, lastAssistantRow, completionEvent);
else rows.push(traceCompletionSummaryRow(effectiveTrace, completionEvent));
}
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));
if (traceNoiseEventCount(events) === events.length) return [traceNoiseSummaryRow(effectiveTrace, events)];
return events.filter((event) => !isNoisyTraceEvent(event) && !isRequestTraceEvent(event) && !isSetupTraceEvent(event)).map((event) => traceDisplayRow(effectiveTrace, event));
}
export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
@@ -140,7 +131,7 @@ function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent): Tr
rowId: `tool:${event.itemId ?? event.seq ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
seq: numberOrNull(event.seq),
tone: traceEventTone(event),
header: `${traceClock(event.createdAt)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
body,
bodyFormat: "text"
};
@@ -161,33 +152,6 @@ export function traceNoiseEventCount(events: TraceEvent[] = []): number {
return Array.isArray(events) ? events.filter((event) => isNoisyTraceEvent(event)).length : 0;
}
function traceRequestSummaryRow(event: TraceEvent): TraceEventRow {
const prompt = cleanTraceText(event.promptSummary ?? event.prompt ?? "");
return {
rowId: `trace-request:${event.seq ?? "accepted"}`,
seq: numberOrNull(event.seq),
tone: traceEventTone(event),
header: `${traceClock(event.createdAt)} 请求接受${prompt ? `,提示词:"${compactTraceOneLine(prompt, 120)}"` : ""}`,
body: null
};
}
function traceSetupSummaryRow(event: TraceEvent): TraceEventRow {
const label = String(event.label ?? "");
const parts = [
/session:reused|thread\/resume/iu.test(label) ? "" : "",
/prompt:sent|sent prompt/iu.test(label) ? "" : null,
/turn[:/]start|turn:started|turn\/start/iu.test(label) ? "" : null
].filter(Boolean).join("");
return {
rowId: `trace-setup:${event.seq ?? "session-turn"}`,
seq: numberOrNull(event.seq),
tone: "source",
header: `${traceClock(event.createdAt)} ${parts || "会话准备完成"}`,
body: null
};
}
function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEvent, { terminal }: { terminal: boolean }): TraceEventRow {
const text = cleanTraceText(event.message ?? event.outputSummary ?? assistantStreamText(trace, event) ?? "");
const index = Number.isInteger(event.messageIndex) ? Number(event.messageIndex) : null;
@@ -196,7 +160,7 @@ function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEv
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
seq: numberOrNull(event.seq),
tone: "ok",
header: `${traceClock(event.createdAt)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
header: `${traceEventClock(event)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
terminal: terminal ? true : undefined,
body: text || null,
bodyFormat: "markdown"
@@ -274,7 +238,7 @@ function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRow
body: finalText,
terminal: true,
tone: "ok",
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`
header: `${traceEventClock(completionEvent)} 助手最终消息`
};
matchedState.comparableText = comparableText;
matchedState.derivedSnapshotSuffix = false;
@@ -285,7 +249,7 @@ function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRow
rowId: `trace-final-response:${completionEvent.seq ?? "completed"}`,
seq: numberOrNull(completionEvent.seq),
tone: "ok",
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`,
header: `${traceEventClock(completionEvent)} 助手最终消息`,
terminal: true,
body: finalText,
bodyFormat: "markdown"
@@ -348,7 +312,7 @@ function traceCompletionSummaryRow(trace: Record<string, unknown>, event: TraceE
rowId: `trace-completion:${event.seq ?? "turn"}`,
seq: numberOrNull(event.seq),
tone: traceEventTone(event),
header: `${traceClock(event.createdAt)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))}`,
header: `${traceEventClock(event)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))}`,
body: null
};
}
@@ -361,7 +325,7 @@ function traceNoiseSummaryRow(trace: Record<string, unknown>, events: TraceEvent
rowId: "trace-noise-summary",
seq: numberOrNull(lastEvent?.seq),
tone: lastEvent ? traceEventTone(lastEvent) : "source",
header: `${traceClock(lastEvent?.createdAt)} Trace ${status},等待可读事件`,
header: `${traceEventClock(lastEvent)} Trace ${status},等待可读事件`,
body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}`,
bodyFormat: "text"
};
@@ -373,7 +337,7 @@ function traceDisplayRow(trace: Record<string, unknown>, event: TraceEvent): Tra
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
seq: numberOrNull(event.seq),
tone: traceEventTone(event),
header: `${traceClock(event.createdAt)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${traceStatusToken(event)} ${label}`.trim(),
header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${traceStatusToken(event)} ${label}`.trim(),
body: traceDisplayBody(event)
};
}
@@ -561,10 +525,25 @@ function traceClock(value: unknown): string {
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19);
}
function traceEventClock(event: TraceEvent | null | undefined): string {
return traceClock(traceEventTimestamp(event));
}
function traceWithInferredStart(trace: Record<string, unknown>, events: TraceEvent[]): Record<string, unknown> {
if (nonEmptyString(trace.startedAt ?? trace.createdAt)) return trace;
const firstTimestamp = events.map((event) => traceEventTimestamp(event)).find(Boolean);
return firstTimestamp ? { ...trace, startedAt: firstTimestamp } : trace;
}
function traceEventTimestamp(event: TraceEvent | null | undefined): string | null {
if (!event) return null;
return nonEmptyString(event.createdAt ?? event.ts ?? event.timestamp ?? event.observedAt);
}
function traceRelativeMs(trace: Record<string, unknown>, event: TraceEvent): number {
if (typeof event.elapsedMs === "number") return event.elapsedMs;
const start = Date.parse(String(trace.startedAt ?? trace.createdAt ?? ""));
const current = Date.parse(String(event.createdAt ?? ""));
const current = Date.parse(String(traceEventTimestamp(event) ?? ""));
return Number.isNaN(start) || Number.isNaN(current) ? 0 : Math.max(0, current - start);
}
+7
View File
@@ -93,6 +93,13 @@ assertIncludes(appSource, "activityRef: () => activityRef.value", "Workbench mus
assertIncludes(appSource, "getAgentChatResult(resultUrl, inactivityTimeoutMs, activityRef)", "Trace result polling must use activityRef-backed inactivity timeout");
assertIncludes(appSource, "for (;;) ", "trace subscription must keep unbounded polling without total timeout");
assertIncludes(appSource, "/v1/agent/chat/trace/", "trace replay must use Cloud Web same-origin trace API");
assertIncludes(appSource, "trace-meta-details", "Trace identity and raw-event controls must stay behind a compact details disclosure");
assertIncludes(appSource, "grid-template-columns: minmax(0, 1fr);", "Trace rows must give the readable body the full content width");
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 === \"steer\"", "Composer primary button must switch to steer while a turn is running and the draft has text");
assertIncludes(appSource, ":data-action=\"primaryAction\"", "Composer primary button must expose the active turn/steer/cancel action");
assert.doesNotMatch(appSource, / Trace/u, "Workbench must not render a separate stale cancel-running-trace button");
assertIncludes(appSource, "session_required", "explicit session policy must be represented");
assertIncludes(appSource, "DEV-LIVE", "evidence labels must remain visible");
assertIncludes(appSource, "CodeWorkbenchView", "Code Workbench must be a route view, not the app shell");
@@ -21,6 +21,7 @@ const noiseCount = computed(() => traceNoiseEventCount(events.value as Record<st
const lastEvent = computed(() => events.value.at(-1) ?? null);
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 detailTitle = computed(() => props.trace?.traceId ? `运行详情 ${props.trace.traceId}` : "运行详情");
const summaryItems = computed(() => {
const agentRun = recordValue(props.trace?.agentRun);
return [
@@ -83,19 +84,28 @@ function recordValue(value: unknown): Record<string, unknown> | null {
<header class="trace-toolbar">
<button class="trace-toggle" type="button" @click="expanded = !expanded">{{ expanded ? "收起" : "展开" }}</button>
<strong>Trace</strong>
<span class="trace-id">{{ trace.traceId || "pending" }}</span>
<small>{{ readableRows.length }} readable / {{ eventCount }} events</small>
<small v-if="noiseCount > 0">hidden={{ noiseCount }}</small>
<small>last={{ lastEventLabel }}</small>
<button class="btn btn-secondary btn-sm" type="button" @click="following = !following">{{ following ? "跟随" : "暂停" }}</button>
<button class="btn btn-secondary btn-sm" type="button" @click="jumpToBottom">到底部</button>
<button class="btn btn-secondary btn-sm" type="button" @click="rawMode = !rawMode">{{ rawMode ? "可读视图" : "原始事件" }}</button>
</header>
<div v-if="summaryItems.length > 0" class="trace-summary-strip" aria-label="Trace identity summary">
<span v-for="item in summaryItems" :key="item.label" class="trace-summary-chip"><b>{{ item.label }}</b>{{ item.value }}</span>
</div>
<p v-if="trace.eventsCompacted" class="trace-warning">当前 result 返回被压缩已通过 trace replay 入口请求完整事件完成前不要把压缩窗口当完整 trace</p>
<p v-if="noiseCount > 0 && !rawMode" class="trace-warning">已按共享 trace renderer 聚合隐藏 {{ noiseCount }} backend/chunk/token 噪声事件需要审计原始 payload 时可切换到原始事件</p>
<details class="trace-meta-details">
<summary>{{ detailTitle }}</summary>
<div class="trace-meta-panel">
<dl class="trace-meta-grid">
<div><dt>trace</dt><dd>{{ trace.traceId || "pending" }}</dd></div>
<div><dt>rows</dt><dd>{{ readableRows.length }} / {{ eventCount }}</dd></div>
<div v-if="noiseCount > 0"><dt>hidden</dt><dd>{{ noiseCount }}</dd></div>
<div><dt>last</dt><dd>{{ lastEventLabel }}</dd></div>
</dl>
<div v-if="summaryItems.length > 0" class="trace-summary-strip" aria-label="Trace identity summary">
<span v-for="item in summaryItems" :key="item.label" class="trace-summary-chip"><b>{{ item.label }}</b>{{ item.value }}</span>
</div>
<p v-if="trace.eventsCompacted" class="trace-warning">当前 result 返回被压缩已通过 trace replay 入口请求完整事件完成前不要把压缩窗口当完整 trace</p>
<p v-if="noiseCount > 0 && !rawMode" class="trace-warning">已聚合隐藏 {{ noiseCount }} backend/chunk/token 噪声事件原始 payload 仍可在原始事件中审计</p>
<div class="trace-meta-actions">
<button class="btn btn-secondary btn-sm" type="button" @click="following = !following">{{ following ? "跟随" : "暂停" }}</button>
<button class="btn btn-secondary btn-sm" type="button" @click="jumpToBottom">到底部</button>
<button class="btn btn-secondary btn-sm" type="button" @click="rawMode = !rawMode">{{ rawMode ? "可读视图" : "原始事件" }}</button>
</div>
</div>
</details>
<ol v-show="expanded" ref="listRef" @scroll="onScroll">
<li v-for="(row, index) in readableRows" v-if="!rawMode" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'">
<code>{{ row.header }}</code>
@@ -1,18 +1,44 @@
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import { useWorkbenchStore } from "@/stores/workbench";
const workbench = useWorkbenchStore();
const draft = ref("");
const draftsOpen = ref(false);
const draftText = computed(() => draft.value.trim());
const primaryAction = computed<"turn" | "steer" | "cancel">(() => {
if (workbench.composer.submitMode === "steer") return draftText.value ? "steer" : "cancel";
return "turn";
});
const primaryLabel = computed(() => primaryAction.value === "cancel" ? "取消" : primaryAction.value === "steer" ? "Steer" : "发送");
const primaryModeLabel = computed(() => {
if (workbench.composer.disabledReason) return workbench.composer.disabledReason;
if (primaryAction.value === "cancel") return `运行中 ${workbench.composer.targetTraceId ?? "trace"}`;
if (primaryAction.value === "steer") return `steer ${workbench.composer.targetTraceId}`;
return "turn / 新请求";
});
const primaryDisabled = computed(() => {
if (primaryAction.value === "cancel") return workbench.composer.disabled || !workbench.composer.targetTraceId;
return workbench.composer.disabled || !draftText.value;
});
async function submit(): Promise<void> {
const value = draft.value;
if (primaryAction.value === "cancel") {
if (primaryDisabled.value) return;
await workbench.cancelRunningTrace();
return;
}
if (!value.trim() || workbench.composer.disabled) return;
draft.value = "";
await workbench.submitMessage(value);
}
async function submitFromInput(): Promise<void> {
if (!draftText.value) return;
await submit();
}
function pickDraft(text: string): void {
draft.value = text;
draftsOpen.value = false;
@@ -21,7 +47,7 @@ function pickDraft(text: string): void {
</script>
<template>
<form id="command-form" class="command-composer" :title="workbench.composer.disabledReason || (workbench.composer.submitMode === 'steer' ? `引导运行中的 turn: ${workbench.composer.targetTraceId}` : '发送新 turn')" @submit.prevent="submit">
<form id="command-form" class="command-composer" :title="workbench.composer.disabledReason || (primaryAction === 'cancel' ? `取消运行中的 turn: ${workbench.composer.targetTraceId}` : primaryAction === 'steer' ? `Steer 运行中的 turn: ${workbench.composer.targetTraceId}` : '发送新 turn')" @submit.prevent="submit">
<div class="composer-toolbar">
<label class="composer-field" for="code-agent-provider-profile">
<span>模型通道</span>
@@ -47,10 +73,9 @@ function pickDraft(text: string): void {
</section>
<div class="composer-actions">
<span v-if="workbench.composer.disabledReason" class="composer-warning">{{ workbench.composer.disabledReason }}</span>
<span v-else class="composer-mode" :data-mode="workbench.composer.submitMode">{{ workbench.composer.submitMode === 'steer' ? `steer ${workbench.composer.targetTraceId}` : 'turn / 新请求' }}</span>
<button class="btn btn-secondary" type="button" @click="workbench.cancelRunningTrace">取消运行中 Trace</button>
<button id="command-send" class="btn btn-primary" type="submit" :disabled="!draft.trim() || workbench.composer.disabled">{{ workbench.composer.submitMode === 'steer' ? '引导' : '发送' }}</button>
<span v-else class="composer-mode" :data-mode="primaryAction">{{ primaryModeLabel }}</span>
<button id="command-send" class="btn btn-primary" type="submit" :data-action="primaryAction" :disabled="primaryDisabled">{{ primaryLabel }}</button>
</div>
<textarea id="command-input" v-model="draft" rows="3" placeholder="输入任务,Enter 发送,Shift+Enter 换行" :disabled="false" @input="workbench.recordActivity('typing')" @keydown.enter.exact.prevent="submit" />
<textarea id="command-input" v-model="draft" rows="3" placeholder="输入任务,Enter 发送,Shift+Enter 换行" :disabled="false" @input="workbench.recordActivity('typing')" @keydown.enter.exact.prevent="submitFromInput" />
</form>
</template>
+94 -20
View File
@@ -636,8 +636,9 @@
.message-card {
display: grid;
align-self: start;
gap: 8px;
padding: 12px;
gap: 6px;
border-radius: 4px;
padding: 8px;
}
.message-card[data-role="agent"] {
@@ -711,7 +712,7 @@
.message-markdown pre,
.message-markdown code {
border-radius: 6px;
border-radius: 3px;
background: #0f172a;
color: #dbeafe;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
@@ -742,14 +743,74 @@
.trace-timeline {
display: grid;
gap: 8px;
border-radius: 8px;
gap: 6px;
border: 1px solid #e2e8f0;
border-radius: 3px;
background: #f8fafc;
padding: 10px;
padding: 6px;
}
.trace-toolbar {
justify-content: flex-start;
gap: 6px;
min-height: 26px;
}
.trace-meta-details {
border: 1px solid #e2e8f0;
border-radius: 3px;
background: #ffffff;
}
.trace-meta-details summary {
cursor: pointer;
padding: 5px 7px;
color: #475569;
font-size: 12px;
font-weight: 700;
}
.trace-meta-panel {
display: grid;
gap: 6px;
border-top: 1px solid #e2e8f0;
padding: 6px;
}
.trace-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 4px 8px;
margin: 0;
}
.trace-meta-grid div {
display: grid;
min-width: 0;
gap: 2px;
}
.trace-meta-grid dt {
color: #64748b;
font-size: 10px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.trace-meta-grid dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: #334155;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
}
.trace-meta-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.trace-summary-strip {
@@ -765,10 +826,10 @@
align-items: center;
gap: 6px;
border: 1px solid #d8e1eb;
border-radius: 8px;
border-radius: 3px;
background: white;
color: #334155;
padding: 5px 8px;
padding: 4px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
}
@@ -795,12 +856,12 @@
}
.trace-toggle {
min-height: 28px;
min-height: 24px;
border: 1px solid #cbd5e1;
border-radius: 8px;
border-radius: 3px;
background: white;
color: #334155;
padding: 0 9px;
padding: 0 7px;
font-size: 12px;
font-weight: 700;
}
@@ -808,8 +869,8 @@
.trace-warning,
.trace-empty {
margin: 0;
border-radius: 8px;
padding: 8px 10px;
border-radius: 3px;
padding: 6px 8px;
font-size: 12px;
line-height: 1.45;
}
@@ -828,8 +889,8 @@
.trace-timeline ol {
display: grid;
max-height: 170px;
gap: 6px;
max-height: 320px;
gap: 4px;
margin: 0;
overflow: auto;
padding: 0;
@@ -838,18 +899,26 @@
.trace-timeline li {
display: grid;
grid-template-columns: minmax(180px, 280px) minmax(0, 1fr) auto;
gap: 8px;
grid-template-columns: minmax(0, 1fr);
gap: 3px;
border-top: 1px solid #e2e8f0;
padding: 5px 0 0;
color: #475569;
font-size: 12px;
}
.trace-timeline li:first-child {
border-top: 0;
padding-top: 0;
}
.trace-timeline li[data-terminal="true"] {
border-left: 3px solid #22c55e;
padding-left: 8px;
}
.trace-render-row .trace-row-body {
width: 100%;
min-width: 0;
margin: 0;
white-space: pre-wrap;
@@ -866,13 +935,13 @@
}
.trace-render-row pre.trace-row-body {
max-height: 120px;
max-height: 220px;
overflow: auto;
border: 1px solid #d8e1eb;
border-radius: 8px;
border-radius: 3px;
background: white;
color: #334155;
padding: 8px;
padding: 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
line-height: 1.45;
@@ -963,6 +1032,11 @@
font-size: 12px;
}
.trace-timeline code {
min-width: 0;
overflow-wrap: anywhere;
}
.command-composer {
display: grid;
gap: 10px;