@json-render/devtools 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,509 @@
1
+ import { Spec, JsonPatch, Catalog, StateStore, StreamChunk } from '@json-render/core';
2
+
3
+ /**
4
+ * Token usage metadata from an AI generation.
5
+ * Matches the shape used by renderers (e.g. `@json-render/react`).
6
+ */
7
+ interface TokenUsage {
8
+ promptTokens: number;
9
+ completionTokens: number;
10
+ totalTokens: number;
11
+ cachedTokens?: number;
12
+ cacheWriteTokens?: number;
13
+ }
14
+ /**
15
+ * A single devtools event. Events flow into the `EventStore` from framework
16
+ * adapters (state changes, action dispatches) and from stream taps.
17
+ */
18
+ type DevtoolsEvent = {
19
+ kind: "spec-changed";
20
+ at: number;
21
+ spec: Spec;
22
+ } | {
23
+ kind: "state-set";
24
+ at: number;
25
+ path: string;
26
+ prev: unknown;
27
+ next: unknown;
28
+ } | {
29
+ kind: "action-dispatched";
30
+ at: number;
31
+ id: string;
32
+ name: string;
33
+ params?: unknown;
34
+ } | {
35
+ kind: "action-settled";
36
+ at: number;
37
+ id: string;
38
+ ok: boolean;
39
+ result?: unknown;
40
+ error?: string;
41
+ durationMs: number;
42
+ } | {
43
+ kind: "stream-patch";
44
+ at: number;
45
+ patch: JsonPatch;
46
+ source: "json" | "yaml";
47
+ } | {
48
+ kind: "stream-text";
49
+ at: number;
50
+ text: string;
51
+ } | {
52
+ kind: "stream-usage";
53
+ at: number;
54
+ usage: TokenUsage;
55
+ } | {
56
+ kind: "stream-lifecycle";
57
+ at: number;
58
+ phase: "start" | "end";
59
+ ok?: boolean;
60
+ };
61
+ type DevtoolsEventKind = DevtoolsEvent["kind"];
62
+ /**
63
+ * Options for creating a picker session. A picker session lets the user
64
+ * click an element in the host DOM to surface the matching element key
65
+ * in the Spec panel, and vice versa.
66
+ */
67
+ interface PickerOptions {
68
+ /** Called when the user clicks an element with a `data-jr-key` attribute. */
69
+ onPick: (key: string) => void;
70
+ /** Called when the user exits picker mode without selecting. */
71
+ onCancel?: () => void;
72
+ }
73
+ /**
74
+ * Handle returned from {@link startPicker}. Call `.stop()` to tear down.
75
+ */
76
+ interface PickerSession {
77
+ stop: () => void;
78
+ }
79
+
80
+ /**
81
+ * Options for {@link createEventStore}.
82
+ */
83
+ interface EventStoreOptions {
84
+ /** Max events to retain. Older events are dropped. Default: 500. */
85
+ bufferSize?: number;
86
+ }
87
+ /**
88
+ * A capped, subscribable ring buffer of {@link DevtoolsEvent}s.
89
+ *
90
+ * Core to every devtools panel — all adapters push into the same store,
91
+ * and panel UIs subscribe for render updates.
92
+ */
93
+ interface EventStore {
94
+ /** Append an event. May drop the oldest event if the buffer is full. */
95
+ push: (event: DevtoolsEvent) => void;
96
+ /**
97
+ * Current events in insertion order (oldest first). Returns a fresh
98
+ * array so callers can treat it as immutable.
99
+ */
100
+ snapshot: () => DevtoolsEvent[];
101
+ /**
102
+ * Register a listener invoked after every push (or `clear`). Returns
103
+ * an unsubscribe function. Listeners are invoked synchronously.
104
+ */
105
+ subscribe: (listener: () => void) => () => void;
106
+ /** Remove all events. Notifies listeners. */
107
+ clear: () => void;
108
+ /** Current event count. */
109
+ size: () => number;
110
+ }
111
+ /**
112
+ * Create an in-memory ring-buffered event store.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const store = createEventStore({ bufferSize: 200 });
117
+ * const unsub = store.subscribe(() => render(store.snapshot()));
118
+ * store.push({ kind: "state-set", at: Date.now(), path: "/x", prev: 1, next: 2 });
119
+ * ```
120
+ */
121
+ declare function createEventStore(options?: EventStoreOptions): EventStore;
122
+
123
+ /**
124
+ * Production guard. Framework adapters short-circuit to a no-op when this
125
+ * returns true, so dropping `<JsonRenderDevtools />` into an app costs
126
+ * nothing in production builds.
127
+ *
128
+ * Tree-shaken bundlers will fold constant `process.env.NODE_ENV` checks.
129
+ * Consumers that run in browsers without `process` also work because the
130
+ * typeof check guards the access.
131
+ */
132
+ declare function isProduction(): boolean;
133
+
134
+ /**
135
+ * Where the devtools panel docks and where the floating toggle lives.
136
+ *
137
+ * - `bottom-right` (default) — panel docks to the bottom; toggle sits in the
138
+ * bottom-right corner.
139
+ * - `bottom-left` — panel docks to the bottom; toggle sits in the bottom-left
140
+ * corner. Useful when the host app already has something bottom-right.
141
+ * - `right` — panel docks to the right edge (full height); toggle sits in the
142
+ * top-right corner. Best for app-shell layouts that already use `100vh` /
143
+ * `position: fixed` at the bottom — right docking avoids the bottom edge
144
+ * entirely.
145
+ */
146
+ type PanelPosition = "bottom-right" | "bottom-left" | "right";
147
+ /**
148
+ * A single spec available for inspection. When the host app renders one
149
+ * spec at a time (the common single-renderer case) this list has a single
150
+ * entry. Multi-renderer hosts (e.g. an AI chat where each assistant reply
151
+ * produces its own spec) surface every generation so the Spec tab can
152
+ * switch between them.
153
+ */
154
+ interface SpecEntry {
155
+ /** Stable id, typically the message id that produced the spec. */
156
+ id: string;
157
+ /** Human-readable label shown in the generation switcher (e.g. "Generation 2"). */
158
+ label: string;
159
+ /** The spec itself. */
160
+ spec: Spec;
161
+ }
162
+ /**
163
+ * Context shared with every tab. Tabs read live values via getters so they
164
+ * always see the latest spec/state/catalog without needing to re-mount.
165
+ */
166
+ interface PanelContext {
167
+ /** Event log shared by all panels. */
168
+ events: EventStore;
169
+ /** Returns the current (usually latest) spec or `null`. */
170
+ getSpec: () => Spec | null;
171
+ /**
172
+ * Optional: returns every inspectable spec on the page.
173
+ * Hosts with multiple concurrent renderers (e.g. a chat with one spec
174
+ * per assistant message) provide this so the Spec tab can offer a
175
+ * generation switcher. When absent, the tab falls back to `getSpec()`
176
+ * alone.
177
+ */
178
+ getSpecs?: () => SpecEntry[];
179
+ /** Returns the current catalog or `null`. */
180
+ getCatalog: () => Catalog | null;
181
+ /** Returns the live `StateStore` or `null` (only set when a provider is mounted). */
182
+ getStateStore: () => StateStore | null;
183
+ /** Start a picker session. Returns `null` if no DOM is available. */
184
+ startPicker?: (options: PickerOptions) => PickerSession | null;
185
+ /** Selection channel shared across tabs (picker <-> Spec tab). */
186
+ selection: SelectionBus;
187
+ /** Activate a specific tab by id. */
188
+ activateTab: (id: string) => void;
189
+ }
190
+ /**
191
+ * Bus for syncing the currently focused spec element key across tabs and the
192
+ * picker overlay. Tabs read the current value and subscribe to changes;
193
+ * anything that wants to focus an element (picker, tree click) calls `set`.
194
+ */
195
+ interface SelectionBus {
196
+ get: () => string | null;
197
+ set: (key: string | null) => void;
198
+ subscribe: (listener: (key: string | null) => void) => () => void;
199
+ }
200
+ /** Create an in-memory {@link SelectionBus}. */
201
+ declare function createSelectionBus(): SelectionBus;
202
+ /**
203
+ * A tab registered with the panel. Tabs are rendered lazily — `mount` is
204
+ * called the first time the tab is activated, and `update` is called on
205
+ * every re-render request after that.
206
+ */
207
+ interface TabDef {
208
+ /** Stable tab id used in URL hash / storage. */
209
+ id: string;
210
+ /** Short label shown in the tab strip. */
211
+ label: string;
212
+ /** Optional count badge, recomputed on every render. */
213
+ badge?: (ctx: PanelContext) => number | string | undefined;
214
+ /** Create the DOM for this tab. Called once per activation. */
215
+ mount: (root: HTMLElement, ctx: PanelContext) => TabInstance;
216
+ }
217
+ /**
218
+ * A live tab. `update` is called whenever an event fires or the host
219
+ * refreshes; `destroy` is called when the tab is unmounted.
220
+ */
221
+ interface TabInstance {
222
+ /** Re-render with latest data. */
223
+ update: () => void;
224
+ /** Clean up subscriptions / listeners. */
225
+ destroy?: () => void;
226
+ }
227
+ /**
228
+ * Options for {@link createPanel}.
229
+ */
230
+ interface PanelOptions {
231
+ /** Shared panel context. */
232
+ context: PanelContext;
233
+ /** Tabs to register. Order is preserved in the tab strip. */
234
+ tabs: TabDef[];
235
+ /** Host-document node to mount under. Default: `document.body`. */
236
+ host?: HTMLElement;
237
+ /** Position of the floating toggle button. */
238
+ position?: PanelPosition;
239
+ /** Start open. Default: `false`. */
240
+ initialOpen?: boolean;
241
+ /**
242
+ * Keyboard shortcut to toggle the panel, or `false` to disable.
243
+ * Syntax: `"mod+shift+j"` (`mod` = meta on mac, ctrl elsewhere).
244
+ * Default: `"mod+shift+j"`.
245
+ */
246
+ hotkey?: string | false;
247
+ /**
248
+ * Whether the panel should push the host app's content out of the way
249
+ * while open, by applying `padding` on `<body>` matching the panel's size.
250
+ *
251
+ * - `true` (default) — body padding is applied. Works for document-flow
252
+ * layouts and `height: 100%` app shells. Modern apps that use
253
+ * `height: 100vh`, `position: fixed; bottom: 0` etc. are effectively
254
+ * *overlaid* by the panel because CSS can't resize `100vh`. Pair this
255
+ * with `"right"` docking or opt those specific elements in via
256
+ * `bottom: var(--jr-devtools-offset-bottom, 0)` /
257
+ * `right: var(--jr-devtools-offset-right, 0)`.
258
+ * - `false` — the panel is a pure overlay. Nothing is pushed; the host
259
+ * app remains visually unchanged. The CSS custom properties
260
+ * `--jr-devtools-offset-bottom` / `--jr-devtools-offset-right` are
261
+ * still published on `:root` so apps can reserve their own space.
262
+ */
263
+ reserveSpace?: boolean;
264
+ /**
265
+ * When `true` (default), show a toolbar button that lets the user flip
266
+ * the panel between bottom-dock and right-dock. The user's choice
267
+ * persists across reloads (localStorage key `__jr_devtools_dock`), and
268
+ * takes precedence over the initial `position` once set.
269
+ *
270
+ * When `false`, the dock follows `position` exactly and the toolbar
271
+ * button is not rendered — useful when the host app's layout only works
272
+ * with one dock.
273
+ */
274
+ allowDockToggle?: boolean;
275
+ }
276
+ /**
277
+ * Returned from {@link createPanel}. Controls the panel lifecycle.
278
+ */
279
+ interface PanelHandle {
280
+ /** Show the panel. */
281
+ open: () => void;
282
+ /** Hide the panel. */
283
+ close: () => void;
284
+ /** Toggle open/closed. */
285
+ toggle: () => void;
286
+ /** True if the panel is currently open. */
287
+ isOpen: () => boolean;
288
+ /** Request that all visible tabs re-render. */
289
+ refresh: () => void;
290
+ /** Remove the panel from the DOM and clean up. */
291
+ destroy: () => void;
292
+ }
293
+
294
+ /**
295
+ * Create a devtools panel and mount it to the host document.
296
+ *
297
+ * Idempotent per-host: if a panel is already mounted under the given host,
298
+ * this returns that panel's handle instead of creating a duplicate.
299
+ */
300
+ declare function createPanel(options: PanelOptions): PanelHandle;
301
+ /**
302
+ * Mount a panel with a minimal placeholder for each tab. Useful for tests
303
+ * or environments where individual tab implementations aren't available.
304
+ *
305
+ * In production, consumers use {@link createPanel} with the full tab set.
306
+ */
307
+ declare function createStubTab(id: string, label: string): TabDef;
308
+
309
+ /**
310
+ * Tiny hyperscript-style helper for building DOM nodes without JSX.
311
+ *
312
+ * Keeps the vanilla-TS panel readable. Supports attributes starting with
313
+ * `on` as event listeners, `className`, inline `style` objects, and a mix
314
+ * of string / node / array children.
315
+ */
316
+ type Child = Node | string | number | null | undefined | false | Child[];
317
+ type Attrs = {
318
+ [key: string]: unknown;
319
+ className?: string;
320
+ style?: Partial<CSSStyleDeclaration>;
321
+ };
322
+ declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Attrs | null, ...children: Child[]): HTMLElementTagNameMap[K];
323
+ /** Replace the children of `parent` with `next`, clearing first. */
324
+ declare function replaceChildren(parent: Node, ...next: Child[]): void;
325
+ /** Render a short JSON preview that truncates long values. */
326
+ declare function jsonPreview(value: unknown, max?: number): string;
327
+ /** Format an absolute timestamp (ms) as `HH:MM:SS.mmm`. */
328
+ declare function formatTime(at: number): string;
329
+
330
+ /**
331
+ * Spec Tree tab — renders the element tree with expand/collapse, shows a
332
+ * one-line summary per node, and highlights structural issues from
333
+ * `validateSpec`. Selecting a node fills the right-hand detail pane with
334
+ * full props + metadata.
335
+ */
336
+ declare function specTab(): TabDef;
337
+
338
+ /**
339
+ * State Inspector tab — lists every leaf path in the state model with an
340
+ * inline editor. Editing a value re-parses it as JSON; if the input
341
+ * doesn't parse, the raw string is used. Reads a snapshot from the
342
+ * underlying StateStore on every render, plus on every state-set event.
343
+ */
344
+ declare function stateTab(): TabDef;
345
+
346
+ /**
347
+ * Action Log tab — chronologically ordered list of dispatched actions.
348
+ * Each dispatch is paired with its settle event (by `id`) to show
349
+ * duration + success/failure.
350
+ */
351
+ declare function actionsTab(): TabDef;
352
+
353
+ /**
354
+ * Stream Log tab — shows spec patches, text chunks, lifecycle markers,
355
+ * and token usage grouped by generation (lifecycle start -> end).
356
+ */
357
+ declare function streamTab(): TabDef;
358
+
359
+ interface CatalogField {
360
+ name: string;
361
+ type: string;
362
+ }
363
+ interface CatalogComponentInfo {
364
+ name: string;
365
+ description: string;
366
+ props: CatalogField[];
367
+ slots: string[];
368
+ events: string[];
369
+ }
370
+ interface CatalogActionInfo {
371
+ name: string;
372
+ description: string;
373
+ params: CatalogField[];
374
+ }
375
+ /**
376
+ * Catalog Browser tab — lists the components and actions declared in the
377
+ * attached catalog with prop/param chips, so developers can see what the
378
+ * AI can use.
379
+ */
380
+ declare function catalogTab(): TabDef;
381
+ /**
382
+ * Extract display data from a raw catalog definition. Handles both Zod v3
383
+ * (`_def.shape()`) and v4 (`shape` getter). Resilient to missing fields.
384
+ *
385
+ * Same logic as `apps/web/lib/render/catalog-display.ts` but lives here
386
+ * so framework adapters don't depend on the web app.
387
+ */
388
+ declare function buildCatalogDisplayData(rawCatalogData: unknown): {
389
+ components: CatalogComponentInfo[];
390
+ actions: CatalogActionInfo[];
391
+ };
392
+
393
+ /**
394
+ * Picker tab — explains the DOM picker flow, starts / stops a picker
395
+ * session, and shows the currently selected element key. Selecting a key
396
+ * in the spec tab reflects here; selecting here (via click-to-pick)
397
+ * jumps back to the spec tab.
398
+ */
399
+ declare function pickerTab(): TabDef;
400
+
401
+ /**
402
+ * Attribute the ElementRenderer (when devtools is active) sets on each
403
+ * rendered element's wrapper. The picker walks up from the hovered node
404
+ * to the nearest ancestor bearing this attribute.
405
+ */
406
+ declare const DEVTOOLS_KEY_ATTR = "data-jr-key";
407
+ /**
408
+ * Start an interactive DOM picker. The user hovers or clicks any element
409
+ * in the page; when a click lands on (or inside) a node carrying
410
+ * {@link DEVTOOLS_KEY_ATTR}, `onPick` fires with that key.
411
+ *
412
+ * Returns `null` in environments without a DOM (SSR, tests).
413
+ *
414
+ * The picker paints a translucent overlay + border over the hovered
415
+ * element (Chrome-DevTools style). See {@link computeBounds} for the
416
+ * `display: contents` fallback.
417
+ */
418
+ declare function startPicker(options: PickerOptions): PickerSession | null;
419
+ /**
420
+ * Show (or hide) a persistent bounding-box overlay over the element
421
+ * identified by `key`. Pass `null` to clear the overlay.
422
+ *
423
+ * Unlike {@link highlightElement}, this does not fade out — callers are
424
+ * expected to clear it explicitly (typically on `mouseleave`). Safe to
425
+ * call repeatedly; each call cheaply repositions the single overlay.
426
+ */
427
+ declare function setHoverHighlight(key: string | null): void;
428
+ /**
429
+ * Look up the DOM element carrying `data-jr-key={key}`. Returns `null`
430
+ * when no match (spec exists but hasn't rendered, or the element is
431
+ * currently hidden by a visibility condition).
432
+ *
433
+ * Note: when an element appears inside a `repeat`, each repetition
434
+ * renders its own DOM instance with the same `data-jr-key`. Use
435
+ * {@link findAllElementsByKey} if you need all of them.
436
+ */
437
+ declare function findElementByKey(key: string): Element | null;
438
+ /**
439
+ * Return every DOM element carrying `data-jr-key={key}`. Intended for
440
+ * repeated spec elements (list items, grid cells…) where a single spec
441
+ * key renders to N DOM instances.
442
+ */
443
+ declare function findAllElementsByKey(key: string): Element[];
444
+ /**
445
+ * Briefly paint an outline around the DOM element for `key`, so clicking
446
+ * a spec-tree row in the panel flashes its position in the page.
447
+ *
448
+ * The target is often a `display: contents` wrapper, so we build the
449
+ * overlay from `computeBounds` (descendants' client rects) instead of
450
+ * using a CSS outline on the target directly.
451
+ */
452
+ declare function highlightElement(key: string, durationMs?: number): void;
453
+
454
+ /**
455
+ * A minimal shape compatible with AI SDK `UIMessage.parts[i]`. Defined
456
+ * locally so the devtools package has no dependency on the `ai` package.
457
+ */
458
+ interface DataPart {
459
+ type: string;
460
+ text?: string;
461
+ data?: unknown;
462
+ state?: string;
463
+ }
464
+ /**
465
+ * Wrap a `ReadableStream<StreamChunk>` (e.g. the output of `pipeJsonRender`
466
+ * or any `TransformStream<StreamChunk, StreamChunk>`) so spec patches are
467
+ * mirrored into a devtools `EventStore` as they fly past.
468
+ *
469
+ * Returns the original stream unchanged — the tap just forks a copy.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * writer.merge(
474
+ * tapJsonRenderStream(
475
+ * pipeJsonRender(result.toUIMessageStream()),
476
+ * devtoolsEventStore,
477
+ * ),
478
+ * );
479
+ * ```
480
+ */
481
+ declare function tapJsonRenderStream(stream: ReadableStream<StreamChunk>, events: EventStore): ReadableStream<StreamChunk>;
482
+ /** YAML variant — identical behaviour with `source: "yaml"`. */
483
+ declare function tapYamlStream(stream: ReadableStream<StreamChunk>, events: EventStore): ReadableStream<StreamChunk>;
484
+ /**
485
+ * Client-side helper: scan a message's `parts` array for spec data parts
486
+ * and push matching events into the store. Use this when rendering chat
487
+ * UIs that use `@ai-sdk/react` `useChat` and pass in `messages`.
488
+ *
489
+ * Idempotent via `seenIds`: only parts not already recorded will emit.
490
+ * Returns the new `seenIds` set so callers can persist it between calls.
491
+ */
492
+ declare function scanMessageParts(parts: readonly DataPart[] | undefined, events: EventStore, seen: WeakSet<object>): void;
493
+ /**
494
+ * Emit a token-usage event. Called by adapters with whatever usage
495
+ * object their framework surfaces (AI SDK `result.usage`).
496
+ */
497
+ declare function recordUsage(events: EventStore, usage: TokenUsage): void;
498
+ /**
499
+ * Replay every `SPEC_DATA_PART` in `parts` and return the resulting spec,
500
+ * or `null` if the message has no UI. Mirrors the framework-side
501
+ * `buildSpecFromParts` helper but without a framework dependency — used by
502
+ * the devtools panel's generation switcher to reconstruct each prior
503
+ * assistant message's spec so the user can inspect any of them.
504
+ */
505
+ declare function extractSpecFromParts(parts: readonly DataPart[] | undefined): Spec | null;
506
+ /** Emit a pre-built devtools event. Useful for manual instrumentation. */
507
+ declare function recordEvent(events: EventStore, event: DevtoolsEvent): void;
508
+
509
+ export { DEVTOOLS_KEY_ATTR, type DevtoolsEvent, type DevtoolsEventKind, type EventStore, type EventStoreOptions, type PanelContext, type PanelHandle, type PanelOptions, type PanelPosition, type PickerOptions, type PickerSession, type SelectionBus, type SpecEntry, type TabDef, type TabInstance, type TokenUsage, actionsTab, buildCatalogDisplayData, catalogTab, createEventStore, createPanel, createSelectionBus, createStubTab, extractSpecFromParts, findAllElementsByKey, findElementByKey, formatTime, h, highlightElement, isProduction, jsonPreview, pickerTab, recordEvent, recordUsage, replaceChildren, scanMessageParts, setHoverHighlight, specTab, startPicker, stateTab, streamTab, tapJsonRenderStream, tapYamlStream };