// ClawdDotNet — run detail: prompt, response and every step in between (#660) // // Reached from Token usage › Runs and from Prompt History. Shows the receipt, the recorded // transcript (task, each model step with its tokens and cost, each tool call and result), // the system prompt, and what the run proposed for approval. // // Everything in a transcript is foreign text - model output, web pages, database rows. // It is only ever inserted via textContent. import { api } from "../modules/api.js"; import { erstelleElement, erstelleKarte, erstellePlakette, formatiereDatumZeit, formatiereTokens, formatiereKosten } from "../modules/dom.js"; import { t } from "../modules/i18n.js"; import { statusBadge, sourceLabel, formatDuration } from "./usage-runs.js"; export async function renderRunDetail(runId, backPath) { const container = document.getElementById("ansicht-inhalt"); if (!container) return; const back = erstelleElement("a", { href: `#${backPath}`, klasse: "zurueck-link", text: t("detail.back") }); // Back to the filtered list if we came from one. back.addEventListener("click", (event) => { if (window.history.length > 1) { event.preventDefault(); window.history.back(); } }); const layout = erstelleElement("div", { klasse: "verbrauch-ansicht lauf-detail" }, back, erstelleElement("h1", { text: t("detail.title") }), erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })); container.replaceChildren(layout); let run; try { run = await api(`/runs/${encodeURIComponent(runId)}`); } catch (err) { layout.lastChild.replaceWith(erstelleKarte(t("detail.notFound"), [ erstelleElement("p", { klasse: "gedaempft", text: err.message }) ])); return; } const parts = [buildReceipt(run)]; parts.push(run.transcript ? buildTranscript(run) : buildTranscriptState(run)); if (!run.transcript && run.toolCalls.length > 0) parts.push(buildToolCalls(run.toolCalls)); if (run.approvals.length > 0) parts.push(buildApprovals(run.approvals)); layout.lastChild.replaceWith(...parts); } function buildReceipt(run) { const r = run.receipt; const list = erstelleElement("dl", { klasse: "datenliste" }); const add = (label, value) => list.append( erstelleElement("dt", { text: t(label) }), value instanceof Node ? erstelleElement("dd", {}, value) : erstelleElement("dd", { text: value ?? "—" })); add("detail.runId", run.runId); if (r) { add("runs.col.time", formatiereDatumZeit(r.occurredAt)); add("runs.col.agent", r.agentId); add("runs.col.model", r.model); add("runs.col.event", r.taskTitle ? `${sourceLabel(r.source)} · ${r.taskTitle}` : sourceLabel(r.source)); add("runs.col.status", statusBadge(r.status)); add("runs.col.steps", String(r.stepCount)); add("detail.tokens", t("detail.tokensValue", { prompt: formatiereTokens(r.promptTokens), completion: formatiereTokens(r.completionTokens), cached: formatiereTokens(r.cachedTokens) })); add("runs.col.cost", r.costIsKnown || r.costUsd > 0 ? formatiereKosten(r.costUsd) : "—"); add("runs.col.duration", formatDuration(r.durationMs)); if (r.result) add("detail.result", r.result); } return erstelleKarte(t("detail.receipt"), [list]); } function buildTranscriptState(run) { const text = { disabled: t("detail.transcript.disabled"), unavailable: t("detail.transcript.unavailable"), missing: t("detail.transcript.missing") }[run.transcriptState] ?? t("detail.transcript.missing"); return erstelleKarte(t("detail.transcript"), [erstelleElement("p", { klasse: "gedaempft", text })]); } function pre(text) { return erstelleElement("pre", { klasse: "code-block transkript-text", text: text ?? "" }); } function prettyJson(text) { if (!text) return ""; try { return JSON.stringify(JSON.parse(text), null, 2); } catch { return text; } } function buildTranscript(run) { const transcript = run.transcript; const items = []; if (run.systemPrompt) { const details = erstelleElement("details", { klasse: "transkript-system" }, erstelleElement("summary", { text: t("detail.systemPrompt", { n: run.systemPrompt.length }) }), pre(run.systemPrompt)); items.push(details); } if (transcript.truncated) { items.push(erstelleElement("div", { klasse: "hinweis-box warnung", text: t("detail.transcript.truncated") })); } let step = 0; for (const entry of transcript.entries) { const kind = entry.kind; const block = erstelleElement("div", { klasse: `transkript-eintrag art-${kind}` }); const head = erstelleElement("div", { klasse: "transkript-kopf" }); if (kind === "assistant") { step++; head.append(erstelleElement("strong", { text: t("detail.entry.assistant", { n: step }) })); if (entry.promptTokens !== undefined && entry.promptTokens !== null) { head.append(erstellePlakette(t("detail.entry.usage", { prompt: formatiereTokens(entry.promptTokens), completion: formatiereTokens(entry.completionTokens ?? 0) }), "neutral")); } if (entry.costUsd) head.append(erstellePlakette(formatiereKosten(entry.costUsd), "neutral")); if (entry.finishReason && entry.finishReason !== "stop" && entry.finishReason !== "tool_calls") { head.append(erstellePlakette(entry.finishReason, "warnung")); } } else { head.append(erstelleElement("strong", { text: kind === "tool_result" ? t("detail.entry.tool_result", { tool: entry.toolName ?? "?" }) : t(`detail.entry.${kind}`) })); } head.append(erstelleElement("span", { klasse: "gedaempft klein", text: formatiereDatumZeit(entry.at) })); if (entry.clipped) head.append(erstellePlakette(t("detail.entry.clipped"), "warnung")); block.append(head); if (entry.text) block.append(pre(entry.text)); for (const call of entry.toolCalls ?? []) { block.append(erstelleElement("div", { klasse: "transkript-aufruf" }, erstelleElement("span", { klasse: "klein", text: t("detail.entry.call", { tool: call.name }) }), pre(prettyJson(call.arguments)))); } items.push(block); } const summary = t("detail.transcript.size", { n: transcript.entries.length, chars: formatiereTokens(transcript.sizeChars) }); return erstelleKarte(t("detail.transcript"), [erstelleElement("p", { klasse: "hinweis", text: summary }), ...items]); } function buildToolCalls(calls) { const table = erstelleElement("table", { klasse: "daten-tabelle" }); const head = erstelleElement("tr", {}); for (const label of ["runs.col.time", "detail.tool", "runs.col.status", "detail.arguments"]) { head.append(erstelleElement("th", { text: t(label) })); } table.append(head); for (const call of calls) { table.append(erstelleElement("tr", {}, erstelleElement("td", { text: formatiereDatumZeit(call.at) }), erstelleElement("td", { text: call.tool }), erstelleElement("td", { text: call.status }), erstelleElement("td", {}, pre(prettyJson(call.arguments))))); } return erstelleKarte(t("detail.toolCalls"), [ erstelleElement("p", { klasse: "hinweis", text: t("detail.toolCalls.hint") }), erstelleElement("div", { klasse: "tabelle-scroll" }, table) ]); } function buildApprovals(approvals) { const list = erstelleElement("ul", { klasse: "schlichte-liste" }); for (const a of approvals) { list.append(erstelleElement("li", {}, erstelleElement("strong", { text: `#${a.id} ${a.tool}.${a.action ?? "?"} ` }), erstellePlakette(a.status, a.status === "Executed" ? "erfolg" : a.status === "Pending" ? "warnung" : "neutral"))); } return erstelleKarte(t("detail.approvals"), [list]); }