@ai-gui/vue 0.2.0 → 0.4.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
@@ -13,13 +13,18 @@ pnpm add @ai-gui/core @ai-gui/vue
13
13
  ```vue
14
14
  <script setup lang="ts">
15
15
  import { ref } from "vue"
16
- import { CardRegistry } from "@ai-gui/core"
17
- import { AIRenderer } from "@ai-gui/vue"
16
+ import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
17
+ import { AIRenderer, useActionState } from "@ai-gui/vue"
18
18
 
19
19
  const registry = new CardRegistry()
20
20
  // A card render component receives props `data` and emits `action`.
21
21
  registry.register({ type: "weather", description: "Weather summary", render: WeatherCard })
22
22
 
23
+ const actions = new ActionRegistry()
24
+ actions.register({ type: "refresh", run: async (params, { signal }) => fetch("/api/weather", { signal }) })
25
+ const cardStore = new CardStore({ registry })
26
+ const actionRuntime = createActionRuntime({ registry: actions, cardStore })
27
+
23
28
  const r = ref<InstanceType<typeof AIRenderer>>()
24
29
 
25
30
  async function ask() {
@@ -30,13 +35,16 @@ async function ask() {
30
35
  </script>
31
36
 
32
37
  <template>
33
- <AIRenderer ref="r" :registry="registry" @card-action="(a) => {/* app makes the real request */}" />
38
+ <AIRenderer ref="r" :registry="registry" :card-store="cardStore" :action-runtime="actionRuntime" @card-action="(a) => console.log(a)" />
34
39
  </template>
35
40
  ```
36
41
 
37
42
  ## Exports
38
43
 
39
- - `<AIRenderer :registry :plugins :sanitize @card-action />` — a render-function component; imperative `push` / `feed` / `reset` via a template ref (exposed).
44
+ - `<AIRenderer :registry :card-store :plugins :sanitize :action-runtime @card-action />` — a render-function component; imperative `push` / `feed` / `reset` via a template ref (exposed).
40
45
  - `useAIRenderer()` — composable returning `{ nodes, push, feed, reset }`.
46
+ - `useActionState(runtime, key)` — reactive action lifecycle state.
47
+
48
+ Cards with a top-level `id` subscribe to the supplied `CardStore`. Their Vue component receives `data` and `state` props and emits `action`; store updates preserve the component instance and local refs.
41
49
 
42
50
  Card component contract: props `data`, `emits: ['action']`. See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ function useAIRenderer(options = {}) {
37
37
  }
38
38
  });
