import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { CapabilityEvidenceModel, CapabilityEvidenceRunnerRecord, CapabilityEvidenceSectionId, CapabilityEvidenceToolRow, CapabilityIssueThreadSnapshot, CapabilityJsonValue, CapabilityToolDisposition, } from "../../../src/issue-thread/types"; import type { CapabilityDevtoolsSnapshot } from "../../../src/devtools"; import { DevtoolsInspector, type CapabilityDevtoolsTab, type EvalInspectorReport } from "./DevtoolsInspector"; import { Icon } from "./Icons"; import { capabilitySemanticToolDescriptor } from "../../../src/semantic-tools/catalog"; import { CAPABILITY_EVIDENCE_SECTIONS, capabilityDispositionLabel, } from "../../../src/issue-thread/types"; /** * Evidence panel (contract §7): eight accordion sections in fixed order with a * turn selector. Every record is rendered from the snapshot; the panel derives * no verdicts of its own. */ const DISPOSITION_ORDER: CapabilityToolDisposition[] = [ "always_agent_tool", "optional_agent_tool", "control_plane_owned", ]; export interface CapabilityRunnerEventGroup { id: string; event: string | null; records: CapabilityEvidenceRunnerRecord[]; } /** * Stream deltas are useful evidence but terrible default reading material. * Aggregate each sanitized delta category within a turn at its first position; * expanding the group still exposes every individual event in ordinal order. */ export function groupCapabilityRunnerEvents( records: readonly CapabilityEvidenceRunnerRecord[], ): CapabilityRunnerEventGroup[] { const groups: CapabilityRunnerEventGroup[] = []; const deltaGroups = new Map(); for (const record of records) { const event = record.details.find((entry) => entry.label === "Event")?.value ?? null; if (event === null || !event.endsWith("_delta")) { groups.push({ id: record.id, event: null, records: [record] }); continue; } const key = `${record.turnId}:${event}`; const existing = deltaGroups.get(key); if (existing !== undefined) { existing.records.push(record); continue; } const group = { id: `delta:${key}`, event, records: [record] }; deltaGroups.set(key, group); groups.push(group); } return groups; } function RunnerEvent({ record }: { record: CapabilityEvidenceRunnerRecord }) { return (
#{record.ordinal} {record.kind} {record.detail}
{record.details.length === 0 ? ( <>
Detail
No additional sanitized fields were retained.
) : ( record.details.map((entry, index) => (
{entry.label}
{entry.value}
)) )}
); } function RunnerEventGroup({ group }: { group: CapabilityRunnerEventGroup }) { if (group.event === null) return ; return (
{group.event} {group.records.length} streamed update{group.records.length === 1 ? "" : "s"}
{group.records.map((record) => )}
); } export interface EvidencePanelProps { snapshot: CapabilityIssueThreadSnapshot; devtools?: CapabilityDevtoolsSnapshot | null; evalReport?: EvalInspectorReport | null; onForkRevision?: (revision: number) => void; layout: "side" | "overlay" | "segment"; width: number; selectedTurnId: string | "all"; openSections: CapabilityEvidenceSectionId[]; highlightedRecordId: string | null; onSelectTurn: (turnId: string | "all") => void; onToggleSection: (section: CapabilityEvidenceSectionId) => void; onClose: () => void; onJumpToThread: (anchorId: string) => void; onInvokeTool?: (operationId: string, input: CapabilityJsonValue) => Promise; } function sectionCount( evidence: CapabilityEvidenceModel, section: CapabilityEvidenceSectionId, turnId: string | "all", ): string { if (section === "tools") { const records = filterByTurn(evidence.tools, turnId); return String(new Set(records.flatMap((record) => record.rows.map((row) => row.operationId))).size); } if (section === "parity") { const rows = filterByTurn(evidence.parity, turnId); const passing = rows.filter((row) => row.verdict === "pass").length; return `${passing}/${rows.length}`; } const rows = filterByTurn( evidence[section] as ReadonlyArray<{ turnId: string }>, turnId, ); return String(rows.length); } function starterInput(schema: { properties?: Readonly>; required?: readonly string[] } | undefined): Record { const result: Record = {}; for (const name of schema?.required ?? []) { const property = schema?.properties?.[name]; if (property?.default !== undefined) result[name] = property.default; else if (property?.enum?.[0] !== undefined) result[name] = property.enum[0]; else if (name === "idempotencyKey") result[name] = `devtools-${Date.now()}`; else if (property?.type === "integer" || property?.type === "number") result[name] = 0; else if (property?.type === "boolean") result[name] = false; else if (property?.type === "array") result[name] = []; else if (property?.type === "object") result[name] = {}; else result[name] = ""; } return result; } function ToolsBrowser({ records, onInvoke }: { records: CapabilityEvidenceModel["tools"]; onInvoke?: (operationId: string, input: CapabilityJsonValue) => Promise }) { const [collapsed, setCollapsed] = useState([]); const [selected, setSelected] = useState(null); const [inputText, setInputText] = useState("{}"); const [invocationResult, setInvocationResult] = useState(null); const [invocationError, setInvocationError] = useState(null); const [invoking, setInvoking] = useState(false); const rows = useMemo(() => { const unique = new Map(); for (const record of records) { for (const row of record.rows) unique.set(row.operationId, row); } return [...unique.values()]; }, [records]); const descriptor = selected === null ? undefined : capabilitySemanticToolDescriptor(selected.operationId); useEffect(() => { if (selected === null) return; setInputText(JSON.stringify(starterInput(descriptor?.inputSchema), null, 2)); setInvocationResult(null); setInvocationError(null); }, [descriptor, selected]); useEffect(() => { if (selected === null) return; const close = (event: KeyboardEvent) => { if (event.key === "Escape") setSelected(null); }; document.addEventListener("keydown", close); return () => document.removeEventListener("keydown", close); }, [selected]); return ( <>
{DISPOSITION_ORDER.map((disposition) => { const group = rows.filter((row) => row.disposition === disposition); if (group.length === 0) return null; const folded = collapsed.includes(disposition); const title = disposition === "control_plane_owned" ? "Control plane (not exposed to the agent)" : capabilityDispositionLabel(disposition); return (

{!folded ? (
{group.map((row) => ( ))}
) : null}
); })}
{selected !== null ? (
{ if (event.target === event.currentTarget) setSelected(null); }}>
Semantic tool · v{descriptor?.version ?? 1}

{descriptor?.title ?? selected.operationId}

{selected.operationId}

{descriptor?.description ?? selected.description}

Placement
{capabilityDispositionLabel(selected.disposition)}
Required claims
{descriptor?.requiredClaims.join(", ") || selected.grant || "None"}
Task modes
{descriptor?.allowedModes.join(", ") || "—"}
Actor roles
{descriptor?.allowedRoles?.join(", ") || "Any allowed role"}

Input schema

{JSON.stringify(descriptor?.inputSchema ?? {}, null, 2)}
{onInvoke !== undefined ? (

Invoke in conversation

Runs through the real semantic dispatcher and records a turn, evidence, and company-state changes.