import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { Listbox, ListboxButton, ListboxOption, ListboxOptions, } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; import { CheckIcon, ChevronUpDownIcon, FunnelIcon, MagnifyingGlassIcon, } from "@heroicons/react/16/solid"; import type { EventEnvelope } from "@qltysh/fabro-api-client"; import { Tooltip } from "./ui"; import { formatAbsoluteTs } from "../lib/format"; export type DebugCategory = | "agent" | "command" | "lifecycle" | "human" | "system"; export const DEBUG_CATEGORIES: readonly DebugCategory[] = [ "agent", "command", "lifecycle", "human", "system", ] as const; const PREFIX_TO_CATEGORY: Record = { agent: "agent", command: "command", run: "lifecycle", stage: "lifecycle", parallel: "lifecycle", subgraph: "lifecycle", edge: "lifecycle", loop: "lifecycle", prompt: "lifecycle", interview: "human", }; const CATEGORY_LABEL: Record = { agent: "Agent", command: "Command", lifecycle: "Lifecycle", human: "Human", system: "System", }; const CATEGORY_TONE: Record = { agent: "bg-teal-500/15 text-teal-500", command: "bg-mint/15 text-mint", lifecycle: "bg-amber/15 text-amber", human: "bg-coral/15 text-coral", system: "bg-overlay-strong text-fg-3", }; const CATEGORY_COLOR: Record = { agent: "var(--color-teal-500)", command: "var(--color-mint)", lifecycle: "var(--color-amber)", human: "var(--color-coral)", system: "var(--color-ice-300)", }; export function debugCategory(eventName: string | null | undefined): DebugCategory { if (!eventName) return "system"; const dot = eventName.indexOf("."); const prefix = dot < 0 ? eventName : eventName.slice(0, dot); return PREFIX_TO_CATEGORY[prefix] ?? "system"; } export function debugCategoryLabel(category: DebugCategory): string { return CATEGORY_LABEL[category]; } export function debugCategoryTone(category: DebugCategory): string { return CATEGORY_TONE[category]; } export function debugCategoryColor(category: DebugCategory): string { return CATEGORY_COLOR[category]; } export function formatElapsed(eventTs: string, runStart: string | undefined): string { if (!runStart) return ""; const startMs = Date.parse(runStart); const eventMs = Date.parse(eventTs); if (Number.isNaN(startMs) || Number.isNaN(eventMs)) return ""; const delta = Math.max(0, Math.floor((eventMs - startMs) / 1000)); const hours = Math.floor(delta / 3600); const minutes = Math.floor((delta % 3600) / 60); const seconds = delta % 60; return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } const JSON_TOKEN_RE = /"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?/g; export function highlightJson(text: string): React.ReactNode[] { const parts: React.ReactNode[] = []; let lastIndex = 0; let match: RegExpExecArray | null; let key = 0; JSON_TOKEN_RE.lastIndex = 0; while ((match = JSON_TOKEN_RE.exec(text)) !== null) { if (match.index > lastIndex) { parts.push(text.slice(lastIndex, match.index)); } const token = match[0]; let cls: string; if (token.startsWith('"')) { const after = text.slice(JSON_TOKEN_RE.lastIndex); cls = /^\s*:/.test(after) ? "text-teal-300" : "text-mint"; } else if (token === "true" || token === "false") { cls = "text-coral"; } else if (token === "null") { cls = "text-fg-muted"; } else { cls = "text-amber"; } parts.push( {token} , ); lastIndex = JSON_TOKEN_RE.lastIndex; } if (lastIndex < text.length) parts.push(text.slice(lastIndex)); return parts; } export function DebugEventRow({ event, runStart, selected, onSelect, }: { event: EventEnvelope; runStart: string | undefined; selected: boolean; onSelect: () => void; }) { const eventName = event.event ?? ""; const category = debugCategory(eventName); return ( ); } export function DetailsPanel({ title, isOpen, onClose, children, }: { title: string; isOpen: boolean; onClose: () => void; children: React.ReactNode; }) { useEffect(() => { if (!isOpen) return; function handleKey(event: KeyboardEvent) { if (event.key === "Escape") onClose(); } window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); return (

{title}

{isOpen ? children : null}
); } export type EventDisplayPayload = { event?: string | null; [key: string]: unknown; }; export function DebugEventDetailsPanel({ event, onClose, }: { event: EventDisplayPayload | null; onClose: () => void; }) { return ( {event ? : null} ); } function DebugEventDetails({ event }: { event: EventDisplayPayload }) { const text = useMemo(() => JSON.stringify(event, null, 2), [event]); const tokens = useMemo(() => highlightJson(text), [text]); return (
      {tokens}
    
); } export function MultiSelectFilter({ selected, options, labelOf, onChange, emptyMeansAll = false, }: { selected: T[]; options: readonly T[]; labelOf: (item: T) => string; onChange: (next: T[]) => void; emptyMeansAll?: boolean; }) { const allSelected = selected.length === options.length; const summary = useMemo(() => { if (allSelected || (emptyMeansAll && selected.length === 0)) return "All types"; if (selected.length === 0) return "No types"; if (selected.length <= 2) { return options .filter((o) => selected.includes(o)) .map(labelOf) .join(", "); } return `${selected.length} types`; }, [allSelected, emptyMeansAll, selected, options, labelOf]); return ( {options.map((option) => ( {labelOf(option)} ))} ); } export function EventSearchInput({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { return (
); } const STRIP_HEIGHT = 32; const BAR_NORMAL_HEIGHT = 22; const BAR_HOVER_HEIGHT = 26; const BAR_SELECTED_HEIGHT = 28; const BAR_WIDTH = 4; const STRIP_MAX_MARKERS = 600; function sampleStripItems( items: T[], maxItems: number, keep: (item: T) => boolean, ): T[] { if (items.length <= maxItems) return items; const indices = new Set(); for (let i = 0; i < items.length; i += 1) { if (keep(items[i])) indices.add(i); } for (let i = 0; i < maxItems; i += 1) { indices.add(Math.round((i * (items.length - 1)) / Math.max(1, maxItems - 1))); } return Array.from(indices) .sort((a, b) => a - b) .map((index) => items[index]); } function friendlyEventName(eventName: string): string { const parts = eventName.split("."); if (parts.length <= 1) return eventName; return parts.slice(1).join("."); } export function DebugDnaStrip({ events, selectedSeq, onSelect, runStart, }: { events: EventEnvelope[]; selectedSeq: number | null; onSelect: (seq: number) => void; runStart: string | undefined; }) { const [hover, setHover] = useState<{ seq: number; rect: DOMRect; } | null>(null); const visibleEvents = useMemo( () => sampleStripItems(events, STRIP_MAX_MARKERS, (event) => event.seq === selectedSeq), [events, selectedSeq], ); const visibleEventBySeq = useMemo( () => new Map(visibleEvents.map((event) => [event.seq, event])), [visibleEvents], ); const range = useMemo(() => { if (events.length === 0) return null; let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const event of events) { const ms = Date.parse(event.ts); if (Number.isNaN(ms)) continue; if (ms < min) min = ms; if (ms > max) max = ms; } if (!Number.isFinite(min) || !Number.isFinite(max)) return null; const startCandidate = runStart ? Date.parse(runStart) : Number.NaN; const start = Number.isFinite(startCandidate) ? Math.min(startCandidate, min) : min; const duration = Math.max(1, max - start); return { start, duration }; }, [events, runStart]); if (!range) { return (