@ai-gui/vue 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
@@ -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,6 +57,17 @@ 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. */
@@ -146,18 +158,25 @@ function sanitizeOutput(html, sanitize) {
146
158
  //#region src/render-node.ts
147
159
  function renderNode(node, ctx) {
148
160
  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, {
161
+ if (r) {
162
+ if (node.complete === false) return (0, vue.h)("div", {
152
163
  key: node.key,
153
- promise: out,
154
- sanitize: ctx.sanitize
164
+ "data-aigui-block-loading": "",
165
+ "data-block-type": node.type
155
166
  });
156
- const vnode = renderOutput(out, ctx.sanitize);
157
- vnode.key = node.key;
158
- return vnode;
159
- } catch {
160
- return renderFallback(node, ctx);
167
+ try {
168
+ const out = r(node);
169
+ if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
170
+ key: node.key,
171
+ promise: out,
172
+ sanitize: ctx.sanitize
173
+ });
174
+ const vnode = renderOutput(out, ctx.sanitize);
175
+ vnode.key = node.key;
176
+ return vnode;
177
+ } catch {
178
+ return renderFallback(node, ctx);
179
+ }
161
180
  }
162
181
  switch (node.type) {
163
182
  case "heading": return (0, vue.h)(node.tag ?? "h1", {
@@ -194,6 +213,19 @@ function renderCard(node, ctx) {
194
213
  "data-aigui-card-invalid": ""
195
214
  }, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
196
215
  const Comp = ctx.registry?.getRender(card.type);
216
+ if (card.id && ctx.cardStore) return (0, vue.h)(CardHost, {
217
+ key: node.key,
218
+ component: Comp ? (0, vue.markRaw)((0, vue.toRaw)(Comp)) : void 0,
219
+ store: ctx.cardStore,
220
+ cardId: card.id,
221
+ cardType: card.type,
222
+ initialData: card.data,
223
+ onAction: (a) => ctx.onCardAction?.({
224
+ ...a,
225
+ cardType: card.type,
226
+ cardId: card.id
227
+ })
228
+ });
197
229
  if (!Comp) return (0, vue.h)("pre", {
198
230
  key: node.key,
199
231
  "data-aigui-card-fallback": ""
@@ -207,6 +239,85 @@ function renderCard(node, ctx) {
207
239
  })
208
240
  });
209
241
  }
242
+ const CardHost = (0, vue.defineComponent)({
243
+ name: "AIGuiCardHost",
244
+ props: {
245
+ component: {
246
+ type: [Object, Function],
247
+ default: void 0
248
+ },
249
+ store: {
250
+ type: Object,
251
+ required: true
252
+ },
253
+ cardId: {
254
+ type: String,
255
+ required: true
256
+ },
257
+ cardType: {
258
+ type: String,
259
+ required: true
260
+ },
261
+ initialData: {
262
+ type: null,
263
+ required: true
264
+ }
265
+ },
266
+ emits: ["action"],
267
+ setup(props, { emit }) {
268
+ const record = (0, vue.shallowRef)();
269
+ const failed = (0, vue.shallowRef)(false);
270
+ let unsubscribe;
271
+ let stopWatch;
272
+ const bind = ([store, cardId, cardType, initialData]) => {
273
+ unsubscribe?.();
274
+ unsubscribe = void 0;
275
+ failed.value = false;
276
+ record.value = void 0;
277
+ unsubscribe = store.subscribe(cardId, (next) => {
278
+ if (!next || next.type !== cardType) {
279
+ failed.value = true;
280
+ record.value = void 0;
281
+ return;
282
+ }
283
+ failed.value = false;
284
+ record.value = next;
285
+ });
286
+ try {
287
+ const current = store.register({
288
+ id: cardId,
289
+ type: cardType,
290
+ data: initialData
291
+ });
292
+ if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
293
+ record.value = current;
294
+ } catch {
295
+ failed.value = true;
296
+ record.value = void 0;
297
+ }
298
+ };
299
+ (0, vue.onMounted)(() => {
300
+ stopWatch = (0, vue.watch)(() => [
301
+ props.store,
302
+ props.cardId,
303
+ props.cardType,
304
+ props.initialData
305
+ ], bind, { immediate: true });
306
+ });
307
+ (0, vue.onBeforeUnmount)(() => {
308
+ stopWatch?.();
309
+ unsubscribe?.();
310
+ });
311
+ return () => {
312
+ 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))]);
313
+ return (0, vue.h)((0, vue.markRaw)((0, vue.toRaw)(props.component)), {
314
+ data: record.value.data,
315
+ state: record.value.action,
316
+ onAction: (action) => emit("action", action)
317
+ });
318
+ };
319
+ }
320
+ });
210
321
  function renderFallback(node, ctx) {
211
322
  return (0, vue.h)("div", {
212
323
  key: node.key,
@@ -236,7 +347,19 @@ const AIRenderer = (0, vue.defineComponent)({
236
347
  type: [Boolean, Object],
237
348
  default: void 0
238
349
  },
239
- onCardAction: {
350
+ actionRuntime: {
351
+ type: Object,
352
+ default: void 0
353
+ },
354
+ cardStore: {
355
+ type: Object,
356
+ default: void 0
357
+ },
358
+ debug: {
359
+ type: Boolean,
360
+ default: false
361
+ },
362
+ onDebugEvent: {
240
363
  type: Function,
241
364
  default: void 0
242
365
  }
@@ -245,38 +368,77 @@ const AIRenderer = (0, vue.defineComponent)({
245
368
  const current = (0, vue.shallowRef)(useAIRenderer({
246
369
  registry: props.registry,
247
370
  sanitize: props.sanitize,
248
- plugins: props.plugins
371
+ plugins: props.plugins,
372
+ debug: props.debug,
373
+ onDebugEvent: props.onDebugEvent
249
374
  }));
250
- const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins));
375
+ const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer }));
376
+ let actionScope = {
377
+ controller: new AbortController(),
378
+ owner: {}
379
+ };
380
+ const resetActionScope = () => {
381
+ actionScope.controller.abort();
382
+ actionScope = {
383
+ controller: new AbortController(),
384
+ owner: {}
385
+ };
386
+ };
251
387
  (0, vue.watch)(() => [
252
388
  props.registry,
253
389
  props.sanitize,
254
- props.plugins
390
+ props.plugins,
391
+ props.debug,
392
+ props.onDebugEvent
255
393
  ], () => {
394
+ resetActionScope();
256
395
  current.value.destroy();
257
396
  current.value = useAIRenderer({
258
397
  registry: props.registry,
259
398
  sanitize: props.sanitize,
260
- plugins: props.plugins
399
+ plugins: props.plugins,
400
+ debug: props.debug,
401
+ onDebugEvent: props.onDebugEvent
261
402
  });
262
- nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins);
403
+ nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer });
404
+ });
405
+ (0, vue.watch)(() => props.actionRuntime, resetActionScope);
406
+ (0, vue.onBeforeUnmount)(() => {
407
+ resetActionScope();
408
+ current.value.destroy();
263
409
  });
