@urbicon-ui/blocks 6.40.4 → 6.43.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.
Files changed (72) hide show
  1. package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
  2. package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
  3. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
  4. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
  5. package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
  6. package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
  7. package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
  8. package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
  9. package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
  10. package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
  11. package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
  12. package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
  13. package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
  14. package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
  15. package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
  16. package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
  17. package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
  18. package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
  19. package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
  20. package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
  21. package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
  22. package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
  23. package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
  24. package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
  25. package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
  26. package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
  27. package/dist/components/Chat/A2UIView/index.d.ts +35 -2
  28. package/dist/components/Chat/A2UIView/index.js +13 -0
  29. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
  30. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
  31. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
  32. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
  33. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
  34. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
  35. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
  36. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
  37. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
  38. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
  39. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +19 -19
  40. package/dist/components/FileUpload/fileUpload.variants.js +15 -4
  41. package/dist/components/PinInput/PinInput.svelte +1 -0
  42. package/dist/components/PinInput/pin-input.variants.js +14 -1
  43. package/dist/components/TimeInput/time-input.variants.js +16 -1
  44. package/dist/internal/field-chrome.d.ts +18 -1
  45. package/dist/internal/field-chrome.js +18 -1
  46. package/dist/primitives/Avatar/avatar.variants.js +4 -1
  47. package/dist/primitives/Badge/badge.variants.js +11 -2
  48. package/dist/primitives/Button/button.variants.js +4 -1
  49. package/dist/primitives/Card/card.variants.js +4 -1
  50. package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
  51. package/dist/primitives/Combobox/Combobox.svelte +9 -1
  52. package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
  53. package/dist/primitives/Combobox/combobox.variants.js +32 -3
  54. package/dist/primitives/Combobox/index.d.ts +8 -2
  55. package/dist/primitives/Dialog/dialog.variants.js +4 -1
  56. package/dist/primitives/Drawer/drawer.variants.js +4 -1
  57. package/dist/primitives/Input/input.variants.js +17 -1
  58. package/dist/primitives/Menu/menu.variants.js +5 -1
  59. package/dist/primitives/Progress/progress.variants.js +5 -2
  60. package/dist/primitives/RadioGroup/radioGroup.variants.js +68 -9
  61. package/dist/primitives/Select/select.variants.js +13 -4
  62. package/dist/primitives/Slider/Slider.svelte +4 -1
  63. package/dist/primitives/Slider/slider.variants.js +9 -2
  64. package/dist/primitives/Textarea/textarea.variants.js +15 -1
  65. package/dist/primitives/Toast/toast.variants.js +5 -1
  66. package/dist/primitives/Toggle/toggle.variants.js +92 -18
  67. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  68. package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
  69. package/dist/utils/persistent-state.svelte.d.ts +25 -1
  70. package/dist/utils/persistent-state.svelte.js +98 -10
  71. package/docs/MIGRATION-v5.md +29 -0
  72. package/package.json +3 -3
@@ -1,18 +1,29 @@
1
1
  /**
2
- * System-prompt generator for the A2UI subset. Renders the agent-facing catalog
3
- * description straight from `A2UI_REGISTRY`, so the prompt and the validator can
4
- * never drift (a drift-guard test asserts every component and enum value is
5
- * covered). Pure TS, no Svelte — a server building the prompt has no DOM.
2
+ * System-prompt generator for an A2UI catalog. Renders the agent-facing catalog
3
+ * description straight from the catalog's registry, so the prompt and the
4
+ * validator can never drift (a drift-guard test asserts every component and enum
5
+ * value is covered). Pure TS, no Svelte — a server building the prompt has no
6
+ * DOM, and this module never imports catalog VALUE code from `urbicon/` (the
7
+ * caller passes the spec), so it stays out of the Basic bundle.
6
8
  *
7
- * Deliberately excludes any TRANSPORT section (how envelopes reach the client):
8
- * that is app-specific and each app appends it (the chat-demo appends its fenced
9
- * `a2ui` JSONL protocol).
9
+ * With no options the output is the Basic-catalog prompt, byte-identical to
10
+ * before the catalog refactor. Pass `catalog` to render a richer catalog: when
11
+ * its specs carry categories / shared axes (the Urbicon catalog), the prompt
12
+ * grows a "Shared axes" section, groups components by category, compresses
13
+ * shared-axis props, and adds catalog-specific don'ts — all gated on features
14
+ * the Basic catalog does not have, so the default stays untouched.
15
+ *
16
+ * Deliberately excludes any TRANSPORT section (how envelopes reach the client)
17
+ * and any data-schema section: those are app-specific and each app appends them
18
+ * (the chat-demo appends its fenced `a2ui` protocol + `a2uiDataSchemaSection`).
10
19
  */
