From ab6b0c8fd14060d0e5f2908030117f500e3b568f Mon Sep 17 00:00:00 2001 From: Josh Purtell Date: Thu, 10 Sep 2026 14:12:50 -0400 Subject: [PATCH 1/2] fix(desktop): consolidate report evidence and trace selection UX --- apps/synth_desktop/polish.md | 23 +++++ .../renderer/src/components/ReportsPage.tsx | 95 +++++++++++++++---- .../tests/report-draft-recovery.test.mjs | 60 +++++++++++- .../synth_desktop/tests/reports_page.test.mjs | 6 +- apps/synth_desktop/tests/trace-ux.test.mjs | 46 +++++++++ .../agent_trace.v1/AgentTraceInspector.tsx | 7 +- .../components/agent_trace.v1/TraceViews.tsx | 14 +-- 7 files changed, 218 insertions(+), 33 deletions(-) create mode 100644 apps/synth_desktop/tests/trace-ux.test.mjs diff --git a/apps/synth_desktop/polish.md b/apps/synth_desktop/polish.md index ca1cba1d9..35891924f 100644 --- a/apps/synth_desktop/polish.md +++ b/apps/synth_desktop/polish.md @@ -393,6 +393,29 @@ Pick from debt flags or CUA; log when done. - **Refs:** `runtime/visualPresentation.ts`, `components/VisualsPage.tsx`, `styles/app.css`, `scripts/test-workshop-mcp.py`, and `docs/engineering/capability-migration-ledger.md`. +## Reports and trace UX consolidation (2026-09-10) + +- **Implemented:** Retained-trace picker with bounded loading, empty/error copy, + duplicate digest filtering and accessible labels; previews mount only when + expanded. Trace attachment is fenced against duplicate clicks, navigation, + concurrent saves and draft edits; edits made during the write retain their + updated revision baseline. Save copy now distinguishes session drafts from + committed revisions. Blank comment/record/log actions are disabled. +- **Trace:** Citation selection reveals annotation events across actor/search + filters. Comparison requires a specific actor. Empty annotation columns are + omitted. Existing useVisualState persistence and research snapshot/generation + safeguards remain intact; the older wholesale rewrites and orphan global + citation bridge are not adopted. Exact reward values remain unchanged. +- **Verification:** Eight model-free tests pass, including actual React browser + interactions, navigation/reload/save races, attachment races, citation reveal + and overflow checks at 960/1280/1440px. Renderer typecheck and production + frontend build pass (existing chunk-size warnings). +- **Still required before visual sign-off:** Native WebView CUA review and the + relevant installed-app acceptance. Browser fixtures are not native proof. + Published app binaries/tags were not changed. No paid/provider tests ran. +- **Refs:** ReportsPage.tsx, AgentTraceInspector.tsx, TraceViews.tsx, + report-draft-recovery.test.mjs, reports_page.test.mjs, trace-ux.test.mjs. + ## Hosted agents and attachable desktop (2026-09-07) - **Changed:** Settings → Context includes hosted ACP task creation, prompt sending, retained history, cancellation, explicit resume and one-time human permission controls. It uses existing Settings components and generated native commands. diff --git a/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx b/apps/synth_desktop/src/renderer/src/components/ReportsPage.tsx index 5b17727c6..a29051ddf 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/tests/report-draft-recovery.test.mjs b/apps/synth_desktop/tests/report-draft-recovery.test.mjs index 96f2871a1..4f28b9e6d 100644 --- a/apps/synth_desktop/tests/report-draft-recovery.test.mjs +++ b/apps/synth_desktop/tests/report-draft-recovery.test.mjs @@ -22,7 +22,14 @@ test("Report drafts survive navigation/reload and asynchronous saves", async () {blockId:'methods',anchor:'methods',kind:'report.prose.v1',title:'Methods',payload:{markdown:''}}], claims:[], limitations:[] }])); - window.fixture = { wait:false, fail:false, seals:0, updates:0, bridge:{ + window.fixture = { wait:false, fail:false, seals:0, updates:0, resolveWait:false, resolveCount:0, catalog:[], inventory:{ + listTraces:async()=>window.fixture.catalog, + resolveTraceProjection:async digest=>{ + window.fixture.resolveCount++; + if(window.fixture.resolveWait) await new Promise(resolve=>window.fixture.finishResolve=resolve); + return {traceDigest:digest,payload:{items:[]}}; + } + }, bridge:{ list:async()=>records, listSeals:async()=>[], getRevision:async id=>structuredClone(revisions[id]), listExperiments:async()=>[], listLog:async()=>[], listVisibilityRequests:async()=>[], validate:async()=>({sealable:true,findings:[]}), listComments:async()=>[], onEvent:()=>()=>{}, @@ -32,7 +39,8 @@ test("Report drafts survive navigation/reload and asynchronous saves", async () if(window.fixture.wait) await new Promise(resolve=>window.fixture.finish=resolve); if(input.expectedRevision!==revisions[id].revision) throw new Error('revision conflict'); Object.assign(revisions[id],input,{revision:revisions[id].revision+1}); - const row=records.find(r=>r.id===id); row.currentRevision=revisions[id].revision; row.title=input.title; + const row=records.find(r=>r.id===id); row.currentRevision=revisions[id].revision; + if(input.title!==undefined) row.title=input.title; return {...row}; }, seal:async()=>{window.fixture.seals++; throw new Error('seal should not be reached');} }}; @@ -41,8 +49,8 @@ test("Report drafts survive navigation/reload and asynchronous saves", async () plugins: [{ name: "local-fixtures", setup(builder) { builder.onResolve({ filter: /desktopBridge$|DocumentContent$|^@synth\/visual-templates\// }, args => ({ path: args.path, namespace: "fixture" })); builder.onLoad({ filter: /.*/, namespace: "fixture" }, args => ({ contents: - args.path.endsWith("desktopBridge") ? "export const bridges={get reports(){return window.fixture.bridge}};" : - args.path.endsWith("DocumentContent") ? "export const Markdown=()=>null;" : "export default ()=>null;", loader: "js" })); + args.path.endsWith("desktopBridge") ? "export const bridges={get reports(){return window.fixture.bridge},get inventory(){return window.fixture.inventory}};" : + args.path.endsWith("DocumentContent") ? "export const Markdown=()=>null;" : "import React from 'react'; export default ()=>React.createElement('p',{'data-testid':'trace-preview'},'Trace preview');", loader: "js", resolveDir: root })); }}] }); const server = createServer((req, res) => { @@ -71,7 +79,7 @@ test("Report drafts survive navigation/reload and asynchronous saves", async () await page.waitForFunction(() => typeof window.fixture.finish === "function"); await findings.fill("typed while saving"); await page.evaluate(() => { window.fixture.finish(); }); - await page.waitForFunction(() => document.body.textContent.includes("Saved · rev 2")); + await page.waitForFunction(() => document.body.textContent.includes("Unsaved changes") && !document.body.textContent.includes("Saving")); assert.equal(await findings.inputValue(), "typed while saving"); await page.getByRole("button", { name: "Save draft", exact: true }).click(); @@ -89,6 +97,48 @@ test("Report drafts survive navigation/reload and asynchronous saves", async () await page.getByTestId("reports-error").waitFor(); assert.equal(await page.evaluate(() => window.fixture.seals), 0); assert.equal(await findings.inputValue(), "must not seal after failure"); + + // Inventory empty state and picker selection use no provider or native bridge. + await selectReport("b"); + await page.evaluate(() => { window.fixture.fail = false; }); + await page.getByRole("button", { name: "Choose retained trace" }).click(); + await page.getByText("No retained traces yet.", { exact:false }).waitFor(); + await page.getByRole("button", { name: "Choose retained trace" }).click(); + await page.evaluate(() => { window.fixture.catalog = [{digest:'sha256:trace-one',title:'Retained evidence',createdAt:'2026-09-10'}]; }); + await page.getByRole("button", { name: "Choose retained trace" }).click(); + await page.getByRole("combobox", { name: "Available retained traces" }).selectOption('sha256:trace-one'); + assert.equal(await page.getByLabel("Trace evidence label").inputValue(), "Retained evidence"); + + // A pending projection must not attach to the old report after navigation. + const before = await page.evaluate(() => window.fixture.updates); + await page.evaluate(() => { window.fixture.resolveWait = true; }); + await page.getByTestId("reports-attach-trace").click(); + await page.waitForFunction(() => typeof window.fixture.finishResolve === 'function'); + assert.equal(await page.getByTestId("reports-attach-trace").isDisabled(), true); + await selectReport("a"); + await page.evaluate(() => window.fixture.finishResolve()); + await page.waitForTimeout(50); + assert.equal(await page.evaluate(() => window.fixture.updates), before); + assert.equal(await findings.inputValue(), "must not seal after failure"); + + await selectReport("b"); + await page.evaluate(() => { window.fixture.resolveWait = false; window.fixture.wait = true; }); + await page.getByLabel("Retained trace digest").fill('sha256:trace-one'); + await page.getByTestId("reports-attach-trace").click(); + await page.waitForFunction(count => window.fixture.updates === count + 1, before); + await findings.fill('edited during attachment'); + await page.evaluate(() => window.fixture.finish()); + await page.getByRole("button", { name: "Inspect attached trace" }).waitFor(); + assert.equal(await findings.inputValue(), 'edited during attachment'); + assert.equal(await page.getByTestId('trace-preview').count(), 0); + await page.getByRole("button", { name: "Inspect attached trace" }).click(); + await page.getByTestId('trace-preview').waitFor(); + await page.getByRole("button", { name: "Hide attached trace" }).click(); + assert.equal(await page.getByTestId('trace-preview').count(), 0); + await page.evaluate(() => { window.fixture.wait = false; }); + await page.getByRole("button", { name: "Save draft", exact:true }).click(); + await page.waitForFunction(() => document.body.textContent.includes('Saved · rev')); + assert.equal(await findings.inputValue(), 'edited during attachment'); } finally { await browser?.close(); await new Promise(resolve => server.close(resolve)); diff --git a/apps/synth_desktop/tests/reports_page.test.mjs b/apps/synth_desktop/tests/reports_page.test.mjs index 37e0dda0e..3b1bcc052 100644 --- a/apps/synth_desktop/tests/reports_page.test.mjs +++ b/apps/synth_desktop/tests/reports_page.test.mjs @@ -43,8 +43,10 @@ test("move controls disable at list boundaries and name the block", () => { assert.match(reportsPage, /disabled=\{index === movable\.length - 1\}/); }); -test("autosave state is visible and Save draft is disabled unless dirty", () => { - assert.match(reportsPage, /Edits save automatically\./); +test("local drafts and explicit saves are described honestly", () => { + assert.match(reportsPage, /Draft edits are retained in this session/); + assert.match(reportsPage, /Unsaved changes/); + assert.doesNotMatch(reportsPage, /Edits save automatically/); assert.match(reportsPage, /Saved · rev \$\{readerRevision\.revision\}/); assert.match(reportsPage, /saveStatus === "saving" \? "Saving"/); assert.match(reportsPage, /saveStatus === "error" \? "Error"/); diff --git a/apps/synth_desktop/tests/trace-ux.test.mjs b/apps/synth_desktop/tests/trace-ux.test.mjs new file mode 100644 index 000000000..c53b02590 --- /dev/null +++ b/apps/synth_desktop/tests/trace-ux.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createServer } from 'node:http'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; +import { chromium } from 'playwright'; + +test('citation selection reveals filtered annotations and comparison needs one actor', async () => { + const root = fileURLToPath(new URL('../../..', import.meta.url)); + const result = await build({ absWorkingDir: root, bundle:true, write:false, format:'iife', jsx:'automatic', + stdin:{ resolveDir:root, loader:'tsx', contents:` + import React,{useState} from 'react'; + import {createRoot} from 'react-dom/client'; + import {AgentTraceInspector} from './packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector'; + const projection={trace_id:'fixture',lanes:[{lane_id:'a',actor_id:'a',display_name:'Agent A'},{lane_id:'b',actor_id:'b',display_name:'Agent B'}],items:[ + {item_id:'message',kind:'model_call.completed',actor_id:'a',title:'Recorded answer',detail:{text:'Answer'}}, + {item_id:'citation',kind:'evidence.annotation',actor_id:'b',title:'Citation target',detail:{text:'Exact retained annotation'}} + ]}; + function App(){const [selection,setSelection]=useState();return <>} + createRoot(document.getElementById('root')).render(); + `} + }); + const server=createServer((req,res)=>{res.setHeader('Content-Type',req.url==='/bundle.js'?'text/javascript':'text/html');res.end(req.url==='/bundle.js'?result.outputFiles[0].text:'
      ');}); + await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); + const browser=await chromium.launch({headless:true}); + try { + const page=await browser.newPage(); + await page.route('**/*',route=>new URL(route.request().url()).hostname==='127.0.0.1'?route.continue():route.abort()); + await page.goto(`http://127.0.0.1:${server.address().port}`); + assert.equal(await page.getByLabel('Compare agent').isDisabled(),true); + assert.equal(await page.getByRole('complementary',{name:'Trace annotations'}).count(),0); + await page.getByLabel('Trace agent',{exact:true}).selectOption('a'); + assert.equal(await page.getByLabel('Compare agent').isDisabled(),false); + await page.getByLabel('Search agent trace').fill('no match'); + await page.getByRole('button',{name:'Jump to citation',exact:true}).click(); + const target=page.locator('[data-trace-item-id="citation"]'); + await target.waitFor(); + assert.equal(await target.getAttribute('aria-current'),'true'); + assert.equal(await page.getByLabel('Search agent trace').inputValue(),''); + assert.equal(await page.getByLabel('Trace agent',{exact:true}).inputValue(),'all'); + for(const width of [960,1280,1440]) { + await page.setViewportSize({width,height:840}); + assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true); + } + } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } +}); diff --git a/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx b/packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx index 46608a5bc..2d8e9a49e 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 7aca7b5e9..b5f932561 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}
      ; } From 403eb1c4d61924ac7f940b62828147271c176697 Mon Sep 17 00:00:00 2001 From: Josh Purtell Date: Thu, 10 Sep 2026 14:38:59 -0400 Subject: [PATCH 2/2] fix(desktop): fit nested report traces to editor width --- .../synth_desktop/src/renderer/src/styles/app.css | 15 +++++++++++++++ apps/synth_desktop/tests/trace-ux.test.mjs | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/synth_desktop/src/renderer/src/styles/app.css b/apps/synth_desktop/src/renderer/src/styles/app.css index 47a70c409..d4a9745e1 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/apps/synth_desktop/tests/trace-ux.test.mjs b/apps/synth_desktop/tests/trace-ux.test.mjs index c53b02590..332494a94 100644 --- a/apps/synth_desktop/tests/trace-ux.test.mjs +++ b/apps/synth_desktop/tests/trace-ux.test.mjs @@ -2,11 +2,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { createServer } from 'node:http'; import { fileURLToPath } from 'node:url'; +import { readFile } from 'node:fs/promises'; import { build } from 'esbuild'; import { chromium } from 'playwright'; test('citation selection reveals filtered annotations and comparison needs one actor', async () => { const root = fileURLToPath(new URL('../../..', import.meta.url)); + const css = await readFile(new URL('../src/renderer/src/styles/app.css', import.meta.url), 'utf8'); const result = await build({ absWorkingDir: root, bundle:true, write:false, format:'iife', jsx:'automatic', stdin:{ resolveDir:root, loader:'tsx', contents:` import React,{useState} from 'react'; @@ -20,7 +22,7 @@ test('citation selection reveals filtered annotations and comparison needs one a createRoot(document.getElementById('root')).render(); `} }); - const server=createServer((req,res)=>{res.setHeader('Content-Type',req.url==='/bundle.js'?'text/javascript':'text/html');res.end(req.url==='/bundle.js'?result.outputFiles[0].text:'
      ');}); + const server=createServer((req,res)=>{res.setHeader('Content-Type',req.url==='/bundle.js'?'text/javascript':'text/html');res.end(req.url==='/bundle.js'?result.outputFiles[0].text:`
      `);}); await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); const browser=await chromium.launch({headless:true}); try { @@ -42,5 +44,9 @@ test('citation selection reveals filtered annotations and comparison needs one a await page.setViewportSize({width,height:840}); assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true); } + for (const width of [420, 540, 720]) { + await page.locator('.reports-preview').evaluate((element, width) => { element.style.width = width + 'px'; }, width); + assert.equal(await page.locator('.reports-preview').evaluate(element => element.scrollWidth <= element.clientWidth + 1), true, `nested report preview fits ${width}px`); + } } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } });