feat: add sentinel dashboard trace frame reader (#952)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 11:22:45 +08:00
committed by GitHub
parent 22294fd43b
commit a8e28d80b1
2 changed files with 193 additions and 6 deletions
@@ -69,6 +69,7 @@ const state = {
/** @type {SentinelRunDetail|null} */
runDetail: null,
runViews: null,
selectedTraceChoiceIndex: 0,
lastUpdatedAt: null,
filters: readFiltersFromLocation(),
findingFilters: readFindingFiltersFromLocation(),
@@ -129,7 +130,11 @@ function createDashboardApi() {
findings: (filters) => getJson(`/api/findings?${findingsQuery(filters)}`),
/** @returns {Promise<SentinelRunDetail>} */
runDetail: (runId) => getJson(`/api/runs/${encodeURIComponent(runId)}`),
runViews: (runId, view) => getJson(`/api/runs/${encodeURIComponent(runId)}/views?view=${encodeURIComponent(view)}&maxBytes=6000`),
runViews: (runId, view = null) => {
const query = new URLSearchParams({ maxBytes: "24000" });
if (view) query.set("view", view);
return getJson(`/api/runs/${encodeURIComponent(runId)}/views?${query.toString()}`);
},
};
}
@@ -232,6 +237,7 @@ async function loadDashboard(options) {
state.selectedRunId = null;
state.runDetail = null;
state.runViews = null;
state.selectedTraceChoiceIndex = 0;
}
renderDashboard();
if (state.selectedRunId && state.runDetail === null) await selectRun(state.selectedRunId);
@@ -248,6 +254,7 @@ async function selectRun(runId) {
state.selectedRunId = runId;
state.runDetail = null;
state.runViews = null;
state.selectedTraceChoiceIndex = 0;
syncLocationQuery();
renderRuns();
refs.detailSubtitle.textContent = runId;
@@ -255,7 +262,7 @@ async function selectRun(runId) {
try {
const [detail, views] = await Promise.all([
dashboardApi.runDetail(runId),
loadRunViews(runId, "turn-summary"),
loadRunViews(runId),
]);
state.runDetail = detail;
state.runViews = views;
@@ -265,7 +272,7 @@ async function selectRun(runId) {
}
}
async function loadRunViews(runId, view) {
async function loadRunViews(runId, view = null) {
try {
return await dashboardApi.runViews(runId, view);
} catch (error) {
@@ -469,6 +476,7 @@ function renderDetail() {
detailArtifacts(artifacts),
detailCommands(commands),
detailTurnSummary(turnSummaryView),
detailTraceReader(state.runViews, commands),
detailEvidence(detail, artifacts),
detailBlock("Redaction", [
["values", detail.valuesRedacted === true ? "redacted" : "-"],
@@ -476,6 +484,7 @@ function renderDetail() {
["assistant", detail.redaction?.assistantFinal || "-"],
]),
].join("");
bindDetailControls();
}
function detailBlock(title, rows, className = "") {
@@ -526,16 +535,83 @@ function detailCommands(commands) {
}
function detailTurnSummary(view) {
if (!view) return detailBlock("Turn Summary", [["status", "not indexed"]], "detail-block-wide");
if (view.ok === false) return detailBlock("Turn Summary", [["status", view.error || "unavailable"]], "detail-block-wide");
if (!view) return detailBlock("Turn Summary - Layer 1", [["status", "not indexed"]], "detail-block-wide");
if (view.ok === false) return detailBlock("Turn Summary - Layer 1", [["status", view.error || "unavailable"]], "detail-block-wide");
const text = redactDisplayText(view.renderedText || "");
const note = `${formatNumber(view.renderedTextBytes || text.length)} bytes${view.truncated ? " truncated" : ""}`;
return `<article class="detail-block detail-block-wide"><strong>Turn Summary</strong>
return `<article class="detail-block detail-block-wide"><strong>Turn Summary - Layer 1</strong>
<div class="view-note">${escapeHtml(note)}</div>
<pre class="detail-pre">${escapeHtml(text || "-")}</pre>
</article>`;
}
function detailTraceReader(response, commands) {
const turnView = selectedView(response, "turn-summary");
const traceView = selectedView(response, "trace-frame");
const choices = traceChoices(turnView?.renderedText || "", traceView?.renderedText || "");
const selectedIndex = Math.min(state.selectedTraceChoiceIndex, Math.max(0, choices.length - 1));
const selected = choices[selectedIndex] || { label: "stored trace-frame", meta: "current run", key: "stored" };
const traceText = traceView?.ok === false ? "" : redactDisplayText(traceView?.renderedText || "");
const traceNote = traceView
? `${formatNumber(traceView.renderedTextBytes || traceText.length)} bytes${traceView.truncated ? " truncated" : ""}`
: "trace-frame view not indexed";
return `<article class="detail-block detail-block-wide trace-reader"><strong>Trace Frame - Layer 2</strong>
<div class="trace-reader-grid">
<section class="trace-turn-picker" aria-label="Turn summary trace choices">
<div class="view-note">Turn / trace / sample choices</div>
<div class="trace-choice-list">${choices.map((choice, index) => `<button type="button" class="trace-choice ${index === selectedIndex ? "active" : ""}" data-trace-choice-index="${escapeAttr(String(index))}">
<span>${escapeHtml(choice.label)}</span>
<small>${escapeHtml(choice.meta)}</small>
</button>`).join("")}</div>
</section>
<section class="trace-frame-view" aria-label="Trace frame text view">
<div class="view-note">Selected: ${escapeHtml(selected.label)} · ${escapeHtml(traceNote)}</div>
<pre class="detail-pre trace-frame-pre">${escapeHtml(traceText || (traceView?.ok === false ? traceView.error || "trace-frame unavailable" : "trace-frame view not indexed"))}</pre>
${finalResponseBlock(traceView)}
<div class="trace-source-note mono">source=${escapeHtml(commands.traceFrame || "-")} · analyzer findings do not rewrite this text</div>
</section>
</div>
</article>`;
}
function finalResponseBlock(traceView) {
if (!traceView) {
return `<section class="final-response-block unavailable"><strong>Final Response</strong><div>trace-frame view not indexed</div></section>`;
}
if (traceView.ok === false) {
return `<section class="final-response-block unavailable"><strong>Final Response</strong><div>${escapeHtml(traceView.error || "trace-frame unavailable")}</div></section>`;
}
const block = traceView.finalResponse || {};
const text = block.empty === true ? "(空内容)" : redactDisplayText(block.text || "");
const bytes = Number(block.byteCount || 0);
return `<section class="final-response-block ${block.empty === true ? "empty" : ""}">
<strong>Final Response</strong>
<div class="view-note">${block.empty === true ? "empty" : "available"} · ${formatNumber(bytes)} bytes · values redacted</div>
<pre class="final-response-text">${escapeHtml(text || "(空内容)")}</pre>
</section>`;
}
function traceChoices(turnSummaryText, traceFrameText) {
const sourceLines = String(turnSummaryText || "")
.split("\n")
.map((line) => line.trim())
.filter((line) => /(turn|trace|sample|final response|轮次|用户消息)/iu.test(line))
.slice(0, 12);
const lines = sourceLines.length > 0 ? sourceLines : String(traceFrameText || "")
.split("\n")
.map((line) => line.trim())
.filter((line) => /(trace|sample|total=|final response)/iu.test(line))
.slice(0, 12);
if (lines.length === 0) return [{ label: "stored trace-frame", meta: "current run", key: "stored" }];
return lines.map((line, index) => {
const trace = line.match(/trace(?:Id)?[=: ]+([A-Za-z0-9_.:-]+)/u)?.[1] || line.match(/\btrc_[A-Za-z0-9_.:-]+/u)?.[0] || null;
const sample = line.match(/sample(?:Seq)?[=: ]+([0-9]+)/u)?.[1] || null;
const turn = line.match(/turn[=: #]+([0-9A-Za-z_.:-]+)/iu)?.[1] || null;
const meta = [turn ? `turn ${turn}` : null, trace ? `trace ${trace}` : null, sample ? `sample ${sample}` : null].filter(Boolean).join(" · ") || `line ${index + 1}`;
return { label: shortText(redactDisplayText(line), 120), meta, key: `${trace || "line"}-${sample || index}` };
});
}
function detailEvidence(detail, artifacts) {
const traceability = detail.traceability || {};
return detailBlock("Evidence", [
@@ -554,6 +630,15 @@ function selectedView(response, viewName) {
return views.find((item) => item.view === viewName) || null;
}
function bindDetailControls() {
for (const button of refs.detailContent.querySelectorAll("[data-trace-choice-index]")) {
button.addEventListener("click", () => {
state.selectedTraceChoiceIndex = Number(button.dataset.traceChoiceIndex || 0);
renderDetail();
});
}
}
function renderLoading(show) {
refs.loadingBanner.hidden = !show;
refs.manualRefresh.disabled = state.loading;