From 32721be3579cf3d138a6611112a4043f7eca46fe Mon Sep 17 00:00:00 2001 From: unidesk-code-queue-runner Date: Sun, 24 May 2026 10:22:24 +0000 Subject: [PATCH] fix: disclose code agent trace preview counts --- scripts/dev-cloud-workbench-smoke.test.mjs | 3 + scripts/src/dev-cloud-workbench-smoke-lib.mjs | 109 ++++++++++++++++-- web/hwlab-cloud-web/app.mjs | 95 +++++++++++++-- web/hwlab-cloud-web/scripts/check.mjs | 13 +++ web/hwlab-cloud-web/styles.css | 45 ++++++++ 5 files changed, 248 insertions(+), 17 deletions(-) diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 82593cbe..990fb13c 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -88,6 +88,9 @@ test("source/default workbench report cannot claim DEV-LIVE and documents the co false ); assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass"); + const traceDisclosure = report.checks.find((check) => check.id === "code-agent-trace-replay-disclosure"); + assert.equal(traceDisclosure?.status, "pass"); + assert.deepEqual(traceDisclosure?.evidence, ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"]); }); test("Code Agent browser failure classifier emits Chinese timeout/provider/browser categories with traceId", () => { diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index e88d08e2..ac92ae17 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -618,6 +618,11 @@ function runStaticSmoke() { evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"] }); + addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose tail preview counts and provide full UI/JSON/download access for long traces.", { + blocker: "observability_blocker", + evidence: ["显示最近 14 / 总计 N", "展开全部", "复制 JSON", "下载 trace", "internal scroll for full trace"] + }); + addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", { blocker: "observability_blocker", evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"] @@ -2618,6 +2623,28 @@ function hasCodeAgentLongTimeoutContract(files) { ); } +function hasCodeAgentTraceReplayDisclosure({ app, styles }) { + const tracePanelBody = functionBody(app, "messageTracePanel"); + const traceToolbarBody = functionBody(app, "messageTraceToolbar"); + return ( + /CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/u.test(app) && + /function\s+messageTraceCountText\s*\(/u.test(app) && + /显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+总计\s+\$\{total\}/u.test(app) && + /function\s+renderTraceEventList\s*\(/u.test(app) && + /tracePreviewEvents\(events\)/u.test(tracePanelBody) && + /messageTraceToolbar\(message,\s*trace,\s*events,\s*list\)/u.test(tracePanelBody) && + /展开全部/u.test(traceToolbarBody) && + /复制 JSON/u.test(traceToolbarBody) && + /下载 trace/u.test(traceToolbarBody) && + /function\s+messageTraceJson\s*\(/u.test(app) && + /function\s+downloadTraceJson\s*\(/u.test(app) && + /\.message-trace-toolbar\s*\{/u.test(styles) && + /\.message-trace-count\s*\{/u.test(styles) && + /\.message-trace-action\s*\{/u.test(styles) && + /\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/u.test(styles) + ); +} + function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus }) { return ( /id=["']code-agent-summary["']/u.test(html) && @@ -4289,6 +4316,23 @@ async function inspectJourneyUi(page) { const runningActions = summarizeActions("status-running"); const failedActions = summarizeActions("status-failed"); const timeoutActions = summarizeActions("status-timeout"); + const tracePanels = [...document.querySelectorAll(".message-card.message-agent .message-trace")]; + const tracePanelMetrics = tracePanels.map((panel) => { + const countText = panel.querySelector(".message-trace-count")?.textContent?.replace(/\s+/gu, " ").trim() ?? ""; + const actions = [...panel.querySelectorAll(".message-trace-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + const events = [...panel.querySelectorAll(".message-trace-events li")].map((item) => item.textContent?.replace(/\s+/gu, " ").trim() ?? ""); + return { + countText, + actions, + eventCount: events.length, + traceMode: panel.querySelector(".message-trace-events")?.dataset.traceMode ?? "", + tailPreviewVisible: /显示最近\s+14\s*\/\s*总计\s+(?:1[5-9]|[2-9]\d+)/u.test(countText), + fullEntryVisible: actions.includes("展开全部"), + copyJsonVisible: actions.includes("复制 JSON"), + downloadTraceVisible: actions.includes("下载 trace"), + cappedTailCount: events.length <= 14 + }; + }); return { title: document.title, workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null, @@ -4331,16 +4375,24 @@ async function inspectJourneyUi(page) { [...document.querySelectorAll(".message-card.status-source .message-meta")] .some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? "")) ), - traceText: [...document.querySelectorAll(".message-card.message-agent .message-trace")] + traceText: tracePanels .map((element) => element.textContent ?? "") .join("\n"), - traceHasTraceId: [...document.querySelectorAll(".message-card.message-agent .message-trace")] + traceHasTraceId: tracePanels .some((element) => /trc_/u.test(element.textContent ?? "")), traceShowsEvents: [...document.querySelectorAll(".message-card.message-agent .message-trace-events li")] .some((element) => /#\d+|session|prompt|tool|assistant|timeout|blocked/u.test(element.textContent ?? "")), + tracePanelMetrics, + traceLongPreviewDisclosureVisible: tracePanelMetrics.some((metric) => metric.tailPreviewVisible), + traceFullViewEntryVisible: tracePanelMetrics.some((metric) => metric.fullEntryVisible), + traceCopyJsonVisible: tracePanelMetrics.some((metric) => metric.copyJsonVisible), + traceDownloadVisible: tracePanelMetrics.some((metric) => metric.downloadTraceVisible), + tracePreviewTailCapped: tracePanelMetrics + .filter((metric) => metric.tailPreviewVisible) + .every((metric) => metric.cappedTailCount && metric.traceMode === "tail"), noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0, attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test( - [...document.querySelectorAll(".message-card.message-agent .message-trace")] + tracePanels .map((element) => element.textContent ?? "") .join("\n") ), @@ -4825,6 +4877,11 @@ async function runLocalAgentFixtureSmoke({ ui.sourceMessageHasNonSensitiveMeta && ui.traceHasTraceId && ui.traceShowsEvents && + ui.traceLongPreviewDisclosureVisible && + ui.tracePreviewTailCapped && + ui.traceFullViewEntryVisible && + ui.traceCopyJsonVisible && + ui.traceDownloadVisible && ui.noMainAttributionNoise && ui.attributionFallbackNotRunnerControl && ui.conversationFactsVisible && @@ -5540,6 +5597,44 @@ async function inspectQuickPromptGeometry(page, viewport) { }, viewport); } +function longSourceFixtureTraceEvents(traceId, count = 18) { + const labels = [ + "source-fixture:session", + "prompt:queued", + "prompt:sent", + "context:loaded", + "session:reused", + "assistant:started", + "tool:skills.inspect", + "tool:skills.inspect:completed", + "tool:workspace.read", + "tool:workspace.read:completed", + "assistant:delta", + "assistant:delta", + "assistant:delta", + "assistant:delta", + "assistant:finalizing", + "message:stored", + "conversation:facts-updated", + "assistant:completed" + ]; + return Array.from({ length: count }, (_, index) => { + const seq = index + 1; + const label = labels[index] ?? `assistant:event:${seq}`; + const toolName = label.startsWith("tool:") ? label.split(":").slice(1).join(":") : undefined; + return { + seq, + traceId, + type: label.split(":")[0] || "event", + stage: label.split(":")[1] || "observed", + status: seq === count ? "completed" : "observed", + label, + ...(toolName ? { toolName } : {}), + elapsedMs: seq * 37 + }; + }); +} + async function startStaticWebServer(options = {}) { const rootDir = path.resolve(options.rootDir ?? webRoot); const authFixtureSessions = new Set(); @@ -6530,6 +6625,7 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {} items: [], blockers: [] }; + const fixtureTraceEvents = longSourceFixtureTraceEvents(traceId); jsonResponse(response, 200, { conversationId, sessionId: conversationId, @@ -6558,11 +6654,8 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {} runnerTrace: { traceId, runnerKind: "openai-responses-fallback", - events: [ - { seq: 1, traceId, type: "session", stage: "fixture", status: "completed", label: "source-fixture:session" }, - { seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" } - ], - lastEvent: { seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" } + events: fixtureTraceEvents, + lastEvent: fixtureTraceEvents.at(-1) }, conversationFacts: { conversationId, diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 23a36ec4..b2c49bd2 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -20,6 +20,7 @@ const DEFAULT_API_TIMEOUT_MS = 4500; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 180000; const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 }); const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 }); +const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; const rpcReadMethods = Object.freeze([ "system.health", "cloud.adapter.describe", @@ -3375,20 +3376,96 @@ function messageTracePanel(message) { item.textContent = `trace=${message.traceId ?? "pending"};等待后端事件。`; list.append(item); } else { - for (const event of events.slice(-14)) { - const item = document.createElement("li"); - item.append( - textSpan(`#${event.seq ?? "?"}`, "message-trace-seq"), - textSpan(event.label ?? `${event.type}:${event.status}`, "message-trace-label"), - textSpan(traceEventMeta(event), "message-trace-meta") - ); - list.append(item); - } + const toolbar = messageTraceToolbar(message, trace, events, list); + list.dataset.traceMode = events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? "tail" : "all"; + renderTraceEventList(list, tracePreviewEvents(events)); + details.append(summary, toolbar, list); + return details; } details.append(summary, list); return details; } +function messageTraceToolbar(message, trace, events, list) { + const toolbar = document.createElement("div"); + toolbar.className = "message-trace-toolbar"; + const count = textSpan(messageTraceCountText(events.length, false), "message-trace-count"); + toolbar.append(count); + if (events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT) { + let expanded = false; + const toggle = traceActionButton("展开全部", () => { + expanded = !expanded; + renderTraceEventList(list, expanded ? events : tracePreviewEvents(events)); + list.classList.toggle("message-trace-events-full", expanded); + list.dataset.traceMode = expanded ? "all" : "tail"; + count.textContent = messageTraceCountText(events.length, expanded); + toggle.textContent = expanded ? "收起为最近 14" : "展开全部"; + }, "在面板内查看完整 trace 时间线"); + toolbar.append(toggle); + } + toolbar.append(traceActionButton("复制 JSON", () => copyTextToClipboard(messageTraceJson(message, trace, events)), "复制完整 trace JSON")); + toolbar.append(traceActionButton("下载 trace", () => downloadTraceJson(message, trace, events), "下载完整 trace JSON 文件")); + return toolbar; +} + +function tracePreviewEvents(events) { + return events.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? events.slice(-CODE_AGENT_TRACE_PREVIEW_LIMIT) : events; +} + +function messageTraceCountText(total, expanded) { + if (total <= CODE_AGENT_TRACE_PREVIEW_LIMIT || expanded) return `显示全部 ${total} / 总计 ${total}`; + return `显示最近 ${CODE_AGENT_TRACE_PREVIEW_LIMIT} / 总计 ${total}`; +} + +function renderTraceEventList(list, events) { + list.replaceChildren(); + for (const event of events) { + const item = document.createElement("li"); + item.append( + textSpan(`#${event.seq ?? "?"}`, "message-trace-seq"), + textSpan(event.label ?? `${event.type}:${event.status}`, "message-trace-label"), + textSpan(traceEventMeta(event), "message-trace-meta") + ); + list.append(item); + } +} + +function traceActionButton(label, onClick, title) { + const button = document.createElement("button"); + button.type = "button"; + button.className = "message-trace-action"; + button.textContent = label; + button.title = title; + button.addEventListener("click", onClick); + return button; +} + +function messageTraceJson(message, trace, events) { + return JSON.stringify({ + traceId: trace?.traceId ?? message.traceId ?? null, + messageId: message.id ?? null, + eventCount: events.length, + lastEvent: trace?.lastEvent ?? events.at(-1) ?? null, + events + }, null, 2); +} + +function downloadTraceJson(message, trace, events) { + const traceId = String(trace?.traceId ?? message.traceId ?? "trace") + .replace(/[^A-Za-z0-9_.-]+/gu, "-") + .replace(/^-|-$/gu, "") + .slice(0, 96) || "trace"; + const blob = new Blob([messageTraceJson(message, trace, events)], { type: "application/json" }); + const href = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = href; + anchor.download = `${traceId}.json`; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + window.setTimeout(() => URL.revokeObjectURL(href), 0); +} + function runnerTraceHeadline(message, trace) { const count = Array.isArray(trace?.events) ? trace.events.length : 0; const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件"; diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 2b33fa60..520b5e62 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -771,6 +771,19 @@ assert.match(app, /codexStdioFeasibility:\s*result\.codexStdioFeasibility/); assert.match(app, /longLivedSessionGate:\s*result\.longLivedSessionGate/); assert.match(app, /conversationFacts:\s*result\.conversationFacts/); assert.match(app, /function messageTracePanel/); +assert.match(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT\s*=\s*14/); +assert.match(app, /function messageTraceToolbar/); +assert.match(app, /function messageTraceCountText/); +assert.match(app, /显示最近\s+\$\{CODE_AGENT_TRACE_PREVIEW_LIMIT\}\s+\/\s+总计\s+\$\{total\}/); +assert.match(app, /展开全部/); +assert.match(app, /复制 JSON/); +assert.match(app, /下载 trace/); +assert.match(app, /function renderTraceEventList/); +assert.match(app, /function downloadTraceJson/); +assert.match(styles, /\.message-trace-toolbar\s*\{/); +assert.match(styles, /\.message-trace-count\s*\{/); +assert.match(styles, /\.message-trace-action\s*\{/); +assert.match(styles, /\.message-trace-events-full\s*\{[\s\S]*?max-height:\s*min\(360px,\s*42vh\)[\s\S]*?overflow:\s*auto/); assert.match(app, /function runnerTraceHeadline/); assert.match(app, /function traceEventMeta/); assert.match(app, /function subscribeRunnerTrace/); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 4c049cf9..6c382fc8 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -1258,6 +1258,44 @@ h3 { list-style-position: inside; } +.message-trace-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin: 7px 0 0; +} + +.message-trace-count { + min-height: 24px; + display: inline-flex; + align-items: center; + padding: 2px 7px; + border: 1px solid var(--line-soft); + background: rgba(20, 24, 23, 0.68); + color: var(--muted); + font-family: var(--mono); + font-size: 10px; +} + +.message-trace-action { + min-height: 24px; + padding: 2px 7px; + border: 1px solid var(--line-strong); + background: var(--surface-2); + color: var(--muted); + cursor: pointer; + font-family: var(--mono); + font-size: 10px; +} + +.message-trace-action:hover, +.message-trace-action:focus-visible { + border-color: var(--accent); + color: var(--text); + outline: 0; +} + .message-trace-events { display: grid; gap: 5px; @@ -1268,6 +1306,13 @@ h3 { font-size: 10px; } +.message-trace-events-full { + max-height: min(360px, 42vh); + overflow: auto; + overscroll-behavior: contain; + padding-right: 4px; +} + .message-trace-events li { min-width: 0; overflow-wrap: anywhere;