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>
  );
};