@ai-gui/vanilla 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,24 +11,37 @@ pnpm add @ai-gui/core @ai-gui/vanilla
11
11
  ## Usage
12
12
 
13
13
  ```ts
14
- import { CardRegistry } from "@ai-gui/core"
14
+ import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
15
15
  import { createRenderer } from "@ai-gui/vanilla"
16
16
 
17
17
  const registry = new CardRegistry()
18
18
  registry.register({
19
19
  type: "weather",
20
20
  description: "Weather summary",
21
- // card render = (data, { onAction }) => HTMLElement
22
- render: (data: any, { onAction }: any) => {
21
+ // HTMLElement remains supported. Return a VanillaCardInstance for stateful cards.
22
+ render: (data: any, { state, onAction }: any) => {
23
23
  const el = document.createElement("div")
24
24
  el.textContent = `${data.city} — ${data.tempC}°C`
25
- return el
25
+ return {
26
+ element: el,
27
+ update(next: any, context: any) {
28
+ el.textContent = `${next.city} — ${next.tempC}°C (${context.state.status})`
29
+ },
30
+ destroy() {},
31
+ }
26
32
  },
27
33
  })
28
34
 
35
+ const actions = new ActionRegistry()
36
+ actions.register({ type: "refresh", run: async (params, { signal }) => fetch("/api/weather", { signal }) })
37
+ const cardStore = new CardStore({ registry })
38
+ const actionRuntime = createActionRuntime({ registry: actions, cardStore })
39
+
29
40
  const r = createRenderer(document.getElementById("out")!, {
30
41
  registry,
31
- onCardAction: ({ type, params, cardType }) => {/* app makes the real request */},
42
+ cardStore,
43
+ actionRuntime,
44
+ onCardAction: (action) => console.log("observed", action),
32
45
  })
33
46
 
34
47
  await r.feed(response.body!) // r.push(chunk) / r.reset() / r.destroy()
@@ -36,6 +49,9 @@ await r.feed(response.body!) // r.push(chunk) / r.reset() / r.destroy()
36
49
 
37
50
  ## Exports
38
51
 
39
- - `createRenderer(el, { registry?, plugins?, sanitize?, onCardAction? })` → `{ push, feed, reset, destroy }`.
52
+ - `createRenderer(el, { registry?, cardStore?, plugins?, sanitize?, actionRuntime?, onCardAction? })` → `{ push, feed, reset, destroy }`.
53
+ - `VanillaCardInstance` is `{ element, update(data, { state, onAction }), destroy? }`. Its host and `element` stay mounted while AST data, store data, or action state changes.
54
+ - Legacy factories returning an `HTMLElement` remain supported. For cards with an `id` and a `cardStore`, the adapter may rebuild that child element after store updates so it displays current data.
55
+ - Cards with a valid top-level `id` register their initial data in the supplied external `CardStore`. The adapter subscribes but never clears that store; a shared store updates every renderer displaying the same card id.
40
56
 
41
57
  `destroy()` tears down the host and cleans up any mounted interactive widgets. See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
package/dist/index.cjs CHANGED
@@ -70,25 +70,33 @@ function sanitizeOutput(html, sanitize) {
70
70
  //#region src/render-node-dom.ts
71
71
  function renderNodeToElement(node, ctx) {
72
72
  const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
73
- if (r) try {
74
- const out = r(node);
75
- if (typeof out?.then === "function") {
76
- const host = document.createElement("div");
77
- host.setAttribute("data-aigui-async-pending", "");
78
- out.then((res) => {
79
- if (host.__aiguiDisposed) return;
80
- host.removeAttribute("data-aigui-async-pending");
81
- host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
82
- }, () => {
83
- if (host.__aiguiDisposed) return;
84
- host.removeAttribute("data-aigui-async-pending");
85
- host.setAttribute("data-aigui-async-error", "");
86
- });
87
- return host;
73
+ if (r) {
74
+ if (node.complete === false) {
75
+ const loading = document.createElement("div");
76
+ loading.setAttribute("data-aigui-block-loading", "");
77
+ loading.setAttribute("data-block-type", node.type);
78
+ return loading;
79
+ }
80
+ try {
81
+ const out = r(node);
82
+ if (typeof out?.then === "function") {
83
+ const host = document.createElement("div");
84
+ host.setAttribute("data-aigui-async-pending", "");
85
+ out.then((res) => {
86
+ if (host.__aiguiDisposed) return;
87
+ host.removeAttribute("data-aigui-async-pending");
88
+ host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
89
+ }, () => {
90
+ if (host.__aiguiDisposed) return;
91
+ host.removeAttribute("data-aigui-async-pending");
92
+ host.setAttribute("data-aigui-async-error", "");
93
+ });
94
+ return host;
95
+ }
96
+ return renderOutputToElement(out, ctx.sanitize);
97
+ } catch {
98
+ return renderFallback(node, ctx);
88
99
  }
89
- return renderOutputToElement(out, ctx.sanitize);
90
- } catch {
91
- return renderFallback(node, ctx);
92
100
  }
93
101
  switch (node.type) {
94
102
  case "heading": {
@@ -146,22 +154,153 @@ function renderCardElement(node, ctx) {
146
154
  return pre;
147
155
  }
148
156
  const factory = ctx.registry?.getRender(card.type);
149
- if (!factory) {
150
- const pre = document.createElement("pre");
151
- pre.setAttribute("data-aigui-card-fallback", "");
152
- const c = document.createElement("code");
153
- c.textContent = JSON.stringify(card.data, null, 2);
154
- pre.appendChild(c);
155
- return pre;
156
- }
157
+ if (!factory) return createCardFallback(card, ctx);
157
158
  const host = document.createElement("div");
158
159
  host.setAttribute("data-aigui-card", card.type);
159
- host.appendChild(factory(card.data, { onAction: (a) => ctx.onCardAction?.({
160
- ...a,
161
- cardType: card.type
162
- }) }));
160
+ const controller = createCardController(host, card, factory, ctx);
161
+ host.__aiguiCardController = controller;
162
+ host.__aiguiCleanup = () => controller.destroy();
163
163
  return host;
164
164
  }
165
+ function createCardFallback(card, ctx) {
166
+ const pre = document.createElement("pre");
167
+ pre.setAttribute("data-aigui-card-fallback", "");
168
+ const code = document.createElement("code");
169
+ pre.appendChild(code);
170
+ const render = (data) => {
171
+ pre.removeAttribute("data-aigui-card-missing");
172
+ code.textContent = JSON.stringify(data, null, 2);
173
+ };
174
+ const missing = () => {
175
+ pre.setAttribute("data-aigui-card-missing", "");
176
+ code.textContent = "Card data is unavailable";
177
+ };
178
+ let unsubscribe = () => {};
179
+ if (card.id !== void 0 && ctx.cardStore) {
180
+ try {
181
+ const record = ctx.cardStore.register({
182
+ id: card.id,
183
+ type: card.type,
184
+ data: card.data
185
+ });
186
+ render(record.data);
187
+ } catch (error) {
188
+ pre.setAttribute("data-aigui-card-store-error", "");
189
+ code.textContent = error instanceof Error ? error.message : "Card registration failed";
190
+ }
191
+ unsubscribe = ctx.cardStore.subscribe(card.id, (next) => {
192
+ if (!next || next.type !== card.type) missing();
193
+ else {
194
+ pre.removeAttribute("data-aigui-card-store-error");
195
+ render(next.data);
196
+ }
197
+ });
198
+ pre.__aiguiCleanup = unsubscribe;
199
+ } else render(card.data);
200
+ return pre;
201
+ }
202
+ function updateCardElement(el, node) {
203
+ return el.__aiguiCardController?.updateNode(node) ?? false;
204
+ }
205
+ function createCardController(host, initialCard, factory, ctx) {
206
+ let card = initialCard;
207
+ let destroyed = false;
208
+ let unsubscribe = () => {};
209
+ let instance;
210
+ let element;
211
+ let missing = false;
212
+ const id = card.id;
213
+ const onAction = (action) => ctx.onCardAction?.({
214
+ ...action,
215
+ cardType: card.type,
216
+ ...id === void 0 ? {} : { cardId: id }
217
+ });
218
+ let data = card.data;
219
+ let state = { status: "idle" };
220
+ let registrationError;
221
+ if (id !== void 0 && ctx.cardStore) try {
222
+ const record = ctx.cardStore.register({
223
+ id,
224
+ type: card.type,
225
+ data
226
+ });
227
+ data = record.data;
228
+ state = record.action;
229
+ } catch (error) {
230
+ registrationError = error;
231
+ }
232
+ const context = () => ({
233
+ state,
234
+ onAction
235
+ });
236
+ const mount = (nextData) => {
237
+ const rendered = factory(nextData, context());
238
+ if (isVanillaCardInstance(rendered)) {
239
+ instance = rendered;
240
+ element = rendered.element;
241
+ } else {
242
+ instance = void 0;
243
+ element = rendered;
244
+ }
245
+ host.replaceChildren(element);
246
+ };
247
+ const update = (nextData, nextState) => {
248
+ if (destroyed) return;
249
+ data = nextData;
250
+ state = nextState;
251
+ host.removeAttribute("data-aigui-card-store-error");
252
+ if (missing) {
253
+ missing = false;
254
+ host.removeAttribute("data-aigui-card-missing");
255
+ if (element) host.replaceChildren(element);
256
+ }
257
+ if (instance) instance.update(data, context());
258
+ else mount(data);
259
+ };
260
+ const showMissing = () => {
261
+ if (destroyed || missing) return;
262
+ missing = true;
263
+ host.removeAttribute("data-aigui-card-store-error");
264
+ host.setAttribute("data-aigui-card-missing", "");
265
+ const fallback = document.createElement("code");
266
+ fallback.textContent = "Card data is unavailable";
267
+ host.replaceChildren(fallback);
268
+ };
269
+ if (registrationError === void 0) mount(data);
270
+ else {
271
+ host.setAttribute("data-aigui-card-store-error", "");
272
+ const code = document.createElement("code");
273
+ code.textContent = registrationError instanceof Error ? registrationError.message : "Card registration failed";
274
+ host.replaceChildren(code);
275
+ }
276
+ if (id !== void 0 && ctx.cardStore) unsubscribe = ctx.cardStore.subscribe(id, (record) => {
277
+ if (!record || record.type !== card.type) {
278
+ showMissing();
279
+ return;
280
+ }
281
+ update(record.data, record.action);
282
+ });
283
+ return {
284
+ updateNode(node) {
285
+ const next = node.card;
286
+ if (!next?.complete || !next.valid || next.type !== card.type || next.id !== id) return false;
287
+ card = next;
288
+ if (id === void 0 || !ctx.cardStore) update(next.data, state);
289
+ return true;
290
+ },
291
+ destroy() {
292
+ if (destroyed) return;
293
+ destroyed = true;
294
+ unsubscribe();
295
+ unsubscribe = () => {};
296
+ instance?.destroy?.();
297
+ instance = void 0;
298
+ }
299
+ };
300
+ }
301
+ function isVanillaCardInstance(value) {
302
+ return !(value instanceof HTMLElement) && value !== null && typeof value === "object" && value.element instanceof HTMLElement && typeof value.update === "function";
303
+ }
165
304
 
166
305
  //#endregion
167
306
  //#region src/reconcile.ts
@@ -239,7 +378,7 @@ function updateElementInPlace(el, node, ctx) {
239
378
  if (el.tagName !== "DIV") return false;
240
379
  el.innerHTML = node.content ?? "";
241
380
  return true;
242
- case "card": return false;
381
+ case "card": return updateCardElement(el, node);
243
382
  default:
244
383
  if (el.tagName !== "DIV") return false;
245
384
  el.innerHTML = node.html ?? node.content ?? "";
@@ -250,14 +389,20 @@ function updateElementInPlace(el, node, ctx) {
250
389
  //#endregion
251
390
  //#region src/create-renderer.ts
252
391
  function createRenderer(el, options = {}) {
253
- const { onCardAction,...rendererOpts } = options;
254
- const ctx = {
255
- registry: options.registry,
256
- onCardAction,
257
- plugins: options.plugins,
258
- nodeRenderers: (0, __ai_gui_core.collectNodeRenderers)(options.plugins),
259
- sanitize: options.sanitize,
260
- sanitized: true
392
+ const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
393
+ let actionScope = {
394
+ owner: {},
395
+ controller: new AbortController()
396
+ };
397
+ const handleCardAction = (action) => {
398
+ if (actionRuntime) actionRuntime.dispatch({
399
+ ...action,
400
+ params: action.params
401
+ }, {
402
+ owner: actionScope.owner,
403
+ signal: actionScope.controller.signal
404
+ }).catch(() => {});
405
+ onCardAction?.(action);
261
406
  };
262
407
  const state = createReconcileState();
263
408
  let destroyed = false;
@@ -267,16 +412,35 @@ function createRenderer(el, options = {}) {
267
412
  if (!destroyed) reconcile(el, nodes, ctx, state);
268
413
  }
269
414
  });
415
+ const ctx = {
416
+ registry: options.registry,
417
+ cardStore,
418
+ onCardAction: handleCardAction,
419
+ plugins: options.plugins,
420
+ nodeRenderers: (0, __ai_gui_core.collectNodeRenderers)(options.plugins, { debugTarget: renderer }),
421
+ sanitize: options.sanitize,
422
+ sanitized: true
423
+ };
270
424
  const disposeAll = () => {
271
425
  for (const entry of state.els.values()) disposeEl(entry.el);
272
426
  };
427
+ const resetActionScope = () => {
428
+ actionScope.controller.abort();
429
+ actionScope = {
430
+ owner: {},
431
+ controller: new AbortController()
432
+ };
433
+ };
273
434
  return {
435
+ debugSource: "renderer",
436
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
274
437
  push: (c) => {
275
438
  if (!destroyed) renderer.push(c);
276
439
  },
277
440
  feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
278
441
  reset: () => {
279
442
  if (!destroyed) {
443
+ resetActionScope();
280
444
  disposeAll();
281
445
  renderer.reset();
282
446
  state.els.clear();
@@ -286,6 +450,7 @@ function createRenderer(el, options = {}) {
286
450
  destroy: () => {
287
451
  if (!destroyed) {
288
452
  destroyed = true;
453
+ actionScope.controller.abort();
289
454
  renderer.reset();
290
455
  disposeAll();
291
456
  state.els.clear();
package/dist/index.d.cts CHANGED
@@ -1,12 +1,28 @@
1
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
2
2
 
3
3
  //#region src/render-node-dom.d.ts
4
+ interface VanillaCardAction {
5
+ type: string;
6
+ params?: unknown;
7
+ }
8
+ interface VanillaCardUpdateContext {
9
+ state: CardAction;
10
+ onAction: (action: VanillaCardAction) => void;
11
+ }
12
+ interface VanillaCardInstance {
13
+ element: HTMLElement;
14
+ update: (data: unknown, context: VanillaCardUpdateContext) => void;
15
+ destroy?: () => void;
16
+ }
17
+ type VanillaCardFactory = (data: unknown, context: VanillaCardUpdateContext) => HTMLElement | VanillaCardInstance;
4
18
  interface DomRenderContext {
5
19
  registry?: CardRegistry;
20
+ cardStore?: CardStore;
6
21
  onCardAction?: (action: {
7
22
  type: string;
8
23
  params?: unknown;
9
24
  cardType: string;
25
+ cardId?: string;
10
26
  }) => void;
11
27
  plugins?: AIGuiPlugin[];
12
28
  nodeRenderers?: Record<string, NodeRenderer>;
@@ -18,9 +34,13 @@ declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTML
18
34
  //#endregion
19
35
  //#region src/create-renderer.d.ts
20
36
  interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
37
+ actionRuntime?: ActionRuntime;
38
+ cardStore?: CardStore;
21
39
  onCardAction?: DomRenderContext["onCardAction"];
22
40
  }
23
41
  interface VanillaRenderer {
42
+ readonly debugSource: "renderer";
43
+ subscribeDebug: (listener: DebugEventListener) => () => void;
24
44
  push: (chunk: string) => void;
25
45
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
26
46
  reset: () => void;
@@ -34,4 +54,4 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
34
54
  declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
35
55
 
36
56
  //#endregion
37
- export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
57
+ export { CreateRendererOptions, DomRenderContext, VanillaCardAction, VanillaCardFactory, VanillaCardInstance, VanillaCardUpdateContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,28 @@
1
- import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
2
2
 
3
3
  //#region src/render-node-dom.d.ts
4
+ interface VanillaCardAction {
5
+ type: string;
6
+ params?: unknown;
7
+ }
8
+ interface VanillaCardUpdateContext {
9
+ state: CardAction;
10
+ onAction: (action: VanillaCardAction) => void;
11
+ }
12
+ interface VanillaCardInstance {
13
+ element: HTMLElement;
14
+ update: (data: unknown, context: VanillaCardUpdateContext) => void;
15
+ destroy?: () => void;
16
+ }
17
+ type VanillaCardFactory = (data: unknown, context: VanillaCardUpdateContext) => HTMLElement | VanillaCardInstance;
4
18
  interface DomRenderContext {
5
19
  registry?: CardRegistry;
20
+ cardStore?: CardStore;
6
21
  onCardAction?: (action: {
7
22
  type: string;
8
23
  params?: unknown;
9
24
  cardType: string;
25
+ cardId?: string;
10
26
  }) => void;
11
27
  plugins?: AIGuiPlugin[];
12
28
  nodeRenderers?: Record<string, NodeRenderer>;
@@ -18,9 +34,13 @@ declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTML
18
34
  //#endregion
19
35
  //#region src/create-renderer.d.ts
20
36
  interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
37
+ actionRuntime?: ActionRuntime;
38
+ cardStore?: CardStore;
21
39
  onCardAction?: DomRenderContext["onCardAction"];
22
40
  }
23
41
  interface VanillaRenderer {
42
+ readonly debugSource: "renderer";
43
+ subscribeDebug: (listener: DebugEventListener) => () => void;
24
44
  push: (chunk: string) => void;
25
45
  feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
26
46
  reset: () => void;
@@ -34,4 +54,4 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
34
54
  declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
35
55
 
36
56
  //#endregion
37
- export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
57
+ export { CreateRendererOptions, DomRenderContext, VanillaCardAction, VanillaCardFactory, VanillaCardInstance, VanillaCardUpdateContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
package/dist/index.js CHANGED
@@ -46,25 +46,33 @@ function sanitizeOutput(html, sanitize) {
46
46
  //#region src/render-node-dom.ts
47
47
  function renderNodeToElement(node, ctx) {
48
48
  const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
49
- if (r) try {
50
- const out = r(node);
51
- if (typeof out?.then === "function") {
52
- const host = document.createElement("div");
53
- host.setAttribute("data-aigui-async-pending", "");
54
- out.then((res) => {
55
- if (host.__aiguiDisposed) return;
56
- host.removeAttribute("data-aigui-async-pending");
57
- host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
58
- }, () => {
59
- if (host.__aiguiDisposed) return;
60
- host.removeAttribute("data-aigui-async-pending");
61
- host.setAttribute("data-aigui-async-error", "");
62
- });
63
- return host;
49
+ if (r) {
50
+ if (node.complete === false) {
51
+ const loading = document.createElement("div");
52
+ loading.setAttribute("data-aigui-block-loading", "");
53
+ loading.setAttribute("data-block-type", node.type);
54
+ return loading;
55
+ }
56
+ try {
57
+ const out = r(node);
58
+ if (typeof out?.then === "function") {
59
+ const host = document.createElement("div");
60
+ host.setAttribute("data-aigui-async-pending", "");
61
+ out.then((res) => {
62
+ if (host.__aiguiDisposed) return;
63
+ host.removeAttribute("data-aigui-async-pending");
64
+ host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
65
+ }, () => {
66
+ if (host.__aiguiDisposed) return;
67
+ host.removeAttribute("data-aigui-async-pending");
68
+ host.setAttribute("data-aigui-async-error", "");
69
+ });
70
+ return host;
71
+ }
72
+ return renderOutputToElement(out, ctx.sanitize);
73
+ } catch {
74
+ return renderFallback(node, ctx);
64
75
  }
65
- return renderOutputToElement(out, ctx.sanitize);
66
- } catch {
67
- return renderFallback(node, ctx);
68
76
  }
69
77
  switch (node.type) {
70
78
  case "heading": {
@@ -122,22 +130,153 @@ function renderCardElement(node, ctx) {
122
130
  return pre;
123
131
  }
124
132
  const factory = ctx.registry?.getRender(card.type);
125
- if (!factory) {
126
- const pre = document.createElement("pre");
127
- pre.setAttribute("data-aigui-card-fallback", "");
128
- const c = document.createElement("code");
129
- c.textContent = JSON.stringify(card.data, null, 2);
130
- pre.appendChild(c);
131
- return pre;
132
- }
133
+ if (!factory) return createCardFallback(card, ctx);
133
134
  const host = document.createElement("div");
134
135
  host.setAttribute("data-aigui-card", card.type);
135
- host.appendChild(factory(card.data, { onAction: (a) => ctx.onCardAction?.({
136
- ...a,
137
- cardType: card.type
138
- }) }));
136
+ const controller = createCardController(host, card, factory, ctx);
137
+ host.__aiguiCardController = controller;
138
+ host.__aiguiCleanup = () => controller.destroy();
139
139
  return host;
140
140
  }
141
+ function createCardFallback(card, ctx) {
142
+ const pre = document.createElement("pre");
143
+ pre.setAttribute("data-aigui-card-fallback", "");
144
+ const code = document.createElement("code");
145
+ pre.appendChild(code);
146
+ const render = (data) => {
147
+ pre.removeAttribute("data-aigui-card-missing");
148
+ code.textContent = JSON.stringify(data, null, 2);
149
+ };
150
+ const missing = () => {
151
+ pre.setAttribute("data-aigui-card-missing", "");
152
+ code.textContent = "Card data is unavailable";
153
+ };
154
+ let unsubscribe = () => {};
155
+ if (card.id !== void 0 && ctx.cardStore) {
156
+ try {
157
+ const record = ctx.cardStore.register({
158
+ id: card.id,
159
+ type: card.type,
160
+ data: card.data
161
+ });
162
+ render(record.data);
163
+ } catch (error) {
164
+ pre.setAttribute("data-aigui-card-store-error", "");
165
+ code.textContent = error instanceof Error ? error.message : "Card registration failed";
166
+ }
167
+ unsubscribe = ctx.cardStore.subscribe(card.id, (next) => {
168
+ if (!next || next.type !== card.type) missing();
169
+ else {
170
+ pre.removeAttribute("data-aigui-card-store-error");
171
+ render(next.data);
172
+ }
173
+ });
174
+ pre.__aiguiCleanup = unsubscribe;
175
+ } else render(card.data);
176
+ return pre;
177
+ }
178
+ function updateCardElement(el, node) {
179
+ return el.__aiguiCardController?.updateNode(node) ?? false;
180
+ }
181
+ function createCardController(host, initialCard, factory, ctx) {
182
+ let card = initialCard;
183
+ let destroyed = false;
184
+ let unsubscribe = () => {};
185
+ let instance;
186
+ let element;
187
+ let missing = false;
188
+ const id = card.id;
189
+ const onAction = (action) => ctx.onCardAction?.({
190
+ ...action,
191
+ cardType: card.type,
192
+ ...id === void 0 ? {} : { cardId: id }
193
+ });
194
+ let data = card.data;
195
+ let state = { status: "idle" };
196
+ let registrationError;
197
+ if (id !== void 0 && ctx.cardStore) try {
198
+ const record = ctx.cardStore.register({
199
+ id,
200
+ type: card.type,
201
+ data
202
+ });
203
+ data = record.data;
204
+ state = record.action;
205
+ } catch (error) {
206
+ registrationError = error;
207
+ }
208
+ const context = () => ({
209
+ state,
210
+ onAction
211
+ });
212
+ const mount = (nextData) => {
213
+ const rendered = factory(nextData, context());
214
+ if (isVanillaCardInstance(rendered)) {
215
+ instance = rendered;
216
+ element = rendered.element;
217
+ } else {
218
+ instance = void 0;
219
+ element = rendered;
220
+ }
221
+ host.replaceChildren(element);
222
+ };
223
+ const update = (nextData, nextState) => {
224
+ if (destroyed) return;
225
+ data = nextData;
226
+ state = nextState;
227
+ host.removeAttribute("data-aigui-card-store-error");
228
+ if (missing) {
229
+ missing = false;
230
+ host.removeAttribute("data-aigui-card-missing");
231
+ if (element) host.replaceChildren(element);
232
+ }
233
+ if (instance) instance.update(data, context());
234
+ else mount(data);
235
+ };
236
+ const showMissing = () => {
237
+ if (destroyed || missing) return;
238
+ missing = true;
239
+ host.removeAttribute("data-aigui-card-store-error");
240
+ host.setAttribute("data-aigui-card-missing", "");
241
+ const fallback = document.createElement("code");
242
+ fallback.textContent = "Card data is unavailable";
243
+ host.replaceChildren(fallback);
244
+ };
245
+ if (registrationError === void 0) mount(data);
246
+ else {
247
+ host.setAttribute("data-aigui-card-store-error", "");
248
+ const code = document.createElement("code");
249
+ code.textContent = registrationError instanceof Error ? registrationError.message : "Card registration failed";
250
+ host.replaceChildren(code);
251
+ }
252
+ if (id !== void 0 && ctx.cardStore) unsubscribe = ctx.cardStore.subscribe(id, (record) => {
253
+ if (!record || record.type !== card.type) {
254
+ showMissing();
255
+ return;
256
+ }
257
+ update(record.data, record.action);
258
+ });
259
+ return {
260
+ updateNode(node) {
261
+ const next = node.card;
262
+ if (!next?.complete || !next.valid || next.type !== card.type || next.id !== id) return false;
263
+ card = next;
264
+ if (id === void 0 || !ctx.cardStore) update(next.data, state);
265
+ return true;
266
+ },
267
+ destroy() {
268
+ if (destroyed) return;
269
+ destroyed = true;
270
+ unsubscribe();
271
+ unsubscribe = () => {};
272
+ instance?.destroy?.();
273
+ instance = void 0;
274
+ }
275
+ };
276
+ }
277
+ function isVanillaCardInstance(value) {
278
+ return !(value instanceof HTMLElement) && value !== null && typeof value === "object" && value.element instanceof HTMLElement && typeof value.update === "function";
279
+ }
141
280
 
142
281
  //#endregion
143
282
  //#region src/reconcile.ts
@@ -215,7 +354,7 @@ function updateElementInPlace(el, node, ctx) {
215
354
  if (el.tagName !== "DIV") return false;
216
355
  el.innerHTML = node.content ?? "";
217
356
  return true;
218
- case "card": return false;
357
+ case "card": return updateCardElement(el, node);
219
358
  default:
220
359
  if (el.tagName !== "DIV") return false;
221
360
  el.innerHTML = node.html ?? node.content ?? "";
@@ -226,14 +365,20 @@ function updateElementInPlace(el, node, ctx) {
226
365
  //#endregion
227
366
  //#region src/create-renderer.ts
228
367
  function createRenderer(el, options = {}) {
229
- const { onCardAction,...rendererOpts } = options;
230
- const ctx = {
231
- registry: options.registry,
232
- onCardAction,
233
- plugins: options.plugins,
234
- nodeRenderers: collectNodeRenderers(options.plugins),
235
- sanitize: options.sanitize,
236
- sanitized: true
368
+ const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
369
+ let actionScope = {
370
+ owner: {},
371
+ controller: new AbortController()
372
+ };
373
+ const handleCardAction = (action) => {
374
+ if (actionRuntime) actionRuntime.dispatch({
375
+ ...action,
376
+ params: action.params
377
+ }, {
378
+ owner: actionScope.owner,
379
+ signal: actionScope.controller.signal
380
+ }).catch(() => {});
381
+ onCardAction?.(action);
237
382
  };
238
383
  const state = createReconcileState();
239
384
  let destroyed = false;
@@ -243,16 +388,35 @@ function createRenderer(el, options = {}) {
243
388
  if (!destroyed) reconcile(el, nodes, ctx, state);
244
389
  }
245
390
  });
391
+ const ctx = {
392
+ registry: options.registry,
393
+ cardStore,
394
+ onCardAction: handleCardAction,
395
+ plugins: options.plugins,
396
+ nodeRenderers: collectNodeRenderers(options.plugins, { debugTarget: renderer }),
397
+ sanitize: options.sanitize,
398
+ sanitized: true
399
+ };
246
400
  const disposeAll = () => {
247
401
  for (const entry of state.els.values()) disposeEl(entry.el);
248
402
  };
403
+ const resetActionScope = () => {
404
+ actionScope.controller.abort();
405
+ actionScope = {
406
+ owner: {},
407
+ controller: new AbortController()
408
+ };
409
+ };
249
410
  return {
411
+ debugSource: "renderer",
412
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
250
413
  push: (c) => {
251
414
  if (!destroyed) renderer.push(c);
252
415
  },
253
416
  feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
254
417
  reset: () => {
255
418
  if (!destroyed) {
419
+ resetActionScope();
256
420
  disposeAll();
257
421
  renderer.reset();
258
422
  state.els.clear();
@@ -262,6 +426,7 @@ function createRenderer(el, options = {}) {
262
426
  destroy: () => {
263
427
  if (!destroyed) {
264
428
  destroyed = true;
429
+ actionScope.controller.abort();
265
430
  renderer.reset();
266
431
  disposeAll();
267
432
  state.els.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/vanilla",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-free DOM adapter for AIGUI — stream LLM output straight into the DOM.",
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
  "scripts": {
56
56
  "build": "tsdown",