Files
pikasTech-HWLAB/web/hwlab-cloud-web/app-trace.ts
T
2026-05-28 21:33:33 +08:00

778 lines
30 KiB
TypeScript

function installWorkbenchTestHooks() {
if (!isLocalWorkbenchTestHost() || !new URLSearchParams(window.location.search).has("hwlab-test-hooks")) return;
window.__hwlabWorkbenchTestHooks = {
seedCompletedAgentMessage,
seedTraceMessage,
appendTraceEvents,
reconcileTraceResultFixture,
latestAgentMessageText,
traceScrollMetrics,
traceDomIdentity,
traceBodyDomIdentity,
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(".message-trace-events[data-trace-ui-key] li:last-child pre");
}
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 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 pre") {
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();
}
function traceDisplayRows(trace, events) {
const rows = [];
const assistantRows = traceAssistantStreamRows(trace);
let noisyRun = [];
let toolOutputRun = [];
const flushNoisyRun = () => {
if (noisyRun.length === 0) return;
const row = traceNoiseSummaryRow(trace, noisyRun);
if (row) rows.push(row);
noisyRun = [];
};
const flushToolOutputRun = () => {
if (toolOutputRun.length === 0) return;
const outputRows = traceToolOutputSummaryRows(trace, toolOutputRun);
rows.push(...outputRows);
toolOutputRun = [];
};
for (const event of events) {
if (isNoisyTraceEvent(event)) {
flushToolOutputRun();
noisyRun.push(event);
continue;
}
if (isToolOutputChunkTraceEvent(event)) {
flushNoisyRun();
toolOutputRun.push(event);
continue;
}
flushNoisyRun();
flushToolOutputRun();
const row = traceDisplayRow(trace, event);
if (!row) continue;
rows.push(row);
}
flushNoisyRun();
flushToolOutputRun();
rows.push(...assistantRows);
return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean);
}
function traceAssistantStreamRows(trace) {
const streams = Array.isArray(trace?.assistantStreams) ? trace.assistantStreams.filter(Boolean) : [];
return streams.map((stream, index) => traceAssistantStreamRow(trace, stream, index)).filter(Boolean);
}
function traceAssistantStreamRow(trace, stream, index = 0) {
const chunkCount = Math.max(0, Number(stream?.chunkCount ?? 0));
if (chunkCount <= 0) return null;
const clock = traceClock(stream.updatedAt ?? stream.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, { createdAt: stream.updatedAt ?? stream.createdAt }));
return {
rowId: `assistant-stream:${stream.itemId ?? stream.messageId ?? index}`,
seq: null,
tone: "source",
header: `${clock} total=${total} stream assistant message x${chunkCount}`,
body: traceAssistantStreamBody(stream, chunkCount)
};
}
function traceAssistantStreamBody(stream, chunkCount) {
const message = cleanTraceText(stream?.text ?? "");
const waitingFor = stream?.waitingFor ? `waiting=${stream.waitingFor}` : null;
return [
`chunks=${chunkCount} assistant message chunks`,
waitingFor,
message ? `message=${message}` : null
].filter(Boolean).join("\n");
}
function traceToolOutputSummaryRows(trace, events) {
const visibleEvents = events.filter(Boolean);
if (visibleEvents.length === 0) return [];
const first = visibleEvents[0];
const combined = visibleEvents.map((event) => rawTraceOutputText(event)).join("");
const parsedItems = parseGatewayJsonRpcOutputs(combined);
if (parsedItems.length > 0) {
return parsedItems.map((parsed, index) => {
const { payload, result, dispatch } = parsed;
const event = visibleEvents[Math.min(index, visibleEvents.length - 1)] ?? first;
const clock = traceClock(event.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, event));
const chunkCount = index === 0 ? visibleEvents.length : 0;
const suffix = chunkCount > 0 ? ` chunks=${chunkCount}` : "";
const status = result.status ?? dispatch.dispatchStatus ?? "unknown";
const exitCode = Number.isInteger(dispatch.exitCode) ? dispatch.exitCode : null;
const ok = status === "succeeded" || status === "completed" || exitCode === 0;
const header = [
`${clock} total=${total}`,
ok ? "ok" : status === "timed_out" ? "timeout" : "fail",
"tool gateway.shell",
`status=${status}`,
result.operationId ? `op=${result.operationId}` : "op=null",
exitCode !== null ? `exit=${exitCode}` : null,
typeof dispatch.durationMs === "number" ? `s=${(dispatch.durationMs / 1000).toFixed(1)}` : null,
suffix.trim() || null
].filter(Boolean).join(" ");
return {
rowId: `gateway-jsonrpc:${event.seq ?? first.seq ?? "unknown"}:${payload.id ?? index}`,
seq: event.seq ?? null,
tone: ok ? "ok" : status === "timed_out" ? "warn" : "blocked",
header,
body: gatewayJsonRpcDisplayBody({ payload, result, dispatch, raw: combined })
};
});
}
const clock = traceClock(first.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, first));
return [{
rowId: `tool-output:${first.seq ?? "unknown"}`,
seq: first.seq ?? null,
tone: "source",
header: `${clock} total=${total} output cmd output chunks=${visibleEvents.length} out=${compactTraceSize(combined)}`,
body: compactTraceTextTail(cleanTraceOutputText(combined), 1600)
}];
}
function traceToolOutputSummaryRow(trace, events) {
return traceToolOutputSummaryRows(trace, events)[0] ?? null;
}
function traceNoiseSummaryRow(trace, events) {
const visibleEvents = events.filter(Boolean);
if (visibleEvents.length === 0) return null;
const last = visibleEvents.at(-1);
const clock = traceClock(last.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, last));
const assistantChunks = visibleEvents.filter((event) => isAssistantChunkTraceEvent(event));
const label = assistantChunks.length === visibleEvents.length
? `assistant message x${visibleEvents.length}`
: `trace noise x${visibleEvents.length}`;
return {
rowId: `trace-noise:${visibleEvents[0]?.seq ?? last.seq ?? "unknown"}:${assistantChunks.length > 0 ? "assistant" : "events"}`,
seq: last.seq ?? null,
tone: "source",
header: `${clock} total=${total} stream ${label}`,
body: traceNoiseSummaryBody(visibleEvents, assistantChunks)
};
}
function traceNoiseSummaryBody(events, assistantChunks) {
if (assistantChunks.length > 0) {
const text = assistantChunks
.map((event) => String(event.chunk ?? ""))
.join("");
const message = cleanTraceText(text);
const waitingFor = events.at(-1)?.waitingFor ? `waiting=${events.at(-1).waitingFor}` : null;
return [
`chunks=${assistantChunks.length} assistant message chunks`,
waitingFor,
message ? `message=${message}` : null
].filter(Boolean).join("\n");
}
const labels = new Map();
for (const event of events) {
const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`);
labels.set(label, (labels.get(label) ?? 0) + 1);
}
return [...labels.entries()]
.slice(0, 6)
.map(([label, count]) => `${readableTraceLabel({ label })} x${count}`)
.join("\n");
}
function compactTraceTextTail(text, maxLength) {
const clean = cleanTraceText(text);
if (clean.length <= maxLength) return clean;
return `...${clean.slice(-maxLength)}`;
}
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 isNoisyTraceEvent(event) {
const label = String(event?.label ?? "");
if (isToolOutputChunkTraceEvent(event)) return false;
if (isAssistantChunkTraceEvent(event)) return true;
if (/token_count|outputDelta:chunk/iu.test(label)) return true;
if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true;
if (event?.label === "request:accepted" && !event.message) return true;
return false;
}
function isAssistantChunkTraceEvent(event) {
const label = String(event?.label ?? "");
return /assistant:chunk/iu.test(label) ||
(event?.type === "assistant_message" && event?.status === "chunk");
}
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}`;
}