# Devtools

A drop-in inspector panel for any json-render app. See the spec tree, edit state inline, watch dispatched actions, follow stream patches live, browse your catalog, and pick DOM elements to map them back to spec keys.

Production-safe: the component tree-shakes to a null render when `NODE_ENV === "production"`.

## Install

Pick the adapter that matches your renderer.

### React

```bash
npm install @json-render/devtools @json-render/devtools-react
```

### Vue

```bash
npm install @json-render/devtools @json-render/devtools-vue
```

### Svelte

```bash
npm install @json-render/devtools @json-render/devtools-svelte
```

### Solid

```bash
npm install @json-render/devtools @json-render/devtools-solid
```

## Quick Start

Drop `<JsonRenderDevtools />` anywhere inside your existing `<JSONUIProvider>` (or the equivalent provider tree).

```tsx
// React

<JSONUIProvider registry={registry} handlers={handlers}>
  <Renderer spec={spec} registry={registry} />
  <JsonRenderDevtools spec={spec} catalog={catalog} />
</JSONUIProvider>
```

That's it. A floating toggle appears in the bottom-right corner. Click it, or press <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>J</kbd>, to open the drawer.

### Chat apps (AI SDK)

When you're using `@ai-sdk/react`'s `useChat`, pass the `messages` prop so the Stream tab captures spec patches as they arrive:

```tsx
<JsonRenderDevtools
  spec={spec}
  catalog={catalog}
  messages={messages}
/>
```

## Panels

<table>
  <thead>
    <tr><th>Tab</th><th>What it shows</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Spec</strong></td>
      <td>Element tree rooted at <code>spec.root</code>. Expand to walk children. Selecting an element fills a detail pane with its full props, visibility condition, event bindings, watchers, and any issues reported by <code>validateSpec</code>.</td>
    </tr>
    <tr>
      <td><strong>State</strong></td>
      <td>Every leaf path in the state model listed via <code>flattenToPointers</code>. Click a value to edit inline — writes go through <code>store.set</code>, so conditional elements and computed props re-evaluate immediately.</td>
    </tr>
    <tr>
      <td><strong>Actions</strong></td>
      <td>Timeline of dispatched actions: name, params, result or error, duration. Newest first. Expand a row for the full JSON payload.</td>
    </tr>
    <tr>
      <td><strong>Stream</strong></td>
      <td>Patches, text chunks, token usage, and lifecycle markers from the AI generation stream. Grouped by generation.</td>
    </tr>
    <tr>
      <td><strong>Catalog</strong></td>
      <td>Components and actions declared in your catalog with prop chips and type hints.</td>
    </tr>
    <tr>
      <td><strong>Pick</strong></td>
      <td>Click any element in the page to surface its entry in the Spec tab. Works because the renderer transparently tags each element with <code>data-jr-key</code> while devtools is mounted.</td>
    </tr>
  </tbody>
</table>

## Props

<table>
  <thead>
    <tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>spec</code></td>
      <td><code>Spec | null</code></td>
      <td><code>null</code></td>
      <td>The spec currently being rendered.</td>
    </tr>
    <tr>
      <td><code>catalog</code></td>
      <td><code>Catalog | null</code></td>
      <td><code>null</code></td>
      <td>Catalog definition — required for the Catalog panel.</td>
    </tr>
    <tr>
      <td><code>messages</code></td>
      <td><code>UIMessage[]</code></td>
      <td><code>undefined</code></td>
      <td>AI SDK <code>useChat</code> messages. Scanned for spec data parts and streamed into the Stream panel.</td>
    </tr>
    <tr>
      <td><code>initialOpen</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Start the drawer open.</td>
    </tr>
    <tr>
      <td><code>position</code></td>
      <td><code>"bottom-right" | "bottom-left" | "right"</code></td>
      <td><code>"bottom-right"</code></td>
      <td>Floating toggle button position.</td>
    </tr>
    <tr>
      <td><code>hotkey</code></td>
      <td><code>string | false</code></td>
      <td><code>"mod+shift+j"</code></td>
      <td>Keyboard shortcut. Use <code>mod</code> for Cmd on macOS / Ctrl elsewhere. Pass <code>false</code> to disable.</td>
    </tr>
    <tr>
      <td><code>bufferSize</code></td>
      <td><code>number</code></td>
      <td><code>500</code></td>
      <td>Max events retained in the ring buffer.</td>
    </tr>
    <tr>
      <td><code>onEvent</code></td>
      <td><code>(evt: DevtoolsEvent) =&gt; void</code></td>
      <td><code>undefined</code></td>
      <td>Optional tap — fires for every event as it is recorded. Useful for forwarding to analytics.</td>
    </tr>
  </tbody>
</table>

## Production Safety

The component renders `null` when `process.env.NODE_ENV === "production"`. Bundlers fold the constant check so the panel's code tree-shakes out of production builds.

If you want extra certainty, gate the import behind an env check:

```tsx

const JsonRenderDevtools = dynamic(
  () =>
    import("@json-render/devtools-react").then((m) => ({
      default: m.JsonRenderDevtools,
    })),
  { ssr: false, loading: () => null },
);
```

## Advanced

### Imperative controls

Use `useJsonRenderDevtools()` (React adapter only) to open / close the panel or record custom events from anywhere in the app:

```tsx

function DebugButton() {
  const devtools = useJsonRenderDevtools();
  return (
    <button onClick={() => devtools?.toggle()}>Toggle devtools</button>
  );
}
```

### Server-side stream tap

Capture stream events before they reach the client. Useful for server logs:

```ts

const tapped = tapJsonRenderStream(
  result.toUIMessageStream(),
  serverEventStore,
);
writer.merge(pipeJsonRender(tapped));
```

The `@json-render/devtools` core package exports `tapJsonRenderStream` and `tapYamlStream` for this pattern.