mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
feat(runner): reuse issue thread viewer for evals
This commit is contained in:
1 parent
eac93e3af2
commit
d8abf61570
6 files changed
+88
-7
No files matched your search
@@ -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<CapabilityIssueThreadSnapshot | null>(null);
|
||||
const [devtools, setDevtools] = useState<CapabilityDevtoolsSnapshot | null>(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}
|
||||
|
||||
<IssueHeader
|
||||
snapshot={snapshot}
|
||||
readOnly={embeddedEval !== null}
|
||||
scenarios={SCENARIOS}
|
||||
surface={route.surface}
|
||||
cleanRoomToken={identity?.token ?? null}
|
||||
@@ -825,6 +857,23 @@ export function App() {
|
||||
onSelectSegment={setSegment}
|
||||
/>
|
||||
|
||||
{embeddedEval !== null ? (
|
||||
<section className="pit-eval-summary" data-passed={embeddedEval.passed} data-testid="eval-summary">
|
||||
<div className="pit-eval-summary-head">
|
||||
<a href="../../index.html">← Eval suite</a>
|
||||
<strong>{embeddedEval.passed ? "PASS" : embeddedEval.disposition.replaceAll("_", " ").toUpperCase()}</strong>
|
||||
<code>{embeddedEval.attemptId}</code>
|
||||
</div>
|
||||
<div className="pit-eval-checks">
|
||||
{embeddedEval.checks.map((check) => (
|
||||
<span key={check.id} data-passed={check.passed} title={check.detail}>
|
||||
{check.passed ? "✓" : "✕"} {check.id}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{actionError !== null ? (
|
||||
<p className="pit-banner" data-tone="danger" role="alert" data-testid="action-error">
|
||||
<span aria-hidden="true">⚠</span>
|
||||
|
||||
@@ -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) {
|
||||
<StatusBadge status={snapshot.issue.status} />
|
||||
</div>
|
||||
<div className="pit-header-controls">
|
||||
{!chat ? (
|
||||
{!chat && !readOnly ? (
|
||||
<>
|
||||
<label className="pit-visually-hidden" htmlFor="scenario-picker">Scenario</label>
|
||||
<select className="pit-select pit-desktop-only" id="scenario-picker" value={snapshot.issue.fixtureProfile} onChange={(event) => onSelectScenario(event.target.value)} data-testid="scenario-picker">
|
||||
@@ -76,8 +79,8 @@ export function IssueHeader(props: IssueHeaderProps) {
|
||||
<button type="button" className="pit-button pit-desktop-only" onClick={onReplay} data-testid="replay-button"><Icon name="play" /> Replay</button>
|
||||
</>
|
||||
) : null}
|
||||
<button type="button" className="pit-icon-button pit-desktop-only" data-variant="destructive" onClick={onReset} data-testid="reset-button" title={chat ? "Reset chat" : "Reset scenario"} aria-label={chat ? "Reset chat" : "Reset scenario"}><Icon name="reset" /></button>
|
||||
<button type="button" className="pit-icon-button" data-variant="destructive" onClick={onStop} disabled={!turnActive} data-testid="stop-button" title="Stop turn" aria-label="Stop turn"><Icon name="stop" /></button>
|
||||
{!readOnly ? <button type="button" className="pit-icon-button pit-desktop-only" data-variant="destructive" onClick={onReset} data-testid="reset-button" title={chat ? "Reset chat" : "Reset scenario"} aria-label={chat ? "Reset chat" : "Reset scenario"}><Icon name="reset" /></button> : null}
|
||||
{!readOnly ? <button type="button" className="pit-icon-button" data-variant="destructive" onClick={onStop} disabled={!turnActive} data-testid="stop-button" title="Stop turn" aria-label="Stop turn"><Icon name="stop" /></button> : null}
|
||||
<button type="button" className="pit-button pit-desktop-only" aria-expanded={evidenceOpen} onClick={onToggleEvidence} data-testid="evidence-toggle"><Icon name="evidence" /> DevTools{denialCount > 0 ? ` (${denialCount})` : ""}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1229,6 +1229,31 @@ textarea {
|
||||
border-bottom-color: var(--pit-danger);
|
||||
}
|
||||
|
||||
.pit-eval-summary {
|
||||
border-block: 1px solid color-mix(in oklch, var(--pit-success) 45%, var(--pit-border));
|
||||
background: color-mix(in oklch, var(--pit-success) 10%, var(--pit-surface-sunken));
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.pit-eval-summary[data-passed="false"] {
|
||||
border-color: color-mix(in oklch, var(--pit-danger) 45%, var(--pit-border));
|
||||
background: color-mix(in oklch, var(--pit-danger) 10%, var(--pit-surface-sunken));
|
||||
}
|
||||
|
||||
.pit-eval-summary-head,
|
||||
.pit-eval-checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
}
|
||||
|
||||
.pit-eval-summary-head code { color: var(--pit-muted); }
|
||||
.pit-eval-checks { margin-top: 8px; }
|
||||
.pit-eval-checks span { padding: 3px 8px; border-radius: 999px; background: var(--pit-surface-raised); }
|
||||
.pit-eval-checks span[data-passed="true"] { color: var(--pit-success); }
|
||||
.pit-eval-checks span[data-passed="false"] { color: var(--pit-danger); }
|
||||
|
||||
@keyframes pit-slide {
|
||||
from {
|
||||
transform: translateY(-0.5rem);
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-issue-thread",
|
||||
"protocol",
|
||||
"styles.css",
|
||||
"README.md"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
||||
|
||||
import { projectCapabilityDevtools } from "../devtools/index.js";
|
||||
import { PAPERCLIP_RUNNER_BUILD_METADATA } from "../evals/build-metadata.js";
|
||||
import { projectCapabilityIssueThread } from "../issue-thread/live-projection.js";
|
||||
import {
|
||||
CapabilityLiveSessionService,
|
||||
reconcileCapabilityLiveUsage,
|
||||
@@ -68,6 +69,7 @@ try {
|
||||
turn,
|
||||
snapshot,
|
||||
devtools: projectCapabilityDevtools(snapshot),
|
||||
issueThread: projectCapabilityIssueThread({ snapshot, mode: "replay", replaySource: "live" }),
|
||||
usage,
|
||||
}, null, 2)}\n`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { capabilityIssueThreadServerPlugin } from "./scripts/capability-issue-th
|
||||
const packageRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
root: resolve(packageRoot, "devtools/issue-thread"),
|
||||
plugins: [react(), capabilityIssueThreadServerPlugin()],
|
||||
server: { host: "127.0.0.1" },
|
||||
|
||||
Reference in new issue
Block a user