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.
'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't collected by the devtools bridge;
tiers show the raw default.
</p>
</div>
)}
</TabPanel>
</div>
</aside>
);
}