264
- (0, vue.onBeforeUnmount)(() => current.value.destroy());
265
410
  const push = (chunk) => current.value.push(chunk);
266
411
  const feed = (source, options) => current.value.feed(source, options);
267
- const reset = () => current.value.reset();
412
+ const reset = () => {
413
+ resetActionScope();
414
+ current.value.reset();
415
+ };
268
416
  expose({
417
+ debugSource: "renderer",
418
+ subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
269
419
  push,
270
420
  feed,
271
421
  reset
272
422
  });
273
423
  return () => {
274
424
  const onCardAction = (action) => {
275
- props.onCardAction?.(action);
425
+ if (props.actionRuntime) {
426
+ const request = {
427
+ type: action.type,
428
+ params: action.params,
429
+ cardType: action.cardType,
430
+ ...action.cardId === void 0 ? {} : { cardId: action.cardId }
431
+ };
432
+ props.actionRuntime.dispatch(request, {
433
+ signal: actionScope.controller.signal,
434
+ owner: actionScope.owner
435
+ }).catch(() => {});
436
+ }
276
437
  emit("card-action", action);
277
438
  };
278
439
  const ctx = {
279
440
  registry: props.registry,
441
+ cardStore: props.cardStore,
280
442
  plugins: props.plugins,
281
443
  nodeRenderers: nodeRenderers.value,
282
444
  onCardAction,
@@ -292,4 +454,5 @@ const AIRenderer = (0, vue.defineComponent)({
292
454
  exports.AIRenderer = AIRenderer
293
455
  exports.renderNode = renderNode
294
456
  exports.renderOutput = renderOutput
295
- exports.useAIRenderer = useAIRenderer
457
+ exports.useAIRenderer = useAIRenderer
458
+ 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, 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;
@@ -39,4 +46,4 @@ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["san
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, 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;
@@ -39,4 +46,4 @@ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["san
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, 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,6 +33,17 @@ 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. */
@@ -122,18 +134,25 @@ function sanitizeOutput(html, sanitize) {
122
134
  //#region src/render-node.ts
123
135
  function renderNode(node, ctx) {
124
136
  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, {
137
+ if (r) {
138
+ if (node.complete === false) return h("div", {
128
139
  key: node.key,
129
- promise: out,
130
- sanitize: ctx.sanitize
140
+ "data-aigui-block-loading": "",
141
+ "data-block-type": node.type
131
142
  });
132
- const vnode = renderOutput(out, ctx.sanitize);
133
- vnode.key = node.key;
134
- return vnode;
135
- } catch {
136
- return renderFallback(node, ctx);
143
+ try {
144
+ const out = r(node);
145
+ if (out && typeof out.then === "function") return h(AsyncOutput, {
146
+ key: node.key,
147
+ promise: out,
148
+ sanitize: ctx.sanitize
149
+ });
150
+ const vnode = renderOutput(out, ctx.sanitize);
151
+ vnode.key = node.key;
152
+ return vnode;
153
+ } catch {
154
+ return renderFallback(node, ctx);
155
+ }
137
156
  }
138
157
  switch (node.type) {
139
158
  case "heading": return h(node.tag ?? "h1", {
@@ -170,6 +189,19 @@ function renderCard(node, ctx) {
170
189
  "data-aigui-card-invalid": ""
171
190
  }, [h("code", JSON.stringify(card.data, null, 2))]);
172
191
  const Comp = ctx.registry?.getRender(card.type);
192
+ if (card.id && ctx.cardStore) return h(CardHost, {
193
+ key: node.key,
194
+ component: Comp ? markRaw(toRaw(Comp)) : void 0,
195
+ store: ctx.cardStore,
196
+ cardId: card.id,
197
+ cardType: card.type,
198
+ initialData: card.data,
199
+ onAction: (a) => ctx.onCardAction?.({
200
+ ...a,
201
+ cardType: card.type,
202
+ cardId: card.id
203
+ })
204
+ });
173
205
  if (!Comp) return h("pre", {
174
206
  key: node.key,
175
207
  "data-aigui-card-fallback": ""
@@ -183,6 +215,85 @@ function renderCard(node, ctx) {
183
215
  })
184
216
  });
185
217
  }
218
+ const CardHost = defineComponent({
219
+ name: "AIGuiCardHost",
220
+ props: {
221
+ component: {
222
+ type: [Object, Function],
223
+ default: void 0
224
+ },
225
+ store: {
226
+ type: Object,
227
+ required: true
228
+ },
229
+ cardId: {
230
+ type: String,
231
+ required: true
232
+ },
233
+ cardType: {
234
+ type: String,
235
+ required: true
236
+ },
237
+ initialData: {
238
+ type: null,
239
+ required: true
240
+ }
241
+ },
242
+ emits: ["action"],
243
+ setup(props, { emit }) {
244
+ const record = shallowRef();
245
+ const failed = shallowRef(false);
246
+ let unsubscribe;
247
+ let stopWatch;
248
+ const bind = ([store, cardId, cardType, initialData]) => {
249
+ unsubscribe?.();
250
+ unsubscribe = void 0;
251
+ failed.value = false;
252
+ record.value = void 0;
253
+ unsubscribe = store.subscribe(cardId, (next) => {
254
+ if (!next || next.type !== cardType) {
255
+ failed.value = true;
256
+ record.value = void 0;
257
+ return;
258
+ }
259
+ failed.value = false;
260
+ record.value = next;
261
+ });
262
+ try {
263
+ const current = store.register({
264
+ id: cardId,
265
+ type: cardType,
266
+ data: initialData
267
+ });
268
+ if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
269
+ record.value = current;
270
+ } catch {
271
+ failed.value = true;
272
+ record.value = void 0;
273
+ }
274
+ };
275
+ onMounted(() => {
276
+ stopWatch = watch(() => [
277
+ props.store,
278
+ props.cardId,
279
+ props.cardType,
280
+ props.initialData
281
+ ], bind, { immediate: true });
282
+ });
283
+ onBeforeUnmount(() => {
284
+ stopWatch?.();
285
+ unsubscribe?.();
286
+ });
287
+ return () => {
288
+ 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))]);
289
+ return h(markRaw(toRaw(props.component)), {
290
+ data: record.value.data,
291
+ state: record.value.action,
292
+ onAction: (action) => emit("action", action)
293
+ });
294
+ };
295
+ }
296
+ });
186
297
  function renderFallback(node, ctx) {
187
298
  return h("div", {
188
299
  key: node.key,
@@ -212,7 +323,19 @@ const AIRenderer = defineComponent({
212
323
  type: [Boolean, Object],
213
324
  default: void 0
214
325
  },
215
- onCardAction: {
326
+ actionRuntime: {
327
+ type: Object,
328
+ default: void 0
329
+ },
330
+ cardStore: {
331
+ type: Object,
332
+ default: void 0
333
+ },
334
+ debug: {
335
+ type: Boolean,
336
+ default: false
337
+ },
338
+ onDebugEvent: {
216
339
  type: Function,
217
340
  default: void 0
218
341
  }
@@ -221,38 +344,77 @@ const AIRenderer = defineComponent({
221
344
  const current = shallowRef(useAIRenderer({
222
345
  registry: props.registry,
223
346
  sanitize: props.sanitize,
224
- plugins: props.plugins
347
+ plugins: props.plugins,
348
+ debug: props.debug,
349
+ onDebugEvent: props.onDebugEvent
225
350
  }));
226
- const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins));
351
+ const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer }));
352
+ let actionScope = {
353
+ controller: new AbortController(),
354
+ owner: {}
355
+ };
356
+ const resetActionScope = () => {
357
+ actionScope.controller.abort();
358
+ actionScope = {
359
+ controller: new AbortController(),
360
+ owner: {}
361
+ };
362
+ };
227
363
  watch(() => [
228
364
  props.registry,
229
365
  props.sanitize,
230
- props.plugins
366
+ props.plugins,
367
+ props.debug,
368
+ props.onDebugEvent
231
369
  ], () => {
370
+ resetActionScope();
232
371
  current.value.destroy();
233
372
  current.value = useAIRenderer({
234
373
  registry: props.registry,
235
374
  sanitize: props.sanitize,
236
- plugins: props.plugins
375
+ plugins: props.plugins,
376
+ debug: props.debug,
377
+ onDebugEvent: props.onDebugEvent
237
378
  });
238
- nodeRenderers.value = collectNodeRenderers(props.plugins);
379
+ nodeRenderers.value = collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer });
380
+ });
381
+ watch(() => props.actionRuntime, resetActionScope);
382
+ onBeforeUnmount(() => {
383
+ resetActionScope();
384
+ current.value.destroy();
239
385
  });
240
- onBeforeUnmount(() => current.value.destroy());
241
386
  const push = (chunk) => current.value.push(chunk);
242
387
  const feed = (source, options) => current.value.feed(source, options);
243
- const reset = () => current.value.reset();
388
+ const reset = () => {
389
+ resetActionScope();
390
+ current.value.reset();
391
+ };
244
392
  expose({
393
+ debugSource: "renderer",
394
+ subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
245
395
  push,
246
396
  feed,
247
397
  reset
248
398
  });
249
399
  return () => {
250
400
  const onCardAction = (action) => {
251
- props.onCardAction?.(action);
401
+ if (props.actionRuntime) {
402
+ const request = {
403
+ type: action.type,
404
+ params: action.params,
405
+ cardType: action.cardType,
406
+ ...action.cardId === void 0 ? {} : { cardId: action.cardId }
407
+ };
408
+ props.actionRuntime.dispatch(request, {
409
+ signal: actionScope.controller.signal,
410
+ owner: actionScope.owner
411
+ }).catch(() => {});
412
+ }
252
413
  emit("card-action", action);
253
414
  };
254
415
  const ctx = {
255
416
  registry: props.registry,
417
+ cardStore: props.cardStore,
256
418
  plugins: props.plugins,
257
419
  nodeRenderers: nodeRenderers.value,
258
420
  onCardAction,
@@ -265,4 +427,4 @@ const AIRenderer = defineComponent({
265
427
  });
266
428
 
267
429
  //#endregion
268
- export { AIRenderer, renderNode, renderOutput, useAIRenderer };
430
+ 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.3.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.3.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vue": ">=3.3"