function installWorkbenchTestHooks() { if (!isLocalWorkbenchTestHost() || !new URLSearchParams(window.location.search).has("hwlab-test-hooks")) return; window.__hwlabWorkbenchTestHooks = { seedCompletedAgentMessage, seedTraceMessage, appendTraceEvents, reconcileTraceResultFixture, latestAgentMessageText, traceScrollMetrics, traceDomIdentity, traceBodyDomIdentity, traceRowTexts, setTraceBodyScrollTop, replaceTraceAssistantStream, setTraceScrollTop, setConversationScrollTop, resetProgrammaticScrollWriteCount }; } function isLocalWorkbenchTestHost() { return ["127.0.0.1", "localhost", "::1", "[::1]"].includes(window.location.hostname); } function seedCompletedAgentMessage(options = {}) { state.chatMessages = [{ id: options.messageId ?? "msg_markdown_contract", role: "agent", title: options.title ?? "Code Agent 回复", text: String(options.text ?? ""), status: options.status ?? "completed", traceId: options.traceId ?? "trc_markdown_contract", conversationId: options.conversationId ?? "cnv_markdown_contract", sessionId: options.sessionId ?? "ses_markdown_contract" }]; renderConversation(); } function seedTraceMessage(options = {}) { const traceId = options.traceId ?? "trc_scroll_contract"; const messageId = options.messageId ?? "msg_scroll_contract"; const count = Math.max(1, Number(options.count ?? 80)); const events = Array.isArray(options.events) ? options.events.map((event, index) => normalizeTestTraceEvent(traceId, event, index + 1)) : Array.from({ length: count }, (_, index) => testTraceEvent(traceId, index + 1)); state.chatMessages = [{ id: messageId, role: "agent", title: "Code Agent 处理中", text: "Trace scroll contract fixture", status: "running", traceId, runnerTrace: { traceId, events, assistantStreams: Array.isArray(options.assistantStreams) ? options.assistantStreams : [], eventCount: Number.isInteger(options.eventCount) ? options.eventCount : events.length, eventsCompacted: options.eventsCompacted === true, eventWindow: options.eventWindow ?? null, fullTraceLoaded: options.fullTraceLoaded === true, lastEvent: options.lastEvent ?? events.at(-1), waitingFor: "turn/completed" } }]; renderConversation(); } function normalizeTestTraceEvent(traceId, event, seq) { return { traceId, seq, createdAt: new Date(1779690000000 + seq * 1000).toISOString(), ...event, traceId: event?.traceId ?? traceId, seq: event?.seq ?? seq, createdAt: event?.createdAt ?? new Date(1779690000000 + seq * 1000).toISOString() }; } function appendTraceEvents(count = 1) { const message = state.chatMessages.find((item) => item.runnerTrace?.traceId); if (!message) return; const trace = message.runnerTrace; const start = Array.isArray(trace.events) ? trace.events.length : 0; const additions = Array.from({ length: Math.max(1, Number(count)) }, (_, index) => testTraceEvent(trace.traceId, start + index + 1)); trace.events = [...trace.events, ...additions]; trace.eventCount = trace.events.length; trace.lastEvent = trace.events.at(-1); patchMessageTracePanel(message); } function replaceTraceAssistantStream(stream = {}) { const message = state.chatMessages.find((item) => item.runnerTrace?.traceId); if (!message) return null; const trace = message.runnerTrace; trace.assistantStreams = [{ itemId: "assistant-message", chunkCount: Math.max(1, Number(stream.chunkCount ?? 1)), createdAt: stream.createdAt ?? new Date(1779699000000).toISOString(), updatedAt: stream.updatedAt ?? new Date(1779699001000 + Math.max(1, Number(stream.chunkCount ?? 1)) * 1000).toISOString(), waitingFor: stream.waitingFor ?? "turn/completed", text: String(stream.text ?? "assistant stream fixture") }]; patchMessageTracePanel(message); return traceBodyDomIdentity(); } async function reconcileTraceResultFixture(result) { const message = state.chatMessages.find((item) => item.runnerTrace?.traceId || item.traceId); if (!message) return null; applyCodeAgentResultToMessage(message.id, { ...(result && typeof result === "object" ? result : {}), traceId: message.traceId, conversationId: message.conversationId ?? "cnv_fixture", sessionId: message.sessionId ?? "ses_fixture", status: "completed", provider: "codex-stdio", model: "gpt-test", backend: "hwlab-cloud-api/codex-app-server-stdio", workspace: "/workspace/hwlab", sandbox: "workspace-write", capabilityLevel: "long-lived-codex-stdio-session", runner: { kind: "codex-app-server-stdio-runner", codexStdio: true, writeCapable: true, durableSession: true, workspace: "/workspace/hwlab", sandbox: "workspace-write", sessionMode: "codex-app-server-stdio-long-lived", implementationType: "repo-owned-codex-app-server-stdio-session", capabilityLevel: "long-lived-codex-stdio-session" }, session: { sessionId: message.sessionId ?? "ses_fixture", status: "idle", idleTimeoutMs: 1800000, lastTraceId: message.traceId, longLivedSession: true, durableSession: true, codexStdio: true, writeCapable: true }, sessionMode: "codex-app-server-stdio-long-lived", implementationType: "repo-owned-codex-app-server-stdio-session", sessionReuse: { reused: true, sessionId: message.sessionId ?? "ses_fixture" }, longLivedSessionGate: { status: "pass", pass: true }, toolCalls: [{ name: "codex-app-server.thread/resume+turn/start", status: "completed" }], skills: [], reply: { content: "fixture reconciled result", ...(result?.reply && typeof result.reply === "object" ? result.reply : {}) }, providerTrace: { protocol: "codex-app-server-jsonrpc-stdio", command: "codex app-server --listen stdio://", terminalStatus: "completed", ...(result?.providerTrace && typeof result.providerTrace === "object" ? result.providerTrace : {}) }, runnerTrace: message.runnerTrace, updatedAt: new Date().toISOString() }, { baseMessage: message, retryInput: message.retryInput, retryOf: message.retryOf }); renderCodeAgentSummary(); renderConversation(); return latestAgentMessageText(); } function latestAgentMessageText() { const message = [...state.chatMessages].reverse().find((item) => item.role === "agent"); return message ? { title: message.title, text: message.text, status: message.status, traceId: message.traceId, providerTrace: message.providerTrace ?? null, sourceKind: message.sourceKind ?? null, capabilityLevel: message.capabilityLevel ?? null, error: message.error ?? null } : null; } function traceRowTexts() { return [...el.conversationList.querySelectorAll(".message-trace-events[data-trace-ui-key] .message-trace-row")] .map((row, index) => ({ index, rowId: row.dataset.traceRowId ?? null, text: row.textContent ?? "" })); } function testTraceEvent(traceId, seq) { return { traceId, label: "trace:scroll-contract", type: "trace", status: "observed", createdAt: new Date(1779690000000 + seq * 1000).toISOString(), message: `trace scroll contract event ${seq}\n${"payload ".repeat(18)}`, waitingFor: "turn/completed" }; } function traceScrollMetrics() { const conversation = el.conversationList; const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); return { conversationTop: conversation.scrollTop, conversationBottomGap: scrollBottomGap(conversation), traceTop: list?.scrollTop ?? null, traceBottomGap: list ? scrollBottomGap(list) : null, traceScrollHeight: list?.scrollHeight ?? null, traceClientHeight: list?.clientHeight ?? null, traceRowCount: list?.querySelectorAll(".message-trace-row").length ?? 0 }; } function traceDomIdentity() { const conversation = el.conversationList; const panel = conversation.querySelector(".message-trace[data-trace-ui-key]"); const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); const firstRow = list?.querySelector(".message-trace-row") ?? null; if (panel && !panel.__hwlabTraceIdentity) panel.__hwlabTraceIdentity = `panel-${Date.now()}-${Math.random()}`; if (list && !list.__hwlabTraceIdentity) list.__hwlabTraceIdentity = `list-${Date.now()}-${Math.random()}`; if (firstRow && !firstRow.__hwlabTraceIdentity) firstRow.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`; return { panel: panel?.__hwlabTraceIdentity ?? null, list: list?.__hwlabTraceIdentity ?? null, firstRow: firstRow?.__hwlabTraceIdentity ?? null, rowCount: list?.querySelectorAll(".message-trace-row").length ?? 0, programmaticScrollWrites: state.traceProgrammaticScrollWrites }; } function traceBodyDomIdentity(selector = ".message-trace-events[data-trace-ui-key] li:last-child .message-trace-body") { const body = el.conversationList.querySelector(selector); const row = body?.closest(".message-trace-row") ?? null; if (body && !body.__hwlabTraceIdentity) body.__hwlabTraceIdentity = `body-${Date.now()}-${Math.random()}`; if (row && !row.__hwlabTraceIdentity) row.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`; return { selector, body: body?.__hwlabTraceIdentity ?? null, row: row?.__hwlabTraceIdentity ?? null, rowId: row?.dataset.traceRowId ?? null, top: body?.scrollTop ?? null, left: body?.scrollLeft ?? null, scrollHeight: body?.scrollHeight ?? null, clientHeight: body?.clientHeight ?? null, text: body?.textContent ?? null }; } function resetProgrammaticScrollWriteCount() { state.traceProgrammaticScrollWrites = 0; return state.traceProgrammaticScrollWrites; } function setTraceScrollTop(top, { user = true } = {}) { const list = el.conversationList.querySelector(".message-trace-events[data-trace-ui-key]"); if (!list) return traceScrollMetrics(); if (user) markTraceScrollIntent(list.dataset.traceUiKey); list.scrollTop = top; rememberTraceScrollPosition(list.dataset.traceUiKey, list); return traceScrollMetrics(); } function setTraceBodyScrollTop(selector, top, { user = true } = {}) { const body = el.conversationList.querySelector(selector); if (!body) return traceBodyDomIdentity(selector); if (user) markTraceBodyScrollIntent(body); body.scrollTop = top; rememberTraceBodyScrollPosition(body); return traceBodyDomIdentity(selector); } function setConversationScrollTop(top, { user = true } = {}) { if (user) markConversationScrollIntent(); el.conversationList.scrollTop = top; captureConversationScrollPosition(); return traceScrollMetrics(); } export function traceDisplayRows(trace, events) { const rows = []; const commandGroups = traceCommandExecutionGroups(events); const requestEvent = tracePrimaryRequestEvent(events); const hasTerminalAssistant = events.some((event) => isTerminalAssistantTraceEvent(event)); const renderedCommands = new Set(); let requestRendered = false; let setupRendered = false; let completionRendered = false; let assistantIndex = 0; for (const event of events) { if (!event) continue; if (isToolOutputChunkTraceEvent(event)) continue; if (isCommandExecutionTraceEvent(event)) { const groupKey = traceCommandGroupKey(event); const group = commandGroups.get(groupKey) ?? { key: groupKey, started: event, completed: event, outputs: [] }; const renderHere = event.label === "item/commandExecution:started" || group.started === event; if (renderHere && !renderedCommands.has(groupKey)) { const row = traceCommandSummaryRow(trace, group); if (row) rows.push(row); renderedCommands.add(groupKey); } continue; } if (isAssistantMessageTraceEvent(event)) { const row = traceAssistantSummaryRow(trace, event, assistantIndex); assistantIndex += 1; if (row) rows.push(row); continue; } if (isRequestTraceEvent(event)) { if (!requestRendered) { const row = traceRequestSummaryRow(trace, event, requestEvent ?? event); if (row) rows.push(row); requestRendered = true; } continue; } if (isSetupTraceEvent(event)) { if (!setupRendered) { const row = traceSetupSummaryRow(trace, events, event); if (row) rows.push(row); setupRendered = true; } continue; } if (isCompletionTraceEvent(event)) { if (!completionRendered && !hasTerminalAssistant) { const row = traceCompletionSummaryRow(trace, event); if (row) rows.push(row); completionRendered = true; } continue; } if (isNoisyTraceEvent(event)) continue; const row = traceDisplayRow(trace, event); if (row) rows.push(row); } return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean); } function compactTraceTextTail(text, maxLength) { const clean = cleanTraceText(text); if (clean.length <= maxLength) return clean; return `...${clean.slice(-maxLength)}`; } function traceCommandExecutionGroups(events) { const groups = new Map(); let activeKey = null; const ensureGroup = (key, event) => { const groupKey = key || traceCommandGroupKey(event); if (!groups.has(groupKey)) groups.set(groupKey, { key: groupKey, started: null, completed: null, outputs: [] }); return groups.get(groupKey); }; for (const event of events) { if (isCommandExecutionTraceEvent(event)) { const key = traceCommandGroupKey(event, activeKey); const group = ensureGroup(key, event); if (event.label === "item/commandExecution:started") { group.started = group.started ?? event; activeKey = key; } else { group.completed = event; group.started = group.started ?? event; if (activeKey === key) activeKey = null; } continue; } if (isToolOutputChunkTraceEvent(event)) { const key = traceCommandGroupKey(event, activeKey); ensureGroup(key, event).outputs.push(event); } } return groups; } function traceCommandGroupKey(event, fallback = null) { return traceNonEmptyString(event?.itemId ?? event?.callId ?? event?.commandId ?? event?.id) || fallback || `cmd:${event?.seq ?? event?.createdAt ?? Math.random()}`; } function traceCommandSummaryRow(trace, group) { const start = group.started ?? group.completed ?? group.outputs?.[0]; const completed = group.completed ?? null; if (!start) return null; const command = displayTraceCommand(completed?.command ?? start.command ?? ""); const clock = traceClock(start.createdAt); const title = traceCommandTitle(command); const ok = !completed ? false : traceStatusToken(completed) === "ok" && (!Number.isInteger(completed.exitCode) || completed.exitCode === 0); return { rowId: `tool-summary:${group.key}`, seq: start.seq ?? null, tone: completed ? (ok ? "ok" : "blocked") : "warn", header: `${clock} 🔧 工具调用:${title}`, body: traceCommandSummaryBody(group, command) }; } function traceCommandSummaryBody(group, command) { const completed = group.completed ?? null; const result = traceCommandResultSummary(group, command); return [ command ? `命令:${command}` : null, completed ? `结果:${result}` : "结果:等待命令完成" ].filter(Boolean).join("\n"); } function traceCommandResultSummary(group, command) { const completed = group.completed ?? {}; const output = traceCommandCombinedOutput(group); const fields = traceOutputFields(output); const success = fields.hostSuccess ?? fields.success ?? commandSuccessFromEvent(completed); const duration = traceCommandDurationSuffix(group); if (fields.content && /read-after-launch-flash/u.test(command)) { return `\n${traceStartupLogSummary(fields.content)}${duration}`; } if (fields.hostJobId) { return [`hostJobId=${fields.hostJobId}`, `success=${success}`].join(", ") + duration; } if (fields.logTail && /Flash Load finished/iu.test(fields.logTail)) { const ticks = fields.content ? traceTickRange(fields.content) : null; const capture = ticks ? ` 捕获的部分串口:tick ${ticks}` : ""; return `success=${success}, Flash Load finished.${capture}${duration}`; } if (fields.logTail) { return `success=${success}, logTail: "${traceLogTailSummary(fields.logTail)}"${duration}`; } if (fields.content) { const ticks = traceTickRange(fields.content); const content = ticks ? `捕获的串口:tick ${ticks}` : compactTraceOneLine(fields.content, 180); return `success=${success}, ${content}${duration}`; } const compact = compactTraceOneLine(output || completed.errorMessage || completed.message || "", 220); return `${success !== null ? `success=${success}` : traceStatusToken(completed)}${compact ? `, ${compact}` : ""}${duration}`; } function traceCommandCombinedOutput(group) { return [ ...(Array.isArray(group.outputs) ? group.outputs.map((event) => rawTraceOutputText(event)) : []), group.completed?.stdoutSummary, group.completed?.outputSummary, group.completed?.stderrSummary ].filter(Boolean).join("\n"); } function commandSuccessFromEvent(event) { if (!event) return null; if (Number.isInteger(event.exitCode)) return event.exitCode === 0; const status = String(event.status ?? ""); if (["completed", "succeeded", "success"].includes(status)) return true; if (["failed", "error", "timeout", "canceled"].includes(status)) return false; return null; } function traceCommandDurationSuffix(group) { const completed = group.completed ?? null; if (!completed) return ""; let seconds = null; if (typeof completed.durationMs === "number") { seconds = completed.durationMs / 1000; } else if (group.started?.createdAt && completed.createdAt) { const start = Date.parse(group.started.createdAt); const end = Date.parse(completed.createdAt); if (Number.isFinite(start) && Number.isFinite(end) && end >= start) seconds = (end - start) / 1000; } return seconds === null ? "" : ` (耗时 ${seconds.toFixed(1)}s)`; } function traceCommandTitle(command) { if (/build\s+start/iu.test(command)) return "启动编译"; if (/build\s+status/iu.test(command)) return "等待编译并获取状态"; if (/download\s+start/iu.test(command)) return "启动下载(含串口捕获)"; if (/download\s+status/iu.test(command)) return "等待下载并获取状态"; if (/read-after-launch-flash/iu.test(command)) return "读取完整启动日志"; return "命令执行"; } function displayTraceCommand(command) { let text = cleanTraceOutputText(command); text = text.replace(/^\/?\s*bin\s*\/\s*bash\s+-lc\s+(["'])([\s\S]*)\1$/u, "$2"); text = text.replace(/^bash\s+-lc\s+(["'])([\s\S]*)\1$/u, "$2"); return normalizeTraceCommandSpacing(text); } function normalizeTraceCommandSpacing(text) { return String(text ?? "") .replace(/:\s+\/\s+/gu, ":/") .replace(/\s+\/\s+/gu, "/") .replace(/\s{2,}/gu, " ") .trim(); } function traceOutputFields(text) { return { hostAction: traceOutputStringField(text, "hostAction"), hostJobId: traceOutputStringField(text, "hostJobId"), hostSuccess: traceOutputBooleanField(text, "hostSuccess"), success: traceOutputBooleanField(text, "success"), logTail: traceOutputStringField(text, "logTail"), content: traceOutputStringField(text, "content") }; } function traceOutputStringField(text, name) { const pattern = new RegExp(`"${name}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "gu"); let value = ""; for (const match of String(text ?? "").matchAll(pattern)) { try { const parsed = JSON.parse(`"${match[1]}"`); if (parsed) value = parsed; } catch { if (match[1]) value = match[1]; } } return value; } function traceOutputBooleanField(text, name) { const pattern = new RegExp(`"${name}"\\s*:\\s*(true|false)`, "giu"); let value = null; for (const match of String(text ?? "").matchAll(pattern)) { value = match[1].toLowerCase() === "true"; } return value; } function traceLogTailSummary(value) { const text = cleanSerialText(value); const build = text.match(/([^\n]*\b\d+ Error\(s\), \d+ Warning\(s\)\.[^\n]*)(?:\n([^\n]*Build Time Elapsed:[^\n]*))?/iu); if (build) return compactTraceOneLine(`... ${[build[1], build[2]].filter(Boolean).join(" ")}`, 220); const flash = text.match(/Flash Load finished[^\n]*/iu); if (flash) return flash[0]; return compactTraceOneLine(text, 220); } function traceStartupLogSummary(value) { const lines = cleanSerialText(value) .split("\n") .map((line) => line.trim()) .filter(Boolean); const selected = []; for (const line of lines) { if (/hello device-pod!/iu.test(line)) selected.push("hello device-pod!"); else if (/STM32F103 USART1 baseline ready/iu.test(line)) selected.push("[01_baseline] STM32F103 USART1 baseline ready"); else if (/baud=/iu.test(line)) selected.push(line.replace(/\s+\/\s+/gu, "/")); else if (/project=/iu.test(line)) selected.push(line.replace(/\s+\/\s+/gu, "/")); else if (/dap=.*idcode=/iu.test(line)) selected.push(line); } const ticks = traceTickRange(value); if (ticks) { const [first, last] = ticks.split("-"); selected.push(last ? `tick=${first}, ..., tick=${last}` : `tick=${first}`); } return selected.slice(0, 8).join("\n"); } function traceTickRange(value) { const ticks = [...String(value ?? "").matchAll(/tick=(\d+)/giu)].map((match) => Number(match[1])).filter(Number.isFinite); if (ticks.length === 0) return null; const first = ticks[0]; const last = ticks.at(-1); return first === last ? String(first) : `${first}-${last}`; } function cleanSerialText(value) { return String(value ?? "") .replace(/\u0000/gu, "") .replace(/\r\n|\r/gu, "\n") .replace(/\n{3,}/gu, "\n\n") .trim(); } function compactTraceOneLine(value, limit = 220) { const text = cleanSerialText(value).replace(/\s+/gu, " ").trim(); return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 3))}...`; } function tracePrimaryRequestEvent(events) { return events.find((event) => isRequestTraceEvent(event) && tracePromptText(event)) ?? events.find(isRequestTraceEvent) ?? null; } function traceRequestSummaryRow(trace, event, promptEvent = event) { const clock = traceClock(event.createdAt); const prompt = tracePromptText(promptEvent); return { rowId: `trace-request:${event.seq ?? "accepted"}`, seq: event.seq ?? null, tone: "warn", header: `${clock} 请求接受${prompt ? `,提示词:"${compactTraceOneLine(prompt, 120)}"` : ""}`, body: null }; } function traceSetupSummaryRow(trace, events, event) { const setupEvents = events.filter(isSetupTraceEvent); const sessionReused = setupEvents.some((item) => /session:reused/iu.test(String(item?.label ?? ""))); const promptSent = setupEvents.some((item) => /prompt:sent|sent prompt/iu.test(String(item?.label ?? ""))); const turnStarted = setupEvents.some((item) => /turn[:/]start|turn:started|turn\/start/iu.test(String(item?.label ?? ""))); const parts = [ sessionReused ? "会话复用" : "会话就绪", promptSent ? "提示词已发送" : null, turnStarted ? "轮次开始" : null ].filter(Boolean); return { rowId: "trace-setup:session-turn", seq: event.seq ?? null, tone: "source", header: `${traceClock(event.createdAt)} ${parts.join(",") || "会话准备完成"}`, body: null }; } function traceCompletionSummaryRow(trace, event) { return { rowId: `trace-completion:${event.seq ?? "turn"}`, seq: event.seq ?? null, tone: traceEventTone(event), header: `${traceClock(event.createdAt)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))})`, body: null }; } function traceAssistantSummaryRow(trace, event, index = 0) { const terminal = isTerminalAssistantTraceEvent(event); const text = traceAssistantTextForEvent(trace, event, index); if (!text && !terminal && traceHasLaterTerminalAssistant(trace, event)) return null; const elapsed = formatTraceDuration(traceRelativeMs(trace, event)); return { rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`, seq: event.seq ?? null, tone: traceEventTone(event), header: terminal ? `${traceClock(event.createdAt)} 🤖 助手最后一条消息,轮次完成(总耗时 ${elapsed})` : `${traceClock(event.createdAt)} 🤖 助手消息${index === 0 ? "(第一条)" : ""}`, body: text ? compactTraceAssistantMarkdown(text, terminal ? 5000 : 2200) : null, bodyFormat: "markdown" }; } function traceAssistantTextForEvent(trace, event, index = 0) { const direct = normalizeTraceAssistantMarkdown(event.message ?? event.outputSummary ?? event.chunk ?? event.text ?? ""); if (direct) return traceAssistantTextDuplicatesTerminal(trace, event, direct) ? "" : direct; const streams = traceAssistantStreams(trace); if (streams.length === 0) return ""; const itemId = traceNonEmptyString(event.itemId ?? event.messageId ?? event.id); if (itemId) { const match = streams.find((stream) => stream.itemId === itemId || stream.messageId === itemId || stream.id === itemId); if (match?.text) { const text = normalizeTraceAssistantMarkdown(match.text); return traceAssistantTextDuplicatesTerminal(trace, event, text) ? "" : text; } } if (isTerminalAssistantTraceEvent(event)) return traceAssistantTerminalText(trace, event); return traceAssistantCumulativeSegment(trace, event, index); } function traceAssistantStreams(trace) { return Array.isArray(trace?.assistantStreams) ? trace.assistantStreams.filter((stream) => stream && typeof stream === "object" && normalizeTraceAssistantMarkdown(stream.text ?? "")) : []; } function traceAssistantEvents(trace) { return Array.isArray(trace?.events) ? trace.events.filter(isAssistantMessageTraceEvent) : []; } function traceAssistantTerminalText(trace, event = null) { const streams = traceAssistantStreams(trace); const itemId = traceNonEmptyString(event?.itemId ?? event?.messageId ?? event?.id); if (itemId) { const matched = streams.find((stream) => stream.itemId === itemId || stream.messageId === itemId || stream.id === itemId); if (matched?.text) return normalizeTraceAssistantMarkdown(matched.text); } return normalizeTraceAssistantMarkdown(streams.at(-1)?.text ?? ""); } function traceAssistantTextDuplicatesTerminal(trace, event, text) { if (isTerminalAssistantTraceEvent(event) || !traceHasLaterTerminalAssistant(trace, event)) return false; const terminalText = traceAssistantTerminalText(trace); return Boolean(terminalText && traceAssistantComparableText(text) === traceAssistantComparableText(terminalText)); } function traceHasLaterTerminalAssistant(trace, event) { const events = traceAssistantEvents(trace); const currentIndex = events.findIndex((item) => item === event || (item?.seq !== undefined && item.seq === event?.seq)); if (currentIndex < 0) return events.some(isTerminalAssistantTraceEvent); return events.slice(currentIndex + 1).some(isTerminalAssistantTraceEvent); } function traceAssistantCumulativeSegment(trace, event, index = 0) { const assistantEvents = traceAssistantEvents(trace).filter((item) => !isTerminalAssistantTraceEvent(item)); const eventIndex = assistantEvents.findIndex((item) => item === event || (item?.seq !== undefined && item.seq === event?.seq)); const segmentIndex = eventIndex >= 0 ? eventIndex : index; if (segmentIndex < 0) return ""; const stream = traceAssistantCumulativeStream(trace); if (!stream?.text) return ""; const cumulative = normalizeTraceAssistantMarkdown(stream.text); const terminalText = traceAssistantTerminalText(trace); const nonTerminalText = traceAssistantRemoveTerminalSuffix(cumulative, terminalText); if (!nonTerminalText || traceAssistantComparableText(nonTerminalText) === traceAssistantComparableText(terminalText)) return ""; const segments = traceAssistantSplitCumulativeText(nonTerminalText, assistantEvents.length); return normalizeTraceAssistantMarkdown(segments[segmentIndex] ?? ""); } function traceAssistantCumulativeStream(trace) { const streams = traceAssistantStreams(trace); if (streams.length === 0) return null; const terminalIds = new Set(traceAssistantEvents(trace) .filter(isTerminalAssistantTraceEvent) .flatMap((event) => [event.itemId, event.messageId, event.id].map(traceNonEmptyString).filter(Boolean))); const candidates = streams.filter((stream) => { const streamId = traceNonEmptyString(stream.itemId ?? stream.messageId ?? stream.id); return !streamId || !terminalIds.has(streamId); }); return [...(candidates.length > 0 ? candidates : streams)] .sort((left, right) => String(right.text ?? "").length - String(left.text ?? "").length)[0] ?? null; } function traceAssistantRemoveTerminalSuffix(text, terminalText) { const clean = normalizeTraceAssistantMarkdown(text); const terminal = normalizeTraceAssistantMarkdown(terminalText); if (!clean || !terminal) return clean; if (clean.endsWith(terminal)) return clean.slice(0, -terminal.length).trim(); const index = traceAssistantCompactLastIndex(clean, terminal); if (index > 0) return clean.slice(0, index).trim(); return clean; } function traceAssistantCompactLastIndex(text, needle) { const haystack = traceAssistantComparableWithMap(text); const target = traceAssistantComparableText(needle); if (!target) return -1; const compactIndex = haystack.text.lastIndexOf(target); return compactIndex >= 0 ? haystack.map[compactIndex] ?? -1 : -1; } function traceAssistantComparableWithMap(value) { const map = []; let text = ""; const raw = String(value ?? ""); for (let index = 0; index < raw.length; index += 1) { const char = raw[index]; if (/\s/u.test(char)) continue; text += char.toLowerCase(); map.push(index); } return { text, map }; } function traceAssistantComparableText(value) { return traceAssistantComparableWithMap(normalizeTraceAssistantMarkdown(value)).text; } function traceAssistantSplitCumulativeText(text, count) { const targetCount = Math.max(1, Number(count) || 1); const clean = normalizeTraceAssistantMarkdown(text); if (!clean) return []; if (targetCount === 1) return [clean]; const colonPieces = []; let remaining = clean; while (remaining && colonPieces.length < targetCount) { const match = /^([\s\S]*?[::])(?=\s*[^\s])/u.exec(remaining); if (!match?.[1] || match[1].trim().length < 8) break; colonPieces.push(match[1].trim()); remaining = remaining.slice(match[1].length).trim(); } const pieces = colonPieces.length > 0 ? colonPieces : traceAssistantSentencePieces(clean); if (colonPieces.length > 0 && remaining) pieces.push(remaining); if (pieces.length <= targetCount) return [...pieces, ...Array.from({ length: targetCount - pieces.length }, () => "")]; return traceAssistantPackPieces(pieces, targetCount); } function traceAssistantSentencePieces(text) { return String(text ?? "").match(/[^。!?.!?]+[。!?.!?]?/gu)?.map((item) => item.trim()).filter(Boolean) ?? []; } function traceAssistantPackPieces(pieces, count) { const packed = Array.from({ length: count }, () => ""); pieces.forEach((piece, index) => { const slot = Math.min(count - 1, Math.floor(index * count / pieces.length)); packed[slot] = [packed[slot], piece].filter(Boolean).join(""); }); return packed; } function compactTraceAssistantMarkdown(text, maxLength) { const clean = normalizeTraceAssistantMarkdown(text); if (clean.length <= maxLength) return clean; return `${clean.slice(0, Math.max(0, maxLength - 12)).trimEnd()}\n\n...内容已截断`; } function normalizeTraceAssistantMarkdown(value) { let text = String(value ?? "") .replace(/\u0000/gu, "") .replace(/\r\n|\r/gu, "\n") .replace(/[ \t]{2,}/gu, " ") .replace(/\n{3,}/gu, "\n\n") .trim(); if (!text) return ""; text = restoreTraceAssistantEnglishSpacing(text); text = restoreTraceAssistantMarkdownBreaks(text); return text; } function restoreTraceAssistantEnglishSpacing(value) { return String(value ?? "") .replace(/\bLetmecheckwhat/gu, "Let me check what ") .replace(/cloudAPI/giu, "Cloud API") .replace(/CloudAPI/gu, "Cloud API") .replace(/gatewaysessions/giu, "gateway sessions") .replace(/gatewaysession/giu, "gateway session") .replace(/gatewaysidecar/giu, "gateway sidecar") .replace(/Windows端/gu, "Windows 端") .replace(/gateway服务/giu, "gateway 服务") .replace(/gateway恢复/giu, "gateway 恢复") .replace(/device-podprofilesareavailable/giu, "device-pod profiles are available") .replace(/profilesareavailable/giu, "profiles are available") .replace(/\b([A-Za-z]+)areavailable\b/gu, "$1 are available") .replace(/(\d+)Error\(s\),\s*(\d+)Warning\(s\)/gu, "$1 Error(s), $2 Warning(s)") .replace(/所有(\d+)个/gu, "所有 $1 个") .replace(/(\d+)个device-pod/giu, "$1 个 device-pod") .replace(/个device-pod/giu, "个 device-pod") .replace(/(\*\*\d+)个/gu, "$1 个") .replace(/([\u4e00-\u9fff])`/gu, "$1 `") .replace(/`([^`]+)`([\u4e00-\u9fff])/gu, "`$1` $2") .replace(/([A-Za-z0-9_-])`/gu, "$1 `") .replace(/([\u4e00-\u9fff])\*\*/gu, "$1 **") .replace(/\*\*([\u4e00-\u9fff])/gu, "** $1") .replace(/([a-z])([\u4e00-\u9fff])/gu, "$1 $2") .replace(/([\u4e00-\u9fff])([A-Za-z][A-Za-z0-9_-]*)/gu, "$1 $2"); } function restoreTraceAssistantMarkdownBreaks(value) { return String(value ?? "") .replace(/([::。.!?])\s*(\|[^\n]+\|)/gu, "$1\n\n$2") .replace(/\|\s*\|(?=(?:---|:---|[^|\n]+\s*\|))/gu, "|\n|") .replace(/\s+-\s+(?=(?:\*\*|`|[\u4e00-\u9fffA-Za-z0-9]))/gu, "\n- ") .replace(/([^\n])\s+(想对哪个pod进行操作?)/gu, "$1\n\n$2") .replace(/([^\n])\s+(编译干净通过,无错误无警告。)/gu, "$1\n\n$2"); } function traceDisplayRow(trace, event, options = {}) { if (!options.includeNoise && isNoisyTraceEvent(event)) return null; if (isCommandExecutionTraceEvent(event)) return traceCommandExecutionRow(trace, event); const clock = traceClock(event.createdAt); const total = formatTraceDuration(traceRelativeMs(trace, event)); const status = traceStatusToken(event); const label = readableTraceLabel(event); const meta = [ status, event.errorCode ? `error=${event.errorCode}` : null, typeof event.timeoutMs === "number" ? `timeout=${formatTraceDuration(event.timeoutMs)}` : null, typeof event.idleMs === "number" ? `idle=${formatTraceDuration(event.idleMs)}` : null, event.outputSummary ? `out=${compactTraceSize(event.outputSummary)}` : null ].filter(Boolean).join(" "); const body = traceDisplayBody(event); return { rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`, seq: event.seq ?? null, tone: traceEventTone(event), header: `${clock} total=${total}${meta ? ` ${meta}` : ""}${label ? ` ${label}` : ""}`, body }; } function traceCommandExecutionRow(trace, event) { const clock = traceClock(event.createdAt); const total = formatTraceDuration(traceRelativeMs(trace, event)); const status = traceStatusToken(event); const command = cleanTraceOutputText(event.command ?? ""); const meta = [ status, "tool cmd", command ? `:: ${compactTraceTextTail(command, 220)}` : null, Number.isInteger(event.exitCode) ? `exit=${event.exitCode}` : null, typeof event.durationMs === "number" ? `s=${(event.durationMs / 1000).toFixed(1)}` : null, typeof event.outputBytes === "number" ? `out=${formatTraceBytes(event.outputBytes)}` : null, event.itemId ? `item=${event.itemId}` : null ].filter(Boolean).join(" "); return { rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "command"}:${event.createdAt ?? "unknown"}`}`, seq: event.seq ?? null, tone: traceEventTone(event), header: `${clock} total=${total} ${meta}`, body: traceCommandExecutionBody(event) }; } function traceCommandExecutionBody(event) { const lines = [ event.command ? `command=${compactTraceTextTail(cleanTraceOutputText(event.command), 900)}` : null, event.stdoutSummary ? `stdout:\n${compactTraceTextTail(cleanTraceOutputText(event.stdoutSummary), 1200)}` : null, event.stderrSummary ? `stderr:\n${compactTraceTextTail(cleanTraceOutputText(event.stderrSummary), 800)}` : null, event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null ].filter(Boolean); return lines.join("\n"); } function isCommandExecutionTraceEvent(event) { return event?.type === "tool_call" && event?.toolName === "commandExecution" && (event?.label === "item/commandExecution:started" || event?.label === "item/commandExecution:completed"); } function isAssistantMessageTraceEvent(event) { const label = String(event?.label ?? ""); return event?.type === "assistant_message" || label.startsWith("assistant:"); } function isTerminalAssistantTraceEvent(event) { const label = String(event?.label ?? ""); return event?.terminal === true || label === "assistant:completed"; } function isRequestTraceEvent(event) { const label = String(event?.label ?? ""); return label === "request:accepted-short-connection" || label === "request:accepted" || label === "request:received"; } function isSetupTraceEvent(event) { const label = String(event?.label ?? ""); return /^session:|^stdio:|^prompt:|^thread:|^turn:started|^turn:start|turn\/start|codex turn|tool:codex-app-server/iu.test(label); } function isCompletionTraceEvent(event) { const label = String(event?.label ?? ""); return label.startsWith("turn:completed") || label.startsWith("result:completed"); } function tracePromptText(event) { return cleanTraceText(event?.promptSummary ?? event?.prompt ?? ""); } function traceNonEmptyString(value) { const text = String(value ?? "").trim(); return text ? text : null; } function isNoisyTraceEvent(event) { const label = String(event?.label ?? ""); if (isToolOutputChunkTraceEvent(event)) return false; if (/token_count|outputDelta:chunk/iu.test(label)) return true; if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true; return false; } function isToolOutputChunkTraceEvent(event) { const label = String(event?.label ?? ""); return label === "item/commandExecution/outputDelta" || (event?.type === "tool_call" && event?.status === "output_chunk" && !String(event?.toolName ?? "").startsWith("codex-app-server")); } function readableTraceLabel(event) { const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`); if (label === "request:accepted-short-connection") return "submit short-connection"; if (label === "request:accepted") return "request accepted"; if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start"; if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume"; if (label === "item/commandExecution:started") return "cmd started"; if (label === "item/commandExecution:completed") return "cmd completed"; if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output"; if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token"; if (label.startsWith("turn:completed")) return "turn completed"; if (label.startsWith("result:completed")) return "result ready"; if (label.startsWith("result:failed")) return "result failed"; if (label.startsWith("timeout:")) return "timeout"; if (label.startsWith("cancel:")) return "cancel"; return label .replace(/^tool:/u, "") .replace(/^assistant:/u, "assistant ") .replace(/:completed:completed$/u, " completed") .replace(/:completed$/u, " completed") .replace(/:started$/u, " started"); } function traceDisplayBody(event) { const lines = [ event.promptSummary ? cleanTraceText(event.promptSummary) : null, event.outputSummary ? cleanTraceOutputText(event.outputSummary) : null, event.message ? cleanTraceText(event.message) : null, event.chunk && !isNoisyTraceEvent(event) ? cleanTraceOutputText(event.chunk) : null, event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null ].filter(Boolean); return lines.join("\n").slice(0, 1400); } function rawTraceOutputText(event) { return String(event?.outputSummary ?? event?.chunk ?? event?.message ?? ""); } function parseGatewayJsonRpcOutputs(text) { const payloads = parseJsonObjects(text); return payloads .filter((payload) => payload && payload.jsonrpc === "2.0" && payload.result && typeof payload.result === "object") .map((payload) => { const result = payload.result; const dispatch = result.dispatch && typeof result.dispatch === "object" ? result.dispatch : {}; if (!("gatewaySessionId" in result) && !("capabilityId" in result) && !("shellExecuted" in dispatch)) return null; return { payload, result, dispatch }; }) .filter(Boolean); } function parseGatewayJsonRpcOutput(text) { return parseGatewayJsonRpcOutputs(text)[0] ?? null; } function parseJsonObjects(text) { const source = String(text ?? ""); const values = []; let searchFrom = 0; while (searchFrom < source.length) { const start = source.indexOf("{", searchFrom); if (start < 0) break; const end = jsonObjectEnd(source, start); if (end < 0) break; try { values.push(JSON.parse(source.slice(start, end + 1))); searchFrom = end + 1; } catch { searchFrom = start + 1; } } return values; } function parseFirstJsonObject(text) { const source = String(text ?? ""); const start = source.indexOf("{"); if (start < 0) return null; const end = jsonObjectEnd(source, start); if (end < 0) return null; try { return JSON.parse(source.slice(start, end + 1)); } catch { return null; } } function jsonObjectEnd(source, start) { let depth = 0; let inString = false; let escaped = false; for (let index = start; index < source.length; index += 1) { const char = source[index]; if (inString) { if (escaped) { escaped = false; } else if (char === "\\") { escaped = true; } else if (char === "\"") { inString = false; } continue; } if (char === "\"") { inString = true; continue; } if (char === "{") depth += 1; if (char === "}") depth -= 1; if (depth === 0) { return index; } } return -1; } function gatewayJsonRpcDisplayBody({ payload, result, dispatch, raw }) { const stdout = cleanTraceOutputText(dispatch.stdout); const stderr = cleanTraceOutputText(dispatch.stderr); const error = dispatch.error?.message ?? dispatch.error ?? dispatch.message ?? result.message ?? null; return [ `request=${payload.id ?? "unknown"}`, `gateway=${result.gatewaySessionId ?? "unknown"} resource=${result.resourceId ?? "unknown"} capability=${result.capabilityId ?? "unknown"}`, `dispatch=${dispatch.dispatchStatus ?? result.status ?? "unknown"} shellExecuted=${dispatch.shellExecuted === true ? "true" : "false"} timedOut=${dispatch.timedOut === true ? "true" : "false"}`, error ? `error=${compactTraceTextTail(cleanTraceOutputText(error), 600)}` : null, dispatch.command ? `command=${compactTraceTextTail(cleanTraceOutputText(dispatch.command), 420)}` : null, result.auditId ? `audit=${result.auditId}` : null, result.evidenceId ? `evidence=${result.evidenceId}` : null, dispatch.stdoutTruncated === true ? "stdoutTruncated=true" : null, stdout ? `stdout:\n${compactTraceTextTail(stdout, 1200)}` : null, stderr ? `stderr:\n${compactTraceTextTail(stderr, 800)}` : null, !stdout && !stderr && raw ? compactTraceTextTail(cleanTraceOutputText(raw), 900) : null ].filter(Boolean).join("\n"); } function traceStatusToken(event) { const status = String(event?.status ?? ""); if (status === "completed" || status === "succeeded") return "ok"; if (["failed", "blocked", "error", "timeout", "canceled"].includes(status) || event?.errorCode) return "fail"; if (["started", "running", "accepted"].includes(status)) return "run"; return status || "event"; } function traceEventTone(event) { const status = traceStatusToken(event); if (status === "ok") return "ok"; if (status === "fail") return "blocked"; if (status === "run") return "warn"; return "source"; } function traceClock(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19); } function traceRelativeMs(trace, event) { if (typeof event?.elapsedMs === "number") return event.elapsedMs; const start = Date.parse(trace?.startedAt ?? trace?.createdAt ?? ""); const current = Date.parse(event?.createdAt ?? ""); return Number.isNaN(start) || Number.isNaN(current) ? 0 : Math.max(0, current - start); } function formatTraceDuration(ms) { const total = Math.max(0, Math.floor(Number(ms) || 0)); const seconds = Math.floor(total / 1000); const hh = String(Math.floor(seconds / 3600)).padStart(2, "0"); const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0"); const ss = String(seconds % 60).padStart(2, "0"); return `${hh}:${mm}:${ss}`; } function compactTraceSize(value) { const length = String(value ?? "").length; if (length >= 1000) return `${(length / 1000).toFixed(length >= 10000 ? 0 : 1)}k`; return String(length); } function formatTraceBytes(value) { const bytes = Math.max(0, Number(value) || 0); if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MiB`; if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KiB`; return `${bytes}B`; } function cleanTraceText(value) { const text = String(value ?? "").trim(); if (!text) return ""; return text .replace(/\s*\/\s*/gu, " / ") .replace(/[ \t]{2,}/gu, " ") .replace(/\n{3,}/gu, "\n\n"); } function cleanTraceOutputText(value) { const text = String(value ?? "").trim(); if (!text) return ""; return text .replace(/[ \t]{2,}/gu, " ") .replace(/\n{3,}/gu, "\n\n"); } 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: Number.isInteger(trace?.eventCount) ? trace.eventCount : events.length, eventsCompacted: trace?.eventsCompacted === true, eventWindow: trace?.eventWindow ?? null, lastEvent: trace?.lastEvent ?? events.at(-1) ?? null, assistantStreams: Array.isArray(trace?.assistantStreams) ? trace.assistantStreams : [], 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 = Number.isInteger(trace?.eventCount) ? trace.eventCount : Array.isArray(trace?.events) ? trace.events.length : 0; const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件"; const elapsed = typeof trace?.elapsedMs === "number" ? ` / ${trace.elapsedMs}ms` : ""; const waiting = trace?.waitingFor ? ` / 等待 ${trace.waitingFor}` : ""; const thread = trace?.threadId ?? message.threadId ? ` / thread=${trace?.threadId ?? message.threadId}` : ""; return `运行 trace ${trace?.traceId ?? message.traceId ?? "pending"} / events=${count} / last=${last}${elapsed}${waiting}${thread}`; }