From d8abf61570b99ab035bd112700e79799ca864a68 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 20 Aug 2026 14:49:58 -0500 Subject: [PATCH] feat(runner): reuse issue thread viewer for evals --- .../devtools/issue-thread/src/App.tsx | 57 +++++++++++++++++-- .../devtools/issue-thread/src/IssueHeader.tsx | 9 ++- .../issue-thread/src/issue-thread.css | 25 ++++++++ packages/paperclip-runner/package.json | 1 + .../paperclip-runner/src/cli/eval-session.ts | 2 + .../vite.issue-thread.config.ts | 1 + 6 files changed, 88 insertions(+), 7 deletions(-) diff --git a/packages/paperclip-runner/devtools/issue-thread/src/App.tsx b/packages/paperclip-runner/devtools/issue-thread/src/App.tsx index 90f5e734101..911708c0b9f 100644 --- a/packages/paperclip-runner/devtools/issue-thread/src/App.tsx +++ b/packages/paperclip-runner/devtools/issue-thread/src/App.tsx @@ -50,6 +50,25 @@ const SETTLED_ANNOUNCEMENT = "The pending request is resolved."; const SCENARIOS = ["hb-baseline", "dp-documents", "ix-interactions", "ar-artifacts"]; const CHAT_HISTORY_KEY = "paperclip-runner.capability.chat.history.v1"; +interface EmbeddedEvalCheck { + id: string; + passed: boolean; + detail: string; +} + +interface EmbeddedEvalReport { + attemptId: string; + caseId: string; + disposition: string; + passed: boolean; + checks: EmbeddedEvalCheck[]; + view: CapabilityIssueThreadSnapshot; +} + +declare global { + interface Window { __PAPERCLIP_EVAL_REPORT__?: EmbeddedEvalReport } +} + interface StoredChatSession { sessionId: string; snapshot: CapabilityIssueThreadSnapshot; @@ -174,15 +193,20 @@ function useLayout(): "side" | "overlay" | "segment" { } export function App() { + const embeddedEval = window.__PAPERCLIP_EVAL_REPORT__ ?? null; const route = useRoute(); const chat = route.surface === "chat"; const layout = useLayout(); useEffect(() => { + if (embeddedEval !== null) { + document.title = `${embeddedEval.passed ? "✓" : "✕"} ${embeddedEval.caseId} · paperclip-runner eval`; + return; + } document.title = chat ? "🫧 Mock Paperclip · Issue thread" : "🧯 Mock Paperclip · Issue thread"; - }, [chat]); + }, [chat, embeddedEval]); const [snapshot, setSnapshot] = useState(null); const [devtools, setDevtools] = useState(null); @@ -244,7 +268,14 @@ export function App() { let cancelled = false; setSettled(false); setError(null); - if (chat) { + if (embeddedEval !== null) { + setIdentity(null); + setSnapshot({ + ...embeddedEval.view, + composer: { state: "disabled", helper: null, reason: "Immutable eval recording", pendingInteractionId: null }, + }); + setSnapshotSurface("issue"); + } else if (chat) { // The clean room has no fixture fallback: if real Codex and real runnerd // cannot start, the surface says so rather than rendering a canned thread. void (async () => { @@ -283,7 +314,7 @@ export function App() { // into a thread that is no longer on screen. abandonTurn(); }; - }, [abandonTurn, chat, route.mode, route.shot, route.fixtureProfile, loadNonce]); + }, [abandonTurn, chat, embeddedEval, route.mode, route.shot, route.fixtureProfile, loadNonce]); /* ------------------------------------------------------- deep-link params */ @@ -794,10 +825,11 @@ export function App() { data-surface={route.surface} data-connection-state={snapshot.connection.state} > - {surfaceNav} + {embeddedEval === null ? surfaceNav : null} + {embeddedEval !== null ? ( +
+
+ ← Eval suite + {embeddedEval.passed ? "PASS" : embeddedEval.disposition.replaceAll("_", " ").toUpperCase()} + {embeddedEval.attemptId} +
+
+ {embeddedEval.checks.map((check) => ( + + {check.passed ? "✓" : "✕"} {check.id} + + ))} +
+
+ ) : null} + {actionError !== null ? (

diff --git a/packages/paperclip-runner/devtools/issue-thread/src/IssueHeader.tsx b/packages/paperclip-runner/devtools/issue-thread/src/IssueHeader.tsx index 493617a012d..6013871557f 100644 --- a/packages/paperclip-runner/devtools/issue-thread/src/IssueHeader.tsx +++ b/packages/paperclip-runner/devtools/issue-thread/src/IssueHeader.tsx @@ -15,6 +15,8 @@ export interface IssueHeaderProps { scenarios: string[]; /** `chat` is the clean room: no preset scenario, no recording to replay. */ surface?: "issue" | "chat"; + /** Immutable eval recordings use the same inspector without live controls. */ + readOnly?: boolean; /** Clean-room tenant token, rendered so identity rotation is visible. */ cleanRoomToken?: string | null; evidenceOpen: boolean; @@ -33,6 +35,7 @@ export function IssueHeader(props: IssueHeaderProps) { snapshot, scenarios, surface = "issue", + readOnly = false, cleanRoomToken = null, evidenceOpen, denialCount, @@ -67,7 +70,7 @@ export function IssueHeader(props: IssueHeaderProps) {

- {!chat ? ( + {!chat && !readOnly ? ( <>