diff --git a/apps/fabro-web/app/components/event-debug.tsx b/apps/fabro-web/app/components/event-debug.tsx index 47a2bbd34..d197192ba 100644 --- a/apps/fabro-web/app/components/event-debug.tsx +++ b/apps/fabro-web/app/components/event-debug.tsx @@ -487,3 +487,203 @@ function DnaPopover({ document.body, ); } + +export type ThreadCategory = "system" | "agent" | "tool" | "user" | "interrupt"; + +const THREAD_CATEGORY_LABEL: Record = { + system: "System", + agent: "Agent", + tool: "Tool", + user: "User", + interrupt: "Interrupt", +}; + +const THREAD_CATEGORY_COLOR: Record = { + system: "var(--color-amber)", + agent: "var(--color-teal-500)", + tool: "var(--color-mint)", + user: "var(--color-ice-300)", + interrupt: "var(--color-coral)", +}; + +export type ThreadDnaSelection = + | { kind: "single"; turnIndex: number } + | { kind: "group"; childTurnIndices: number[] }; + +export interface ThreadDnaItem { + category: ThreadCategory; + label: string; + startMs: number; + durationMs: number; + selection: ThreadDnaSelection; +} + +const INSTANT_MARKER_PX = 4; +const MIN_DURATION_PX = 3; + +function selectionKey(s: ThreadDnaSelection): string { + return s.kind === "single" + ? `s:${s.turnIndex}` + : `g:${s.childTurnIndices.join(",")}`; +} + +function selectionsEqual( + a: ThreadDnaSelection, + b: ThreadDnaSelection | null, +): boolean { + if (b == null) return false; + if (a.kind === "single" && b.kind === "single") { + return a.turnIndex === b.turnIndex; + } + if (a.kind === "group" && b.kind === "group") { + if (a.childTurnIndices.length !== b.childTurnIndices.length) return false; + return a.childTurnIndices.every((v, i) => v === b.childTurnIndices[i]); + } + return false; +} + +function formatThreadElapsed(ms: number): string { + const total = Math.max(0, Math.floor(ms / 1000)); + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const seconds = total % 60; + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; +} + +function formatThreadDuration(ms: number): string { + if (ms < 1000) return `${Math.max(0, Math.round(ms))} ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`; + const minutes = Math.floor(ms / 60000); + const seconds = Math.round((ms % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +} + +export function ThreadDnaStrip({ + items, + selection, + onSelect, +}: { + items: ThreadDnaItem[]; + selection: ThreadDnaSelection | null; + onSelect: (s: ThreadDnaSelection) => void; +}) { + const [hover, setHover] = useState<{ key: string; rect: DOMRect } | null>( + null, + ); + + const totalMs = useMemo(() => { + let max = 0; + for (const item of items) { + const end = item.startMs + Math.max(0, item.durationMs); + if (end > max) max = end; + } + return Math.max(1, max); + }, [items]); + + if (items.length === 0) { + return ( +