import { useEffect, useMemo } from "react"; 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"; const DEBUG_CATEGORY_TONE: Record = { agent: "bg-teal-500/15 text-teal-500", command: "bg-mint/15 text-mint", interview: "bg-coral/15 text-coral", run: "bg-overlay-strong text-fg-2", stage: "bg-amber/15 text-amber", tool: "bg-mint/15 text-mint", }; export function debugCategory(eventName: string): string { const dot = eventName.indexOf("."); return dot < 0 ? eventName : eventName.slice(0, dot); } export function debugCategoryLabel(category: string): string { if (!category) return "Other"; return category.charAt(0).toUpperCase() + category.slice(1); } export function debugCategoryTone(category: string): string { return DEBUG_CATEGORY_TONE[category] ?? "bg-overlay text-fg-muted"; } 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 function DebugEventDetailsPanel({ event, onClose, }: { event: EventEnvelope | null; onClose: () => void; }) { return ( {event ? : null} ); } function DebugEventDetails({ event }: { event: EventEnvelope }) { 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 (
); }