Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/run-detail.js
T
RichardandClaude Opus 5 5adf4c1ca6 Prompt History und Verbrauchsauswertung (#660), englische Hauptsprache (#661)
Prompt history: every run records its transcript (task, each model step with
tokens and cost, tool calls and results, engine follow-ups) - bounded in size,
scrubbed of recognisable secrets, system prompts stored once per text.
Retention in days per instance (default 14) and per agent (0 = never, -1 =
forever); an hourly janitor deletes what is past it, "never" also purges.

Usage analysis over run receipts: filter by period, agent, model, event, status,
cost and free text; sort by every column; totals over the whole filter; hourly
or daily series grouped by agent, model or event; markers for budget denials and
model changes; run detail with receipt, transcript, tool calls and approvals.

WebUI: Analysis > Token usage > Runs / Timeline, run detail, Prompt History with
retention settings. New code, comments and texts in English; texts come from
language packs (i18n/en.json, de.json) via modules/i18n.js. CLAUDE.md records
English as the main language.

Version 0.1.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:33:09 +02:00

196 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]);
}