ngx-json-render 0.7.8 → 0.8.1

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.
package/README.md CHANGED
@@ -62,7 +62,7 @@ the Material catalog has one too (`--skill ngx-json-render-material`).
62
62
 
63
63
  Writing a catalog is the honest first step, but you do not have to take it to
64
64
  see a spec render. [`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)
65
- ships 28 Angular Material components already registered, so a generated spec
65
+ ships 29 Angular Material components already registered, so a generated spec
66
66
  renders with nothing else wired up:
67
67
 
68
68
  ```bash
@@ -119,7 +119,7 @@ against `@hashbrownai/angular` 0.6.1, as published on 2026-09-30:
119
119
  | What the model emits | A json-render spec, streamed as RFC 6902 patches — the format the React, Vue, Svelte and Solid renderers read, so a catalog and a spec move between frameworks unchanged | Hashbrown's own JSON, generated against a schema built from the components you expose; read by Hashbrown's Angular and React clients |
120
120
  | Server | Anything that streams text: the [AI SDK](https://ai-sdk.dev) in a few lines, or your own endpoint; `catalog.prompt()` writes the system prompt | An [AG-UI](https://docs.ag-ui.com) endpoint, built with Hashbrown's adapters (OpenAI, Anthropic, Google, Azure, Bedrock, Ollama) or your own mapping |
121
121
  | Angular | 20, 21 and 22, each tested in CI | 22 only since 0.6 (0.5 supported 20 and 21) |
122
- | Components | Your own, or 28 ready-made Angular Material ones ([`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)) | Your own (`exposeComponent`, UI kits) |
122
+ | Components | Your own, or 29 ready-made Angular Material ones ([`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)) | Your own (`exposeComponent`, UI kits) |
123
123
  | Beyond rendering | Rendering-focused: state and two-way bindings in the spec, actions, visibility, repeat, watch, confirmation dialogs | An agent toolkit: client-side tools, a QuickJS sandbox for model-written code, shared agent state, interrupts, streaming Markdown |
124
124
  | License | Apache-2.0 | MIT |
125
125
 
@@ -135,7 +135,7 @@ ready-made Material catalog before you have written a component.
135
135
 
136
136
  ```ts
137
137
  // catalog.ts
138
- import { schema } from 'ngx-json-render';
138
+ import { schema } from 'ngx-json-render/schema';
139
139
  import { z } from 'zod';
140
140
 
141
141
  export const catalog = schema.createCatalog({
@@ -302,10 +302,44 @@ app.post('/api/generate', async (req, res) => {
302
302
  });
303
303
  ```
304
304
 
305
- `catalog.ts` imports `schema` from `ngx-json-render`, and that entry point also loads Angular and the renderer's components, which are published partially compiled. In an Angular SSR app (`ng new --ssr`) the Angular CLI finishes compiling them when it builds the server, so the route above goes into its `server.ts` unchanged and works both under `ng serve` and from the built server. On Angular 20 and 21, add `"prebundle": { "exclude": ["zod"] }` to the `serve` options in `angular.json` first: their Vite 7 dev server can't transform zod for the server (`Cannot split a chunk that has already been edited`) and answers every page and API route with a 500. A Node server the Angular CLI doesn't build, run with `node` or `tsx`, fails on that import with `'@angular/compiler' is not available` — make `import '@angular/compiler';` its first import.
305
+ `catalog.ts` imports `schema` from `ngx-json-render/schema`, an entry point that needs nothing but `@json-render/core`, so any server can load the catalog without Angular: plain Node, `tsx`, an edge function. (`ngx-json-render` exports the same `schema`, but that entry point also loads Angular and the renderer's partially compiled components. A Node server the Angular CLI doesn't build then fails with `'@angular/compiler' is not available`.) In an Angular SSR app (`ng new --ssr`) the route goes into its `server.ts`. On Angular 20 and 21, add `"prebundle": { "exclude": ["zod"] }` to the `serve` options in `angular.json` first: their Vite 7 dev server can't transform zod for the server (`Cannot split a chunk that has already been edited`) and answers every page and API route with a 500.
306
306
 
307
307
  The patches apply to the `spec` signal as each line arrives, so the UI assembles on screen while the model is still generating — exactly what the [demo's Streaming tab](https://shteynu.github.io/ngx-json-render/) replays. Prefer structured output? `catalog.jsonSchema()` exports a JSON Schema for `streamObject`/tool calls, and `checkSpec(spec, 'strict', { catalog })` checks a finished spec against the catalog, props included — see [Checking what the model produced](#checking-what-the-model-produced) for why that and not `catalog.validate(spec)` alone.
308
308
 
309
+ ### With Genkit
310
+
311
+ On Firebase, or anywhere else [Genkit](https://genkit.dev) already runs, the route has the same shape. `ai.generateStream` streams the model's text, and that text is the JSONL the system prompt asked for, so it goes to the browser unchanged:
312
+
313
+ ```ts
314
+ // server.ts — Express with Genkit and its Google AI plugin
315
+ import express from 'express';
316
+ import { genkit } from 'genkit';
317
+ import { googleAI } from '@genkit-ai/google-genai';
318
+ import { buildUserPrompt } from '@json-render/core';
319
+ import { catalog } from './catalog';
320
+
321
+ const ai = genkit({ plugins: [googleAI()] }); // reads GEMINI_API_KEY
322
+
323
+ const app = express();
324
+ app.use(express.json());
325
+
326
+ app.post('/api/generate', async (req, res) => {
327
+ const { prompt, currentSpec } = req.body; // and `context`, yours to use
328
+
329
+ const { stream } = ai.generateStream({
330
+ model: googleAI.model('gemini-3.5-flash'),
331
+ system: catalog.prompt(),
332
+ prompt: buildUserPrompt({ prompt, currentSpec }), // currentSpec: refine, not restart
333
+ });
334
+
335
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
336
+ for await (const chunk of stream) res.write(chunk.text);
337
+ res.end();
338
+ });
339
+ ```
340
+
341
+ `buildUserPrompt` comes from `@json-render/core`. Given a `currentSpec`, it asks the model for patches to that spec, so `ui.send(prompt, { previousSpec })` refines the UI instead of regenerating it; without one, it adds a reminder of the order to stream in. Another model is another plugin and `model` line, such as `vertexAI` from the same package. Serve the text from a plain route like this one rather than through `expressHandler` from `@genkit-ai/express`: that wraps every chunk in its own server-sent event (`data: {"message": …}`), which `injectUIStream` does not read.
342
+
309
343
  ### Chat + GenUI
310
344
 
311
345
  When the model answers in prose _and_ renders a UI in the same turn, reach for
@@ -346,6 +380,14 @@ readonly msg = jsonRenderMessage(() => this.message().parts);
346
380
  // template: {{ msg.text() }} @if (msg.hasSpec()) { <json-render [spec]="msg.spec()" ... /> }
347
381
  ```
348
382
 
383
+ With `@ai-sdk/angular`, hand that component a copy of the message being
384
+ streamed. The SDK's `Chat` writes every chunk into one message object and
385
+ passes that same object back each time, so a signal input holding it never
386
+ changes, and the message renders empty even after the stream ends.
387
+ [`examples/ai-sdk-chat`](https://github.com/shteynu/ngx-json-render/tree/main/examples/ai-sdk-chat)
388
+ is the whole path, server to rendered message, and makes the copy in
389
+ `app.ts`.
390
+
349
391
  Two things about the parts themselves, both the SDK's semantics rather than
350
392
  this package's, and both silent when you get them wrong:
351
393
 
@@ -507,6 +549,52 @@ stack proving they exceed it. If you accept specs you did not generate, set
507
549
  `maxDepth` — with no cap there is nothing to stop the check from recursing as
508
550
  deep as the spec asks.
509
551
 
552
+ ## Inside an MCP App host
553
+
554
+ An [MCP App](https://modelcontextprotocol.io/docs/extensions/apps) is a tool whose result a host (Claude, ChatGPT, VS Code) renders as an inline view. `ngx-json-render/mcp` is the Angular side of that view: `injectJsonRenderApp()` connects to the host and keeps the spec the model passed to the tool in a signal. It is the counterpart of `useJsonRenderApp` from `@json-render/mcp/app`. It needs two optional peers:
555
+
556
+ ```bash
557
+ npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
558
+ ```
559
+
560
+ ```ts
561
+ import { Component } from '@angular/core';
562
+ import { JsonRenderer } from 'ngx-json-render';
563
+ import { injectJsonRenderApp } from 'ngx-json-render/mcp';
564
+ import { materialRegistry } from 'ngx-json-render-material';
565
+
566
+ @Component({
567
+ selector: 'app-root',
568
+ imports: [JsonRenderer],
569
+ template: `<json-render [spec]="mcp.spec()" [loading]="mcp.loading()" [registry]="registry" />`,
570
+ })
571
+ export class App {
572
+ readonly mcp = injectJsonRenderApp({ name: 'my-app', version: '1.0.0' });
573
+ readonly registry = materialRegistry;
574
+ }
575
+ ```
576
+
577
+ The spec renders while the model is still writing the tool call, from the host's `toolinputpartial` notifications; pass `streamPartialInput: false` to wait for the result. `mcp.sendMessage(text, data)` posts a user message to the chat, so a button can continue the conversation, and `mcp.callServerTool(name, args)` replaces the spec with another tool's result. The connection closes with the injector that created it.
578
+
579
+ The server side is upstream's `@json-render/mcp`, which serves the view as a single HTML resource. [`projects/mcp-app`](https://github.com/shteynu/ngx-json-render/tree/main/projects/mcp-app) is a complete example with the Material catalog: the view, the build that inlines it into one page, and a server that works around two `createMcpApp` problems.
580
+
581
+ ## Devtools
582
+
583
+ `ngx-json-render/devtools` is the Angular counterpart of `@json-render/devtools-react` (and its Vue, Svelte and Solid siblings): a floating panel with the spec tree, the live state, every action dispatched, every stream patch with its token usage, and the catalog, plus a picker that finds the element under the pointer. The panel itself is upstream's `@json-render/devtools`, an optional peer. It pins `@json-render/core` exactly, so install the version that matches your core:
584
+
585
+ ```bash
586
+ npm install -D @json-render/devtools@0.21
587
+ ```
588
+
589
+ ```html
590
+ <json-render #renderer [spec]="ui.spec()" [registry]="registry" />
591
+ <json-render-devtools [renderer]="renderer" [catalog]="catalog" />
592
+ ```
593
+
594
+ Angular has no provider above `<json-render>` for the panel to read, so the renderer is passed in by template reference. Streams need no wiring: every `injectUIStream` and `injectChatUI` generation in the app shows up in the Stream tab. Pass `[chat]="chat"` and each assistant reply with a spec becomes a generation you can switch between. `Cmd/Ctrl+Shift+J` toggles the panel (`hotkey`), and `(event)` hands every recorded event to your own logging.
595
+
596
+ It renders nothing outside dev mode, and loads the panel with a dynamic `import()` that a production build removes, so you can leave the tag in your template.
597
+
510
598
  ## Testing
511
599
 
512
600
  `ngx-json-render/testing` is a separate entry point, so nothing in it can reach
@@ -874,6 +962,12 @@ is how you tell the two apart. One dialog is open at a time: an action with a
874
962
 
875
963
  Registry & schema: `defineRegistry`, `createStoreSetState`, `schema`, and the catalog types `InferComponentProps`, `InferCatalogComponents`, `InferActionParams` (re-exported from core).
876
964
 
965
+ Schema alone (`ngx-json-render/schema`): `schema`, `AngularSchema`, `AngularSpec`, with no Angular behind them, for a server that defines a catalog.
966
+
967
+ MCP Apps (`ngx-json-render/mcp`): `injectJsonRenderApp`, `parseSpecFromToolResult`, `messageText`, and the types `JsonRenderApp`, `JsonRenderAppOptions`. Needs the optional peers `@modelcontextprotocol/ext-apps` and `@modelcontextprotocol/sdk`.
968
+
969
+ Devtools (`ngx-json-render/devtools`): `JsonRenderDevtools` (`<json-render-devtools>`), and the types `DevtoolsEvent`, `PanelPosition`. Needs the optional peer `@json-render/devtools`.
970
+
877
971
  Testing (`ngx-json-render/testing`): `renderSpec`, `renderComponent`, `recordedTransport`, `specStream`, `usageLine`.
878
972
 
879
973
  Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`, prompt builders, spec validators, SpecStream compiler) composes with this package; the most common symbols are re-exported.
@@ -0,0 +1,262 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, inject, DestroyRef, isDevMode, afterNextRender, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { registerActionObserver, markDevtoolsActive } from '@json-render/core';
4
+ import { ɵregisterStreamObserver as _registerStreamObserver } from 'ngx-json-render';
5
+
6
+ /**
7
+ * A floating json-render devtools panel for an Angular app — the counterpart
8
+ * of `<JsonRenderDevtools />` in `@json-render/devtools-react`, `-vue`,
9
+ * `-svelte` and `-solid`, with the same tabs (Spec, State, Actions, Stream,
10
+ * Catalog) and the same element picker.
11
+ *
12
+ * Angular has no provider above `<json-render>` to read the renderer from, so
13
+ * the renderer is passed in by template reference. Streams need no wiring:
14
+ * every `injectUIStream` and `injectChatUI` generation in the app reaches the
15
+ * Stream tab.
16
+ *
17
+ * Renders nothing outside dev mode, and loads the panel itself
18
+ * (`@json-render/devtools`) with a dynamic `import()`, so a production build
19
+ * never fetches it.
20
+ *
21
+ * @example
22
+ * ```html
23
+ * <json-render #renderer [spec]="ui.spec()" [registry]="registry" />
24
+ * <json-render-devtools [renderer]="renderer" [catalog]="catalog" />
25
+ * ```
26
+ */
27
+ class JsonRenderDevtools {
28
+ /** The renderer to inspect: its spec, state store and catalog. */
29
+ renderer = input(null, ...(ngDevMode ? [{ debugName: "renderer" }] : /* istanbul ignore next */ []));
30
+ /** Spec to show instead of the renderer's. */
31
+ spec = input(undefined, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
32
+ /** Catalog for the Catalog tab, if the renderer was not given one. */
33
+ catalog = input(undefined, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
34
+ /**
35
+ * A chat from `injectChatUI`: every assistant reply with a spec becomes a
36
+ * generation in the Spec tab's switcher.
37
+ */
38
+ chat = input(null, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
39
+ initialOpen = input(false, ...(ngDevMode ? [{ debugName: "initialOpen" }] : /* istanbul ignore next */ []));
40
+ position = input('bottom-right', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
41
+ /** Toggle shortcut; `false` disables it. */
42
+ hotkey = input('mod+shift+j', ...(ngDevMode ? [{ debugName: "hotkey" }] : /* istanbul ignore next */ []));
43
+ /** Events kept in the ring buffer. Read once, at creation. */
44
+ bufferSize = input(500, ...(ngDevMode ? [{ debugName: "bufferSize" }] : /* istanbul ignore next */ []));
45
+ /** Pad the page while the panel is open instead of overlaying it. */
46
+ reserveSpace = input(true, ...(ngDevMode ? [{ debugName: "reserveSpace" }] : /* istanbul ignore next */ []));
47
+ /** Let the viewer flip the panel between bottom and right dock. */
48
+ allowDockToggle = input(true, ...(ngDevMode ? [{ debugName: "allowDockToggle" }] : /* istanbul ignore next */ []));
49
+ /** Every event the panel records, for an app's own logging. */
50
+ event = output();
51
+ destroyRef = inject(DestroyRef);
52
+ events = null;
53
+ panel = null;
54
+ destroyed = false;
55
+ constructor() {
56
+ if (!isDevMode())
57
+ return;
58
+ this.destroyRef.onDestroy(() => (this.destroyed = true));
59
+ afterNextRender(() => {
60
+ void this.mount();
61
+ });
62
+ // The panel reads everything through getters, so it only needs telling
63
+ // when to re-read: whenever the spec, the chat or the renderer changes.
64
+ effect(() => {
65
+ this.currentSpec();
66
+ this.chat()?.messages();
67
+ this.renderer();
68
+ untracked(() => this.panel?.refresh());
69
+ });
70
+ // A state write becomes an event, which redraws the State tab — the
71
+ // store has no subscribe of the shape the panel's StateStore expects.
72
+ effect((onCleanup) => {
73
+ const store = this.renderer()?.stateStore;
74
+ if (!store)
75
+ return;
76
+ onCleanup(store.subscribeChanges((changes) => {
77
+ for (const change of changes) {
78
+ this.events?.push({
79
+ kind: 'state-set',
80
+ at: Date.now(),
81
+ path: change.path,
82
+ prev: undefined,
83
+ next: change.value,
84
+ });
85
+ }
86
+ }));
87
+ });
88
+ }
89
+ currentSpec() {
90
+ const override = this.spec();
91
+ if (override !== undefined)
92
+ return override;
93
+ const chat = this.chat();
94
+ if (chat) {
95
+ const replies = chat.messages().filter((m) => m.spec);
96
+ return replies.at(-1)?.spec ?? null;
97
+ }
98
+ return this.renderer()?.spec() ?? null;
99
+ }
100
+ currentCatalog() {
101
+ const own = this.catalog();
102
+ if (own !== undefined)
103
+ return own;
104
+ // The renderer's catalog input is typed structurally; a catalog made with
105
+ // `schema.createCatalog` is a core Catalog, which is what the tab reads.
106
+ const fromRenderer = this.renderer()?.catalog();
107
+ return fromRenderer && 'data' in fromRenderer
108
+ ? fromRenderer
109
+ : null;
110
+ }
111
+ generations() {
112
+ const chat = this.chat();
113
+ if (!chat) {
114
+ const spec = this.currentSpec();
115
+ return spec ? [{ id: 'current', label: 'Current', spec }] : [];
116
+ }
117
+ return chat
118
+ .messages()
119
+ .filter((m) => m.role === 'assistant' && m.spec)
120
+ .map((m, i) => ({
121
+ id: m.id,
122
+ label: `Generation ${i + 1}`,
123
+ spec: m.spec,
124
+ }));
125
+ }
126
+ stateStore() {
127
+ const store = this.renderer()?.stateStore;
128
+ if (!store)
129
+ return null;
130
+ return {
131
+ get: (path) => store.get(path),
132
+ set: (path, value) => store.set(path, value),
133
+ update: (updates) => store.update(updates),
134
+ getSnapshot: () => store.getSnapshot(),
135
+ subscribe: () => () => { },
136
+ };
137
+ }
138
+ async mount() {
139
+ // A production build defines `ngDevMode` as false, so the bundler drops
140
+ // the import below: nothing references the panel chunk, even if esbuild
141
+ // still writes it to disk.
142
+ if (typeof ngDevMode !== 'undefined' && !ngDevMode)
143
+ return;
144
+ const devtools = await import('@json-render/devtools');
145
+ if (this.destroyed)
146
+ return;
147
+ const events = devtools.createEventStore({
148
+ bufferSize: this.bufferSize(),
149
+ });
150
+ this.events = events;
151
+ const unsubscribeEvents = events.subscribe(() => {
152
+ const last = events.snapshot().at(-1);
153
+ if (last)
154
+ this.event.emit(last);
155
+ });
156
+ // Both observers come from the app's own `@json-render/core` and
157
+ // `ngx-json-render`, never through `@json-render/devtools`: the registries
158
+ // are module singletons, and devtools may carry a nested copy of core.
159
+ const unsubscribeActions = registerActionObserver({
160
+ onDispatch: (e) => events.push({
161
+ kind: 'action-dispatched',
162
+ at: e.at,
163
+ id: e.id,
164
+ name: e.name,
165
+ params: e.params,
166
+ }),
167
+ onSettle: (e) => events.push({
168
+ kind: 'action-settled',
169
+ at: e.at,
170
+ id: e.id,
171
+ ok: e.ok,
172
+ durationMs: e.durationMs,
173
+ result: e.result,
174
+ error: e.error !== undefined ? String(e.error) : undefined,
175
+ }),
176
+ });
177
+ const unsubscribeStreams = _registerStreamObserver({
178
+ onStart: () => events.push({
179
+ kind: 'stream-lifecycle',
180
+ at: Date.now(),
181
+ phase: 'start',
182
+ }),
183
+ onPatch: (patch) => events.push({
184
+ kind: 'stream-patch',
185
+ at: Date.now(),
186
+ patch,
187
+ source: 'json',
188
+ }),
189
+ onText: (text) => events.push({ kind: 'stream-text', at: Date.now(), text }),
190
+ onUsage: (usage) => devtools.recordUsage(events, usage),
191
+ onEnd: (ok) => events.push({
192
+ kind: 'stream-lifecycle',
193
+ at: Date.now(),
194
+ phase: 'end',
195
+ ok,
196
+ }),
197
+ });
198
+ const selection = devtools.createSelectionBus();
199
+ this.panel = devtools.createPanel({
200
+ context: {
201
+ events,
202
+ getSpec: () => this.currentSpec(),
203
+ getSpecs: () => this.generations(),
204
+ getCatalog: () => this.currentCatalog(),
205
+ getStateStore: () => this.stateStore(),
206
+ startPicker: (options) => devtools.startPicker(options),
207
+ selection,
208
+ activateTab: () => { },
209
+ },
210
+ tabs: [
211
+ devtools.specTab(),
212
+ devtools.stateTab(),
213
+ devtools.actionsTab(),
214
+ devtools.streamTab(),
215
+ devtools.catalogTab(),
216
+ ],
217
+ initialOpen: this.initialOpen(),
218
+ position: this.position(),
219
+ hotkey: this.hotkey(),
220
+ reserveSpace: this.reserveSpace(),
221
+ allowDockToggle: this.allowDockToggle(),
222
+ });
223
+ const unsubscribeSelection = selection.subscribe((key) => {
224
+ if (key)
225
+ devtools.highlightElement(key);
226
+ });
227
+ // Makes every renderer wrap its elements in `data-jr-key` spans, which is
228
+ // what the picker and the highlight look for.
229
+ const releaseActive = markDevtoolsActive();
230
+ this.destroyRef.onDestroy(() => {
231
+ releaseActive();
232
+ unsubscribeSelection();
233
+ this.panel?.destroy();
234
+ this.panel = null;
235
+ unsubscribeStreams();
236
+ unsubscribeActions();
237
+ unsubscribeEvents();
238
+ this.events = null;
239
+ });
240
+ }
241
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderDevtools, deps: [], target: i0.ɵɵFactoryTarget.Component });
242
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.22", type: JsonRenderDevtools, isStandalone: true, selector: "json-render-devtools", inputs: { renderer: { classPropertyName: "renderer", publicName: "renderer", isSignal: true, isRequired: false, transformFunction: null }, spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null }, catalog: { classPropertyName: "catalog", publicName: "catalog", isSignal: true, isRequired: false, transformFunction: null }, chat: { classPropertyName: "chat", publicName: "chat", isSignal: true, isRequired: false, transformFunction: null }, initialOpen: { classPropertyName: "initialOpen", publicName: "initialOpen", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, bufferSize: { classPropertyName: "bufferSize", publicName: "bufferSize", isSignal: true, isRequired: false, transformFunction: null }, reserveSpace: { classPropertyName: "reserveSpace", publicName: "reserveSpace", isSignal: true, isRequired: false, transformFunction: null }, allowDockToggle: { classPropertyName: "allowDockToggle", publicName: "allowDockToggle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { event: "event" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
243
+ }
244
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderDevtools, decorators: [{
245
+ type: Component,
246
+ args: [{
247
+ selector: 'json-render-devtools',
248
+ template: '',
249
+ changeDetection: ChangeDetectionStrategy.OnPush,
250
+ }]
251
+ }], ctorParameters: () => [], propDecorators: { renderer: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderer", required: false }] }], spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }], catalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: false }] }], chat: [{ type: i0.Input, args: [{ isSignal: true, alias: "chat", required: false }] }], initialOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialOpen", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], bufferSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferSize", required: false }] }], reserveSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "reserveSpace", required: false }] }], allowDockToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDockToggle", required: false }] }], event: [{ type: i0.Output, args: ["event"] }] } });
252
+
253
+ /*
254
+ * Public API Surface of ngx-json-render/devtools
255
+ */
256
+
257
+ /**
258
+ * Generated bundle index. Do not edit.
259
+ */
260
+
261
+ export { JsonRenderDevtools };
262
+ //# sourceMappingURL=ngx-json-render-devtools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-json-render-devtools.mjs","sources":["../../../projects/ngx-json-render/devtools/src/json-render-devtools.ts","../../../projects/ngx-json-render/devtools/src/public-api.ts","../../../projects/ngx-json-render/devtools/src/ngx-json-render-devtools.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n afterNextRender,\n effect,\n inject,\n input,\n isDevMode,\n output,\n untracked,\n} from '@angular/core';\nimport {\n type Catalog,\n type Spec,\n type StateStore,\n markDevtoolsActive,\n registerActionObserver,\n} from '@json-render/core';\nimport type {\n DevtoolsEvent,\n EventStore,\n PanelHandle,\n PanelPosition,\n SpecEntry,\n} from '@json-render/devtools';\nimport {\n type ChatUIReturn,\n type JsonRenderer,\n ɵregisterStreamObserver,\n} from 'ngx-json-render';\n\n/**\n * A floating json-render devtools panel for an Angular app — the counterpart\n * of `<JsonRenderDevtools />` in `@json-render/devtools-react`, `-vue`,\n * `-svelte` and `-solid`, with the same tabs (Spec, State, Actions, Stream,\n * Catalog) and the same element picker.\n *\n * Angular has no provider above `<json-render>` to read the renderer from, so\n * the renderer is passed in by template reference. Streams need no wiring:\n * every `injectUIStream` and `injectChatUI` generation in the app reaches the\n * Stream tab.\n *\n * Renders nothing outside dev mode, and loads the panel itself\n * (`@json-render/devtools`) with a dynamic `import()`, so a production build\n * never fetches it.\n *\n * @example\n * ```html\n * <json-render #renderer [spec]=\"ui.spec()\" [registry]=\"registry\" />\n * <json-render-devtools [renderer]=\"renderer\" [catalog]=\"catalog\" />\n * ```\n */\n@Component({\n selector: 'json-render-devtools',\n template: '',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class JsonRenderDevtools {\n /** The renderer to inspect: its spec, state store and catalog. */\n readonly renderer = input<JsonRenderer | null>(null);\n /** Spec to show instead of the renderer's. */\n readonly spec = input<Spec | null | undefined>(undefined);\n /** Catalog for the Catalog tab, if the renderer was not given one. */\n readonly catalog = input<Catalog | null | undefined>(undefined);\n /**\n * A chat from `injectChatUI`: every assistant reply with a spec becomes a\n * generation in the Spec tab's switcher.\n */\n readonly chat = input<ChatUIReturn | null>(null);\n readonly initialOpen = input(false);\n readonly position = input<PanelPosition>('bottom-right');\n /** Toggle shortcut; `false` disables it. */\n readonly hotkey = input<string | false>('mod+shift+j');\n /** Events kept in the ring buffer. Read once, at creation. */\n readonly bufferSize = input(500);\n /** Pad the page while the panel is open instead of overlaying it. */\n readonly reserveSpace = input(true);\n /** Let the viewer flip the panel between bottom and right dock. */\n readonly allowDockToggle = input(true);\n /** Every event the panel records, for an app's own logging. */\n readonly event = output<DevtoolsEvent>();\n\n private readonly destroyRef = inject(DestroyRef);\n private events: EventStore | null = null;\n private panel: PanelHandle | null = null;\n private destroyed = false;\n\n constructor() {\n if (!isDevMode()) return;\n\n this.destroyRef.onDestroy(() => (this.destroyed = true));\n afterNextRender(() => {\n void this.mount();\n });\n\n // The panel reads everything through getters, so it only needs telling\n // when to re-read: whenever the spec, the chat or the renderer changes.\n effect(() => {\n this.currentSpec();\n this.chat()?.messages();\n this.renderer();\n untracked(() => this.panel?.refresh());\n });\n\n // A state write becomes an event, which redraws the State tab — the\n // store has no subscribe of the shape the panel's StateStore expects.\n effect((onCleanup) => {\n const store = this.renderer()?.stateStore;\n if (!store) return;\n onCleanup(\n store.subscribeChanges((changes) => {\n for (const change of changes) {\n this.events?.push({\n kind: 'state-set',\n at: Date.now(),\n path: change.path,\n prev: undefined,\n next: change.value,\n });\n }\n }),\n );\n });\n }\n\n private currentSpec(): Spec | null {\n const override = this.spec();\n if (override !== undefined) return override;\n const chat = this.chat();\n if (chat) {\n const replies = chat.messages().filter((m) => m.spec);\n return replies.at(-1)?.spec ?? null;\n }\n return this.renderer()?.spec() ?? null;\n }\n\n private currentCatalog(): Catalog | null {\n const own = this.catalog();\n if (own !== undefined) return own;\n // The renderer's catalog input is typed structurally; a catalog made with\n // `schema.createCatalog` is a core Catalog, which is what the tab reads.\n const fromRenderer = this.renderer()?.catalog();\n return fromRenderer && 'data' in fromRenderer\n ? (fromRenderer as unknown as Catalog)\n : null;\n }\n\n private generations(): SpecEntry[] {\n const chat = this.chat();\n if (!chat) {\n const spec = this.currentSpec();\n return spec ? [{ id: 'current', label: 'Current', spec }] : [];\n }\n return chat\n .messages()\n .filter((m) => m.role === 'assistant' && m.spec)\n .map((m, i) => ({\n id: m.id,\n label: `Generation ${i + 1}`,\n spec: m.spec as Spec,\n }));\n }\n\n private stateStore(): StateStore | null {\n const store = this.renderer()?.stateStore;\n if (!store) return null;\n return {\n get: (path) => store.get(path),\n set: (path, value) => store.set(path, value),\n update: (updates) => store.update(updates),\n getSnapshot: () => store.getSnapshot(),\n subscribe: () => () => {},\n };\n }\n\n private async mount(): Promise<void> {\n // A production build defines `ngDevMode` as false, so the bundler drops\n // the import below: nothing references the panel chunk, even if esbuild\n // still writes it to disk.\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n const devtools = await import('@json-render/devtools');\n if (this.destroyed) return;\n\n const events = devtools.createEventStore({\n bufferSize: this.bufferSize(),\n });\n this.events = events;\n\n const unsubscribeEvents = events.subscribe(() => {\n const last = events.snapshot().at(-1);\n if (last) this.event.emit(last);\n });\n\n // Both observers come from the app's own `@json-render/core` and\n // `ngx-json-render`, never through `@json-render/devtools`: the registries\n // are module singletons, and devtools may carry a nested copy of core.\n const unsubscribeActions = registerActionObserver({\n onDispatch: (e) =>\n events.push({\n kind: 'action-dispatched',\n at: e.at,\n id: e.id,\n name: e.name,\n params: e.params,\n }),\n onSettle: (e) =>\n events.push({\n kind: 'action-settled',\n at: e.at,\n id: e.id,\n ok: e.ok,\n durationMs: e.durationMs,\n result: e.result,\n error: e.error !== undefined ? String(e.error) : undefined,\n }),\n });\n\n const unsubscribeStreams = ɵregisterStreamObserver({\n onStart: () =>\n events.push({\n kind: 'stream-lifecycle',\n at: Date.now(),\n phase: 'start',\n }),\n onPatch: (patch) =>\n events.push({\n kind: 'stream-patch',\n at: Date.now(),\n patch,\n source: 'json',\n }),\n onText: (text) =>\n events.push({ kind: 'stream-text', at: Date.now(), text }),\n onUsage: (usage) => devtools.recordUsage(events, usage),\n onEnd: (ok) =>\n events.push({\n kind: 'stream-lifecycle',\n at: Date.now(),\n phase: 'end',\n ok,\n }),\n });\n\n const selection = devtools.createSelectionBus();\n this.panel = devtools.createPanel({\n context: {\n events,\n getSpec: () => this.currentSpec(),\n getSpecs: () => this.generations(),\n getCatalog: () => this.currentCatalog(),\n getStateStore: () => this.stateStore(),\n startPicker: (options) => devtools.startPicker(options),\n selection,\n activateTab: () => {},\n },\n tabs: [\n devtools.specTab(),\n devtools.stateTab(),\n devtools.actionsTab(),\n devtools.streamTab(),\n devtools.catalogTab(),\n ],\n initialOpen: this.initialOpen(),\n position: this.position(),\n hotkey: this.hotkey(),\n reserveSpace: this.reserveSpace(),\n allowDockToggle: this.allowDockToggle(),\n });\n\n const unsubscribeSelection = selection.subscribe((key) => {\n if (key) devtools.highlightElement(key);\n });\n // Makes every renderer wrap its elements in `data-jr-key` spans, which is\n // what the picker and the highlight look for.\n const releaseActive = markDevtoolsActive();\n\n this.destroyRef.onDestroy(() => {\n releaseActive();\n unsubscribeSelection();\n this.panel?.destroy();\n this.panel = null;\n unsubscribeStreams();\n unsubscribeActions();\n unsubscribeEvents();\n this.events = null;\n });\n }\n}\n","/*\n * Public API Surface of ngx-json-render/devtools\n */\n\nexport { JsonRenderDevtools } from './json-render-devtools';\nexport type { DevtoolsEvent, PanelPosition } from '@json-render/devtools';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["ɵregisterStreamObserver"],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ import { signal, inject, DestroyRef, computed } from '@angular/core';
2
+ import { App } from '@modelcontextprotocol/ext-apps';
3
+
4
+ /** The text of a {@link JsonRenderApp.sendMessage} message. */
5
+ function messageText(text, data) {
6
+ if (!data)
7
+ return text;
8
+ return `${text}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
9
+ }
10
+ /**
11
+ * Read a spec out of a tool result. Same rule as `@json-render/mcp/app`: the
12
+ * first text block, parsed as JSON, either a bare spec or `{ spec }`.
13
+ */
14
+ function parseSpecFromToolResult(result) {
15
+ const text = result.content?.find((c) => c.type === 'text')?.text;
16
+ if (!text)
17
+ return null;
18
+ try {
19
+ return specFrom(JSON.parse(text));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** A bare spec, or the `spec` field of a tool's arguments or result. */
26
+ function specFrom(value) {
27
+ if (!value || typeof value !== 'object')
28
+ return null;
29
+ const spec = 'spec' in value ? value.spec : value;
30
+ if (!spec || typeof spec !== 'object' || !('root' in spec))
31
+ return null;
32
+ return spec;
33
+ }
34
+ /**
35
+ * Connect an Angular app running inside an MCP Apps iframe to its host and
36
+ * keep the json-render spec the model sent in a signal. The Angular
37
+ * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server
38
+ * side is `createMcpApp` from `@json-render/mcp`, unchanged.
39
+ *
40
+ * Call it in an injection context. The connection closes when the injector
41
+ * that created it is destroyed.
42
+ *
43
+ * ```ts
44
+ * readonly mcp = injectJsonRenderApp();
45
+ * // <json-render [spec]="mcp.spec()" [loading]="mcp.loading()" [registry]="registry" />
46
+ * ```
47
+ */
48
+ function injectJsonRenderApp(options = {}) {
49
+ const { name = 'json-render', version = '1.0.0', streamPartialInput = true, transport, autoResize = true, } = options;
50
+ const spec = signal(null, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
51
+ const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
52
+ const connected = signal(false, ...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
53
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
54
+ const app = new App({ name, version }, {}, { autoResize });
55
+ // Listeners go on before connect(), so a notification the host sends right
56
+ // after the handshake is not lost.
57
+ if (streamPartialInput) {
58
+ app.addEventListener('toolinputpartial', ({ arguments: args }) => {
59
+ const partial = specFrom(args);
60
+ if (partial)
61
+ spec.set(partial);
62
+ });
63
+ }
64
+ app.addEventListener('toolinput', ({ arguments: args }) => {
65
+ const input = specFrom(args);
66
+ if (input)
67
+ spec.set(input);
68
+ });
69
+ app.addEventListener('toolresult', (result) => {
70
+ const parsed = parseSpecFromToolResult(result);
71
+ if (parsed)
72
+ spec.set(parsed);
73
+ loading.set(false);
74
+ });
75
+ app.addEventListener('toolcancelled', () => loading.set(false));
76
+ app
77
+ .connect(transport)
78
+ .then(() => connected.set(true))
79
+ .catch((err) => error.set(err instanceof Error ? err : new Error(String(err))));
80
+ inject(DestroyRef).onDestroy(() => {
81
+ app.close().catch(() => undefined);
82
+ });
83
+ return {
84
+ spec: spec.asReadonly(),
85
+ connecting: computed(() => !connected() && !error()),
86
+ connected: connected.asReadonly(),
87
+ error: error.asReadonly(),
88
+ loading: loading.asReadonly(),
89
+ app,
90
+ async callServerTool(toolName, args = {}) {
91
+ loading.set(true);
92
+ try {
93
+ const result = await app.callServerTool({
94
+ name: toolName,
95
+ arguments: args,
96
+ });
97
+ const parsed = parseSpecFromToolResult(result);
98
+ if (parsed)
99
+ spec.set(parsed);
100
+ }
101
+ finally {
102
+ loading.set(false);
103
+ }
104
+ },
105
+ async sendMessage(text, data) {
106
+ if (!app.getHostCapabilities()?.message) {
107
+ throw new Error('The host does not accept messages from the view.');
108
+ }
109
+ const result = await app.sendMessage({
110
+ role: 'user',
111
+ content: [{ type: 'text', text: messageText(text, data) }],
112
+ });
113
+ if (result.isError)
114
+ throw new Error('The host declined the message.');
115
+ },
116
+ };
117
+ }
118
+
119
+ /*
120
+ * Public API Surface of ngx-json-render/mcp
121
+ *
122
+ * Render json-render specs inside an MCP Apps host (Claude, ChatGPT, VS Code).
123
+ * A separate entry point so `@modelcontextprotocol/ext-apps`, an optional
124
+ * peer, is only needed by apps that import it.
125
+ */
126
+
127
+ /**
128
+ * Generated bundle index. Do not edit.
129
+ */
130
+
131
+ export { injectJsonRenderApp, messageText, parseSpecFromToolResult };
132
+ //# sourceMappingURL=ngx-json-render-mcp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-json-render-mcp.mjs","sources":["../../../projects/ngx-json-render/mcp/src/json-render-app.ts","../../../projects/ngx-json-render/mcp/src/public-api.ts","../../../projects/ngx-json-render/mcp/src/ngx-json-render-mcp.ts"],"sourcesContent":["import {\n DestroyRef,\n type Signal,\n computed,\n inject,\n signal,\n} from '@angular/core';\nimport type { Spec } from '@json-render/core';\nimport { App } from '@modelcontextprotocol/ext-apps';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\n\n/** Options for {@link injectJsonRenderApp}. */\nexport interface JsonRenderAppOptions {\n /** App name sent to the host during initialization. Defaults to `\"json-render\"`. */\n name?: string;\n /** App version sent to the host. Defaults to `\"1.0.0\"`. */\n version?: string;\n /**\n * Render the spec while the model is still writing the tool call, from the\n * host's `toolinputpartial` notifications. The host \"heals\" the truncated\n * JSON, so each partial spec is valid but may end in a half-written\n * element; the renderer skips children that do not exist yet. Defaults to\n * `true`. `useJsonRenderApp` in `@json-render/mcp/app` has no equivalent —\n * it waits for the tool result.\n */\n streamPartialInput?: boolean;\n /**\n * Transport to the host. Omit it inside a real MCP Apps iframe: `App`\n * then talks to `window.parent` over `postMessage`. Tests pass one end of\n * an in-memory pair here and an `AppBridge` on the other.\n */\n transport?: Transport;\n /**\n * Report the page's size to the host as it changes, so the iframe grows\n * with the rendered UI. Defaults to `true`; needs `ResizeObserver`.\n */\n autoResize?: boolean;\n}\n\n/**\n * What {@link injectJsonRenderApp} returns — the fields of\n * `UseJsonRenderAppReturn` from `@json-render/mcp/app`, as signals.\n */\nexport interface JsonRenderApp {\n /** The current spec; `null` until the first tool input or result. */\n readonly spec: Signal<Spec | null>;\n /** Whether the app is still connecting to the host. */\n readonly connecting: Signal<boolean>;\n /** Whether the app is connected to the host. */\n readonly connected: Signal<boolean>;\n /** Connection error, if any. */\n readonly error: Signal<Error | null>;\n /** Whether the spec is still arriving: pass it to `<json-render [loading]>`. */\n readonly loading: Signal<boolean>;\n /** The underlying MCP Apps `App`. */\n readonly app: App;\n /**\n * Call a tool on the MCP server and replace the spec with its result —\n * for refresh and drill-down interactions.\n */\n callServerTool(name: string, args?: Record<string, unknown>): Promise<void>;\n /**\n * Post a user message to the host's chat, so the model answers what the\n * user did in the view. `data`, if given, follows the text as a JSON block.\n * Rejects when the host does not accept messages from views or turns this\n * one down, so an action's `onError` runs.\n */\n sendMessage(text: string, data?: Record<string, unknown>): Promise<void>;\n}\n\n/** The text of a {@link JsonRenderApp.sendMessage} message. */\nexport function messageText(\n text: string,\n data?: Record<string, unknown>,\n): string {\n if (!data) return text;\n return `${text}\\n\\n\\`\\`\\`json\\n${JSON.stringify(data, null, 2)}\\n\\`\\`\\``;\n}\n\ninterface ToolResultLike {\n content?: { type: string; text?: string }[];\n}\n\n/**\n * Read a spec out of a tool result. Same rule as `@json-render/mcp/app`: the\n * first text block, parsed as JSON, either a bare spec or `{ spec }`.\n */\nexport function parseSpecFromToolResult(result: ToolResultLike): Spec | null {\n const text = result.content?.find((c) => c.type === 'text')?.text;\n if (!text) return null;\n try {\n return specFrom(JSON.parse(text));\n } catch {\n return null;\n }\n}\n\n/** A bare spec, or the `spec` field of a tool's arguments or result. */\nfunction specFrom(value: unknown): Spec | null {\n if (!value || typeof value !== 'object') return null;\n const spec = 'spec' in value ? (value as { spec: unknown }).spec : value;\n if (!spec || typeof spec !== 'object' || !('root' in spec)) return null;\n return spec as Spec;\n}\n\n/**\n * Connect an Angular app running inside an MCP Apps iframe to its host and\n * keep the json-render spec the model sent in a signal. The Angular\n * counterpart of `useJsonRenderApp` from `@json-render/mcp/app`; the server\n * side is `createMcpApp` from `@json-render/mcp`, unchanged.\n *\n * Call it in an injection context. The connection closes when the injector\n * that created it is destroyed.\n *\n * ```ts\n * readonly mcp = injectJsonRenderApp();\n * // <json-render [spec]=\"mcp.spec()\" [loading]=\"mcp.loading()\" [registry]=\"registry\" />\n * ```\n */\nexport function injectJsonRenderApp(\n options: JsonRenderAppOptions = {},\n): JsonRenderApp {\n const {\n name = 'json-render',\n version = '1.0.0',\n streamPartialInput = true,\n transport,\n autoResize = true,\n } = options;\n\n const spec = signal<Spec | null>(null);\n const loading = signal(true);\n const connected = signal(false);\n const error = signal<Error | null>(null);\n\n const app = new App({ name, version }, {}, { autoResize });\n\n // Listeners go on before connect(), so a notification the host sends right\n // after the handshake is not lost.\n if (streamPartialInput) {\n app.addEventListener('toolinputpartial', ({ arguments: args }) => {\n const partial = specFrom(args);\n if (partial) spec.set(partial);\n });\n }\n app.addEventListener('toolinput', ({ arguments: args }) => {\n const input = specFrom(args);\n if (input) spec.set(input);\n });\n app.addEventListener('toolresult', (result) => {\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n loading.set(false);\n });\n app.addEventListener('toolcancelled', () => loading.set(false));\n\n app\n .connect(transport)\n .then(() => connected.set(true))\n .catch((err: unknown) =>\n error.set(err instanceof Error ? err : new Error(String(err))),\n );\n\n inject(DestroyRef).onDestroy(() => {\n app.close().catch(() => undefined);\n });\n\n return {\n spec: spec.asReadonly(),\n connecting: computed(() => !connected() && !error()),\n connected: connected.asReadonly(),\n error: error.asReadonly(),\n loading: loading.asReadonly(),\n app,\n async callServerTool(toolName, args = {}) {\n loading.set(true);\n try {\n const result = await app.callServerTool({\n name: toolName,\n arguments: args,\n });\n const parsed = parseSpecFromToolResult(result);\n if (parsed) spec.set(parsed);\n } finally {\n loading.set(false);\n }\n },\n async sendMessage(text, data) {\n if (!app.getHostCapabilities()?.message) {\n throw new Error('The host does not accept messages from the view.');\n }\n const result = await app.sendMessage({\n role: 'user',\n content: [{ type: 'text', text: messageText(text, data) }],\n });\n if (result.isError) throw new Error('The host declined the message.');\n },\n };\n}\n","/*\n * Public API Surface of ngx-json-render/mcp\n *\n * Render json-render specs inside an MCP Apps host (Claude, ChatGPT, VS Code).\n * A separate entry point so `@modelcontextprotocol/ext-apps`, an optional\n * peer, is only needed by apps that import it.\n */\n\nexport {\n injectJsonRenderApp,\n messageText,\n parseSpecFromToolResult,\n type JsonRenderApp,\n type JsonRenderAppOptions,\n} from './json-render-app';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}