39
39
  return {
40
+ renderer,
40
41
  nodes,
41
42
  push: (c) => {
42
43
  if (active) renderer.push(c);
@@ -56,24 +57,41 @@ function useAIRenderer(options = {}) {
56
57
  };
57
58
  }
58
59
 
60
+ //#endregion
61
+ //#region src/use-action-state.ts
62
+ function useActionState(runtime, key) {
63
+ const state = (0, vue.shallowRef)(runtime.getState(key));
64
+ const unsubscribe = runtime.subscribe((next) => {
65
+ if (next.key === key) state.value = next;
66
+ });
67
+ (0, vue.onScopeDispose)(unsubscribe);
68
+ return state;
69
+ }
70
+
59
71
  //#endregion
60
72
  //#region src/render-output.ts
61
73
  /** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
62
74
  const MountHost = (0, vue.defineComponent)({
63
75
  name: "MountHost",
64
- props: { mount: {
65
- type: Function,
66
- required: true
67
- } },
76
+ props: {
77
+ mount: {
78
+ type: Function,
79
+ required: true
80
+ },
81
+ context: {
82
+ type: Object,
83
+ required: true
84
+ }
85
+ },
68
86
  setup(props) {
69
87
  const elRef = (0, vue.ref)(null);
70
88
  let cleanup;
71
89
  (0, vue.onMounted)(() => {
72
- if (elRef.value) cleanup = props.mount(elRef.value);
90
+ if (elRef.value) cleanup = props.mount(elRef.value, props.context);
73
91
  });
74
92
  (0, vue.watch)(() => props.mount, (mount) => {
75
93
  if (typeof cleanup === "function") cleanup();
76
- cleanup = elRef.value ? mount(elRef.value) : void 0;
94
+ cleanup = elRef.value ? mount(elRef.value, props.context) : void 0;
77
95
  });
78
96
  (0, vue.onBeforeUnmount)(() => {
79
97
  if (typeof cleanup === "function") cleanup();
@@ -85,11 +103,14 @@ const MountHost = (0, vue.defineComponent)({
85
103
  }
86
104
  });
87
105
  /** Translate a framework-neutral RenderOutput into a Vue VNode. */
88
- function renderOutput(out, sanitize) {
106
+ function renderOutput(out, sanitize, context = {}) {
89
107
  switch (out.kind) {
90
108
  case "html": return (0, vue.h)("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
91
- case "element": return (0, vue.h)(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
92
- case "mount": return (0, vue.h)(MountHost, { mount: out.mount });
109
+ case "element": return (0, vue.h)(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize, context)));
110
+ case "mount": return (0, vue.h)(MountHost, {
111
+ mount: out.mount,
112
+ context
113
+ });
93
114
  case "card": return (0, vue.h)("pre", { "data-aigui-card-fallback": "" }, [(0, vue.h)("code", JSON.stringify(out.data, null, 2))]);
94
115
  }
95
116
  }
@@ -104,6 +125,10 @@ const AsyncOutput = (0, vue.defineComponent)({
104
125
  sanitize: {
105
126
  type: [Boolean, Object],
106
127
  default: void 0
128
+ },
129
+ context: {
130
+ type: Object,
131
+ default: () => ({})
107
132
  }
108
133
  },
109
134
  setup(props) {
@@ -130,13 +155,42 @@ const AsyncOutput = (0, vue.defineComponent)({
130
155
  if (failed.value) return (0, vue.h)("span", { "data-aigui-async-error": "" });
131
156
  if (!resolved.value) return (0, vue.h)("span", { "data-aigui-async-pending": "" });
132
157
  try {
133
- return renderOutput(resolved.value, props.sanitize);
158
+ return renderOutput(resolved.value, props.sanitize, props.context);
134
159
  } catch {
135
160
  return (0, vue.h)("span", { "data-aigui-async-error": "" });
136
161
  }
137
162
  };
138
163
  }
139
164
  });
165
+ function createRenderMountContext(registry, onCardAction) {
166
+ return { mountCard(host, request) {
167
+ const component = registry?.getRender(request.type);
168
+ if (!component) return void 0;
169
+ const card = (0, vue.markRaw)((0, vue.toRaw)(component));
170
+ let data = request.data;
171
+ let destroyed = false;
172
+ const renderCard$1 = () => (0, vue.render)((0, vue.h)(card, {
173
+ data,
174
+ onAction: (action) => onCardAction?.({
175
+ ...action,
176
+ cardType: request.type
177
+ })
178
+ }), host);
179
+ renderCard$1();
180
+ return {
181
+ update(nextData) {
182
+ if (destroyed) return;
183
+ data = nextData;
184
+ renderCard$1();
185
+ },
186
+ destroy() {
187
+ if (destroyed) return;
188
+ destroyed = true;
189
+ (0, vue.render)(null, host);
190
+ }
191
+ };
192
+ } };
193
+ }
140
194
  function sanitizeOutput(html, sanitize) {
141
195
  if (sanitize === false) return html;
142
196
  return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
@@ -146,18 +200,27 @@ function sanitizeOutput(html, sanitize) {
146
200
  //#region src/render-node.ts
147
201
  function renderNode(node, ctx) {
148
202
  const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
149
- if (r) try {
150
- const out = r(node);
151
- if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
203
+ if (r) {
204
+ if (node.complete === false) return (0, vue.h)("div", {
152
205
  key: node.key,
153
- promise: out,
154
- sanitize: ctx.sanitize
206
+ "data-aigui-block-loading": "",
207
+ "data-block-type": node.type
155
208
  });
156
- const vnode = renderOutput(out, ctx.sanitize);
157
- vnode.key = node.key;
158
- return vnode;
159
- } catch {
160
- return renderFallback(node, ctx);
209
+ try {
210
+ const out = r(node);
211
+ const mountContext = createRenderMountContext(ctx.registry, ctx.onCardAction);
212
+ if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
213
+ key: node.key,
214
+ promise: out,
215
+ sanitize: ctx.sanitize,
216
+ context: mountContext
217
+ });
218
+ const vnode = renderOutput(out, ctx.sanitize, mountContext);
219
+ vnode.key = node.key;
220
+ return vnode;
221
+ } catch {
222
+ return renderFallback(node, ctx);
223
+ }
161
224
  }
162
225
  switch (node.type) {
163
226
  case "heading": return (0, vue.h)(node.tag ?? "h1", {
@@ -194,6 +257,19 @@ function renderCard(node, ctx) {
194
257
  "data-aigui-card-invalid": ""
195
258
  }, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
196
259
  const Comp = ctx.registry?.getRender(card.type);
260
+ if (card.id && ctx.cardStore) return (0, vue.h)(CardHost, {
261
+ key: node.key,
262
+ component: Comp ? (0, vue.markRaw)((0, vue.toRaw)(Comp)) : void 0,
263
+ store: ctx.cardStore,
264
+ cardId: card.id,
265
+ cardType: card.type,
266
+ initialData: card.data,
267
+ onAction: (a) => ctx.onCardAction?.({
268
+ ...a,
269
+ cardType: card.type,
270
+ cardId: card.id
271
+ })
272
+ });
197
273
  if (!Comp) return (0, vue.h)("pre", {
198
274
  key: node.key,
199
275
  "data-aigui-card-fallback": ""
@@ -207,6 +283,85 @@ function renderCard(node, ctx) {
207
283
  })
208
284
  });
209
285
  }
286
+ const CardHost = (0, vue.defineComponent)({
287
+ name: "AIGuiCardHost",
288
+ props: {
289
+ component: {
290
+ type: [Object, Function],
291
+ default: void 0
292
+ },
293
+ store: {
294
+ type: Object,
295
+ required: true
296
+ },
297
+ cardId: {
298
+ type: String,
299
+ required: true
300
+ },
301
+ cardType: {
302
+ type: String,
303
+ required: true
304
+ },
305
+ initialData: {
306
+ type: null,
307
+ required: true
308
+ }
309
+ },
310
+ emits: ["action"],
311
+ setup(props, { emit }) {
312
+ const record = (0, vue.shallowRef)();
313
+ const failed = (0, vue.shallowRef)(false);
314
+ let unsubscribe;
315
+ let stopWatch;
316
+ const bind = ([store, cardId, cardType, initialData]) => {
317
+ unsubscribe?.();
318
+ unsubscribe = void 0;
319
+ failed.value = false;
320
+ record.value = void 0;
321
+ unsubscribe = store.subscribe(cardId, (next) => {
322
+ if (!next || next.type !== cardType) {
323
+ failed.value = true;
324
+ record.value = void 0;
325
+ return;
326
+ }
327
+ failed.value = false;
328
+ record.value = next;
329
+ });
330
+ try {
331
+ const current = store.register({
332
+ id: cardId,
333
+ type: cardType,
334
+ data: initialData
335
+ });
336
+ if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
337
+ record.value = current;
338
+ } catch {
339
+ failed.value = true;
340
+ record.value = void 0;
341
+ }
342
+ };
343
+ (0, vue.onMounted)(() => {
344
+ stopWatch = (0, vue.watch)(() => [
345
+ props.store,
346
+ props.cardId,
347
+ props.cardType,
348
+ props.initialData
349
+ ], bind, { immediate: true });
350
+ });
351
+ (0, vue.onBeforeUnmount)(() => {
352
+ stopWatch?.();
353
+ unsubscribe?.();
354
+ });
355
+ return () => {
356
+ if (failed.value || !record.value || !props.component) return (0, vue.h)("pre", { "data-aigui-card-fallback": "" }, [(0, vue.h)("code", JSON.stringify(record.value?.data ?? props.initialData, null, 2))]);
357
+ return (0, vue.h)((0, vue.markRaw)((0, vue.toRaw)(props.component)), {
358
+ data: record.value.data,
359
+ state: record.value.action,
360
+ onAction: (action) => emit("action", action)
361
+ });
362
+ };
363
+ }
364
+ });
210
365
  function renderFallback(node, ctx) {
211
366
  return (0, vue.h)("div", {
212
367
  key: node.key,
@@ -236,7 +391,19 @@ const AIRenderer = (0, vue.defineComponent)({
236
391
  type: [Boolean, Object],
237
392
  default: void 0
238
393
  },
239
- onCardAction: {
394
+ actionRuntime: {
395
+ type: Object,
396
+ default: void 0
397
+ },
398
+ cardStore: {
399
+ type: Object,
400
+ default: void 0
401
+ },
402
+ debug: {
403
+ type: Boolean,
404
+ default: false
405
+ },
406
+ onDebugEvent: {
240
407
  type: Function,
241
408
  default: void 0
242
409
  }
@@ -245,38 +412,77 @@ const AIRenderer = (0, vue.defineComponent)({
245
412
  const current = (0, vue.shallowRef)(useAIRenderer({
246
413
  registry: props.registry,
247
414
  sanitize: props.sanitize,
248
- plugins: props.plugins
415
+ plugins: props.plugins,
416
+ debug: props.debug,
417
+ onDebugEvent: props.onDebugEvent
249
418
  }));
250
- const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins));
419
+ const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer }));
420
+ let actionScope = {
421
+ controller: new AbortController(),
422
+ owner: {}
423
+ };
424
+ const resetActionScope = () => {
425
+ actionScope.controller.abort();
426
+ actionScope = {
427
+ controller: new AbortController(),
428
+ owner: {}
429
+ };
430
+ };
251
431
  (0, vue.watch)(() => [
252
432
  props.registry,
253
433
  props.sanitize,
254
- props.plugins
434
+ props.plugins,
435
+ props.debug,
436
+ props.onDebugEvent
255
437
  ], () => {
438
+ resetActionScope();
256
439
  current.value.destroy();
257
440
  current.value = useAIRenderer({
258
441
  registry: props.registry,
259
442
  sanitize: props.sanitize,
260
- plugins: props.plugins
443
+ plugins: props.plugins,
444
+ debug: props.debug,
445
+ onDebugEvent: props.onDebugEvent
261
446
  });
262
- nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins);
447
+ nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer });
448
+ });
449
+ (0, vue.watch)(() => props.actionRuntime, resetActionScope);
450
+ (0, vue.onBeforeUnmount)(() => {
451
+ resetActionScope();
452
+ current.value.destroy();
263
453
  });
264
- (0, vue.onBeforeUnmount)(() => current.value.destroy());
265
454
  const push = (chunk) => current.value.push(chunk);
266
455
  const feed = (source, options) => current.value.feed(source, options);
267
- const reset = () => current.value.reset();
456
+ const reset = () => {
457
+ resetActionScope();
458
+ current.value.reset();
459
+ };
268
460
  expose({
461
+ debugSource: "renderer",
462
+ subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
269
463
  push,
270
464
  feed,
271
465
  reset
272
466
  });
273
467
  return () => {
274
468
  const onCardAction = (action) => {
275
- props.onCardAction?.(action);
469
+ if (props.actionRuntime) {
470
+ const request = {
471
+ type: action.type,
472
+ params: action.params,
473
+ cardType: action.cardType,
474
+ ...action.cardId === void 0 ? {} : { cardId: action.cardId }
475
+ };
476
+ props.actionRuntime.dispatch(request, {
477
+ signal: actionScope.controller.signal,
478
+ owner: actionScope.owner
479
+ }).catch(() => {});
480
+ }
276
481
  emit("card-action", action);
277
482
  };
278
483
  const ctx = {
279
484
  registry: props.registry,
485
+ cardStore: props.cardStore,
280
486
  plugins: props.plugins,
281
487
  nodeRenderers: nodeRenderers.value,
282
488
  onCardAction,
@@ -292,4 +498,5 @@ const AIRenderer = (0, vue.defineComponent)({
292
498
  exports.AIRenderer = AIRenderer
293
499
  exports.renderNode = renderNode
294
500
  exports.renderOutput = renderOutput
295
- exports.useAIRenderer = useAIRenderer
501
+ exports.useAIRenderer = useAIRenderer
502
+ exports.useActionState = useActionState
package/dist/index.d.cts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { ShallowRef, VNode } from "vue";
2
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
2
+ import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardRegistry, CardStore, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
3
3
 
4
4
  //#region src/use-ai-renderer.d.ts
5
5
  interface UseAIRendererResult {
6
+ renderer: Renderer;
6
7
  nodes: ShallowRef<ASTNode[]>;
7
8
  push: (chunk: string) => void;
8
9
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
@@ -11,6 +12,10 @@ interface UseAIRendererResult {
11
12
  }
12
13
  declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
13
14
 
15
+ //#endregion
16
+ //#region src/use-action-state.d.ts
17
+ declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
18
+
14
19
  //#endregion
15
20
  //#region src/ai-renderer.d.ts
16
21
  declare const AIRenderer: any;
@@ -19,12 +24,14 @@ declare const AIRenderer: any;
19
24
  //#region src/render-node.d.ts
20
25
  interface RenderContext {
21
26
  registry?: CardRegistry;
27
+ cardStore?: CardStore;
22
28
  plugins?: AIGuiPlugin[];
23
29
  nodeRenderers?: Record<string, NodeRenderer>;
24
30
  onCardAction?: (action: {
25
31
  type: string;
26
32
  params?: unknown;
27
33
  cardType: string;
34
+ cardId?: string;
28
35
  }) => void;
29
36
  sanitize?: RendererOptions["sanitize"];
30
37
  sanitized?: boolean;
@@ -34,9 +41,9 @@ declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
34
41
  //#endregion
35
42
  //#region src/render-output.d.ts
36
43
  /** Translate a framework-neutral RenderOutput into a Vue VNode. */
37
- declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
44
+ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"], context?: RenderMountContext): VNode;
38
45
 
39
46
  //#endregion
40
47
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
41
48
 
42
- export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
49
+ export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { ShallowRef, VNode } from "vue";
2
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
2
+ import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardRegistry, CardStore, FeedOptions, FeedSource, NodeRenderer, RenderMountContext, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
3
3
 
4
4
  //#region src/use-ai-renderer.d.ts
5
5
  interface UseAIRendererResult {
6
+ renderer: Renderer;
6
7
  nodes: ShallowRef<ASTNode[]>;
7
8
  push: (chunk: string) => void;
8
9
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
@@ -11,6 +12,10 @@ interface UseAIRendererResult {
11
12
  }
12
13
  declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
13
14
 
15
+ //#endregion
16
+ //#region src/use-action-state.d.ts
17
+ declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
18
+
14
19
  //#endregion
15
20
  //#region src/ai-renderer.d.ts
16
21
  declare const AIRenderer: any;
@@ -19,12 +24,14 @@ declare const AIRenderer: any;
19
24
  //#region src/render-node.d.ts
20
25
  interface RenderContext {
21
26
  registry?: CardRegistry;
27
+ cardStore?: CardStore;
22
28
  plugins?: AIGuiPlugin[];
23
29
  nodeRenderers?: Record<string, NodeRenderer>;
24
30
  onCardAction?: (action: {
25
31
  type: string;
26
32
  params?: unknown;
27
33
  cardType: string;
34
+ cardId?: string;
28
35
  }) => void;
29
36
  sanitize?: RendererOptions["sanitize"];
30
37
  sanitized?: boolean;
@@ -34,9 +41,9 @@ declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
34
41
  //#endregion
35
42
  //#region src/render-output.d.ts
36
43
  /** Translate a framework-neutral RenderOutput into a Vue VNode. */
37
- declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
44
+ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"], context?: RenderMountContext): VNode;
38
45
 
39
46
  //#endregion
40
47
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
41
48
 
42
- export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
49
+ export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineComponent, h, markRaw, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch } from "vue";
1
+ import { defineComponent, h, markRaw, onBeforeUnmount, onMounted, onScopeDispose, ref, render, shallowRef, toRaw, watch } from "vue";
2
2
  import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
3
3
 
4
4
  //#region src/use-ai-renderer.ts
@@ -13,6 +13,7 @@ function useAIRenderer(options = {}) {
13
13
  }
14
14
  });
15
15
  return {
16
+ renderer,
16
17
  nodes,
17
18
  push: (c) => {
18
19
  if (active) renderer.push(c);
@@ -32,24 +33,41 @@ function useAIRenderer(options = {}) {
32
33
  };
33
34
  }
34
35
 
36
+ //#endregion
37
+ //#region src/use-action-state.ts
38
+ function useActionState(runtime, key) {
39
+ const state = shallowRef(runtime.getState(key));
40
+ const unsubscribe = runtime.subscribe((next) => {
41
+ if (next.key === key) state.value = next;
42
+ });
43
+ onScopeDispose(unsubscribe);
44
+ return state;
45
+ }
46
+
35
47
  //#endregion
36
48
  //#region src/render-output.ts
37
49
  /** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
38
50
  const MountHost = defineComponent({
39
51
  name: "MountHost",
40
- props: { mount: {
41
- type: Function,
42
- required: true
43
- } },
52
+ props: {
53
+ mount: {
54
+ type: Function,
55
+ required: true
56
+ },
57
+ context: {
58
+ type: Object,
59
+ required: true
60
+ }
61
+ },
44
62
  setup(props) {
45
63
  const elRef = ref(null);
46
64
  let cleanup;
47
65
  onMounted(() => {
48
- if (elRef.value) cleanup = props.mount(elRef.value);
66
+ if (elRef.value) cleanup = props.mount(elRef.value, props.context);
49
67
  });
50
68
  watch(() => props.mount, (mount) => {
51
69
  if (typeof cleanup === "function") cleanup();
52
- cleanup = elRef.value ? mount(elRef.value) : void 0;
70
+ cleanup = elRef.value ? mount(elRef.value, props.context) : void 0;
53
71
  });
54
72
  onBeforeUnmount(() => {
55
73
  if (typeof cleanup === "function") cleanup();
@@ -61,11 +79,14 @@ const MountHost = defineComponent({
61
79
  }
62
80
  });
63
81
  /** Translate a framework-neutral RenderOutput into a Vue VNode. */
64
- function renderOutput(out, sanitize) {
82
+ function renderOutput(out, sanitize, context = {}) {
65
83
  switch (out.kind) {
66
84
  case "html": return h("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
67
- case "element": return h(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
68
- case "mount": return h(MountHost, { mount: out.mount });
85
+ case "element": return h(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize, context)));
86
+ case "mount": return h(MountHost, {
87
+ mount: out.mount,
88
+ context
89
+ });
69
90
  case "card": return h("pre", { "data-aigui-card-fallback": "" }, [h("code", JSON.stringify(out.data, null, 2))]);
70
91
  }
71
92
  }
@@ -80,6 +101,10 @@ const AsyncOutput = defineComponent({
80
101
  sanitize: {
81
102
  type: [Boolean, Object],
82
103
  default: void 0
104
+ },
105
+ context: {
106
+ type: Object,
107
+ default: () => ({})
83
108
  }
84
109
  },
85
110
  setup(props) {
@@ -106,13 +131,42 @@ const AsyncOutput = defineComponent({
106
131
  if (failed.value) return h("span", { "data-aigui-async-error": "" });
107
132
  if (!resolved.value) return h("span", { "data-aigui-async-pending": "" });
108
133
  try {
109
- return renderOutput(resolved.value, props.sanitize);
134
+ return renderOutput(resolved.value, props.sanitize, props.context);
110
135
  } catch {
111
136
  return h("span", { "data-aigui-async-error": "" });
112
137
  }
113
138
  };
114
139
  }
115
140
  });
141
+ function createRenderMountContext(registry, onCardAction) {
142
+ return { mountCard(host, request) {
143
+ const component = registry?.getRender(request.type);
144
+ if (!component) return void 0;
145
+ const card = markRaw(toRaw(component));
146
+ let data = request.data;
147
+ let destroyed = false;
148
+ const renderCard$1 = () => render(h(card, {
149
+ data,
150
+ onAction: (action) => onCardAction?.({
151
+ ...action,
152
+ cardType: request.type
153
+ })
154
+ }), host);
155
+ renderCard$1();
156
+ return {
157
+ update(nextData) {
158
+ if (destroyed) return;
159
+ data = nextData;
160
+ renderCard$1();
161
+ },
162
+ destroy() {
163
+ if (destroyed) return;
164
+ destroyed = true;
165
+ render(null, host);
166
+ }
167
+ };
168
+ } };
169
+ }
116
170
  function sanitizeOutput(html, sanitize) {
117
171
  if (sanitize === false) return html;
118
172
  return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
@@ -122,18 +176,27 @@ function sanitizeOutput(html, sanitize) {
122
176
  //#region src/render-node.ts
123
177
  function renderNode(node, ctx) {
124
178
  const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
125
- if (r) try {
126
- const out = r(node);
127
- if (out && typeof out.then === "function") return h(AsyncOutput, {
179
+ if (r) {
180
+ if (node.complete === false) return h("div", {
128
181
  key: node.key,
129
- promise: out,
130
- sanitize: ctx.sanitize
182
+ "data-aigui-block-loading": "",
183
+ "data-block-type": node.type
131
184
  });
132
- const vnode = renderOutput(out, ctx.sanitize);
133
- vnode.key = node.key;
134
- return vnode;
135
- } catch {
136
- return renderFallback(node, ctx);
185
+ try {
186
+ const out = r(node);
187
+ const mountContext = createRenderMountContext(ctx.registry, ctx.onCardAction);
188
+ if (out && typeof out.then === "function") return h(AsyncOutput, {
189
+ key: node.key,
190
+ promise: out,
191
+ sanitize: ctx.sanitize,
192
+ context: mountContext
193
+ });
194
+ const vnode = renderOutput(out, ctx.sanitize, mountContext);
195
+ vnode.key = node.key;
196
+ return vnode;
197
+ } catch {
198
+ return renderFallback(node, ctx);
199
+ }
137
200
  }
138
201
  switch (node.type) {
139
202
  case "heading": return h(node.tag ?? "h1", {
@@ -170,6 +233,19 @@ function renderCard(node, ctx) {
170
233
  "data-aigui-card-invalid": ""
171
234
  }, [h("code", JSON.stringify(card.data, null, 2))]);
172
235
  const Comp = ctx.registry?.getRender(card.type);
236
+ if (card.id && ctx.cardStore) return h(CardHost, {
237
+ key: node.key,
238
+ component: Comp ? markRaw(toRaw(Comp)) : void 0,
239
+ store: ctx.cardStore,
240
+ cardId: card.id,
241
+ cardType: card.type,
242
+ initialData: card.data,
243
+ onAction: (a) => ctx.onCardAction?.({
244
+ ...a,
245
+ cardType: card.type,
246
+ cardId: card.id
247
+ })
248
+ });
173
249
  if (!Comp) return h("pre", {
174
250
  key: node.key,
175
251
  "data-aigui-card-fallback": ""
@@ -183,6 +259,85 @@ function renderCard(node, ctx) {
183
259
  })
184
260
  });
185
261
  }
262
+ const CardHost = defineComponent({
263
+ name: "AIGuiCardHost",
264
+ props: {
265
+ component: {
266
+ type: [Object, Function],
267
+ default: void 0
268
+ },
269
+ store: {
270
+ type: Object,
271
+ required: true
272
+ },
273
+ cardId: {
274
+ type: String,
275
+ required: true
276
+ },
277
+ cardType: {
278
+ type: String,
279
+ required: true
280
+ },
281
+ initialData: {
282
+ type: null,
283
+ required: true
284
+ }
285
+ },
286
+ emits: ["action"],
287
+ setup(props, { emit }) {
288
+ const record = shallowRef();
289
+ const failed = shallowRef(false);
290
+ let unsubscribe;
291
+ let stopWatch;
292
+ const bind = ([store, cardId, cardType, initialData]) => {
293
+ unsubscribe?.();
294
+ unsubscribe = void 0;
295
+ failed.value = false;
296
+ record.value = void 0;
297
+ unsubscribe = store.subscribe(cardId, (next) => {
298
+ if (!next || next.type !== cardType) {
299
+ failed.value = true;
300
+ record.value = void 0;
301
+ return;
302
+ }
303
+ failed.value = false;
304
+ record.value = next;
305
+ });
306
+ try {
307
+ const current = store.register({
308
+ id: cardId,
309
+ type: cardType,
310
+ data: initialData
311
+ });
312
+ if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
313
+ record.value = current;
314
+ } catch {
315
+ failed.value = true;
316
+ record.value = void 0;
317
+ }
318
+ };
319
+ onMounted(() => {
320
+ stopWatch = watch(() => [
321
+ props.store,
322
+ props.cardId,
323
+ props.cardType,
324
+ props.initialData
325
+ ], bind, { immediate: true });
326
+ });
327
+ onBeforeUnmount(() => {
328
+ stopWatch?.();
329
+ unsubscribe?.();
330
+ });
331
+ return () => {
332
+ if (failed.value || !record.value || !props.component) return h("pre", { "data-aigui-card-fallback": "" }, [h("code", JSON.stringify(record.value?.data ?? props.initialData, null, 2))]);
333
+ return h(markRaw(toRaw(props.component)), {
334
+ data: record.value.data,
335
+ state: record.value.action,
336
+ onAction: (action) => emit("action", action)
337
+ });
338
+ };
339
+ }
340
+ });
186
341
  function renderFallback(node, ctx) {
187
342
  return h("div", {
188
343
  key: node.key,
@@ -212,7 +367,19 @@ const AIRenderer = defineComponent({
212
367
  type: [Boolean, Object],
213
368
  default: void 0
214
369
  },
215
- onCardAction: {
370
+ actionRuntime: {
371
+ type: Object,
372
+ default: void 0
373
+ },
374
+ cardStore: {
375
+ type: Object,
376
+ default: void 0
377
+ },
378
+ debug: {
379
+ type: Boolean,
380
+ default: false
381
+ },
382
+ onDebugEvent: {
216
383
  type: Function,
217
384
  default: void 0
218
385
  }
@@ -221,38 +388,77 @@ const AIRenderer = defineComponent({
221
388
  const current = shallowRef(useAIRenderer({
222
389
  registry: props.registry,
223
390
  sanitize: props.sanitize,
224
- plugins: props.plugins
391
+ plugins: props.plugins,
392
+ debug: props.debug,
393
+ onDebugEvent: props.onDebugEvent
225
394
  }));
226
- const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins));
395
+ const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer }));
396
+ let actionScope = {
397
+ controller: new AbortController(),
398
+ owner: {}
399
+ };
400
+ const resetActionScope = () => {
401
+ actionScope.controller.abort();
402
+ actionScope = {
403
+ controller: new AbortController(),
404
+ owner: {}
405
+ };
406
+ };
227
407
  watch(() => [
228
408
  props.registry,
229
409
  props.sanitize,
230
- props.plugins
410
+ props.plugins,
411
+ props.debug,
412
+ props.onDebugEvent
231
413
  ], () => {
414
+ resetActionScope();
232
415
  current.value.destroy();
233
416
  current.value = useAIRenderer({
234
417
  registry: props.registry,
235
418
  sanitize: props.sanitize,
236
- plugins: props.plugins
419
+ plugins: props.plugins,
420
+ debug: props.debug,
421
+ onDebugEvent: props.onDebugEvent
237
422
  });
238
- nodeRenderers.value = collectNodeRenderers(props.plugins);
423
+ nodeRenderers.value = collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer });
424
+ });
425
+ watch(() => props.actionRuntime, resetActionScope);
426
+ onBeforeUnmount(() => {
427
+ resetActionScope();
428
+ current.value.destroy();
239
429
  });
240
- onBeforeUnmount(() => current.value.destroy());
241
430
  const push = (chunk) => current.value.push(chunk);
242
431
  const feed = (source, options) => current.value.feed(source, options);
243
- const reset = () => current.value.reset();
432
+ const reset = () => {
433
+ resetActionScope();
434
+ current.value.reset();
435
+ };
244
436
  expose({
437
+ debugSource: "renderer",
438
+ subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
245
439
  push,
246
440
  feed,
247
441
  reset
248
442
  });
249
443
  return () => {
250
444
  const onCardAction = (action) => {
251
- props.onCardAction?.(action);
445
+ if (props.actionRuntime) {
446
+ const request = {
447
+ type: action.type,
448
+ params: action.params,
449
+ cardType: action.cardType,
450
+ ...action.cardId === void 0 ? {} : { cardId: action.cardId }
451
+ };
452
+ props.actionRuntime.dispatch(request, {
453
+ signal: actionScope.controller.signal,
454
+ owner: actionScope.owner
455
+ }).catch(() => {});
456
+ }
252
457
  emit("card-action", action);
253
458
  };
254
459
  const ctx = {
255
460
  registry: props.registry,
461
+ cardStore: props.cardStore,
256
462
  plugins: props.plugins,
257
463
  nodeRenderers: nodeRenderers.value,
258
464
  onCardAction,
@@ -265,4 +471,4 @@ const AIRenderer = defineComponent({
265
471
  });
266
472
 
267
473
  //#endregion
268
- export { AIRenderer, renderNode, renderOutput, useAIRenderer };
474
+ export { AIRenderer, renderNode, renderOutput, useAIRenderer, useActionState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/vue",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Vue 3 adapter for AIGUI — stream LLM output into live Vue components with cards and plugins.",
5
5
  "keywords": [
6
6
  "llm",
@@ -51,7 +51,7 @@
51
51
  "access": "public"
52
52
  },
53
53
  "dependencies": {
54
- "@ai-gui/core": "0.2.0"
54
+ "@ai-gui/core": "0.4.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vue": ">=3.3"