From aa7e7d6a9006054584eea661bcbc4b2cd6e7fc13 Mon Sep 17 00:00:00 2001 From: Josh Purtell Date: Thu, 10 Sep 2026 14:45:43 -0400 Subject: [PATCH] fix(desktop): publish consolidated Reports and trace UX source --- PUBLIC_EXPORT_MANIFEST.json | 20 ++-- .../renderer/src/components/ReportsPage.tsx | 95 +++++++++++++++---- .../src/renderer/src/styles/app.css | 15 +++ .../agent_trace.v1/AgentTraceInspector.tsx | 7 +- .../components/agent_trace.v1/TraceViews.tsx | 14 +-- 5 files changed, 115 insertions(+), 36 deletions(-) diff --git a/PUBLIC_EXPORT_MANIFEST.json b/PUBLIC_EXPORT_MANIFEST.json index 45fd74a6..624d4c57 100644 --- a/PUBLIC_EXPORT_MANIFEST.json +++ b/PUBLIC_EXPORT_MANIFEST.json @@ -4003,10 +4003,10 @@ "sha256": "f44aae70d7a18a2c5b0f90467951f611bc861ba228f32d56cbb1683c5e5facda" }, { - "bytes": 56823, + "bytes": 61226, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx", - "sha256": "b8f4bfe17d008d59227d1cde173f5d472bd8dc186ad64f9c2fa9bb0e4f2a2f48" + "sha256": "6ec108fb175076e170c184fab74764366af5fe728424f90a68a3d74942c97a99" }, { "bytes": 4739, @@ -5011,10 +5011,10 @@ "sha256": "b97bc591fffb8a9d354a39125ed955238190831b3935e2113cb9162c596f99fe" }, { - "bytes": 341534, + "bytes": 341847, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/styles/app.css", - "sha256": "40b9bdf29dfecfae768716dd91eb0522ae1d7a547b319b9085ad6222b4d3fabf" + "sha256": "ff8e3ba8cb60cf7a26f4ce0cced369dac078d6649d86f4c7efeefb2c4b53af98" }, { "bytes": 1855, @@ -5671,10 +5671,10 @@ "sha256": "5ebd8decfd3ddaa2f4704ec9aacdec81fa8877a7595f4c28f37a3d27b74dd23d" }, { - "bytes": 14906, + "bytes": 15197, "mode": "100644", "path": "packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx", - "sha256": "77fc520f81d97b7813db72c12a6ce7f409ed5309902a2653aa817dda27cc2b25" + "sha256": "2c3d852c8cfcc705d9bba203b6da6dc4ec5eb95433f13b86eca9af24b2ebfcc6" }, { "bytes": 3753, @@ -5707,10 +5707,10 @@ "sha256": "c229cfb751271e0bfa77e4819eb51d5ddbf8540032c525b1095fb60c6c8e988d" }, { - "bytes": 19977, + "bytes": 20277, "mode": "100644", "path": "packages/workshop-visuals/components/agent_trace.v1/TraceViews.tsx", - "sha256": "d566793902e5fbe49a6e1aec56554cd1b00f8d4999a15047a602b3972714557d" + "sha256": "6dcf7671faea00baf255843658603cb3947fc99b04cc42eacb0366cb4d9d54fc" }, { "bytes": 2042, @@ -7690,7 +7690,7 @@ "policySha256": "f5ef8fbb4560ee9e401c26c94da6f6189732be76291915027bd7ee3f5f5684dd", "schema": "workshop.public-export-manifest.v1", "source": { - "commit": "b39c7f686cb51522179a3c938d1ab7756026c89c", - "tree": "bb76f53df76aa90e60f8565b52d6e1700497b8f5" + "commit": "7ef1b6fb69770e997e8e62c6d9cacffbd26d06b5", + "tree": "76ea0cabbe5a32ded58158131ae1c1e3e5a6e4b8" } } diff --git a/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx b/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx index 5b17727c..a29051dd 100644 --- a/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx +++ b/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx @@ -1,5 +1,5 @@ // @ts-nocheck — P0-1 generated protocol is stricter than prior handwritten DTOs; UI follow-up is out of specta-cutover file ownership. -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; import { readReportDraft, writeReportDraft, type ReportDraft } from "../runtime/reportDrafts"; import { bridges } from "../runtime/desktopBridge"; import TraceInspector from "@synth/visual-templates/analysis/trace.rollout_inspector.v1/shell"; @@ -190,6 +190,17 @@ function traceEntries(block: ReportBlock | undefined): ReportTraceEntry[] { return []; } +function ReportTracePreview({ entry }: { entry: ReportTraceEntry }) { + const [open, setOpen] = useState(false); + const previewId = useId(); + return
+ +
{open ? : null}
+
; +} + export function ReportsPage({ onBack, initialReportId }: Props) { const [tab, setTab] = useState("all"); const [search, setSearch] = useState(""); @@ -221,6 +232,34 @@ export function ReportsPage({ onBack, initialReportId }: Props) { const [comments, setComments] = useState([]); const [commentBody, setCommentBody] = useState(""); const [traceDigest, setTraceDigest] = useState(""); + const traceAttachInFlight = useRef(false); + const [tracePickerOpen, setTracePickerOpen] = useState(false); + const tracePickerId = useId(); + const [traceCatalog, setTraceCatalog] = useState>([]); + const [traceCatalogStatus, setTraceCatalogStatus] = useState(""); + useEffect(() => { + if (!tracePickerOpen) return; + let cancelled = false; + setTraceCatalog([]); + setTraceCatalogStatus("Loading retained traces…"); + const timer = window.setTimeout(() => { + cancelled = true; + setTraceCatalogStatus("Trace catalog did not respond within 10 seconds. Close and reopen to retry, or paste a known digest."); + }, 10000); + void (async () => { + try { + if (!bridges.inventory) throw new Error("Trace catalog is unavailable in this environment."); + const inventory = await bridges.inventory.listTraces(); + if (cancelled) return; + const rows = [...new Map(inventory.filter(row => typeof row.digest === "string" && row.digest.trim()).map(row => [row.digest, row])).values()]; + setTraceCatalog(rows.slice(0, 100)); + setTraceCatalogStatus(rows.length > 100 ? "Showing the first 100 traces. Paste a digest for another trace." : rows.length ? "Choose a trace, then attach it to this report." : "No retained traces yet. Record or import a trace before attaching evidence."); + } catch (reason) { + if (!cancelled) setTraceCatalogStatus(reportSurfaceError(reason)); + } finally { window.clearTimeout(timer); } + })(); + return () => { cancelled = true; window.clearTimeout(timer); }; + }, [tracePickerOpen]); const [traceLabel, setTraceLabel] = useState(""); const [selectedTraceIndex, setSelectedTraceIndex] = useState(0); const [visibilityRequests, setVisibilityRequests] = useState([]); @@ -245,7 +284,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) { const saveInFlight = useRef(false); async function load(reportId?: string | null, background = false) { - if (background && (loadInFlight.current || saveInFlight.current)) return; + if (background && (loadInFlight.current || saveInFlight.current || traceAttachInFlight.current)) return; const generation = ++loadGeneration.current; loadInFlight.current = true; const bridge = bridges.reports; @@ -372,7 +411,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) { } async function saveDraft() { - if (!selected || !revision || saveInFlight.current) return null; + if (!selected || !revision || saveInFlight.current || traceAttachInFlight.current) return null; saveInFlight.current = true; const blocks: ReportBlock[] = revision.blocks.map((block) => { if (block.anchor === "findings") { @@ -625,10 +664,16 @@ export function ReportsPage({ onBack, initialReportId }: Props) { } async function attachTrace() { - if (!selected || !revision || !traceDigest.trim()) return; + if (!selected || !revision || sealedBundle || dirty || !traceDigest.trim() || traceAttachInFlight.current || saveInFlight.current) return; + traceAttachInFlight.current = true; + const generation = loadGeneration.current; + const isCurrent = () => generation === loadGeneration.current && activeDraft.current.reportId === selected.id; setSaveStatus("saving"); try { + if (!bridges.inventory || !bridges.reports) throw new Error("Trace attachment requires Synth Desktop."); const resolved = await bridges.inventory!.resolveTraceProjection(traceDigest.trim(), "rollout-inspector"); + if (!isCurrent()) return; + if (activeDraft.current.dirty) throw new Error("Save your draft changes before attaching trace evidence."); const existing = revision.blocks.find((block) => block.kind === "report.trace-v5.v1"); const traces = traceEntries(existing); const entry: ReportTraceEntry = { @@ -654,15 +699,20 @@ export function ReportsPage({ onBack, initialReportId }: Props) { const blocks = existing ? revision.blocks.map((block) => (block.kind === "report.trace-v5.v1" ? traceBlock : block)) : [...revision.blocks, traceBlock]; - await bridges.reports!.update(selected.id, { expectedRevision: revision.revision, blocks }); + const saved = await bridges.reports!.update(selected.id, { expectedRevision: revision.revision, blocks }); + const pending = readReportDraft(selected.id); + if (pending) writeReportDraft(selected.id, { ...pending, base: { ...revision, revision: saved.currentRevision, blocks } }); + if (!isCurrent()) return; setTraceDigest(""); setTraceLabel(""); setSelectedTraceIndex(Math.max(nextTraces.length - 1, 0)); await load(selected.id); } catch (reason) { - setSaveStatus("error"); - setError(reportSurfaceError(reason)); - } + if (isCurrent()) { + setSaveStatus("error"); + setError(reportSurfaceError(reason)); + } + } finally { traceAttachInFlight.current = false; } } async function addComment() { @@ -807,9 +857,9 @@ export function ReportsPage({ onBack, initialReportId }: Props) { />

{selected.status} · {readerRevision.schemaVersion} · rev {readerRevision.revision}

- {saveStatus === "saving" ? "Saving" : saveStatus === "error" ? "Error" : `Saved · rev ${readerRevision.revision}`} + {saveStatus === "saving" ? "Saving" : saveStatus === "error" ? "Error" : dirty ? "Unsaved changes" : `Saved · rev ${readerRevision.revision}`}

-

Edits save automatically.

+

Draft edits are retained in this session. Use Save draft to commit them.

@@ -1010,9 +1060,20 @@ export function ReportsPage({ onBack, initialReportId }: Props) {

{readerRevision.blocks.find((block) => block.kind === "report.trace-v5.v1")?.title || "Trace evidence"}

- setTraceDigest(event.target.value)} placeholder="Trace digest (sha256:…)" disabled={Boolean(sealedBundle)} /> - setTraceLabel(event.target.value)} placeholder="Label (OSS-20B · seed 0)" disabled={Boolean(sealedBundle)} /> - + + {tracePickerOpen ?
+

{traceCatalogStatus}

+ +
: null} + setTraceDigest(event.target.value)} aria-label="Retained trace digest" placeholder="Trace digest (sha256:…)" disabled={Boolean(sealedBundle) || saveStatus === "saving"} /> + setTraceLabel(event.target.value)} aria-label="Trace evidence label" placeholder="Describe this trace" disabled={Boolean(sealedBundle) || saveStatus === "saving"} /> +
{(() => { const traces = traceEntries(readerRevision.blocks.find((block) => block.kind === "report.trace-v5.v1")); @@ -1035,7 +1096,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) { ) : null} {selected?.projection ? ( - + ) : (

{MISSING}

)} @@ -1085,7 +1146,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) {

Comments overlay the sealed revision and do not change its digest.

setCommentBody(event.target.value)} placeholder="Add a private review comment" /> - +
    {comments.map((comment) => ( @@ -1113,7 +1174,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) {
    setExperimentTitle(event.target.value)} placeholder="Experiment title or exp_…" disabled={Boolean(sealedBundle)} /> - +
    {appendixView === "ledger" ? ( @@ -1184,7 +1245,7 @@ export function ReportsPage({ onBack, initialReportId }: Props) {
    setLogTitle(event.target.value)} placeholder="Log title" disabled={Boolean(sealedBundle)} /> setLogBody(event.target.value)} placeholder="What happened" disabled={Boolean(sealedBundle)} /> - +
      {(logView === "decisions" ? log.filter((entry) => decisionKinds.has(entry.entryKind)) : log).map((entry) => ( diff --git a/apps/synth_desktop/src/renderer/src/styles/app.css b/apps/synth_desktop/src/renderer/src/styles/app.css index 47a70c40..d4a9745e 100644 --- a/apps/synth_desktop/src/renderer/src/styles/app.css +++ b/apps/synth_desktop/src/renderer/src/styles/app.css @@ -10303,6 +10303,8 @@ html.visual-expanded .sidebar { .reports-field, .reports-section { display: grid; + grid-template-columns: minmax(0, 1fr); + min-width: 0; gap: 8px; margin-top: 16px; } @@ -10364,11 +10366,24 @@ html.visual-expanded .sidebar { } .reports-trace-inspector { display: grid; + grid-template-columns: minmax(0, 1fr); + min-width: 0; gap: 12px; padding: 12px; border: 1px solid var(--color-border); background: var(--color-surface); } +.reports-trace-inspector > *, +.reports-inline-form > * { + min-width: 0; + max-width: 100%; +} +.reports-layout .visuals-card-main { + display: grid; + gap: 4px; + text-align: left; + overflow-wrap: anywhere; +} .reports-lineage { display: grid; gap: 10px; diff --git a/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx b/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx index 46608a5b..2d8e9a49 100644 --- a/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx +++ b/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx @@ -57,7 +57,7 @@ export function AgentTraceInspector({ projection, actorId, onActorChange, cursor const [view, setView] = useVisualState(`${scope}.view`,initialView); const previousFilter=useRef(`${actor}:${section}:${query}`); useEffect(() => { const filter=`${actor}:${section}:${query}`;if(filter!==previousFilter.current){previousFilter.current=filter;setLimit(60);} }, [actor, section, query]); - useEffect(() => { if (selection) { setSection('rollout'); setQuery(''); setPin(''); } }, [selection?.itemId, selection?.revision]); + useEffect(() => { if (selection) { setSection('events'); setQuery(''); changeActor('all'); } }, [selection?.itemId, selection?.revision]); const filtered = useMemo(() => items.filter(item => { const family = eventFamily(item); const inSection = section === 'events' || (section === 'messages' ? family === 'message' : section === 'evidence' ? family === 'reward' || family === 'annotation' : family !== 'annotation'); @@ -92,11 +92,12 @@ export function AgentTraceInspector({ projection, actorId, onActorChange, cursor
      {agents.map(lane => )}
      - + + {actor === 'all' ? Select one agent before comparing. : null} setQuery(e.target.value)}/>

      {items.length} recorded items · {projection.state ?? 'Unknown state'}{cursorMs != null ? ` · replay ${time(cursorMs)}` : ''}. History retained while scrubbing.

      {projection.losses?.length ?
      Capture coverage{projection.losses.map(loss =>

      {loss}

      )}
      : null} -
      {laneContent(actor)}
      {pin &&
      {lanes.find(l => l.actor_id === pin)?.display_name}{laneContent(pin)}
      }
      +
      {laneContent(actor)}
      {pin && actor !== 'all' &&
      {lanes.find(l => l.actor_id === pin)?.display_name}{laneContent(pin)}
      }
      ; } diff --git a/packages/workshop-visuals/components/agent_trace.v1/TraceViews.tsx b/packages/workshop-visuals/components/agent_trace.v1/TraceViews.tsx index 7aca7b5e..b5f93256 100644 --- a/packages/workshop-visuals/components/agent_trace.v1/TraceViews.tsx +++ b/packages/workshop-visuals/components/agent_trace.v1/TraceViews.tsx @@ -116,7 +116,7 @@ export function GeneralTraceView({ projection, items = projection.items ?? [], v const attached = (item: TraceItem) => notes.filter(note => annotationMatches(note, item, projection)); const active = (item: TraceItem) => attached(item).filter(note => !superseded.has(note.id)); const focused = !full && extension?.filterItems ? extension.filterItems(items) : items; - const rows = items.filter(item => full || focused.includes(item) || active(item).length > 0 || item.item_id === selectedId).filter(item => item.kind !== 'evidence.annotation' && + const rows = items.filter(item => full || focused.includes(item) || active(item).length > 0 || item.item_id === selectedId).filter(item => (item.kind !== 'evidence.annotation' || item.item_id === selectedId) && (full || !/^(eval\.|media\.|policy\.finished)/.test(item.kind) || active(item).length > 0) && (!onlyAnnotated || active(item).length > 0)); const base = extension?.base ?? view; const groups = base === 'general' || full ? rows.map(item => ({ id: item.item_id, items: [item] })) : extension?.groupItems?.(rows) ?? decisionGroups(rows); @@ -129,7 +129,7 @@ export function GeneralTraceView({ projection, items = projection.items ?? [], v if (row) setSelectedId(row.item.item_id); }, [cursorMs, projection.trace_id]); useEffect(() => { - if (selection && items.some(item => item.item_id === selection.itemId)) { setSelectedId(selection.itemId); setFollow(true); } + if (selection && items.some(item => item.item_id === selection.itemId)) { setSelectedId(selection.itemId); setFollow(true); setFull(true); setOnlyAnnotated(false); } }, [selection?.itemId, selection?.revision]); useEffect(() => { if (!selectedId || !follow) return; @@ -156,6 +156,8 @@ export function GeneralTraceView({ projection, items = projection.items ?? [], v } function jump(direction: number) { const index = markers.findIndex(item => item.item_id === selectedId); const next = index < 0 ? (direction > 0 ? 0 : markers.length - 1) : index + direction; if (markers[next]) choose(markers[next]); } const Content = base === 'codex' ? CodexContent : base === 'react' ? ReActContent : GeneralContent; + const extensionContext = extension?.renderContext?.(selected); + const showNotes = notes.length > 0 || Boolean(context) || Boolean(extensionContext); return
      @@ -168,7 +170,7 @@ export function GeneralTraceView({ projection, items = projection.items ?? [], v {rows.length} items · {notes.filter(note => !superseded.has(note.id)).length} current annotations
      -
      +
      {!groups.length &&

      No matching trace items.

      } {groups.slice(0, limit).map((group, index) =>
      @@ -185,16 +187,16 @@ export function GeneralTraceView({ projection, items = projection.items ?? [], v
      )} {groups.length > limit && }
      - : null}
      ; }