setOpen(false)} aria-hidden />}
+ >
+ );
+}
diff --git a/dashboard/components/StatusPill.module.css b/dashboard/components/StatusPill.module.css
new file mode 100644
index 0000000..62415e0
--- /dev/null
+++ b/dashboard/components/StatusPill.module.css
@@ -0,0 +1,39 @@
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-family: var(--mono);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 2px 9px;
+ border-radius: 99px;
+ white-space: nowrap;
+}
+
+/* Five distinct treatments — must not look identical (AC). */
+.pill[data-status="pending-approval"] {
+ color: var(--amber);
+ background: var(--amber-dim);
+}
+
+.pill[data-status="approved"] {
+ color: var(--cyan);
+ background: #173a45;
+}
+
+.pill[data-status="verified"] {
+ color: var(--green);
+ background: var(--green-dim);
+}
+
+.pill[data-status="rejected"] {
+ color: var(--red);
+ background: var(--red-dim);
+}
+
+.pill[data-status="verification-failed"] {
+ color: var(--red);
+ background: var(--red-dim);
+ border: 1px dashed var(--red);
+}
diff --git a/dashboard/components/StatusPill.tsx b/dashboard/components/StatusPill.tsx
new file mode 100644
index 0000000..8c4c58d
--- /dev/null
+++ b/dashboard/components/StatusPill.tsx
@@ -0,0 +1,15 @@
+import type { DisplayStatus } from "@/lib/evidence";
+import styles from "./StatusPill.module.css";
+
+/**
+ * Consistent status pill across every page. The five states (pending-approval,
+ * approved, verified, rejected, verification-failed) are visually distinct —
+ * an acceptance criterion: they must not look identical.
+ */
+export function StatusPill({ status, label }: { status: DisplayStatus; label: string }) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/dashboard/components/TracePanel.module.css b/dashboard/components/TracePanel.module.css
index 8293d78..ee1a59a 100644
--- a/dashboard/components/TracePanel.module.css
+++ b/dashboard/components/TracePanel.module.css
@@ -58,6 +58,68 @@
color: var(--green);
}
+.legend {
+ margin: 0;
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.03em;
+ color: var(--text-dim);
+}
+
+.sections {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.sectionHead {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+
+.sectionTitle {
+ font-family: var(--mono);
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--text);
+}
+
+.sectionCaption {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--text-faint);
+}
+
+.roleGroups {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.roleGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.roleName {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.04em;
+ color: var(--amber);
+}
+
.traceList {
list-style: none;
display: flex;
@@ -70,9 +132,9 @@
.traceItem,
.emptyTrace {
display: grid;
- grid-template-columns: auto minmax(220px, 0.8fr) minmax(0, 1.2fr);
+ grid-template-columns: auto minmax(180px, 0.8fr) minmax(0, 1.2fr);
align-items: center;
- gap: 12px;
+ gap: 8px 12px;
min-height: 38px;
padding: 8px 10px;
background: var(--bg-elevated);
@@ -92,11 +154,14 @@
color: var(--green);
}
-.traceState[data-status="drift"],
-.traceState[data-status="failed"] {
+.traceState[data-status="drift"] {
color: var(--amber);
}
+.traceState[data-status="failed"] {
+ color: var(--red);
+}
+
.traceMeta {
display: flex;
align-items: center;
@@ -129,6 +194,8 @@
border: 1px solid var(--border-bright);
border-radius: 4px;
padding: 2px 6px;
+ min-width: 0;
+ overflow-wrap: anywhere;
}
.traceSummary {
@@ -136,6 +203,21 @@
color: var(--text-dim);
font-size: 13px;
line-height: 1.45;
+ overflow-wrap: anywhere;
+}
+
+.ledgerRef {
+ grid-column: 2 / -1;
+ min-width: 0;
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--text-faint);
+ overflow-wrap: anywhere;
+}
+
+.traceItem[data-failed] {
+ border-color: var(--red);
+ background: var(--red-dim);
}
.drift {
@@ -154,11 +236,19 @@
}
.traceItem {
- grid-template-columns: auto 1fr;
+ grid-template-columns: auto minmax(0, 1fr);
align-items: start;
}
+ .traceMeta {
+ grid-column: 2;
+ }
+
.traceSummary {
grid-column: 2;
}
+
+ .ledgerRef {
+ grid-column: 2;
+ }
}
diff --git a/dashboard/components/TracePanel.tsx b/dashboard/components/TracePanel.tsx
index bfb88cb..0aa7109 100644
--- a/dashboard/components/TracePanel.tsx
+++ b/dashboard/components/TracePanel.tsx
@@ -1,7 +1,37 @@
-import { Brain, CheckCircle, Database, Fingerprint, Warning } from "@phosphor-icons/react/dist/ssr";
+import {
+ Brain,
+ CheckCircle,
+ Database,
+ Fingerprint,
+ Warning,
+ XCircle,
+} from "@phosphor-icons/react/dist/ssr";
import type { AgentTraceEvent } from "@/lib/evidence";
import styles from "./TracePanel.module.css";
+const LEGEND = "Production model: 3 read-only agent roles / 4 read-only diagnosis tools.";
+
+// The 4 read-only diagnosis tools, in diagnosis-flow order (matches the trace
+// event order: explain → compare → diagnose → rationalize). Do NOT rename.
+const DIAGNOSIS_TOOLS = [
+ "explain_slow_query",
+ "compare_candidate_indexes",
+ "diagnose_candidate",
+ "rationalize_recommendation",
+] as const;
+
+// The 3 read-only agent roles, keyed by the stage that names them. The Diagnose
+// Agent owns two stages (detect + diagnose) — that is why there are 3 roles but
+// 4 tools.
+const ROLE_BY_STAGE: Partial
> = {
+ detect: "Diagnose Agent",
+ diagnose: "Diagnose Agent",
+ candidate: "Candidate Agent",
+ rationale: "Rationale Agent",
+};
+
+const ROLE_ORDER = ["Diagnose Agent", "Candidate Agent", "Rationale Agent"] as const;
+
function actorLabel(actor: AgentTraceEvent["actor"]): string {
switch (actor) {
case "approval_gate":
@@ -19,6 +49,68 @@ function stageLabel(stage: AgentTraceEvent["stage"]): string {
return stage.charAt(0).toUpperCase() + stage.slice(1);
}
+function StatusIcon({ status }: { status: AgentTraceEvent["status"] }) {
+ if (status === "ok") return ;
+ if (status === "failed") return ;
+ return ;
+}
+
+/**
+ * One trace row. The meta column is rendered per the lens it appears under:
+ * - "tool": lead with the tool name (the Tool Calls lens).
+ * - "plain": stage + actor + any chips (Controller / Human lenses).
+ * The Roles lens renders its own role-grouped rows and never uses this.
+ */
+function TraceRow({ event, variant }: { event: AgentTraceEvent; variant: "tool" | "plain" }) {
+ return (
+
+
+
+
+
+ {variant === "tool" ? (
+ <>
+ {event.tool && {event.tool}}
+ {stageLabel(event.stage)}
+ >
+ ) : (
+ <>
+ {stageLabel(event.stage)}
+ {actorLabel(event.actor)}
+ {event.tool && {event.tool}}
+ >
+ )}
+
+ {event.summary}
+ {event.ledger_ref && {event.ledger_ref}}
+
+ );
+}
+
+function TraceSection({
+ title,
+ caption,
+ children,
+}: {
+ title: string;
+ caption: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+ {caption}
+
+ {children}
+
+ );
+}
+
+function rowKey(event: AgentTraceEvent, index: number): string {
+ return `${index}-${event.stage}-${event.actor}-${event.tool ?? "event"}`;
+}
+
export function TracePanel({
trace,
evidenceHash,
@@ -29,7 +121,29 @@ export function TracePanel({
ledgerPersisted: boolean;
}) {
const agentEvents = trace.filter((event) => event.actor === "agent_engine");
- const hasDrift = trace.some((event) => event.status === "drift" || event.status === "failed");
+ // Drift banner is for overridden drift only. A failed verify surfaces as its
+ // own red row, not as a "drift was overridden" success message.
+ const hasDrift = trace.some((event) => event.status === "drift");
+
+ // §1 Roles — group the agent_engine events under the 3 named roles, so the
+ // section visibly reads as 3 roles (the Diagnose Agent's two events nest).
+ const roleGroups = ROLE_ORDER.map((role) => ({
+ role,
+ events: agentEvents.filter((event) => ROLE_BY_STAGE[event.stage] === role),
+ })).filter((group) => group.events.length > 0);
+
+ // §2 Tool Calls — only events whose tool is one of the exact 4 diagnosis tools.
+ const toolEvents = DIAGNOSIS_TOOLS.flatMap((tool) =>
+ trace.filter((event) => event.tool === tool),
+ );
+
+ // §3 Deterministic Safety / Controller.
+ const controllerEvents = trace.filter((event) => event.actor === "deterministic_controller");
+
+ // §4 Human / Approval Gate.
+ const gateEvents = trace.filter(
+ (event) => event.actor === "human" || event.actor === "approval_gate",
+ );
return (
@@ -53,31 +167,65 @@ export function TracePanel({