# DevTools Drawer Block

> A global on-device observability drawer: six tabbed surfaces (inference queues, event log, pipeline runs, model cache, device capabilities, per-collection VectorDB stats) over the @localmode/devtools bridge, plus a lazy framework-agnostic host — devtools code loads only on first open, closing keeps collectors running, power-off fully disables.

A shadcn/ui block from the [@localmode](https://localmode.ai/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/localmode/ui/blocks/devtools-drawer).

- Page, with a live preview: https://shadcn.exchange/r/localmode/ui/blocks/devtools-drawer
- Screenshot: https://shadcn.exchange/shot/localmode/ui/blocks/devtools-drawer/light.png
- Registry item: https://localmode.ai/r/ui/blocks/devtools-drawer.json
- Categories: blocks, devtools-drawer
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/localmode/ui/blocks/devtools-drawer.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@localmode/devtools`, `lucide-react`
- Registry items: `https://localmode.ai/r/ui/lib/utils.json`, `https://localmode.ai/r/ui/devtools/inference-queue-monitor.json`, `https://localmode.ai/r/ui/devtools/event-log-viewer.json`, `https://localmode.ai/r/ui/devtools/pipeline-run-inspector.json`, `https://localmode.ai/r/ui/devtools/model-cache-table.json`, `https://localmode.ai/r/ui/local-first/device-capability-grid.json`, `https://localmode.ai/r/ui/local-first/vector-storage-observability.json`

## Files

### `src/app/blocks/devtools-drawer/devtools-drawer.tsx`

```tsx
'use client';

/**
 * @file devtools-drawer.tsx
 * @description The global observability drawer body (`ui/blocks/devtools-drawer`) — six tabbed surfaces (Queue / Events / Pipeline / Models / Device / VectorDB) over the `@localmode/devtools` bridge.
 * @constraint onClear/onEvict are intentionally unwired — the devtools bridge exposes no provider-agnostic clear/evict API.
 */

import * as React from 'react';
import { Database, Power, X } from 'lucide-react';
import { disableDevTools, enableDevTools } from '@localmode/devtools';
import {
  useDevToolsEvents,
  useDevToolsModelCache,
  useDevToolsPipelineRuns,
  useDevToolsQueueStats,
  useDevToolsVectorDBs,
} from '@localmode/devtools/react';

import { DeviceCapabilityGrid } from '@/components/device-capability-grid';
import { EventLogViewer } from '@/components/event-log-viewer';
import { InferenceQueueMonitor } from '@/components/inference-queue-monitor';
import { ModelCacheTable } from '@/components/model-cache-table';
import { PipelineRunInspector } from '@/components/pipeline-run-inspector';
import { VectorStorageObservability } from '@/components/vector-storage-observability';
import { cn } from '@/lib/utils';

const TABS = [
  { id: 'queue', label: 'Queue' },
  { id: 'events', label: 'Events' },
  { id: 'pipeline', label: 'Pipeline' },
  { id: 'models', label: 'Models' },
  { id: 'device', label: 'Device' },
  { id: 'vectordb', label: 'VectorDB' },
] as const;

type TabId = (typeof TABS)[number]['id'];

export interface DevToolsDrawerProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onPowerOff?: () => void;
}

function formatRelative(iso: string): string {
  const then = new Date(iso).getTime();
  if (Number.isNaN(then)) return iso;
  const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
  if (seconds < 5) return 'just now';
  if (seconds < 60) return `${seconds}s ago`;
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.round(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  return `${Math.round(hours / 24)}d ago`;
}

function TabPanel({
  id,
  active,
  children,
}: {
  id: TabId;
  active: boolean;
  children: React.ReactNode;
}) {
  return (
    <div
      role="tabpanel"
      id={`devtools-panel-${id}`}
      aria-labelledby={`devtools-tab-${id}`}
      className={cn(!active && 'hidden')}
    >
      {children}
    </div>
  );
}

export function DevToolsDrawer({ open, onOpenChange, onPowerOff }: DevToolsDrawerProps) {
  const [tab, setTab] = React.useState<TabId>('queue');

  React.useEffect(() => {
    enableDevTools();
  }, []);

  const queues = useDevToolsQueueStats();
  const events = useDevToolsEvents();
  const runs = useDevToolsPipelineRuns();
  const models = useDevToolsModelCache();
  const vectorDBs = useDevToolsVectorDBs();

  const handlePowerOff = () => {
    disableDevTools();
    if (onPowerOff) onPowerOff();
    else onOpenChange(false);
  };

  const collections = Object.entries(vectorDBs);

  return (
    <aside
      role="dialog"
      aria-label="LocalMode DevTools"
      className={cn(
        'fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col border-l border-border bg-background text-foreground shadow-2xl',
        !open && 'hidden',
      )}
    >
      <header className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-3">
        <h2 className="text-sm font-semibold">LocalMode DevTools</h2>
        <div className="ml-auto flex items-center gap-1">
          <button
            type="button"
            onClick={handlePowerOff}
            title="Power off - stop collecting and reset"
            aria-label="Power off devtools"
            className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            <Power className="size-4" aria-hidden="true" />
          </button>
          <button
            type="button"
            onClick={() => onOpenChange(false)}
            aria-label="Close devtools drawer"
            className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            <X className="size-4" aria-hidden="true" />
          </button>
        </div>
      </header>

      <nav
        role="tablist"
        aria-label="DevTools surfaces"
        className="flex shrink-0 items-center gap-1 overflow-x-auto border-b border-border px-2 py-1.5"
      >
        {TABS.map((t) => (
          <button
            key={t.id}
            type="button"
            role="tab"
            id={`devtools-tab-${t.id}`}
            aria-selected={tab === t.id}
            aria-controls={`devtools-panel-${t.id}`}
            onClick={() => setTab(t.id)}
            className={cn(
              'shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
              tab === t.id
                ? 'bg-muted text-foreground'
                : 'text-muted-foreground hover:text-foreground',
            )}
          >
            {t.label}
          </button>
        ))}
      </nav>

      {}
      <div className="flex-1 overflow-y-auto p-4 pb-16">
        <TabPanel id="queue" active={tab === 'queue'}>
          <InferenceQueueMonitor queues={queues} className="max-w-none" />
        </TabPanel>

        <TabPanel id="events" active={tab === 'events'}>
          <EventLogViewer events={events} />
        </TabPanel>

        <TabPanel id="pipeline" active={tab === 'pipeline'}>
          <PipelineRunInspector runs={runs} className="max-w-none" />
        </TabPanel>

        <TabPanel id="models" active={tab === 'models'}>
          <ModelCacheTable entries={models} className="max-w-none" />
        </TabPanel>

        <TabPanel id="device" active={tab === 'device'}>
          {}
          <DeviceCapabilityGrid className="max-w-none" />
        </TabPanel>

        <TabPanel id="vectordb" active={tab === 'vectordb'}>
          {collections.length === 0 ? (
            <div className="flex flex-col items-center gap-2 rounded-xl border border-dashed border-border bg-card px-6 py-8 text-center text-card-foreground">
              <Database className="size-5 text-muted-foreground" aria-hidden="true" />
              <p className="text-sm font-medium">No VectorDB activity yet</p>
              <p className="text-xs text-muted-foreground">
                Per-collection stats appear once a block adds, searches, or
                deletes vectors.
              </p>
            </div>
          ) : (
            <div className="flex flex-col gap-4">
              {collections.map(([name, s]) => (
                <section key={name} className="flex flex-col gap-2">
                  {}
                  <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs">
                    <span className="font-mono text-sm font-medium text-foreground">
                      {name}
                    </span>
                    <span className="tabular-nums text-muted-foreground">
                      {s.totalAdds.toLocaleString()} adds ·{' '}
                      {s.totalSearches.toLocaleString()} searches ·{' '}
                      {s.totalDeletes.toLocaleString()} deletes
                    </span>
                    <span className="text-muted-foreground/80" title={s.lastActivity}>
                      last activity {formatRelative(s.lastActivity)}
                    </span>
                  </div>
                  <VectorStorageObservability
                    stats={{ ratio: 1, originalSizeBytes: 0, compressedSizeBytes: 0 }}
                    tier="raw"
                    searchLatencyMs={
                      s.totalSearches > 0 ? s.avgSearchDurationMs : undefined
                    }
                    className="max-w-none"
                  />
                </section>
              ))}
              <p className="text-xs text-muted-foreground">
                Compression stats aren&apos;t collected by the devtools bridge;
                tiers show the raw default.
              </p>
            </div>
          )}
        </TabPanel>
      </div>
    </aside>
  );
}
```

### `src/app/blocks/devtools-drawer/drawer-host.tsx`

```tsx
'use client';

/**
 * @file drawer-host.tsx
 * @description The SHIPPED devtools drawer host for the
 */

import * as React from 'react';
import { Activity } from 'lucide-react';

const STORAGE_KEY = 'localmode:devtools-enabled';

const DevToolsDrawer = React.lazy(() =>
  import('./devtools-drawer').then((m) => ({ default: m.DevToolsDrawer })),
);

export function DevToolsDrawerHost() {
  const [open, setOpen] = React.useState(false);
  const [everOpened, setEverOpened] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    let persisted = false;
    try {
      persisted = window.localStorage.getItem(STORAGE_KEY) === '1';
    } catch {
    }
    if (!persisted) return;
    void import('@localmode/devtools').then((m) => {
      if (!cancelled) m.enableDevTools();
    });
    return () => {
      cancelled = true;
    };
  }, []);

  const handleToggle = () => {
    if (!everOpened) {
      setEverOpened(true);
      try {
        window.localStorage.setItem(STORAGE_KEY, '1');
      } catch {
      }
      setOpen(true);
      return;
    }
    setOpen((o) => !o);
  };

  const handlePowerOff = () => {
    setOpen(false);
    setEverOpened(false);
    try {
      window.localStorage.removeItem(STORAGE_KEY);
    } catch {
    }
  };

  return (
    <>
      {}
      <button
        type="button"
        onClick={handleToggle}
        aria-label={open ? 'Close LocalMode DevTools' : 'Open LocalMode DevTools'}
        title="LocalMode DevTools"
        className={
          open
            ? 'fixed bottom-4 right-4 z-[60] inline-flex size-10 items-center justify-center rounded-full border border-border bg-muted text-foreground shadow-lg transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'
            : 'fixed bottom-4 right-4 z-[60] inline-flex size-10 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-lg transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'
        }
      >
        <Activity className="size-5" aria-hidden="true" />
      </button>
      {everOpened && (
        <React.Suspense fallback={null}>
          <DevToolsDrawer open={open} onOpenChange={setOpen} onPowerOff={handlePowerOff} />
        </React.Suspense>
      )}
    </>
  );
}
```