20
+ import { type A2uiCatalogSpec } from './a2ui-catalog.js';
11
21
  /**
12
- * Build the A2UI system prompt for this subset. Pass `catalogId` to override the
13
- * default `createSurface` catalog identifier (the value must round-trip
14
- * unchanged in every envelope).
22
+ * Build the A2UI system prompt. Pass `catalogId` to override the advertised
23
+ * `createSurface` catalog id, and `catalog` to render a specific catalog's
24
+ * components (defaults to the Basic catalog — byte-identical output).
15
25
  */
16
26
  export declare function a2uiSystemPrompt(options?: {
17
27
  catalogId?: string;
28
+ catalog?: A2uiCatalogSpec;
18
29
  }): string;
@@ -1,17 +1,49 @@
1
1
  /**
2
- * System-prompt generator for the A2UI subset. Renders the agent-facing catalog
3
- * description straight from `A2UI_REGISTRY`, so the prompt and the validator can
4
- * never drift (a drift-guard test asserts every component and enum value is
5
- * covered). Pure TS, no Svelte — a server building the prompt has no DOM.
2
+ * System-prompt generator for an A2UI catalog. Renders the agent-facing catalog
3
+ * description straight from the catalog's registry, so the prompt and the
4
+ * validator can never drift (a drift-guard test asserts every component and enum
5
+ * value is covered). Pure TS, no Svelte — a server building the prompt has no
6
+ * DOM, and this module never imports catalog VALUE code from `urbicon/` (the
7
+ * caller passes the spec), so it stays out of the Basic bundle.
6
8
  *
7
- * Deliberately excludes any TRANSPORT section (how envelopes reach the client):
8
- * that is app-specific and each app appends it (the chat-demo appends its fenced
9
- * `a2ui` JSONL protocol).
9
+ * With no options the output is the Basic-catalog prompt, byte-identical to
10
+ * before the catalog refactor. Pass `catalog` to render a richer catalog: when
11
+ * its specs carry categories / shared axes (the Urbicon catalog), the prompt
12
+ * grows a "Shared axes" section, groups components by category, compresses
13
+ * shared-axis props, and adds catalog-specific don'ts — all gated on features
14
+ * the Basic catalog does not have, so the default stays untouched.
15
+ *
16
+ * Deliberately excludes any TRANSPORT section (how envelopes reach the client)
17
+ * and any data-schema section: those are app-specific and each app appends them
18
+ * (the chat-demo appends its fenced `a2ui` protocol + `a2uiDataSchemaSection`).
10
19
  */
11
- import { A2UI_CATALOG_ID, A2UI_ICON_NAMES, A2UI_REGISTRY, UNSUPPORTED_A2UI_COMPONENTS } from './a2ui-registry.js';
20
+ import { basicA2uiCatalogSpec } from './a2ui-catalog.js';
12
21
  /** Props documented once in the shared section rather than per component. */
13
22
  const COMMON_PROP_KEYS = new Set(['accessibility', 'weight']);
