@adia-ai/a2ui-runtime 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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ActionDock — Binds a UIEvent on a component to a handler.
3
+ *
4
+ * The UIEvent is a typed object: { event, target?, debounce?, throttle?, condition? }
5
+ * When the DOM event fires, the handler runs with the surface context.
6
+ * onSuccess/onError chains run follow-up handlers based on the outcome.
7
+ */
8
+ import { Dockable } from './base.js';
9
+
10
+ /**
11
+ * UIEvent type → DOM event name.
12
+ * AdiaUI components emit semantic events; these map to the actual DOM event to listen on.
13
+ */
14
+ const A2UI_EVENT_TO_DOM = {
15
+ press: 'click',
16
+ submit: 'submit',
17
+ input: 'input',
18
+ change: 'change',
19
+ select: 'select',
20
+ toggle: 'change',
21
+ dismiss: 'close',
22
+ navigate: 'click',
23
+ mount: 'connectedCallback',
24
+ unmount: 'disconnectedCallback',
25
+ focus: 'focusin',
26
+ blur: 'focusout',
27
+ drag: 'dragstart',
28
+ drop: 'drop',
29
+ };
30
+
31
+ export { A2UI_EVENT_TO_DOM };
32
+
33
+ export class ActionDock extends Dockable {
34
+ kind = 'action';
35
+
36
+ /** @type {string} */
37
+ id;
38
+
39
+ /** @type {{ event: string, target?: string, debounce?: number, throttle?: number, condition?: object }} */
40
+ event;
41
+
42
+ /** @type {string} handler name */
43
+ handler;
44
+
45
+ /** @type {object} handler config */
46
+ config;
47
+
48
+ /** @type {Array|null} follow-up actions on success */
49
+ onSuccess;
50
+
51
+ /** @type {Array|null} follow-up actions on error */
52
+ onError;
53
+
54
+ /** @type {Function} (handlerName) => handlerFn */
55
+ #resolveHandler;
56
+
57
+ /**
58
+ * @param {object} decl — { event, handler, config?, onSuccess?, onError? }
59
+ * @param {Function} resolveHandler — (name) => async (config, ctx) => result
60
+ */
61
+ constructor(decl, resolveHandler) {
62
+ super();
63
+ this.event = decl.event;
64
+ this.handler = decl.handler;
65
+ this.config = decl.config || {};
66
+ this.onSuccess = decl.onSuccess || null;
67
+ this.onError = decl.onError || null;
68
+ this.#resolveHandler = resolveHandler;
69
+
70
+ // Auto-generate id from event + target
71
+ this.id = `action:${this.event.event}:${this.event.target || 'root'}:${this.handler}`;
72
+ }
73
+
74
+ dock(ctx) {
75
+ const target = this.event.target
76
+ ? ctx.getElement(this.event.target)
77
+ : ctx.getRootElement();
78
+
79
+ if (!target) {
80
+ console.warn(`ActionDock: target "${this.event.target}" not found`);
81
+ return;
82
+ }
83
+
84
+ const domEventName = A2UI_EVENT_TO_DOM[this.event.event] || this.event.event;
85
+ const handlerFn = this.#resolveHandler(this.handler);
86
+
87
+ if (!handlerFn) {
88
+ console.warn(`ActionDock: unknown handler "${this.handler}"`);
89
+ return;
90
+ }
91
+
92
+ let listener = async (domEvent) => {
93
+ // Condition guard
94
+ if (this.event.condition) {
95
+ const val = ctx.getModel(this.event.condition.path);
96
+ if ('equals' in this.event.condition && val !== this.event.condition.equals) return;
97
+ if ('notEquals' in this.event.condition && val === this.event.condition.notEquals) return;
98
+ if ('exists' in this.event.condition && this.event.condition.exists && val == null) return;
99
+ }
100
+
101
+ try {
102
+ const result = await handlerFn(this.config, ctx, domEvent);
103
+
104
+ // Run onSuccess chain
105
+ if (this.onSuccess) {
106
+ for (const follow of this.onSuccess) {
107
+ const followFn = this.#resolveHandler(follow.handler);
108
+ if (followFn) await followFn(follow.config || {}, ctx, result);
109
+ }
110
+ }
111
+ } catch (err) {
112
+ // Run onError chain
113
+ if (this.onError) {
114
+ for (const follow of this.onError) {
115
+ const followFn = this.#resolveHandler(follow.handler);
116
+ if (followFn) await followFn(follow.config || {}, ctx, err);
117
+ }
118
+ } else {
119
+ console.error(`ActionDock: handler "${this.handler}" failed`, err);
120
+ }
121
+ }
122
+ };
123
+
124
+ // Debounce
125
+ if (this.event.debounce > 0) {
126
+ const origListener = listener;
127
+ let timer;
128
+ listener = (...args) => {
129
+ clearTimeout(timer);
130
+ timer = setTimeout(() => origListener(...args), this.event.debounce);
131
+ };
132
+ }
133
+
134
+ // Throttle
135
+ if (this.event.throttle > 0) {
136
+ const origListener = listener;
137
+ let last = 0;
138
+ listener = (...args) => {
139
+ const now = Date.now();
140
+ if (now - last >= this.event.throttle) {
141
+ last = now;
142
+ origListener(...args);
143
+ }
144
+ };
145
+ }
146
+
147
+ target.addEventListener(domEventName, listener);
148
+ return () => target.removeEventListener(domEventName, listener);
149
+ }
150
+
151
+ undock() {}
152
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dockable — Base class for objects that attach to a Surface.
3
+ *
4
+ * Every dockable has:
5
+ * kind — which port type it uses (controller, source, action, provider, lifecycle)
6
+ * id — unique identity within the surface
7
+ * dock(context) — attach and return cleanup function
8
+ * undock() — teardown
9
+ */
10
+ export class Dockable {
11
+ /** @type {'controller'|'source'|'action'|'provider'|'lifecycle'} */
12
+ get kind() { throw new Error('Dockable subclass must define kind'); }
13
+
14
+ /** @type {string} */
15
+ get id() { throw new Error('Dockable subclass must define id'); }
16
+
17
+ /**
18
+ * Attach to a surface. Receives the surface context.
19
+ * Return a cleanup function, or void.
20
+ * @param {import('../surface.js').SurfaceContext} ctx
21
+ * @returns {(() => void)|void}
22
+ */
23
+ dock(ctx) { throw new Error('Dockable subclass must implement dock()'); }
24
+
25
+ /**
26
+ * Detach from the surface. Cleanup fn from dock() is called
27
+ * automatically before this.
28
+ */
29
+ undock() {}
30
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ControllerDock — Wraps a AdiaUI controller as a dockable.
3
+ *
4
+ * Instantiates the controller, connects it to its host element,
5
+ * and sets up two-way binding between controller state and model paths.
6
+ */
7
+ import { Dockable } from './base.js';
8
+
9
+ export class ControllerDock extends Dockable {
10
+ kind = 'controller';
11
+
12
+ /** @type {string} */
13
+ id;
14
+
15
+ /** @type {string} controller type name, e.g., 'FormController' */
16
+ type;
17
+
18
+ /** @type {string} component id to attach to */
19
+ hostId;
20
+
21
+ /** @type {object} controller config */
22
+ config;
23
+
24
+ /** @type {object|null} model path bindings { stateKey: '/model/path' } */
25
+ bind;
26
+
27
+ /** @type {import('../controllers/base.js').BaseController|null} */
28
+ controller = null;
29
+
30
+ /** @type {Function|null} resolve controller class */
31
+ #resolveClass;
32
+
33
+ /**
34
+ * @param {object} decl — { id, type, host, config?, bind? }
35
+ * @param {Function} resolveClass — async (type) => ControllerClass
36
+ */
37
+ constructor(decl, resolveClass) {
38
+ super();
39
+ this.id = decl.id;
40
+ this.type = decl.type;
41
+ this.hostId = decl.host;
42
+ this.config = decl.config || {};
43
+ this.bind = decl.bind || null;
44
+ this.#resolveClass = resolveClass;
45
+ }
46
+
47
+ async dock(ctx) {
48
+ const host = ctx.getElement(this.hostId);
49
+ if (!host) {
50
+ console.warn(`ControllerDock: host element "${this.hostId}" not found`);
51
+ return;
52
+ }
53
+
54
+ const ControllerClass = await this.#resolveClass(this.type);
55
+ if (!ControllerClass) {
56
+ console.warn(`ControllerDock: unknown controller type "${this.type}"`);
57
+ return;
58
+ }
59
+
60
+ this.controller = new ControllerClass(this.config);
61
+ this.controller.connect(host);
62
+
63
+ // Two-way bind: controller state ↔ model paths
64
+ const unbinders = [];
65
+ if (this.bind) {
66
+ for (const [stateKey, modelPath] of Object.entries(this.bind)) {
67
+ // Controller → model
68
+ if (this.controller.subscribe) {
69
+ const unsub = this.controller.subscribe(stateKey, (value) => {
70
+ ctx.setModel(modelPath, value);
71
+ });
72
+ if (unsub) unbinders.push(unsub);
73
+ }
74
+
75
+ // Model → controller (initial sync)
76
+ const initial = ctx.getModel(modelPath);
77
+ if (initial !== undefined && this.controller.setState) {
78
+ this.controller.setState(stateKey, initial);
79
+ }
80
+ }
81
+ }
82
+
83
+ host.controller = this.controller;
84
+
85
+ return () => {
86
+ host.controller = null;
87
+ unbinders.forEach(u => u());
88
+ };
89
+ }
90
+
91
+ undock() {
92
+ if (this.controller) {
93
+ this.controller.disconnect?.();
94
+ this.controller = null;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * DataSourceDock — Fetches data and pushes it into the surface model.
3
+ *
4
+ * Supports refresh strategies: once, on-focus, interval:{ms}, stream.
5
+ * Re-fetch on demand via refetch() (used by refresh-source handler).
6
+ */
7
+ import { Dockable } from './base.js';
8
+
9
+ export class DataSourceDock extends Dockable {
10
+ kind = 'source';
11
+
12
+ /** @type {string} */
13
+ id;
14
+
15
+ /** @type {string} URI template, e.g., "resource://users/{userId}" */
16
+ uri;
17
+
18
+ /** @type {string} model path to write to */
19
+ path;
20
+
21
+ /** @type {string} refresh strategy */
22
+ refresh;
23
+
24
+ /** @type {Function} (uri, params) => data */
25
+ #resolveData;
26
+
27
+ /** @type {import('../surface.js').SurfaceContext|null} */
28
+ #ctx = null;
29
+
30
+ /**
31
+ * @param {object} decl — { id, uri, path, refresh? }
32
+ * @param {Function} resolveData — async (uri, params) => data
33
+ */
34
+ constructor(decl, resolveData) {
35
+ super();
36
+ this.id = decl.id;
37
+ this.uri = decl.uri;
38
+ this.path = decl.path;
39
+ this.refresh = decl.refresh || 'once';
40
+ this.#resolveData = resolveData;
41
+ }
42
+
43
+ dock(ctx) {
44
+ this.#ctx = ctx;
45
+
46
+ // Initial fetch
47
+ this.#fetch(ctx);
48
+
49
+ // Refresh strategy
50
+ if (this.refresh === 'on-focus') {
51
+ const handler = () => this.#fetch(ctx);
52
+ window.addEventListener('focus', handler);
53
+ return () => window.removeEventListener('focus', handler);
54
+ }
55
+
56
+ if (this.refresh.startsWith('interval:')) {
57
+ const ms = parseInt(this.refresh.split(':')[1], 10);
58
+ if (ms > 0) {
59
+ const id = setInterval(() => this.#fetch(ctx), ms);
60
+ return () => clearInterval(id);
61
+ }
62
+ }
63
+
64
+ if (this.refresh === 'stream') {
65
+ const resolvedUri = this.#resolveUri(ctx);
66
+ try {
67
+ const source = new EventSource(resolvedUri);
68
+ source.onmessage = (e) => {
69
+ try { ctx.setModel(this.path, JSON.parse(e.data)); }
70
+ catch { /* malformed data */ }
71
+ };
72
+ return () => source.close();
73
+ } catch {
74
+ console.warn(`DataSourceDock: stream failed for ${resolvedUri}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ undock() {
80
+ this.#ctx = null;
81
+ }
82
+
83
+ /** Re-fetch on demand (called by refresh-source handler). */
84
+ refetch() {
85
+ if (this.#ctx) this.#fetch(this.#ctx);
86
+ }
87
+
88
+ async #fetch(ctx) {
89
+ const resolvedUri = this.#resolveUri(ctx);
90
+ try {
91
+ const data = await this.#resolveData(resolvedUri, ctx);
92
+ if (data !== undefined) ctx.setModel(this.path, data);
93
+ } catch (err) {
94
+ console.warn(`DataSourceDock: fetch failed for ${resolvedUri}`, err);
95
+ }
96
+ }
97
+
98
+ #resolveUri(ctx) {
99
+ return this.uri.replace(/\{(\w+)\}/g, (_, key) => {
100
+ return ctx.getParam(key) ?? ctx.getModel('/' + key) ?? '';
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,6 @@
1
+ export { Dockable } from './base.js';
2
+ export { ControllerDock } from './controller.js';
3
+ export { DataSourceDock } from './data-source.js';
4
+ export { ActionDock, A2UI_EVENT_TO_DOM } from './action.js';
5
+ export { ProviderDock } from './provider.js';
6
+ export { LifecycleDock } from './lifecycle.js';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * LifecycleDock — Runs actions on mount, unmount, and model changes.
3
+ *
4
+ * One per surface. Fires onMount actions immediately on dock,
5
+ * watches model paths for changes, and runs onUnmount on undock.
6
+ */
7
+ import { Dockable } from './base.js';
8
+
9
+ export class LifecycleDock extends Dockable {
10
+ kind = 'lifecycle';
11
+ id = 'lifecycle';
12
+
13
+ /** @type {Array} actions to run on mount */
14
+ onMount;
15
+
16
+ /** @type {Array} actions to run on unmount */
17
+ onUnmount;
18
+
19
+ /** @type {Array} model path watchers */
20
+ onModelChange;
21
+
22
+ /** @type {Function} (handlerName) => handlerFn */
23
+ #resolveHandler;
24
+
25
+ /** @type {import('../surface.js').SurfaceContext|null} */
26
+ #ctx = null;
27
+
28
+ /**
29
+ * @param {object} decl — { onMount?, onUnmount?, onModelChange? }
30
+ * @param {Function} resolveHandler — (name) => async (config, ctx) => result
31
+ */
32
+ constructor(decl, resolveHandler) {
33
+ super();
34
+ this.onMount = decl.onMount || [];
35
+ this.onUnmount = decl.onUnmount || [];
36
+ this.onModelChange = decl.onModelChange || [];
37
+ this.#resolveHandler = resolveHandler;
38
+ }
39
+
40
+ dock(ctx) {
41
+ this.#ctx = ctx;
42
+
43
+ // Run onMount actions
44
+ for (const action of this.onMount) {
45
+ this.#run(action, ctx);
46
+ }
47
+
48
+ // Set up model watchers
49
+ const unwatchers = this.onModelChange.map((watcher) => {
50
+ let handler = () => this.#run(watcher, ctx);
51
+
52
+ // Debounce if specified
53
+ if (watcher.debounce > 0) {
54
+ const origHandler = handler;
55
+ let timer;
56
+ handler = () => {
57
+ clearTimeout(timer);
58
+ timer = setTimeout(origHandler, watcher.debounce);
59
+ };
60
+ }
61
+
62
+ return ctx.watchModel(watcher.path, handler);
63
+ });
64
+
65
+ return () => unwatchers.forEach(u => u());
66
+ }
67
+
68
+ undock() {
69
+ if (this.#ctx) {
70
+ for (const action of this.onUnmount) {
71
+ this.#run(action, this.#ctx);
72
+ }
73
+ this.#ctx = null;
74
+ }
75
+ }
76
+
77
+ async #run(action, ctx) {
78
+ const fn = this.#resolveHandler(action.handler);
79
+ if (fn) {
80
+ try { await fn(action.config || {}, ctx); }
81
+ catch (err) { console.warn(`LifecycleDock: ${action.handler} failed`, err); }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ProviderDock — Injects shared context into a component subtree.
3
+ *
4
+ * Creates an <a2ui-provider> wrapper around the host element.
5
+ * Children can consume the context by name.
6
+ */
7
+ import { Dockable } from './base.js';
8
+
9
+ export class ProviderDock extends Dockable {
10
+ kind = 'provider';
11
+
12
+ /** @type {string} context name, e.g., "auth" */
13
+ id;
14
+
15
+ /** @type {string|null} component id to wrap (null = root) */
16
+ hostId;
17
+
18
+ /** @type {*} initial context value */
19
+ value;
20
+
21
+ /**
22
+ * @param {object} decl — { name, host?, value? }
23
+ */
24
+ constructor(decl) {
25
+ super();
26
+ this.id = `provider:${decl.name}`;
27
+ this.hostId = decl.host || null;
28
+ this.value = decl.value ?? null;
29
+ }
30
+
31
+ dock(ctx) {
32
+ const host = this.hostId
33
+ ? ctx.getElement(this.hostId)
34
+ : ctx.getRootElement();
35
+
36
+ if (!host) {
37
+ console.warn(`ProviderDock: host "${this.hostId}" not found`);
38
+ return;
39
+ }
40
+
41
+ const provider = document.createElement('a2ui-provider');
42
+ provider.setAttribute('name', this.id.replace('provider:', ''));
43
+ provider.value = this.value;
44
+
45
+ // Wrap: insert provider before host, move host inside
46
+ host.parentNode?.insertBefore(provider, host);
47
+ provider.appendChild(host);
48
+
49
+ return () => {
50
+ // Unwrap: move host back out, remove provider
51
+ if (provider.parentNode) {
52
+ provider.parentNode.insertBefore(host, provider);
53
+ provider.remove();
54
+ }
55
+ };
56
+ }
57
+
58
+ undock() {}
59
+ }
package/index.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @adia-ai/a2ui-runtime — A2UI runtime surface.
3
+ *
4
+ * Framework-agnostic leaf: no runtime dep on @adia-ai/web-components.
5
+ * Pairs with any A2UI-conformant custom-element catalog.
6
+ *
7
+ * Tier 1 — Render: resolve A2UI protocol → custom-element tags, run the
8
+ * renderer, consume transports.
9
+ *
10
+ * Tier 2 — Wire: surface-manifest + wiring-engine + dockable primitives
11
+ * for cross-surface data flow and lifecycle.
12
+ */
13
+
14
+ // ── Tier 1: Render ─────────────────────────────────────────────────
15
+ export { registry, resolveTag, registerType } from './registry.js';
16
+ export { A2UIRenderer } from './renderer.js';
17
+ export { sseStream, wsStream, mockStream, mcpStream, jsonlStream } from './stream.js';
18
+
19
+ // ── Tier 2: Wire ───────────────────────────────────────────────────
20
+ export { SurfaceManifest } from './surface-manifest.js';
21
+ export { Surface } from './surface.js';
22
+
23
+ // Wiring primitives
24
+ export { createDockables } from './wire-factory.js';
25
+ export { WiringEngine } from './wiring-engine.js';
26
+ export {
27
+ wiringRegistry,
28
+ registerController,
29
+ registerHandler,
30
+ registerResolver,
31
+ resolveController,
32
+ resolveHandler,
33
+ resolveData,
34
+ } from './wiring-registry.js';
35
+
36
+ // Dockable base + implementations
37
+ export {
38
+ Dockable,
39
+ ControllerDock,
40
+ DataSourceDock,
41
+ ActionDock,
42
+ A2UI_EVENT_TO_DOM,
43
+ ProviderDock,
44
+ LifecycleDock,
45
+ } from './dockables/index.js';
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@adia-ai/a2ui-runtime",
3
+ "version": "0.3.0",
4
+ "description": "A2UI runtime \u2014 renderer, registry, streams, surface manifest, and wiring primitives for the A2UI (Agent-to-UI) protocol. Framework-agnostic; pairs with any A2UI-conformant component set.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./registry": "./registry.js",
9
+ "./renderer": "./renderer.js",
10
+ "./streams": "./stream.js",
11
+ "./surface": "./surface.js",
12
+ "./wiring": "./wiring-engine.js",
13
+ "./dockables": "./dockables/index.js"
14
+ },
15
+ "files": [
16
+ "*.js",
17
+ "dockables/",
18
+ "controllers/",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/adiahealth/gen-ui-kit.git",
29
+ "directory": "packages/a2ui/runtime"
30
+ }
31
+ }