From 5daa07152ab9fcde77b0f8d4766c6f0c99bac850 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 7 May 2026 13:52:08 -0700 Subject: [PATCH] feat(web): make debug events clickable with a JSON detail panel Promotes DebugRow to a button with the same padding and hover/selected chrome as the transcript rows, and opens a side panel showing the event name as the header and the full EventEnvelope JSON, pretty- printed and syntax-highlighted via highlightJson. Refactors the panel chrome into a shared DetailsPanel so EventDetailsPanel and the new DebugEventDetailsPanel both reuse the slide-out animation, header, and Esc-to-close behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/routes/run-stages.tsx | 117 +++++++++++++++++++---- 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 3a68a26e7..5d7b7e578 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -410,14 +410,25 @@ function EventRow({ function DebugRow({ event, runStart, + selected, + onSelect, }: { event: EventEnvelope; runStart: string | undefined; + selected: boolean; + onSelect: () => void; }) { const eventName = event.event ?? ""; const category = debugCategory(eventName); return ( -
+
+ ); } @@ -577,36 +588,36 @@ function EventDetails({ turn, runStart }: { turn: TurnType; runStart: string | u ); } -function EventDetailsPanel({ - turn, - runStart, +function DetailsPanel({ + title, + isOpen, onClose, + children, }: { - turn: TurnType | null; - runStart: string | undefined; + title: string; + isOpen: boolean; onClose: () => void; + children: React.ReactNode; }) { useEffect(() => { - if (!turn) return; + if (!isOpen) return; function handleKey(event: KeyboardEvent) { if (event.key === "Escape") onClose(); } window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); - }, [turn, onClose]); + }, [isOpen, onClose]); return (
-

- {turn ? `${turnLabel(turn)} event` : ""} -

+

{title}

- {turn ? : null} + {isOpen ? children : null}
); } +function EventDetailsPanel({ + turn, + runStart, + onClose, +}: { + turn: TurnType | null; + runStart: string | undefined; + onClose: () => void; +}) { + return ( + + {turn ? : null} + + ); +} + +function DebugEventDetails({ event }: { event: EventEnvelope }) { + const text = useMemo(() => JSON.stringify(event, null, 2), [event]); + const tokens = useMemo(() => highlightJson(text), [text]); + return ( +
+      {tokens}
+    
+ ); +} + +function DebugEventDetailsPanel({ + event, + onClose, +}: { + event: EventEnvelope | null; + onClose: () => void; +}) { + return ( + + {event ? : null} + + ); +} + function EventsTabToggle({ tab, onTabChange, @@ -845,8 +904,10 @@ export default function RunStages() { ); const [openIndex, setOpenIndex] = useState(null); + const [openDebugSeq, setOpenDebugSeq] = useState(null); useEffect(() => { setOpenIndex(null); + setOpenDebugSeq(null); }, [selectedStageId]); const openTurn = openIndex != null ? turns[openIndex] ?? null : null; @@ -874,6 +935,13 @@ export default function RunStages() { (e) => activityEventStageId(e) === selectedStageId, ); }, [stageEventsQuery.data, selectedStageId]); + const openDebugEvent = useMemo( + () => + openDebugSeq != null + ? debugEvents.find((e) => e.seq === openDebugSeq) ?? null + : null, + [debugEvents, openDebugSeq], + ); const availableDebugCategories = useMemo(() => { const set = new Set(); for (const event of debugEvents) { @@ -970,17 +1038,26 @@ export default function RunStages() { key={`debug-${event.seq}`} event={event} runStart={runStart} + selected={openDebugSeq === event.seq} + onSelect={() => setOpenDebugSeq(event.seq)} /> )) )} - setOpenIndex(null)} - /> + {tab === "transcript" ? ( + setOpenIndex(null)} + /> + ) : ( + setOpenDebugSeq(null)} + /> + )} ); }