23
+ /** Category order for grouped rendering; unknown categories follow, sorted. */
24
+ const CATEGORY_ORDER = ['Layout', 'Text', 'Form', 'Status', 'Media'];
25
+ function sharedAxisOf(spec) {
26
+ return spec.sharedAxis;
27
+ }
28
+ function categoryOf(spec) {
29
+ return spec.category;
30
+ }
31
+ /** Collect the distinct shared axes (name → value list) referenced across the registry. */
32
+ function collectSharedAxes(registry) {
33
+ const axes = new Map();
34
+ for (const spec of Object.values(registry)) {
35
+ for (const propSpec of Object.values(spec.props)) {
36
+ const shared = sharedAxisOf(propSpec);
37
+ if (shared && propSpec.values && !axes.has(shared))
38
+ axes.set(shared, propSpec.values);
39
+ }
40
+ }
41
+ return axes;
42
+ }
14
43
  function renderKind(spec) {
44
+ const shared = sharedAxisOf(spec);
45
+ if (shared)
46
+ return `shared ${shared} (see Shared axes)`;
15
47
  switch (spec.kind) {
16
48
  case 'enum':
17
49
  case 'icon':
@@ -28,10 +60,14 @@ function renderKind(spec) {
28
60
  return 'component id (string)';
29
61
  case 'childList':
30
62
  return 'array of ids OR template { componentId, path }';
63
+ case 'labeledChildren':
64
+ return 'array of { label, child } (child is a component id)';
31
65
  case 'action':
32
66
  return 'action { event: { name, context } }';
33
67
  case 'options':
34
- return 'array of { label, value }';
68
+ return spec.dynamic
69
+ ? 'array of { label, value }, or { path } binding to such an array'
70
+ : 'array of { label, value }';
35
71
  case 'accessibility':
36
72
  return 'object { label?, description? }';
37
73
  }
@@ -44,9 +80,8 @@ function renderProp(key, spec) {
44
80
  flags.push(`default ${JSON.stringify(spec.default)}`);
45
81
  return ` - ${key} (${flags.join('; ')}): ${spec.description}`;
46
82
  }
47
- function renderComponent(name) {
48
- const spec = A2UI_REGISTRY[name];
49
- const lines = [`### ${name}`, spec.description, 'Props:'];
83
+ function renderComponent(name, spec, heading) {
84
+ const lines = [`${heading} ${name}`, spec.description, 'Props:'];
50
85
  for (const [key, propSpec] of Object.entries(spec.props)) {
51
86
  if (COMMON_PROP_KEYS.has(key) || propSpec.promptHidden)
52
87
  continue;
@@ -54,14 +89,53 @@ function renderComponent(name) {
54
89
  }
55
90
  return lines.join('\n');
56
91
  }
92
+ /** Flat component list — the Basic rendering (byte-identical). */
93
+ function renderFlatComponents(registry, names) {
94
+ return [
95
+ '## Components',
96
+ '',
97
+ ...names.map((name) => renderComponent(name, registry[name], '###'))
98
+ ].join('\n\n');
99
+ }
100
+ /** Category-grouped component list — the Urbicon rendering. */
101
+ function renderGroupedComponents(registry, names) {
102
+ const byCategory = new Map();
103
+ for (const name of names) {
104
+ const category = categoryOf(registry[name]) ?? 'Other';
105
+ let list = byCategory.get(category);
106
+ if (!list) {
107
+ list = [];
108
+ byCategory.set(category, list);
109
+ }
110
+ list.push(name);
111
+ }
112
+ const known = CATEGORY_ORDER.filter((category) => byCategory.has(category));
113
+ const extra = [...byCategory.keys()]
114
+ .filter((category) => !CATEGORY_ORDER.includes(category))
115
+ .sort();
116
+ const parts = ['## Components'];
117
+ for (const category of [...known, ...extra]) {
118
+ parts.push(`### ${category}`);
119
+ for (const name of byCategory.get(category) ?? []) {
120
+ parts.push(renderComponent(name, registry[name], '####'));
121
+ }
122
+ }
123
+ return parts.join('\n\n');
124
+ }
57
125
  /**
58
- * Build the A2UI system prompt for this subset. Pass `catalogId` to override the
59
- * default `createSurface` catalog identifier (the value must round-trip
60
- * unchanged in every envelope).
126
+ * Build the A2UI system prompt. Pass `catalogId` to override the advertised
127
+ * `createSurface` catalog id, and `catalog` to render a specific catalog's
128
+ * components (defaults to the Basic catalog — byte-identical output).
61
129
  */
62
130
  export function a2uiSystemPrompt(options) {
63
- const catalogId = options?.catalogId ?? A2UI_CATALOG_ID;
64
- const componentNames = Object.keys(A2UI_REGISTRY);
131
+ const catalog = options?.catalog ?? basicA2uiCatalogSpec;
132
+ const catalogId = options?.catalogId ?? catalog.catalogId;
133
+ const registry = catalog.registry;
134
+ const componentNames = Object.keys(registry);
135
+ const sharedAxes = collectSharedAxes(registry);
136
+ const grouped = componentNames.some((name) => categoryOf(registry[name]) !== undefined);
137
+ const hasRichText = 'RichText' in registry;
138
+ const hasSelectArray = 'Select' in registry && registry.Select.props.value?.kind === 'stringList';
65
139
  const sections = [];
66
140
  sections.push([
67
141
  '# A2UI rendering',
@@ -99,6 +173,46 @@ export function a2uiSystemPrompt(options) {
99
173
  ' The client buffers until "root" exists and shows placeholders for',
100
174
  ' children that have not arrived yet.'
101
175
  ].join('\n'));
176
+ sections.push([
177
+ '## Surfaces live on, and you only hear from them when the user acts',
178
+ '',
179
+ 'A surfaceId is unique for as long as the client runs, and a surface stays',
180
+ 'alive and editable after the reply that created it. Send further envelopes',
181
+ 'for the SAME surfaceId later and it updates in place, keeping what the user',
182
+ 'typed. Prefer ONE surface that grows over a chain of near-identical ones.',
183
+ '',
184
+ '- updateDataModel to fill in a value you just learned; updateComponents to',
185
+ ' add or change controls. Re-sending a component id REPLACES it — to append',
186
+ ' a child, re-send its container with the full children list.',
187
+ '- Never createSurface twice for one id, and never rebuild an equivalent',
188
+ " surface under a new id to change a field: both discard the user's input.",
189
+ '- deleteSurface when a surface has served its purpose.',
190
+ '',
191
+ 'But you are NOT watching the form. The client speaks to you in exactly one',
192
+ 'case: the user activates a control carrying an `action`. Typing, picking a',
193
+ 'date, choosing an option — all of it updates the data model LOCALLY and',
194
+ 'tells you nothing.',
195
+ '',
196
+ '- Anything you mean to fill in later needs a control the user can press to',
197
+ ' ask you for it ("Show available times"), with the fields it depends on in',
198
+ ' its action.context. An empty area with no trigger is a dead end — never',
199
+ ' write "pick a date and I will show the times" without one.',
200
+ '- But do NOT route a plain choice through you. A control that only records',
201
+ ' what the user picked should BIND: a single-choice input with',
202
+ ' value: { path } highlights the selection instantly, no round-trip. A row',
203
+ ' of Buttons that each set a value is the wrong shape — buttons show no',
204
+ ' selected state, so the choice looks lost, and every tap costs a turn.',
205
+ ' Reach for an action only when YOU must fetch or commit something.',
206
+ '- Options you fetched belong in the data model: write them with',
207
+ ' updateDataModel and bind options: { path } (see the prop docs), rather',
208
+ ' than rewriting the component for every result.',
209
+ '- Set "sendDataModel": true on createSurface: every action then carries the',
210
+ " surface's ENTIRE data model, so you see what the user filled in even for",
211
+ ' fields you left out of context. Prefer it on any form you will act on.',
212
+ '- If an action arrives with required fields still empty, do NOT carry it',
213
+ ' out — patch a short message next to the offending field instead. You are',
214
+ ' the only validator; the client submits whatever it is given.'
215
+ ].join('\n'));
102
216
  sections.push([
103
217
  '## Data bindings',
104
218
  '',
@@ -120,7 +234,27 @@ export function a2uiSystemPrompt(options) {
120
234
  '"context": { ... } } }. Context values may be literals or { path } bindings and',
121
235
  'are resolved before dispatch. Do NOT use local function-call actions.'
122
236
  ].join('\n'));
123
- sections.push(['## Components', '', ...componentNames.map(renderComponent)].join('\n\n'));
237
+ if (sharedAxes.size > 0) {
238
+ const lines = [
239
+ '## Shared axes',
240
+ '',
241
+ 'Several components share these value sets. A prop documented as "shared intent"',
242
+ 'or "shared size" accepts exactly the values listed here — pick one:'
243
+ ];
244
+ for (const axis of ['intent', 'size']) {
245
+ const values = sharedAxes.get(axis);
246
+ if (values)
247
+ lines.push(`- ${axis}: ${values.join(' | ')}`);
248
+ }
249
+ for (const [axis, values] of sharedAxes) {
250
+ if (axis !== 'intent' && axis !== 'size')
251
+ lines.push(`- ${axis}: ${values.join(' | ')}`);
252
+ }
253
+ sections.push(lines.join('\n'));
254
+ }
255
+ sections.push(grouped
256
+ ? renderGroupedComponents(registry, componentNames)
257
+ : renderFlatComponents(registry, componentNames));
124
258
  sections.push([
125
259
  '## Common props (every component)',
126
260
  '',
@@ -130,18 +264,30 @@ export function a2uiSystemPrompt(options) {
130
264
  sections.push([
131
265
  '## Icons',
132
266
  '',
133
- `Icon.name must be one of: ${A2UI_ICON_NAMES.join(', ')}. Unknown names render a`,
267
+ `Icon.name must be one of: ${catalog.iconNames.join(', ')}. Unknown names render a`,
134
268
  'fallback glyph.'
135
269
  ].join('\n'));
136
- sections.push([
270
+ const donts = [
137
271
  "## Don'ts",
138
272
  '',
139
273
  '- Do NOT define children inline; reference them by id.',
140
274
  '- Do NOT use more than one "root".',
141
275
  '- Do NOT use function-call bindings or local function-call actions.',
142
- `- Do NOT use unsupported components: ${[...UNSUPPORTED_A2UI_COMPONENTS].join(', ')}.`,
143
- '- Do NOT put HTML, images or links in Text markdown (inline bold/italic/code only).',
144
- '- Images are blocked by default — always give Image a meaningful description.'
145
- ].join('\n'));
276
+ `- Do NOT use unsupported components: ${[...catalog.unsupportedComponents].join(', ')}.`
277
+ ];
278
+ if (hasRichText) {
279
+ donts.push('- Text renders PLAIN — a literal "**x**" shows the asterisks. For bold/italic/', ' lists/links/code use RichText, never Text.');
280
+ }
281
+ else {
282
+ donts.push('- Do NOT put HTML, images or links in Text markdown (inline bold/italic/code only).');
283
+ }
284
+ if (hasSelectArray) {
285
+ donts.push('- A Select `value` is ALWAYS a string ARRAY — even single-select writes a', ' one-element array. RadioGroup `value` is a single string.');
286
+ }
287
+ if (hasRichText) {
288
+ donts.push('- Do NOT set an intent on a form field for errors — use its `error` string prop.');
289
+ }
290
+ donts.push('- Images are blocked by default — always give Image a meaningful description.');
291
+ sections.push(donts.join('\n'));
146
292
  return sections.join('\n\n');
147
293
  }
@@ -13,7 +13,7 @@
13
13
  * is written to be read by an agent — it is emitted verbatim into the prompt.
14
14
  */
15
15
  /** How a prop value is shaped and validated. */
16
- export type A2uiPropKind = 'string' | 'number' | 'boolean' | 'stringList' | 'enum' | 'childId' | 'childList' | 'action' | 'options' | 'icon' | 'accessibility';
16
+ export type A2uiPropKind = 'string' | 'number' | 'boolean' | 'stringList' | 'enum' | 'childId' | 'childList' | 'labeledChildren' | 'action' | 'options' | 'icon' | 'accessibility';
17
17
  export interface A2uiPropSpec {
18
18
  kind: A2uiPropKind;
19
19
  /** Required on the component. @default false */
@@ -283,7 +283,8 @@ export const A2UI_REGISTRY = Object.freeze({
283
283
  options: {
284
284
  kind: 'options',
285
285
  required: true,
286
- description: 'The available options as an array of { label, value } objects. `value` is a stable string.'
286
+ dynamic: true,
287
+ description: 'The available options as an array of { label, value } objects; `value` is a stable string. Bind it when the options come from data you fetched.'
287
288
  },
288
289
  value: {
289
290
  kind: 'stringList',
@@ -14,6 +14,7 @@
14
14
  * still produced by `collectGraphIssues` in `a2ui-validate.ts`; this walk only
15
15
  * shapes what renders.
16
16
  */
17
+ import type { Component, Snippet } from 'svelte';
17
18
  import type { IconComponent } from '../../../icons/index.js';
18
19
  import type { MarkdownUrlPolicy } from '../markdown/types.js';
19
20
  import type { A2uiActionEvent, A2uiValidationIssue } from './a2ui.types.js';
@@ -37,6 +38,12 @@ export interface A2uiRenderNode {
37
38
  children: A2uiRenderNode[];
38
39
  /** flex-grow, present only when this node is a direct Row/Column child with a finite `weight`. */
39
40
  weight?: number;
41
+ /**
42
+ * The parent prop this child came from (e.g. Card `header`/`footer`/`child`).
43
+ * Lets a dispatcher with several named childId slots pick the right child by
44
+ * name instead of position; single-slot / list components ignore it.
45
+ */
46
+ slot?: string;
40
47
  }
41
48
  /** Static text labels threaded to the node dispatcher (i18n comes from A2UIView props). */
42
49
  export interface A2uiRenderLabels {
@@ -60,6 +67,12 @@ export interface A2uiRenderContext {
60
67
  inline: boolean;
61
68
  surfaceId: string;
62
69
  onAction: ((event: A2uiActionEvent) => void) | undefined;
70
+ /**
71
+ * The data model to attach to a dispatched action, or `undefined` when the
72
+ * surface did not ask for it (`createSurface.sendDataModel`). Read as a
73
+ * function so the action carries the state at CLICK time, not at render time.
74
+ */
75
+ actionDataModel: (() => unknown) | undefined;
63
76
  /** Resolve a Dynamic value (literal | { path } | function-call) against the model in `scope`. */
64
77
  resolve: (value: unknown, scopePrefix: string | undefined) => {
65
78
  value: unknown;
@@ -74,6 +87,23 @@ export interface A2uiRenderContext {
74
87
  /** Fallback glyph for an unmapped icon name. */
75
88
  fallbackIcon: IconComponent;
76
89
  labels: A2uiRenderLabels;
90
+ /**
91
+ * The catalog's recursive node dispatcher (Basic → `A2UINode`; a custom
92
+ * catalog → its own dispatcher). A2UIView's `renderNode` snippet renders
93
+ * THIS rather than importing one dispatcher directly, so each surface renders
94
+ * through the component that matches its catalog.
95
+ */
96
+ nodeComponent: Component<A2uiNodeProps>;
97
+ }
98
+ /**
99
+ * Props of a catalog node dispatcher. Every dispatcher (Basic/Urbicon/…) takes
100
+ * the same triple: the node, its render context, and the self-referencing
101
+ * `renderChild` snippet A2UIView threads down for bounded recursion.
102
+ */
103
+ export interface A2uiNodeProps {
104
+ node: A2uiRenderNode;
105
+ context: A2uiRenderContext;
106
+ renderChild: Snippet<[A2uiRenderNode, A2uiRenderContext]>;
77
107
  }
78
108
  /**
79
109
  * Resolve a `{ path }` data binding to the ABSOLUTE JSON Pointer it addresses,
@@ -58,33 +58,67 @@ export function toInputString(value) {
58
58
  return String(value);
59
59
  return '';
60
60
  }
61
+ /**
62
+ * Resolve a component's child references by walking its spec's child-bearing
63
+ * props (`childId` = one id; `childList` = an id array or a `{ componentId,
64
+ * path }` template; `labeledChildren` = a `[{ label, child }]` list). Kind-
65
+ * driven rather than reading fixed `child`/`children` keys, so a catalog may
66
+ * name its child slots freely (Card header/footer, Section child, Accordion
67
+ * items, …). Behaviour is unchanged for the Basic catalog, whose only child
68
+ * props ARE `child`/`children`. One ref is pushed per `labeledChildren` item
69
+ * (even a dangling one) so the dispatcher can zip labels with resolved children
70
+ * by index.
71
+ */
61
72
  function childRefs(instance, scopePrefix, state) {
62
73
  const out = [];
63
- const single = instance.props.get('child');
64
- if (typeof single === 'string')
65
- out.push({ id: single, scope: scopePrefix });
66
- const children = instance.props.get('children');
67
- if (Array.isArray(children)) {
68
- for (const childId of children) {
69
- if (typeof childId === 'string')
70
- out.push({ id: childId, scope: scopePrefix });
74
+ const spec = state.registry[instance.component];
75
+ if (!spec)
76
+ return out;
77
+ for (const [key, propSpec] of Object.entries(spec.props)) {
78
+ if (propSpec.kind === 'childId') {
79
+ const single = instance.props.get(key);
80
+ if (typeof single === 'string')
81
+ out.push({ id: single, scope: scopePrefix, slot: key });
71
82
  }
72
- }
73
- else if (children !== null && typeof children === 'object') {
74
- const template = children;
75
- if (typeof template.componentId === 'string' && typeof template.path === 'string') {
76
- const absPath = template.path.startsWith('/')
77
- ? template.path
78
- : `${scopePrefix ?? ''}/${template.path}`;
79
- const list = getAtPointer(state.dataModel, absPath);
80
- if (Array.isArray(list)) {
81
- for (let i = 0; i < list.length; i++) {
82
- out.push({ id: template.componentId, scope: `${absPath}/${i}` });
83
+ else if (propSpec.kind === 'labeledChildren') {
84
+ const items = instance.props.get(key);
85
+ if (Array.isArray(items)) {
86
+ for (const item of items) {
87
+ if (item !== null && typeof item === 'object') {
88
+ const child = item.child;
89
+ if (typeof child === 'string')
90
+ out.push({ id: child, scope: scopePrefix, slot: key });
91
+ }
83
92
  if (out.length > state.maxNodes)
84
- break; // bound the expansion itself
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ else if (propSpec.kind === 'childList') {
98
+ const children = instance.props.get(key);
99
+ if (Array.isArray(children)) {
100
+ for (const childId of children) {
101
+ if (typeof childId === 'string')
102
+ out.push({ id: childId, scope: scopePrefix, slot: key });
103
+ }
104
+ }
105
+ else if (children !== null && typeof children === 'object') {
106
+ const template = children;
107
+ if (typeof template.componentId === 'string' && typeof template.path === 'string') {
108
+ const absPath = template.path.startsWith('/')
109
+ ? template.path
110
+ : `${scopePrefix ?? ''}/${template.path}`;
111
+ const list = getAtPointer(state.dataModel, absPath);
112
+ if (Array.isArray(list)) {
113
+ for (let i = 0; i < list.length; i++) {
114
+ out.push({ id: template.componentId, scope: `${absPath}/${i}`, slot: key });
115
+ if (out.length > state.maxNodes)
116
+ break; // bound the expansion itself
117
+ }
118
+ }
119
+ // non-array / undefined → render nothing (issue reported by collectGraphIssues)
85
120
  }
86
121
  }
87
- // non-array / undefined → render nothing (issue reported by collectGraphIssues)
88
122
  }
89
123
  }
90
124
  return out;
@@ -104,7 +138,7 @@ function buildNode(id, scopePrefix, key, parentIsFlex, ancestors, depth, state)
104
138
  node.weight = clampWeight(weight, 0, 100);
105
139
  }
106
140
  }
107
- const isFlex = instance.component === 'Row' || instance.component === 'Column';
141
+ const isFlex = state.flexContainers.has(instance.component);
108
142
  const nextAncestors = new Set(ancestors);
109
143
  nextAncestors.add(id);
110
144
  const refs = childRefs(instance, scopePrefix, state);
@@ -116,8 +150,10 @@ function buildNode(id, scopePrefix, key, parentIsFlex, ancestors, depth, state)
116
150
  continue; // cycle: truncate (issue reported elsewhere)
117
151
  const childKey = `${ref.id}@${ref.scope ?? ''}#${i}`;
118
152
  const child = buildNode(ref.id, ref.scope, childKey, isFlex, nextAncestors, depth + 1, state);
119
- if (child)
153
+ if (child) {
154
+ child.slot = ref.slot;
120
155
  node.children.push(child);
156
+ }
121
157
  }
122
158
  return node;
123
159
  }
@@ -132,6 +168,8 @@ export function buildRenderTree(surface, options) {
132
168
  const state = {
133
169
  components: surface.components,
134
170
  dataModel: surface.dataModel,
171
+ registry: surface.catalog.registry,
172
+ flexContainers: surface.catalog.flexContainers,
135
173
  maxDepth: options?.maxDepth ?? A2UI_MAX_DEPTH,
136
174
  maxNodes: options?.maxNodes ?? A2UI_MAX_NODES,
137
175
  count: 0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Surface routing across message boundaries.
3
+ *
4
+ * `A2UIView` is payload-driven: one view renders one envelope array, and its
5
+ * processor lives with that view. In a chat that means one view per assistant
6
+ * message — so a surface dies with the message that created it. The A2UI spec
7
+ * says the opposite: a `surfaceId` is "globally unique for the renderer's
8
+ * lifetime", `deleteSurface` is the only way to retire one, and
9
+ * `updateComponents`/`updateDataModel` are patches an agent may send at any
10
+ * later point. A model that patches a form it built two turns ago is reading
11
+ * the protocol correctly; the renderer just had nowhere to put the envelope.
12
+ *
13
+ * This router closes that gap WITHOUT giving the engine a second state
14
+ * lifetime. It is a pure bookkeeping layer over the transcript: it remembers
15
+ * which source (message + a2ui part) owns which surface, and splits each
16
+ * source's envelopes into the ones that stay put and the ones that belong to an
17
+ * earlier source's payload. The consumer appends those to that payload
18
+ * (`[...payload, ...patch]`, keeping element identity) and `A2UIView` consumes
19
+ * them incrementally — two-way edits the user already made survive, because the
20
+ * view never rebuilds.
21
+ *
22
+ * The transcript stays the single source of truth: every envelope still lives
23
+ * in exactly one payload, so undo (`revoke`) and replay are plain array work.
24
+ *
25
+ * Zero-dependency and Svelte-free — usable from a server-side transcript
26
+ * reducer, not just from the browser.
27
+ */
28
+ import { type A2uiValidationIssue } from './a2ui.types.js';
29
+ /**
30
+ * Envelopes destined for one source's payload.
31
+ *
32
+ * `envelopes` are the very objects the caller passed in — element identity is
33
+ * what lets `A2UIView` apply them incrementally instead of rebuilding, and what
34
+ * makes {@link A2uiSurfaceRouter.revoke} a reference filter.
35
+ */
36
+ export interface A2uiRoutePatch {
37
+ /** The `sourceKey` whose payload these envelopes belong to. */
38
+ targetKey: string;
39
+ envelopes: unknown[];
40
+ }
41
+ /** The verdict for one {@link A2uiSurfaceRouter.route} call. */
42
+ export interface A2uiRouteResult {
43
+ /**
44
+ * The full envelope list this source keeps — assign it as the part's payload.
45
+ * Envelopes routed to an earlier source are absent; everything else stays,
46
+ * including malformed ones (the processor is what reports those).
47
+ */
48
+ own: unknown[];
49
+ /** Envelopes to append to earlier sources' payloads, grouped by target. */
50
+ patches: A2uiRoutePatch[];
51
+ /**
52
+ * Envelopes to REMOVE from earlier payloads (by reference) because this
53
+ * source was rebuilt — a regenerated turn must not leave its old patches
54
+ * behind. Empty on an append-only call.
55
+ */
56
+ revoked: A2uiRoutePatch[];
57
+ /**
58
+ * Surfaces that this call patched from a later source — i.e. surfaces that
59
+ * just proved they outlive their own message. This is the promotion signal
60
+ * for a client heuristic: a surface listed here is long-lived and deserves
61
+ * treatment beyond an inline transcript entry (a jump anchor, a pinned card,
62
+ * an artifact panel). Only fires on the transition, not on every patch.
63
+ */
64
+ promoted: string[];
65
+ /** Router-level findings, shaped for the same channel as processor issues. */
66
+ issues: A2uiValidationIssue[];
67
+ }
68
+ /**
69
+ * Tracks surface ownership across sources and routes envelopes to the payload
70
+ * that owns their surface.
71
+ *
72
+ * A "source" is one A2UI payload in the transcript — in a chat app, one `a2ui`
73
+ * part of one message. The key is the consumer's to choose; it only has to be
74
+ * stable for that payload's lifetime (e.g. `` `${messageId}#${partIndex}` ``).
75
+ *
76
+ * Ownership rules:
77
+ * - `createSurface` for an unknown id → the calling source owns it.
78
+ * - `createSurface` for an id owned by ANOTHER source → ownership transfers to
79
+ * the caller (last writer wins, so the new surface actually renders) and a
80
+ * `SURFACE_RECREATED` issue is raised for the agent.
81
+ * - Any operation on a surface owned by another source → routed to that owner.
82
+ * - Any operation on an UNKNOWN surface stays with the caller, so the
83
+ * processor's `NO_SURFACE` error surfaces instead of vanishing here.
84
+ * - `deleteSurface` routes to the owner and then frees the id for re-use.
85
+ *
86
+ * @example One assistant turn, wired into the transcript
87
+ * ```ts
88
+ * const result = router.route(`${messageId}#0`, part.payload);
89
+ * part.payload = result.own;
90
+ * for (const patch of result.patches) appendToPayload(patch.targetKey, patch.envelopes);
91
+ * for (const surfaceId of result.promoted) markLongLived(surfaceId);
92
+ * ```
93
+ */
94
+ export declare class A2uiSurfaceRouter {
95
+ #private;
96
+ /**
97
+ * Route one source's envelope list.
98
+ *
99
+ * Call it as often as you like — on every streamed token, if that is when the
100
+ * payload grows. Only envelopes appended since the last call are examined; an
101
+ * unchanged prefix is never re-routed, so patches are never duplicated. If
102
+ * the prefix DID change (a regenerated turn), the source is rebuilt from
103
+ * scratch and its previous patches come back in `revoked`.
104
+ *
105
+ * @param sourceKey Stable id of the payload being routed.
106
+ * @param envelopes The payload's accumulated envelopes, in stream order.
107
+ */
108
+ route(sourceKey: string, envelopes: readonly unknown[]): A2uiRouteResult;
109
+ /**
110
+ * Forget a source entirely — call it when a message leaves the transcript
111
+ * (regenerate, delete, branch switch). The returned patches are the envelopes
112
+ * this source had pushed into other payloads; remove them by reference so no
113
+ * orphaned patch survives its author.
114
+ */
115
+ revoke(sourceKey: string): A2uiRoutePatch[];
116
+ /** The source whose payload currently holds `surfaceId`, if any. */
117
+ ownerOf(surfaceId: string): string | undefined;
118
+ /**
119
+ * Whether a surface has been patched from a later source — the promotion
120
+ * predicate, for clients that need to ask after the fact rather than react to
121
+ * {@link A2uiRouteResult.promoted}.
122
+ */
123
+ isLongLived(surfaceId: string): boolean;
124
+ /** Every surface currently owned by a source, in creation order. */
125
+ surfaceIds(): string[];
126
+ /**
127
+ * Every source key routed so far, in first-seen order.
128
+ *
129
+ * Needed to retire a source whose payload no longer shows it: a message whose
130
+ * envelopes ALL travelled to earlier surfaces keeps no a2ui part of its own,
131
+ * so the caller cannot reconstruct its keys by counting parts.
132
+ */
133
+ sourceKeys(): string[];
134
+ /** Drop all bookkeeping (new conversation). */
135
+ reset(): void;
136
+ }