Add a run transcript UX fixture lab

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-11 10:47:22 -05:00
parent 87b8e21701
commit 6e4694716b
3 changed files with 575 additions and 0 deletions

View File

@@ -23,6 +23,7 @@ import { Activity } from "./pages/Activity";
import { Inbox } from "./pages/Inbox";
import { CompanySettings } from "./pages/CompanySettings";
import { DesignGuide } from "./pages/DesignGuide";
import { RunTranscriptUxLab } from "./pages/RunTranscriptUxLab";
import { OrgChart } from "./pages/OrgChart";
import { NewAgent } from "./pages/NewAgent";
import { AuthPage } from "./pages/Auth";
@@ -145,6 +146,7 @@ function boardRoutes() {
<Route path="inbox/all" element={<Inbox />} />
<Route path="inbox/new" element={<Navigate to="/inbox/recent" replace />} />
<Route path="design-guide" element={<DesignGuide />} />
<Route path="tests/ux/runs" element={<RunTranscriptUxLab />} />
<Route path="*" element={<NotFoundPage scope="board" />} />
</>
);
@@ -246,6 +248,7 @@ export function App() {
<Route path="projects/:projectId/issues" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/issues/:filter" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/configuration" element={<UnprefixedBoardRedirect />} />
<Route path="tests/ux/runs" element={<UnprefixedBoardRedirect />} />
<Route path=":companyPrefix" element={<Layout />}>
{boardRoutes()}
</Route>

View File

@@ -0,0 +1,226 @@
import type { TranscriptEntry } from "../adapters";
export interface RunTranscriptFixtureMeta {
sourceRunId: string;
fixtureLabel: string;
agentName: string;
agentId: string;
issueIdentifier: string;
issueTitle: string;
startedAt: string;
finishedAt: string | null;
}
export const runTranscriptFixtureMeta: RunTranscriptFixtureMeta = {
sourceRunId: "65a79d5d-5f85-4392-a5cc-8fb48beb9e71",
fixtureLabel: "Sanitized development fixture",
agentName: "CodexCoder",
agentId: "codexcoder-fixture",
issueIdentifier: "PAP-473",
issueTitle: "Humanize run transcripts across run detail and live surfaces",
startedAt: "2026-03-11T15:21:05.948Z",
finishedAt: null,
};
// Sanitized from a real development run. Paths, secrets, env vars, and user-local identifiers
// are replaced with safe placeholders while preserving the interaction shape.
export const runTranscriptFixtureEntries: TranscriptEntry[] = [
{
kind: "stderr",
ts: "2026-03-11T15:21:05.594Z",
text: "[paperclip] Skipping saved session resume for task \"PAP-473\" because wake reason is issue_assigned.",
},
{
kind: "init",
ts: "2026-03-11T15:21:05.948Z",
model: "codex",
sessionId: "sess_fixture_01",
},
{
kind: "system",
ts: "2026-03-11T15:21:07.249Z",
text: "turn started",
},
{
kind: "assistant",
ts: "2026-03-11T15:21:18.851Z",
text: "Im starting with the Paperclip heartbeat procedure and the repo-required docs so I can see what task is actually assigned and then work only on that scope.",
},
{
kind: "tool_call",
ts: "2026-03-11T15:21:19.026Z",
name: "command_execution",
toolUseId: "item_1",
input: {
command: "sed -n '1,220p' doc/GOAL.md",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_call",
ts: "2026-03-11T15:21:19.026Z",
name: "command_execution",
toolUseId: "item_2",
input: {
command: "sed -n '1,320p' doc/SPEC-implementation.md",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:21:19.034Z",
toolUseId: "item_1",
content: "command: sed -n '1,220p' doc/GOAL.md\nstatus: completed\nexit_code: 0\n\n# Paperclip\n\nPaperclip is the backbone of the autonomous economy.",
isError: false,
},
{
kind: "tool_result",
ts: "2026-03-11T15:21:19.035Z",
toolUseId: "item_2",
content: "command: sed -n '1,320p' doc/SPEC-implementation.md\nstatus: completed\nexit_code: 0\n\n# Paperclip V1 Implementation Spec\n\nPaperclip V1 must provide a full control-plane loop for autonomous agents.",
isError: false,
},
{
kind: "assistant",
ts: "2026-03-11T15:22:08.806Z",
text: "Ive got the rendering entry points. Im reading the existing transcript parser and the three UI surfaces together so I can collapse them onto one renderer instead of layering another one-off view.",
},
{
kind: "thinking",
ts: "2026-03-11T15:22:12.044Z",
text: "The current transcript UX duplicates parsing logic in multiple surfaces. A shared presentation layer will make the detail page and live surfaces behave the same way while keeping raw fallback available.",
},
{
kind: "tool_call",
ts: "2026-03-11T15:22:15.401Z",
name: "command_execution",
toolUseId: "item_3",
input: {
command: "rg -n \"LiveRunWidget|ActiveAgentsPanel|buildTranscript|TranscriptEntry|transcript\" ui/src server/src packages",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:22:15.982Z",
toolUseId: "item_3",
content: "command: rg -n ...\nstatus: completed\nexit_code: 0\n\nui/src/components/ActiveAgentsPanel.tsx\nui/src/components/LiveRunWidget.tsx\nui/src/pages/AgentDetail.tsx",
isError: false,
},
{
kind: "system",
ts: "2026-03-11T15:22:21.617Z",
text: "item started: web_search (id=ws_fixture_reasoning_ui)",
},
{
kind: "system",
ts: "2026-03-11T15:22:24.806Z",
text: "item completed: web_search (id=ws_fixture_reasoning_ui)",
},
{
kind: "assistant",
ts: "2026-03-11T15:22:29.214Z",
text: "Ive confirmed the duplication: the run page has the only full transcript renderer, while live surfaces still collapse output into feed strings. Im pulling markdown utilities and adapter shapes now so the new renderer can stay shared.",
},
{
kind: "tool_call",
ts: "2026-03-11T15:23:04.251Z",
name: "command_execution",
toolUseId: "item_4",
input: {
command: "pnpm -r typecheck",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:23:26.903Z",
toolUseId: "item_4",
content: "command: pnpm -r typecheck\nstatus: completed\nexit_code: 0\n\npackages/adapter-utils typecheck: Done\nserver typecheck: Done\nui typecheck: Done\ncli typecheck: Done",
isError: false,
},
{
kind: "assistant",
ts: "2026-03-11T15:23:34.102Z",
text: "Typecheck is clean. Im running the remaining repo checks now so the transcript work is verified under the full workspace build, not just the UI package.",
},
{
kind: "tool_call",
ts: "2026-03-11T15:23:40.511Z",
name: "command_execution",
toolUseId: "item_5",
input: {
command: "pnpm test:run",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:23:56.944Z",
toolUseId: "item_5",
content: "command: pnpm test:run\nstatus: failed\nexit_code: 1\n\nFAIL packages/db/src/runtime-config.test.ts\nFAIL src/__tests__/worktree.test.ts\n\nAll transcript-adapter tests passed after snapshot updates.",
isError: true,
},
{
kind: "thinking",
ts: "2026-03-11T15:24:02.188Z",
text: "The remaining failures are unrelated to transcript rendering, so I should isolate them in the handoff and keep the UI change set focused.",
},
{
kind: "assistant",
ts: "2026-03-11T15:24:08.490Z",
text: "The new parser metadata changed a few adapter test snapshots exactly where expected, and the remaining red tests are outside this change set. Im keeping the transcript patch focused and shipping with that caveat called out.",
},
{
kind: "tool_call",
ts: "2026-03-11T15:24:21.327Z",
name: "command_execution",
toolUseId: "item_6",
input: {
command: "pnpm build",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:24:44.030Z",
toolUseId: "item_6",
content: "command: pnpm build\nstatus: completed\nexit_code: 0\n\nui build: built successfully\ncli build: Done",
isError: false,
},
{
kind: "tool_call",
ts: "2026-03-11T15:24:52.504Z",
name: "command_execution",
toolUseId: "item_7",
input: {
command: "git commit -m \"Humanize run transcripts across run detail and live surfaces\"",
cwd: "/workspace/paperclip",
},
},
{
kind: "tool_result",
ts: "2026-03-11T15:24:53.104Z",
toolUseId: "item_7",
content: "command: git commit ...\nstatus: completed\nexit_code: 0\n\n87b8e21 Humanize run transcripts across run detail and live surfaces",
isError: false,
},
{
kind: "assistant",
ts: "2026-03-11T15:25:00.908Z",
text: "Implemented the transcript UI refactor and switched the run detail page, issue live widget, and dashboard agent card onto the same rendering language.",
},
{
kind: "result",
ts: "2026-03-11T15:25:05.440Z",
text: "Transcript rollout complete with shared nice/raw rendering and compact live variants.",
inputTokens: 11240,
outputTokens: 3460,
cachedTokens: 520,
costUsd: 0.048121,
subtype: "success",
isError: false,
errors: [],
},
];

View File

@@ -0,0 +1,346 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn, formatDateTime } from "../lib/utils";
import { Identity } from "../components/Identity";
import { StatusBadge } from "../components/StatusBadge";
import { RunTranscriptView, type TranscriptDensity, type TranscriptMode } from "../components/transcript/RunTranscriptView";
import { runTranscriptFixtureEntries, runTranscriptFixtureMeta } from "../fixtures/runTranscriptFixtures";
import { ExternalLink, FlaskConical, LayoutPanelLeft, MonitorCog, PanelsTopLeft, RadioTower, ShieldCheck } from "lucide-react";
type SurfaceId = "detail" | "live" | "dashboard";
const surfaceOptions: Array<{
id: SurfaceId;
label: string;
eyebrow: string;
description: string;
icon: typeof LayoutPanelLeft;
}> = [
{
id: "detail",
label: "Run Detail",
eyebrow: "Full transcript",
description: "The long-form run page with the `Nice | Raw` toggle and the most inspectable transcript view.",
icon: MonitorCog,
},
{
id: "live",
label: "Issue Widget",
eyebrow: "Live stream",
description: "The issue-detail live run widget, optimized for following an active run without leaving the task page.",
icon: RadioTower,
},
{
id: "dashboard",
label: "Dashboard Card",
eyebrow: "Dense card",
description: "The active-agents dashboard card, tuned for compact scanning while keeping the same transcript language.",
icon: PanelsTopLeft,
},
];
function previewEntries(surface: SurfaceId) {
if (surface === "dashboard") {
return runTranscriptFixtureEntries.slice(-9);
}
if (surface === "live") {
return runTranscriptFixtureEntries.slice(-14);
}
return runTranscriptFixtureEntries;
}
function RunDetailPreview({
mode,
streaming,
density,
}: {
mode: TranscriptMode;
streaming: boolean;
density: TranscriptDensity;
}) {
return (
<div className="overflow-hidden rounded-[28px] border border-border/70 bg-background/80 shadow-[0_24px_60px_rgba(15,23,42,0.08)]">
<div className="border-b border-border/60 bg-background/90 px-5 py-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="uppercase tracking-[0.18em] text-[10px]">
Run Detail
</Badge>
<StatusBadge status={streaming ? "running" : "succeeded"} />
<span className="text-xs text-muted-foreground">
{formatDateTime(runTranscriptFixtureMeta.startedAt)}
</span>
</div>
<div className="mt-2 text-sm font-medium">
Transcript ({runTranscriptFixtureEntries.length})
</div>
</div>
<div className="max-h-[720px] overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(8,145,178,0.08),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(245,158,11,0.10),transparent_28%)] p-5">
<RunTranscriptView
entries={runTranscriptFixtureEntries}
mode={mode}
density={density}
streaming={streaming}
/>
</div>
</div>
);
}
function LiveWidgetPreview({
streaming,
mode,
density,
}: {
streaming: boolean;
mode: TranscriptMode;
density: TranscriptDensity;
}) {
return (
<div className="overflow-hidden rounded-[28px] border border-cyan-500/25 bg-background/85 shadow-[0_20px_50px_rgba(6,182,212,0.10)]">
<div className="border-b border-border/60 bg-cyan-500/[0.05] px-5 py-4">
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-cyan-700 dark:text-cyan-300">
Live Runs
</div>
<div className="mt-1 text-xs text-muted-foreground">
Compact live transcript stream for the issue detail page.
</div>
</div>
<div className="px-5 py-4">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<Identity name={runTranscriptFixtureMeta.agentName} size="sm" />
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="rounded-full border border-border/70 bg-background/70 px-2 py-1 font-mono">
{runTranscriptFixtureMeta.sourceRunId.slice(0, 8)}
</span>
<StatusBadge status={streaming ? "running" : "succeeded"} />
<span>{formatDateTime(runTranscriptFixtureMeta.startedAt)}</span>
</div>
</div>
<span className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-[11px] text-muted-foreground">
Open run
<ExternalLink className="h-3 w-3" />
</span>
</div>
<div className="max-h-[460px] overflow-y-auto pr-1">
<RunTranscriptView
entries={previewEntries("live")}
mode={mode}
density={density}
limit={density === "compact" ? 10 : 12}
streaming={streaming}
/>
</div>
</div>
</div>
);
}
function DashboardPreview({
streaming,
mode,
density,
}: {
streaming: boolean;
mode: TranscriptMode;
density: TranscriptDensity;
}) {
return (
<div className="max-w-md">
<div className={cn(
"flex min-h-[420px] flex-col overflow-hidden rounded-[28px] border shadow-[0_20px_40px_rgba(15,23,42,0.10)]",
streaming
? "border-cyan-500/25 bg-cyan-500/[0.04]"
: "border-border bg-background/75",
)}>
<div className="border-b border-border/60 px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={cn(
"inline-flex h-2.5 w-2.5 rounded-full",
streaming ? "bg-cyan-500 shadow-[0_0_0_6px_rgba(34,211,238,0.12)]" : "bg-muted-foreground/35",
)} />
<Identity name={runTranscriptFixtureMeta.agentName} size="sm" />
</div>
<div className="mt-2 text-[11px] text-muted-foreground">
{streaming ? "Live now" : "Finished 2m ago"}
</div>
</div>
<span className="rounded-full border border-border/70 bg-background/70 px-2 py-1 text-[10px] text-muted-foreground">
<ExternalLink className="h-2.5 w-2.5" />
</span>
</div>
<div className="mt-3 rounded-2xl border border-border/60 bg-background/60 px-3 py-2 text-xs text-cyan-700 dark:text-cyan-300">
{runTranscriptFixtureMeta.issueIdentifier} - {runTranscriptFixtureMeta.issueTitle}
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
<RunTranscriptView
entries={previewEntries("dashboard")}
mode={mode}
density={density}
limit={density === "compact" ? 6 : 8}
streaming={streaming}
/>
</div>
</div>
</div>
);
}
export function RunTranscriptUxLab() {
const [selectedSurface, setSelectedSurface] = useState<SurfaceId>("detail");
const [detailMode, setDetailMode] = useState<TranscriptMode>("nice");
const [streaming, setStreaming] = useState(true);
const [density, setDensity] = useState<TranscriptDensity>("comfortable");
const selected = surfaceOptions.find((option) => option.id === selectedSurface) ?? surfaceOptions[0];
return (
<div className="space-y-6">
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-[linear-gradient(135deg,rgba(8,145,178,0.08),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent_40%),var(--background)] shadow-[0_28px_70px_rgba(15,23,42,0.10)]">
<div className="grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
<aside className="border-b border-border/60 bg-background/75 p-5 lg:border-b-0 lg:border-r">
<div className="mb-5">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:text-cyan-300">
<FlaskConical className="h-3.5 w-3.5" />
UX Lab
</div>
<h1 className="mt-4 text-2xl font-semibold tracking-tight">Run Transcript Fixtures</h1>
<p className="mt-2 text-sm text-muted-foreground">
Built from a real Paperclip development run, then sanitized so no secrets, local paths, or environment details survive into the fixture.
</p>
</div>
<div className="space-y-2">
{surfaceOptions.map((option) => {
const Icon = option.icon;
return (
<button
key={option.id}
type="button"
onClick={() => setSelectedSurface(option.id)}
className={cn(
"w-full rounded-2xl border px-4 py-3 text-left transition-all",
selectedSurface === option.id
? "border-cyan-500/35 bg-cyan-500/[0.10] shadow-[0_12px_24px_rgba(6,182,212,0.12)]"
: "border-border/70 bg-background/70 hover:border-cyan-500/20 hover:bg-cyan-500/[0.04]",
)}
>
<div className="flex items-start gap-3">
<span className="rounded-xl border border-current/15 p-2 text-cyan-700 dark:text-cyan-300">
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0">
<span className="block text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
{option.eyebrow}
</span>
<span className="mt-1 block text-sm font-medium">{option.label}</span>
<span className="mt-1 block text-xs text-muted-foreground">
{option.description}
</span>
</span>
</div>
</button>
);
})}
</div>
<div className="mt-6 rounded-2xl border border-emerald-500/20 bg-emerald-500/[0.07] p-4 text-xs text-muted-foreground">
<div className="mb-2 flex items-center gap-2 text-emerald-700 dark:text-emerald-300">
<ShieldCheck className="h-4 w-4" />
Sanitization rules
</div>
<ul className="space-y-1.5 leading-5">
<li>No real home-directory segments or workstation names.</li>
<li>No API keys, env var payloads, or secret-bearing commands.</li>
<li>Only safe, representative command/result excerpts remain.</li>
</ul>
</div>
</aside>
<main className="min-w-0 p-5">
<div className="mb-5 flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
{selected.eyebrow}
</div>
<h2 className="mt-1 text-2xl font-semibold">{selected.label}</h2>
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">
{selected.description}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="rounded-full px-3 py-1 text-[10px] uppercase tracking-[0.18em]">
Source run {runTranscriptFixtureMeta.sourceRunId.slice(0, 8)}
</Badge>
<Badge variant="outline" className="rounded-full px-3 py-1 text-[10px] uppercase tracking-[0.18em]">
{runTranscriptFixtureMeta.issueIdentifier}
</Badge>
</div>
</div>
<div className="mb-5 flex flex-wrap items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Controls
</span>
<div className="inline-flex rounded-full border border-border/70 bg-background/80 p-1">
{(["nice", "raw"] as const).map((mode) => (
<button
key={mode}
type="button"
className={cn(
"rounded-full px-3 py-1 text-xs font-medium capitalize transition-colors",
detailMode === mode ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground",
)}
onClick={() => setDetailMode(mode)}
>
{mode}
</button>
))}
</div>
<div className="inline-flex rounded-full border border-border/70 bg-background/80 p-1">
{(["comfortable", "compact"] as const).map((nextDensity) => (
<button
key={nextDensity}
type="button"
className={cn(
"rounded-full px-3 py-1 text-xs font-medium capitalize transition-colors",
density === nextDensity ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground",
)}
onClick={() => setDensity(nextDensity)}
>
{nextDensity}
</button>
))}
</div>
<Button
variant="outline"
size="sm"
className="rounded-full"
onClick={() => setStreaming((value) => !value)}
>
{streaming ? "Show settled state" : "Show streaming state"}
</Button>
</div>
{selectedSurface === "detail" ? (
<div className={cn(density === "compact" && "max-w-5xl")}>
<RunDetailPreview mode={detailMode} streaming={streaming} density={density} />
</div>
) : selectedSurface === "live" ? (
<div className={cn(density === "compact" && "max-w-4xl")}>
<LiveWidgetPreview streaming={streaming} mode={detailMode} density={density} />
</div>
) : (
<DashboardPreview streaming={streaming} mode={detailMode} density={density} />
)}
</main>
</div>
</div>
</div>
);
}