Merge pull request #2258 from pikasTech/fix/2255-workbench-render-budget
fix: bound workbench trace rendering
This commit is contained in:
@@ -9,14 +9,22 @@ import { traceDisplayRows, type TraceEventRow } from "../../../../../tools/src/h
|
||||
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null }>();
|
||||
|
||||
const TRACE_RENDER_ROW_WINDOW = 120;
|
||||
const listRef = ref<HTMLOListElement | null>(null);
|
||||
const expanded = ref(false);
|
||||
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottomFollowScroll(listRef, { threshold: 24 });
|
||||
|
||||
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
|
||||
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
|
||||
const rawReadableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }));
|
||||
const rawReadableRows = computed(() => expanded.value ? traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }) : []);
|
||||
const readableRows = computed(() => rawReadableRows.value);
|
||||
const hiddenRowCount = computed(() => Math.max(0, readableRows.value.length - TRACE_RENDER_ROW_WINDOW));
|
||||
const renderedRows = computed(() => hiddenRowCount.value > 0 ? readableRows.value.slice(-TRACE_RENDER_ROW_WINDOW) : readableRows.value);
|
||||
const traceSummaryLabel = computed(() => {
|
||||
const count = eventCount.value;
|
||||
if (count <= 0) return String(props.trace?.status ?? "").trim() || "pending";
|
||||
return `${count} events`;
|
||||
});
|
||||
const projectionDiagnosticLabel = computed(() => {
|
||||
const projection = props.trace?.projection ?? null;
|
||||
const health = projection?.projectionHealth ?? props.trace?.projectionHealth;
|
||||
@@ -128,14 +136,16 @@ function firstNonEmptyString(...values: unknown[]): string | null {
|
||||
|
||||
<template>
|
||||
<section v-if="trace" class="trace-timeline" :data-trace-id="trace.traceId || undefined" :data-status="trace.status" :data-projection-health="trace.projection?.projectionHealth || trace.projectionHealth || undefined" :data-following="following ? 'true' : 'false'">
|
||||
<details class="trace-disclosure" :open="expanded" @toggle="onDisclosureToggle">
|
||||
<details class="trace-disclosure" :open="expanded" :data-event-count="eventCount" :data-readable-row-count="readableRows.length" :data-rendered-row-count="renderedRows.length" :data-row-windowed="hiddenRowCount > 0 ? 'true' : 'false'" @toggle="onDisclosureToggle">
|
||||
<summary class="trace-disclosure-summary" aria-label="折叠运行记录">
|
||||
<span class="trace-disclosure-caret" aria-hidden="true" />
|
||||
<span class="trace-disclosure-label">运行记录</span>
|
||||
<span class="trace-disclosure-count">{{ traceSummaryLabel }}</span>
|
||||
</summary>
|
||||
<div class="trace-disclosure-body">
|
||||
<ol ref="listRef" @scroll="onScroll">
|
||||
<li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" data-testid="trace-render-row" :data-trace-id="trace.traceId || undefined" :data-projected-seq="row.seq ?? undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="row.seq ?? index + 1">
|
||||
<div v-if="expanded" class="trace-disclosure-body">
|
||||
<p v-if="hiddenRowCount > 0" class="trace-window-notice">显示最近 {{ renderedRows.length }} / {{ readableRows.length }} 行</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-projected-seq="row.seq ?? undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="row.seq ?? hiddenRowCount + index + 1">
|
||||
<details v-if="rowIsTool(row)" class="trace-tool-details">
|
||||
<summary>
|
||||
<code>{{ row.header }}</code>
|
||||
|
||||
@@ -34,10 +34,8 @@ const traceTimelinePolicy = computed(() => workbenchTraceTimelinePolicy());
|
||||
const scrollSignature = computed(() => workbench.activeMessages.map((message) => [
|
||||
message.id,
|
||||
message.status,
|
||||
visibleMessageText(message).length,
|
||||
message.updatedAt ?? "",
|
||||
message.runnerTrace?.eventCount ?? message.runnerTrace?.events?.length ?? 0
|
||||
].join(":")));
|
||||
visibleMessageText(message).length
|
||||
].join(":")).join("|"));
|
||||
|
||||
watch(scrollSignature, async () => {
|
||||
await keepBottomAfterUpdate();
|
||||
|
||||
@@ -5,14 +5,34 @@ import { firstNonEmptyString } from "@/utils";
|
||||
|
||||
marked.setOptions({ async: false, breaks: true, gfm: true });
|
||||
|
||||
const MARKDOWN_RENDER_CACHE_LIMIT = 160;
|
||||
const markdownRenderCache = new Map<string, string>();
|
||||
|
||||
export function renderSafeMarkdown(source: string | null | undefined): string {
|
||||
const text = source?.trim();
|
||||
if (!text) return "";
|
||||
const cached = markdownRenderCache.get(text);
|
||||
if (cached !== undefined) {
|
||||
markdownRenderCache.delete(text);
|
||||
markdownRenderCache.set(text, cached);
|
||||
return cached;
|
||||
}
|
||||
const rendered = marked.parse(text) as string;
|
||||
return sanitizeHtml(rendered, {
|
||||
const html = sanitizeHtml(rendered, {
|
||||
ADD_ATTR: ["target", "rel"],
|
||||
USE_PROFILES: { html: true }
|
||||
});
|
||||
rememberMarkdownRender(text, html);
|
||||
return html;
|
||||
}
|
||||
|
||||
function rememberMarkdownRender(source: string, html: string): void {
|
||||
markdownRenderCache.set(source, html);
|
||||
while (markdownRenderCache.size > MARKDOWN_RENDER_CACHE_LIMIT) {
|
||||
const oldest = markdownRenderCache.keys().next().value;
|
||||
if (typeof oldest !== "string") break;
|
||||
markdownRenderCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
interface HtmlSanitizeConfig {
|
||||
|
||||
@@ -15,13 +15,13 @@ import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMes
|
||||
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||
const TRACE_HYDRATION_PAGE_LIMIT = 100;
|
||||
const TRACE_HYDRATION_MAX_PAGES = 60;
|
||||
const TRACE_HYDRATION_PAGE_LIMIT = 50;
|
||||
const TRACE_HYDRATION_MAX_PAGES = 4;
|
||||
const TRACE_HYDRATION_MAX_ATTEMPTS = 3;
|
||||
const TRACE_HYDRATION_RETRY_DELAY_MS = 700;
|
||||
const TRACE_HYDRATION_AUTO_QUEUE_LIMIT = 12;
|
||||
const TRACE_HYDRATION_BACKGROUND_CONCURRENCY = 2;
|
||||
const TRACE_HYDRATION_BACKGROUND_DELAY_MS = 150;
|
||||
const TRACE_HYDRATION_AUTO_QUEUE_LIMIT = 4;
|
||||
const TRACE_HYDRATION_BACKGROUND_CONCURRENCY = 1;
|
||||
const TRACE_HYDRATION_BACKGROUND_DELAY_MS = 400;
|
||||
const SESSION_LIST_PAGE_LIMIT = 20;
|
||||
const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3;
|
||||
const WORKBENCH_READ_FAILURE_COOLDOWN_MS = 5_000;
|
||||
|
||||
@@ -1391,6 +1391,13 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.trace-disclosure-count {
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.trace-disclosure-body {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
@@ -1535,6 +1542,18 @@
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.trace-window-notice {
|
||||
margin: 0;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.trace-empty {
|
||||
border: 1px solid #d8e1eb;
|
||||
background: white;
|
||||
|
||||
Reference in New Issue
Block a user