import { useCallback, useEffect, useState } from 'react'; import { Copy, Timer, Trash2, X } from 'lucide-react'; import { cn } from '@/lib/utils'; import { copyToClipboard } from '@/lib/clipboard'; import { toast } from '@/components/ui/toast-store'; import { useHydrationTiming } from '@/hooks/useHydrationTiming'; import { clearReport, getHydrationReport } from '@/lib/hydrationTiming'; import type { HydrationOutcome } from '@/lib/hydrationTiming'; /** Format an elapsed duration compactly: seconds with one decimal at or above * 1s, whole milliseconds below. */ function formatMs(ms: number): string { return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`; } function formatOffset(ms: number | null): string { return ms == null ? '-' : formatMs(ms); } function outcomeDotClass(outcome: HydrationOutcome | undefined, critical: boolean): string { if (outcome === 'error') return 'bg-destructive'; if (outcome === 'aborted' || outcome === 'superseded') return 'bg-muted-foreground'; return critical ? 'bg-brand' : 'bg-success'; } const POSITION_CLASS = 'fixed left-4 bottom-6 z-[100] max-md:left-3 max-md:right-3 max-md:bottom-[calc(var(--sn-mobile-tabbar-h)_+_env(safe-area-inset-bottom)_+_0.75rem)]'; /** * Developer-mode-only overlay for startup and stack-hydration timing. * * Mount this only when developer mode is on for the active node; it does not * gate itself. It shows a collapsed chip with the boot-to-`list_visible` * elapsed time, expanding to a phase table with copy/clear actions. It sits * below toasts and modals and never covers the mobile tab bar or safe area. */ export function HydrationTimingPanel() { const { listVisibleMs } = useHydrationTiming(); const [expanded, setExpanded] = useState(false); useEffect(() => { if (!expanded) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setExpanded(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [expanded]); const chipLabel = listVisibleMs == null ? 'list …' : `list ${formatMs(listVisibleMs)}`; const handleCopy = useCallback(async () => { try { await copyToClipboard(JSON.stringify(getHydrationReport(), null, 2)); toast.success('Hydration report copied.'); } catch (e) { console.error('[HydrationTiming] copy failed:', e); toast.error('Could not copy the hydration report.'); } }, []); if (!expanded) { return ( ); } // Only build the (potentially large) report while the panel is open. const report = getHydrationReport(); return (
Hydration timing {chipLabel}
{report.phases.length === 0 ? ( ) : ( report.phases.map((p, i) => ( )) )}
Phase At Dur
No events recorded yet.
{p.phase} {p.proxied && proxy} {formatOffset(p.offsetMs)} {p.durationMs == null ? '-' : formatMs(p.durationMs)}
{report.anyProxied && (

Some requests were proxied to a remote node. Nodes and proxy debug logs need developer mode enabled on the gateway to appear.

)}
); }