FreezesDebuggerTab.tsx5.7 KBView on GitHub import { Check, Copy, Download, Trash2, Snowflake, RefreshCw } from 'lucide-react';
import React, { useCallback, useEffect, useState } from 'react';
import {
clearSnapshots,
deleteSnapshot,
listSnapshots,
type StoredSnapshot,
} from '@/modules/debugger/deepDebugger/freeze/snapshotStore';
import { formatAsMarkdown } from '@/modules/debugger/deepDebugger/format';
import { cn } from '@/styles/stylingUtils';
function fmtTime(ts: number): string {
return new Date(ts).toLocaleTimeString();
}
function fmtBytes(n?: number): string {
if (!n) return '–';
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${(n / 1024 / 1024).toFixed(1)} MB`;
}
export const FreezesDebuggerTab: React.FC = () => {
const [snapshots, setSnapshots] = useState<StoredSnapshot[]>([]);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const refresh = useCallback(async () => {
const items = await listSnapshots();
setSnapshots(items.reverse());
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
const copy = async (text: string, key=[redacted] => {
try {
await navigator.clipboard.writeText(text);
setCopiedKey(key);
setTimeout(() => setCopiedKey((k) => (k === key ? null : k)), 1500);
} catch {
// ignore
}
};
const download = (snap: StoredSnapshot) => {
const blob = new Blob([JSON.stringify(snap, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `freeze-${snap.id}.json`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b border-gray-200 px-2 py-1.5 dark:border-gray-700">
<Snowflake className="h-3.5 w-3.5 text-blue-500" />
<span className="text-xs font-medium">Freeze snapshots</span>
<span className="text-[10px] text-gray-500 dark:text-gray-400">
({snapshots.length})
</span>
<div className="ml-auto flex items-center gap-1">
<button
onClick={refresh}
className="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
title="Refresh"
>
<RefreshCw className="h-3 w-3" />
</button>
<button
onClick={async () => {
await clearSnapshots();
await refresh();
}}
className="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
title="Clear all"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
<div className="flex-1 space-y-1 overflow-y-auto p-2">
{snapshots.length === 0 ? (
<div className="py-4 text-center text-xs text-gray-500 dark:text-gray-400">
No freezes captured yet. Try blocking the main thread to test:
<pre className="mx-auto mt-2 inline-block rounded bg-gray-100 px-2 py-1 text-[10px] dark:bg-gray-800">
{`let t=Date.now();while(Date.now()-t<2000){}`}
</pre>
</div>
) : (
snapshots.map((snap) => (
<div
key=[redacted]
className={cn(
'rounded border border-blue-200 bg-blue-50 p-2 dark:border-blue-800 dark:bg-blue-950',
)}
>
<div className="flex items-center gap-2">
<span className="rounded bg-white/60 px-1 py-0.5 text-[10px] dark:bg-black/30">
{snap.reason}
</span>
<span className="font-mono text-xs">{snap.blockedMs.toFixed(0)}ms</span>
<span className="text-[10px] text-gray-500 dark:text-gray-400">
{fmtTime(snap.ts)}
</span>
<span className="text-[10px] text-gray-500 dark:text-gray-400">
{snap.recentTimeline.length} entries · {fmtBytes(snap.memory?.usedJSHeapSize)} heap
</span>
<div className="ml-auto flex items-center gap-1">
<button
onClick={() =>
copy(formatAsMarkdown(snap.recentTimeline, { mode: 'freeze' }), `md-${snap.id}`)
}
className="flex items-center gap-1 rounded border border-gray-300 px-1.5 py-0.5 text-[10px] hover:bg-white/60 dark:border-gray-600 dark:hover:bg-black/30"
title="Copy timeline as Markdown"
>
{copiedKey === `md-${snap.id}` ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
MD
</button>
<button
onClick={() => download(snap)}
className="rounded p-1 hover:bg-white/60 dark:hover:bg-black/30"
title="Download full snapshot JSON"
>
<Download className="h-3 w-3" />
</button>
<button
onClick={async () => {
await deleteSnapshot(snap.id);
await refresh();
}}
className="rounded p-1 hover:bg-white/60 dark:hover:bg-black/30"
title="Delete snapshot"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
<div className="mt-1 truncate text-[10px] text-gray-500 dark:text-gray-400">
{snap.url}
</div>
</div>
))
)}
</div>
</div>
);
};