@ai-gui/react 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -11,8 +11,8 @@ pnpm add @ai-gui/core @ai-gui/react
11
11
  ## Usage
12
12
 
13
13
  ```tsx
14
- import { CardRegistry } from "@ai-gui/core"
15
- import { AIRenderer } from "@ai-gui/react"
14
+ import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
15
+ import { AIRenderer, useActionState } from "@ai-gui/react"
16
16
  import { useRef } from "react"
17
17
 
18
18
  const registry = new CardRegistry()
@@ -28,6 +28,14 @@ registry.register({
28
28
  ),
29
29
  })
30
30
 
31
+ const actions = new ActionRegistry()
32
+ actions.register({
33
+ type: "refresh",
34
+ run: async (params, { signal }) => fetch("/api/weather", { signal }).then((r) => r.json()),
35
+ })
36
+ const cardStore = new CardStore({ registry })
37
+ const actionRuntime = createActionRuntime({ registry: actions, cardStore })
38
+
31
39
  function Chat() {
32
40
  const ref = useRef<React.ComponentRef<typeof AIRenderer>>(null)
33
41
  // ref.current?.push(chunk) / feed(source) / reset()
@@ -35,7 +43,9 @@ function Chat() {
35
43
  <AIRenderer
36
44
  ref={ref}
37
45
  registry={registry}
38
- onCardAction={({ type, params, cardType }) => {/* app makes the real request */}}
46
+ cardStore={cardStore}
47
+ actionRuntime={actionRuntime}
48
+ onCardAction={(action) => console.log("observed", action)}
39
49
  />
40
50
  )
41
51
  }
@@ -43,7 +53,10 @@ function Chat() {
43
53
 
44
54
  ## Exports
45
55
 
46
- - `<AIRenderer ref registry plugins sanitize onCardAction />` — imperative `ref.current.push/feed/reset`.
56
+ - `<AIRenderer ref registry cardStore plugins sanitize actionRuntime onCardAction />` — imperative `ref.current.push/feed/reset`.
47
57
  - `useAIRenderer(options)` → `{ nodes, push, feed, reset }` — the hook form when you want to render `nodes` yourself.
58
+ - `useActionState(runtime, key)` — subscribe to one action's `idle | pending | success | error | cancelled` state.
59
+
60
+ Cards with a top-level `id` use the supplied `CardStore`. Their React component receives `{ data, state, onAction }`; patches update props while preserving component and DOM state.
48
61
 
49
62
  See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
package/dist/index.cjs CHANGED
@@ -66,7 +66,9 @@ function useAIRenderer(options = {}) {
66
66
  options.registry,
67
67
  options.sanitize,
68
68
  options.plugins,
69
- options.scheduler
69
+ options.scheduler,
70
+ options.debug,
71
+ options.onDebugEvent
70
72
  ]);
71
73
  (0, react.useEffect)(() => {
72
74
  setNodes([]);
@@ -84,6 +86,7 @@ function useAIRenderer(options = {}) {
84
86
  setNodes([]);
85
87
  }, [session]);
86
88
  return {
89
+ renderer: session.renderer,
87
90
  nodes,
88
91
  push,
89
92
  feed,
@@ -91,6 +94,22 @@ function useAIRenderer(options = {}) {
91
94
  };
92
95
  }
93
96
 
97
+ //#endregion
98
+ //#region src/use-action-state.ts
99
+ function useActionState(runtime, key) {
100
+ const idle = (0, react.useMemo)(() => (0, __ai_gui_core.getIdleActionState)(key), [key]);
101
+ const subscribe = (0, react.useCallback)((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
102
+ const getSnapshot = (0, react.useCallback)(() => {
103
+ const state = runtime?.getState(key) ?? idle;
104
+ return state.status === "idle" ? idle : state;
105
+ }, [
106
+ idle,
107
+ key,
108
+ runtime
109
+ ]);
110
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot, () => idle);
111
+ }
112
+
94
113
  //#endregion
95
114
  //#region src/render-output.tsx
96
115
  /** Translate a framework-neutral RenderOutput into React nodes. */
@@ -157,15 +176,21 @@ function sanitizeOutput(html, sanitize) {
157
176
  //#region src/render-node.tsx
158
177
  function renderNode(node, ctx) {
159
178
  const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
160
- if (r) try {
161
- const out = r(node);
162
- if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
163
- promise: out,
164
- sanitize: ctx.sanitize
179
+ if (r) {
180
+ if (node.complete === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
181
+ "data-aigui-block-loading": "",
182
+ "data-block-type": node.type
165
183
  }, node.key);
166
- return renderOutput(out, node.key, ctx.sanitize);
167
- } catch {
168
- return renderFallback(node, ctx);
184
+ try {
185
+ const out = r(node);
186
+ if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
187
+ promise: out,
188
+ sanitize: ctx.sanitize
189
+ }, node.key);
190
+ return renderOutput(out, node.key, ctx.sanitize);
191
+ } catch {
192
+ return renderFallback(node, ctx);
193
+ }
169
194
  }
170
195
  switch (node.type) {
171
196
  case "heading": return (0, react.createElement)(node.tag ?? "h1", {
@@ -203,10 +228,15 @@ function renderCard(node, ctx) {
203
228
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
204
229
  }, node.key);
205
230
  const Comp = getCardComponent(ctx.registry, card.type);
206
- if (!Comp) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
207
- "data-aigui-card-fallback": true,
208
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
231
+ if (card.id && ctx.cardStore) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatefulCardHost, {
232
+ cardStore: ctx.cardStore,
233
+ cardId: card.id,
234
+ cardType: card.type,
235
+ initialData: card.data,
236
+ Comp,
237
+ onCardAction: ctx.onCardAction
209
238
  }, node.key);
239
+ if (!Comp) return renderCardFallback(node.key, card.data);
210
240
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
211
241
  data: card.data,
212
242
  onAction: (a) => ctx.onCardAction?.({
@@ -215,6 +245,49 @@ function renderCard(node, ctx) {
215
245
  })
216
246
  }, node.key);
217
247
  }
248
+ const getServerCardSnapshot = () => void 0;
249
+ function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
250
+ const subscribe = (0, react.useCallback)((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
251
+ const getSnapshot = (0, react.useCallback)(() => cardStore.get(cardId), [cardStore, cardId]);
252
+ const record = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getServerCardSnapshot);
253
+ (0, react.useEffect)(() => {
254
+ if (cardStore.get(cardId)) return;
255
+ try {
256
+ cardStore.register({
257
+ id: cardId,
258
+ type: cardType,
259
+ data: initialData
260
+ });
261
+ } catch {}
262
+ }, [
263
+ cardStore,
264
+ cardId,
265
+ cardType,
266
+ initialData
267
+ ]);
268
+ if (!record) return renderCardFallback(void 0, initialData);
269
+ if (record.type !== cardType) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
270
+ "data-aigui-card-invalid": true,
271
+ "data-card-type": cardType,
272
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(initialData, null, 2) })
273
+ });
274
+ if (!Comp) return renderCardFallback(void 0, record.data);
275
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
276
+ data: record.data,
277
+ state: record.action,
278
+ onAction: (action) => onCardAction?.({
279
+ ...action,
280
+ cardType,
281
+ cardId
282
+ })
283
+ });
284
+ }
285
+ function renderCardFallback(key, data) {
286
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
287
+ "data-aigui-card-fallback": true,
288
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(data, null, 2) })
289
+ }, key);
290
+ }
218
291
  function getCardComponent(registry, type) {
219
292
  if (!registry) return void 0;
220
293
  return registry.getRender(type);
@@ -222,29 +295,71 @@ function getCardComponent(registry, type) {
222
295
 
223
296
  //#endregion
224
297
  //#region src/ai-renderer.tsx
298
+ function createActionScope() {
299
+ return {
300
+ controller: new AbortController(),
301
+ owner: {}
302
+ };
303
+ }
225
304
  const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
226
- const { registry, sanitize, plugins, onCardAction, className } = props;
305
+ const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
227
306
  const opts = {
228
307
  registry,
229
308
  sanitize,
230
- plugins
309
+ plugins,
310
+ debug,
311
+ onDebugEvent
231
312
  };
232
- const { nodes, push, feed, reset } = useAIRenderer(opts);
313
+ const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
314
+ const actionScope = (0, react.useRef)(createActionScope());
315
+ (0, react.useEffect)(() => () => {
316
+ actionScope.current.controller.abort();
317
+ actionScope.current = createActionScope();
318
+ }, [
319
+ actionRuntime,
320
+ registry,
321
+ sanitize,
322
+ plugins
323
+ ]);
324
+ const reset = (0, react.useCallback)(() => {
325
+ actionScope.current.controller.abort();
326
+ actionScope.current = createActionScope();
327
+ resetRenderer();
328
+ }, [resetRenderer]);
329
+ const handleCardAction = (0, react.useCallback)((action) => {
330
+ if (actionRuntime) {
331
+ const scope = actionScope.current;
332
+ actionRuntime.dispatch({
333
+ type: action.type,
334
+ params: action.params,
335
+ cardType: action.cardType,
336
+ cardId: action.cardId
337
+ }, {
338
+ signal: scope.controller.signal,
339
+ owner: scope.owner
340
+ }).catch(() => {});
341
+ }
342
+ onCardAction?.(action);
343
+ }, [actionRuntime, onCardAction]);
233
344
  (0, react.useImperativeHandle)(ref, () => ({
345
+ debugSource: "renderer",
346
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
234
347
  push,
235
348
  feed,
236
349
  reset
237
350
  }), [
351
+ renderer,
238
352
  push,
239
353
  feed,
240
354
  reset
241
355
  ]);
242
- const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins), [plugins]);
356
+ const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins, { debugTarget: renderer }), [plugins, renderer]);
243
357
  const ctx = {
244
358
  registry,
359
+ cardStore,
245
360
  plugins,
246
361
  nodeRenderers,
247
- onCardAction,
362
+ onCardAction: handleCardAction,
248
363
  sanitize,
249
364
  sanitized: true
250
365
  };
@@ -260,4 +375,5 @@ exports.AIRenderer = AIRenderer
260
375
  exports.applyPatches = applyPatches
261
376
  exports.renderNode = renderNode
262
377
  exports.renderOutput = renderOutput
263
- exports.useAIRenderer = useAIRenderer
378
+ exports.useAIRenderer = useAIRenderer
379
+ exports.useActionState = useActionState
package/dist/index.d.cts CHANGED
@@ -1,10 +1,11 @@
1
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
2
2
  import * as react0 from "react";
3
3
  import * as react1 from "react";
4
4
  import { ReactNode } from "react";
5
5
 
6
6
  //#region src/use-ai-renderer.d.ts
7
7
  interface UseAIRendererResult {
8
+ renderer: Renderer;
8
9
  nodes: ASTNode[];
9
10
  push: (chunk: string) => void;
10
11
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
@@ -12,35 +13,56 @@ interface UseAIRendererResult {
12
13
  }
13
14
  declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
14
15
 
16
+ //#endregion
17
+ //#region src/use-action-state.d.ts
18
+ declare function useActionState(runtime: ActionRuntime | undefined, key: string): ActionState;
19
+
15
20
  //#endregion
16
21
  //#region src/render-node.d.ts
22
+ interface CardActionPayload {
23
+ type: string;
24
+ params?: unknown;
25
+ cardType: string;
26
+ cardId?: string;
27
+ }
17
28
  interface RenderContext {
18
29
  registry?: CardRegistry;
30
+ cardStore?: CardStore;
19
31
  plugins?: AIGuiPlugin[];
20
32
  nodeRenderers?: Record<string, NodeRenderer>;
21
- onCardAction?: (action: {
22
- type: string;
23
- params?: unknown;
24
- cardType: string;
25
- }) => void;
33
+ onCardAction?: (action: CardActionPayload) => void;
26
34
  sanitize?: RendererOptions["sanitize"];
27
35
  sanitized?: boolean;
28
36
  }
29
37
  declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
38
+ interface CardComponentProps {
39
+ data: unknown;
40
+ state?: CardAction;
41
+ onAction: (a: {
42
+ type: string;
43
+ params?: unknown;
44
+ }) => void;
45
+ }
30
46
 
31
47
  //#endregion
32
48
  //#region src/ai-renderer.d.ts
33
49
  interface AIRendererHandle {
50
+ readonly debugSource: "renderer";
51
+ subscribeDebug: (listener: DebugEventListener) => () => void;
34
52
  push: (chunk: string) => void;
35
53
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
36
54
  reset: () => void;
37
55
  }
38
56
  interface AIRendererProps {
39
57
  registry?: CardRegistry;
58
+ cardStore?: CardStore;
40
59
  sanitize?: RendererOptions["sanitize"];
41
60
  plugins?: AIGuiPlugin[];
61
+ actionRuntime?: ActionRuntime;
42
62
  onCardAction?: RenderContext["onCardAction"];
43
63
  className?: string;
64
+ debug?: RendererOptions["debug"];
65
+ onDebugEvent?: RendererOptions["onDebugEvent"];
44
66
  }
45
67
  declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
46
68
 
@@ -54,4 +76,4 @@ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: Render
54
76
  declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
55
77
 
56
78
  //#endregion
57
- export { AIRenderer, AIRendererHandle, AIRendererProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer };
79
+ export { AIRenderer, AIRendererHandle, AIRendererProps, CardActionPayload, CardComponentProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as react0 from "react";
2
2
  import * as react1 from "react";
3
3
  import { ReactNode } from "react";
4
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
4
+ import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
5
5
 
6
6
  //#region src/use-ai-renderer.d.ts
7
7
  interface UseAIRendererResult {
8
+ renderer: Renderer;
8
9
  nodes: ASTNode[];
9
10
  push: (chunk: string) => void;
10
11
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
@@ -12,35 +13,56 @@ interface UseAIRendererResult {
12
13
  }
13
14
  declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
14
15
 
16
+ //#endregion
17
+ //#region src/use-action-state.d.ts
18
+ declare function useActionState(runtime: ActionRuntime | undefined, key: string): ActionState;
19
+
15
20
  //#endregion
16
21
  //#region src/render-node.d.ts
22
+ interface CardActionPayload {
23
+ type: string;
24
+ params?: unknown;
25
+ cardType: string;
26
+ cardId?: string;
27
+ }
17
28
  interface RenderContext {
18
29
  registry?: CardRegistry;
30
+ cardStore?: CardStore;
19
31
  plugins?: AIGuiPlugin[];
20
32
  nodeRenderers?: Record<string, NodeRenderer>;
21
- onCardAction?: (action: {
22
- type: string;
23
- params?: unknown;
24
- cardType: string;
25
- }) => void;
33
+ onCardAction?: (action: CardActionPayload) => void;
26
34
  sanitize?: RendererOptions["sanitize"];
27
35
  sanitized?: boolean;
28
36
  }
29
37
  declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
38
+ interface CardComponentProps {
39
+ data: unknown;
40
+ state?: CardAction;
41
+ onAction: (a: {
42
+ type: string;
43
+ params?: unknown;
44
+ }) => void;
45
+ }
30
46
 
31
47
  //#endregion
32
48
  //#region src/ai-renderer.d.ts
33
49
  interface AIRendererHandle {
50
+ readonly debugSource: "renderer";
51
+ subscribeDebug: (listener: DebugEventListener) => () => void;
34
52
  push: (chunk: string) => void;
35
53
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
36
54
  reset: () => void;
37
55
  }
38
56
  interface AIRendererProps {
39
57
  registry?: CardRegistry;
58
+ cardStore?: CardStore;
40
59
  sanitize?: RendererOptions["sanitize"];
41
60
  plugins?: AIGuiPlugin[];
61
+ actionRuntime?: ActionRuntime;
42
62
  onCardAction?: RenderContext["onCardAction"];
43
63
  className?: string;
64
+ debug?: RendererOptions["debug"];
65
+ onDebugEvent?: RendererOptions["onDebugEvent"];
44
66
  }
45
67
  declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
46
68
 
@@ -54,4 +76,4 @@ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: Render
54
76
  declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
55
77
 
56
78
  //#endregion
57
- export { AIRenderer, AIRendererHandle, AIRendererProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer };
79
+ export { AIRenderer, AIRendererHandle, AIRendererProps, CardActionPayload, CardComponentProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
- import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
1
+ import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { Renderer, collectNodeRenderers, getIdleActionState, sanitizeHtml } from "@ai-gui/core";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/apply-patches.ts
@@ -42,7 +42,9 @@ function useAIRenderer(options = {}) {
42
42
  options.registry,
43
43
  options.sanitize,
44
44
  options.plugins,
45
- options.scheduler
45
+ options.scheduler,
46
+ options.debug,
47
+ options.onDebugEvent
46
48
  ]);
47
49
  useEffect(() => {
48
50
  setNodes([]);
@@ -60,6 +62,7 @@ function useAIRenderer(options = {}) {
60
62
  setNodes([]);
61
63
  }, [session]);
62
64
  return {
65
+ renderer: session.renderer,
63
66
  nodes,
64
67
  push,
65
68
  feed,
@@ -67,6 +70,22 @@ function useAIRenderer(options = {}) {
67
70
  };
68
71
  }
69
72
 
73
+ //#endregion
74
+ //#region src/use-action-state.ts
75
+ function useActionState(runtime, key) {
76
+ const idle = useMemo(() => getIdleActionState(key), [key]);
77
+ const subscribe = useCallback((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
78
+ const getSnapshot = useCallback(() => {
79
+ const state = runtime?.getState(key) ?? idle;
80
+ return state.status === "idle" ? idle : state;
81
+ }, [
82
+ idle,
83
+ key,
84
+ runtime
85
+ ]);
86
+ return useSyncExternalStore(subscribe, getSnapshot, () => idle);
87
+ }
88
+
70
89
  //#endregion
71
90
  //#region src/render-output.tsx
72
91
  /** Translate a framework-neutral RenderOutput into React nodes. */
@@ -133,15 +152,21 @@ function sanitizeOutput(html, sanitize) {
133
152
  //#region src/render-node.tsx
134
153
  function renderNode(node, ctx) {
135
154
  const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
136
- if (r) try {
137
- const out = r(node);
138
- if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
139
- promise: out,
140
- sanitize: ctx.sanitize
155
+ if (r) {
156
+ if (node.complete === false) return /* @__PURE__ */ jsx("div", {
157
+ "data-aigui-block-loading": "",
158
+ "data-block-type": node.type
141
159
  }, node.key);
142
- return renderOutput(out, node.key, ctx.sanitize);
143
- } catch {
144
- return renderFallback(node, ctx);
160
+ try {
161
+ const out = r(node);
162
+ if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
163
+ promise: out,
164
+ sanitize: ctx.sanitize
165
+ }, node.key);
166
+ return renderOutput(out, node.key, ctx.sanitize);
167
+ } catch {
168
+ return renderFallback(node, ctx);
169
+ }
145
170
  }
146
171
  switch (node.type) {
147
172
  case "heading": return createElement(node.tag ?? "h1", {
@@ -179,10 +204,15 @@ function renderCard(node, ctx) {
179
204
  children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
180
205
  }, node.key);
181
206
  const Comp = getCardComponent(ctx.registry, card.type);
182
- if (!Comp) return /* @__PURE__ */ jsx("pre", {
183
- "data-aigui-card-fallback": true,
184
- children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
207
+ if (card.id && ctx.cardStore) return /* @__PURE__ */ jsx(StatefulCardHost, {
208
+ cardStore: ctx.cardStore,
209
+ cardId: card.id,
210
+ cardType: card.type,
211
+ initialData: card.data,
212
+ Comp,
213
+ onCardAction: ctx.onCardAction
185
214
  }, node.key);
215
+ if (!Comp) return renderCardFallback(node.key, card.data);
186
216
  return /* @__PURE__ */ jsx(Comp, {
187
217
  data: card.data,
188
218
  onAction: (a) => ctx.onCardAction?.({
@@ -191,6 +221,49 @@ function renderCard(node, ctx) {
191
221
  })
192
222
  }, node.key);
193
223
  }
224
+ const getServerCardSnapshot = () => void 0;
225
+ function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
226
+ const subscribe = useCallback((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
227
+ const getSnapshot = useCallback(() => cardStore.get(cardId), [cardStore, cardId]);
228
+ const record = useSyncExternalStore(subscribe, getSnapshot, getServerCardSnapshot);
229
+ useEffect(() => {
230
+ if (cardStore.get(cardId)) return;
231
+ try {
232
+ cardStore.register({
233
+ id: cardId,
234
+ type: cardType,
235
+ data: initialData
236
+ });
237
+ } catch {}
238
+ }, [
239
+ cardStore,
240
+ cardId,
241
+ cardType,
242
+ initialData
243
+ ]);
244
+ if (!record) return renderCardFallback(void 0, initialData);
245
+ if (record.type !== cardType) return /* @__PURE__ */ jsx("pre", {
246
+ "data-aigui-card-invalid": true,
247
+ "data-card-type": cardType,
248
+ children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(initialData, null, 2) })
249
+ });
250
+ if (!Comp) return renderCardFallback(void 0, record.data);
251
+ return /* @__PURE__ */ jsx(Comp, {
252
+ data: record.data,
253
+ state: record.action,
254
+ onAction: (action) => onCardAction?.({
255
+ ...action,
256
+ cardType,
257
+ cardId
258
+ })
259
+ });
260
+ }
261
+ function renderCardFallback(key, data) {
262
+ return /* @__PURE__ */ jsx("pre", {
263
+ "data-aigui-card-fallback": true,
264
+ children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(data, null, 2) })
265
+ }, key);
266
+ }
194
267
  function getCardComponent(registry, type) {
195
268
  if (!registry) return void 0;
196
269
  return registry.getRender(type);
@@ -198,29 +271,71 @@ function getCardComponent(registry, type) {
198
271
 
199
272
  //#endregion
200
273
  //#region src/ai-renderer.tsx
274
+ function createActionScope() {
275
+ return {
276
+ controller: new AbortController(),
277
+ owner: {}
278
+ };
279
+ }
201
280
  const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
202
- const { registry, sanitize, plugins, onCardAction, className } = props;
281
+ const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
203
282
  const opts = {
204
283
  registry,
205
284
  sanitize,
206
- plugins
285
+ plugins,
286
+ debug,
287
+ onDebugEvent
207
288
  };
208
- const { nodes, push, feed, reset } = useAIRenderer(opts);
289
+ const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
290
+ const actionScope = useRef(createActionScope());
291
+ useEffect(() => () => {
292
+ actionScope.current.controller.abort();
293
+ actionScope.current = createActionScope();
294
+ }, [
295
+ actionRuntime,
296
+ registry,
297
+ sanitize,
298
+ plugins
299
+ ]);
300
+ const reset = useCallback(() => {
301
+ actionScope.current.controller.abort();
302
+ actionScope.current = createActionScope();
303
+ resetRenderer();
304
+ }, [resetRenderer]);
305
+ const handleCardAction = useCallback((action) => {
306
+ if (actionRuntime) {
307
+ const scope = actionScope.current;
308
+ actionRuntime.dispatch({
309
+ type: action.type,
310
+ params: action.params,
311
+ cardType: action.cardType,
312
+ cardId: action.cardId
313
+ }, {
314
+ signal: scope.controller.signal,
315
+ owner: scope.owner
316
+ }).catch(() => {});
317
+ }
318
+ onCardAction?.(action);
319
+ }, [actionRuntime, onCardAction]);
209
320
  useImperativeHandle(ref, () => ({
321
+ debugSource: "renderer",
322
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
210
323
  push,
211
324
  feed,
212
325
  reset
213
326
  }), [
327
+ renderer,
214
328
  push,
215
329
  feed,
216
330
  reset
217
331
  ]);
218
- const nodeRenderers = useMemo(() => collectNodeRenderers(plugins), [plugins]);
332
+ const nodeRenderers = useMemo(() => collectNodeRenderers(plugins, { debugTarget: renderer }), [plugins, renderer]);
219
333
  const ctx = {
220
334
  registry,
335
+ cardStore,
221
336
  plugins,
222
337
  nodeRenderers,
223
- onCardAction,
338
+ onCardAction: handleCardAction,
224
339
  sanitize,
225
340
  sanitized: true
226
341
  };
@@ -232,4 +347,4 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
232
347
  });
233
348
 
234
349
  //#endregion
235
- export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer };
350
+ export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "React adapter for AIGUI — stream LLM output into live React components with cards, plugins, and interactive widgets.",
5
5
  "keywords": [
6
6
  "llm",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@ai-gui/core": "0.2.0"
53
+ "@ai-gui/core": "0.3.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": ">=18"