@lynx-js/genui 0.0.1-rc.0 → 0.0.1

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 (78) hide show
  1. package/a2ui/README.md +777 -100
  2. package/a2ui/dist/catalog/ChoicePicker/catalog.json +265 -0
  3. package/a2ui/dist/catalog/ChoicePicker/index.d.ts +56 -0
  4. package/a2ui/dist/catalog/ChoicePicker/index.js +66 -0
  5. package/a2ui/dist/catalog/ChoicePicker/index.js.map +1 -0
  6. package/a2ui/dist/catalog/ChoicePicker/utils.d.ts +13 -0
  7. package/a2ui/dist/catalog/ChoicePicker/utils.js +80 -0
  8. package/a2ui/dist/catalog/ChoicePicker/utils.js.map +1 -0
  9. package/a2ui/dist/catalog/Column/index.js +3 -7
  10. package/a2ui/dist/catalog/Column/index.js.map +1 -1
  11. package/a2ui/dist/catalog/DateTimeInput/catalog.json +165 -0
  12. package/a2ui/dist/catalog/DateTimeInput/index.d.ts +43 -0
  13. package/a2ui/dist/catalog/DateTimeInput/index.js +147 -0
  14. package/a2ui/dist/catalog/DateTimeInput/index.js.map +1 -0
  15. package/a2ui/dist/catalog/DateTimeInput/utils.d.ts +53 -0
  16. package/a2ui/dist/catalog/DateTimeInput/utils.js +248 -0
  17. package/a2ui/dist/catalog/DateTimeInput/utils.js.map +1 -0
  18. package/a2ui/dist/catalog/Icon/catalog.json +173 -2
  19. package/a2ui/dist/catalog/Icon/index.d.ts +2 -2
  20. package/a2ui/dist/catalog/Icon/index.js.map +1 -1
  21. package/a2ui/dist/catalog/Image/catalog.json +1 -17
  22. package/a2ui/dist/catalog/Image/index.d.ts +1 -3
  23. package/a2ui/dist/catalog/Image/index.js +3 -11
  24. package/a2ui/dist/catalog/Image/index.js.map +1 -1
  25. package/a2ui/dist/catalog/LineChart/index.js +3 -3
  26. package/a2ui/dist/catalog/LineChart/index.js.map +1 -1
  27. package/a2ui/dist/catalog/Modal/index.js +1 -1
  28. package/a2ui/dist/catalog/Modal/index.js.map +1 -1
  29. package/a2ui/dist/catalog/index.d.ts +4 -0
  30. package/a2ui/dist/catalog/index.js +2046 -3
  31. package/a2ui/dist/catalog/index.js.map +1 -1
  32. package/a2ui/dist/index.d.ts +1 -1
  33. package/a2ui/dist/index.js +1 -1
  34. package/a2ui/dist/index.js.map +1 -1
  35. package/a2ui/dist/react/A2UIRenderer.d.ts +1 -0
  36. package/a2ui/dist/react/A2UIRenderer.js +8 -6
  37. package/a2ui/dist/react/A2UIRenderer.js.map +1 -1
  38. package/a2ui/dist/react/useAction.js +2 -1
  39. package/a2ui/dist/react/useAction.js.map +1 -1
  40. package/a2ui/dist/react/useChecks.js +7 -2
  41. package/a2ui/dist/react/useChecks.js.map +1 -1
  42. package/a2ui/dist/react/useDataBinding.d.ts +1 -1
  43. package/a2ui/dist/react/useDataBinding.js +24 -48
  44. package/a2ui/dist/react/useDataBinding.js.map +1 -1
  45. package/a2ui/dist/store/MessageProcessor.js +7 -19
  46. package/a2ui/dist/store/MessageProcessor.js.map +1 -1
  47. package/a2ui/dist/store/SignalStore.d.ts +2 -0
  48. package/a2ui/dist/store/SignalStore.js +5 -0
  49. package/a2ui/dist/store/SignalStore.js.map +1 -1
  50. package/a2ui/dist/store/index.d.ts +3 -1
  51. package/a2ui/dist/store/index.js +3 -1
  52. package/a2ui/dist/store/index.js.map +1 -1
  53. package/a2ui/dist/store/resolveDynamic.d.ts +9 -0
  54. package/a2ui/dist/store/resolveDynamic.js +88 -0
  55. package/a2ui/dist/store/resolveDynamic.js.map +1 -0
  56. package/a2ui/dist/store/resolveFunctionCall.d.ts +2 -4
  57. package/a2ui/dist/store/resolveFunctionCall.js +24 -82
  58. package/a2ui/dist/store/resolveFunctionCall.js.map +1 -1
  59. package/a2ui/dist/store/signalResolution.d.ts +4 -0
  60. package/a2ui/dist/store/signalResolution.js +25 -0
  61. package/a2ui/dist/store/signalResolution.js.map +1 -0
  62. package/a2ui/dist/store/utils.d.ts +7 -0
  63. package/a2ui/dist/store/utils.js +24 -0
  64. package/a2ui/dist/store/utils.js.map +1 -0
  65. package/a2ui/dist/tsconfig.build.tsbuildinfo +1 -1
  66. package/a2ui/styles/catalog/ChoicePicker.css +157 -0
  67. package/a2ui/styles/catalog/DateTimeInput.css +375 -0
  68. package/a2ui/styles/catalog/Icon.css +1 -1
  69. package/a2ui/styles/catalog/Text.css +11 -11
  70. package/a2ui/styles/theme.css +2 -1
  71. package/a2ui-catalog-extractor/README.md +23 -15
  72. package/a2ui-catalog-extractor/bin/a2ui-catalog-extractor.js +0 -1
  73. package/a2ui-catalog-extractor/dist/tsconfig.build.tsbuildinfo +1 -1
  74. package/a2ui-prompt/README.md +1 -2
  75. package/a2ui-prompt/dist/index.js +65 -31
  76. package/cli/bin/cli.js +5 -2
  77. package/dist/tsconfig.build.tsbuildinfo +1 -1
  78. package/package.json +8 -1
package/a2ui/README.md CHANGED
@@ -1,140 +1,817 @@
1
1
  # @lynx-js/genui/a2ui
2
2
 
3
- ReactLynx renderer for the A2UI v0.9 protocol. **Headless** — the package
4
- ships no styles or chrome; consumers wrap surfaces themselves.
5
-
6
- This package includes:
7
-
8
- - `<A2UI>`: all-in-one component that owns a `MessageProcessor`,
9
- subscribes to a developer-supplied `MessageStore`, and renders the
10
- most recent surface.
11
- - `MessageStore`: an append-only buffer of raw protocol messages the
12
- developer pushes into from any IO transport (fetch, SSE, WebSocket,
13
- in-process mock, …).
14
- - `defineCatalog` / `mergeCatalogs` / `serializeCatalog`: the pluggable
15
- catalog API. No global registry every consumer composes the set of
16
- components they want available.
17
- - `catalog/<Name>`: built-in component renderers (`Text`, `Button`,
18
- `Card`, `Column`, `Row`, `List`, `CheckBox`, `RadioGroup`, `Slider`,
19
- `Image`, `Divider`, `Icon`, `Modal`, `Tabs`).
20
- - `catalog/<Name>/catalog.json`: per-component JSON-Schema manifests
21
- for the agent handshake.
22
-
23
- ## Exports
24
-
25
- - `@lynx-js/genui/a2ui`: `<A2UI>`, `createMessageStore`,
26
- `defineCatalog`, the built-ins, plus protocol types.
27
- - `@lynx-js/genui/a2ui/catalog`: re-exports of the catalog API and
28
- built-ins for tree-shake-friendly subpath access.
29
- - `@lynx-js/genui/a2ui/catalog/<Name>`: import a single built-in.
30
- - `@lynx-js/genui/a2ui/catalog/<Name>/catalog.json`: import the
31
- per-component manifest.
32
- - `@lynx-js/genui/a2ui/store`: `MessageStore`, `MessageProcessor`,
33
- `Resource`, payload normalizers — the pure data layer.
34
- - `@lynx-js/genui/a2ui/react`: lower-level renderer pieces for
35
- consumers that want manual surface lifecycle control.
36
-
37
- ## Installation
38
-
39
- Make sure your app provides the peer dependencies:
40
-
41
- - `@lynx-js/react`
42
-
43
- ## Quick Start
44
-
45
- 1. Create a `MessageStore`.
46
- 2. Wire your IO module (mock / SSE / fetch / …) to push raw protocol
47
- messages into the store.
48
- 3. Render `<A2UI messageStore={store} catalogs={[...]}>`.
3
+ English | [简体中文](./README_zh.md)
4
+
5
+ Lynx GenUI is the generated-UI stack for developers who already know React and
6
+ want AI to assemble native Lynx interfaces from trusted components.
7
+
8
+ If you have never heard of A2UI, think of it this way:
9
+
10
+ - In React, your code chooses components and passes props.
11
+ - In GenUI, an agent chooses from a component catalog that you publish.
12
+ - The client still renders real ReactLynx components. The model only sends
13
+ data that says which approved component to render and what props to use.
14
+
15
+ A2UI is the message protocol in the middle. It is not a replacement for React,
16
+ and it is not a new styling system. It is a safe, JSON-based way for an agent to
17
+ say: create a surface, render these components, update this data, and report
18
+ this user action back to the agent.
19
+
20
+ ## Why It Exists
21
+
22
+ Generated UI becomes useful when it has product constraints:
23
+
24
+ - The agent can only use components your app has registered.
25
+ - Component props are described with TypeScript-derived schemas.
26
+ - Model output is validated before the client renders it.
27
+ - UI can stream in progressively instead of waiting for one giant response.
28
+ - User actions are sent back as structured events, similar to React event
29
+ handlers crossing a network boundary.
30
+
31
+ The result is not arbitrary generated code. It is a ReactLynx UI tree assembled
32
+ from a trusted catalog.
33
+
34
+ ## From React To GenUI
35
+
36
+ Here is the React mental model:
37
+
38
+ ```tsx
39
+ function WeatherCard(props: WeatherCardProps) {
40
+ return (
41
+ <Card>
42
+ <Text>{props.city}</Text>
43
+ <Text>{props.temperature}</Text>
44
+ <Button onClick={props.onRefresh}>Refresh</Button>
45
+ </Card>
46
+ );
47
+ }
48
+ ```
49
+
50
+ Here is the GenUI mental model:
51
+
52
+ 1. You publish `Card`, `Text`, `Button`, and any custom components into a
53
+ catalog.
54
+ 2. The agent receives the user's request and the catalog description.
55
+ 3. The agent emits A2UI messages such as "render a Card with these children".
56
+ 4. The client pushes those messages into a `MessageStore`.
57
+ 5. `<A2UI>` renders the matching ReactLynx components.
58
+ 6. When a user taps a generated button, `onAction` fires and your app sends the
59
+ action back to the agent.
60
+
61
+ The model never imports your code. It only names components that the renderer
62
+ has already allowed.
63
+
64
+ ## What You Use
65
+
66
+ For a product app using A2UI, the important surfaces are:
67
+
68
+ | Surface | Role |
69
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
70
+ | `@lynx-js/genui/a2ui` | ReactLynx renderer for A2UI v0.9. It provides `<A2UI>`, `MessageStore`, catalog APIs, built-in components, and protocol helpers. |
71
+ | `genui a2ui` | Build-time command for generating catalog artifacts from TypeScript contracts and A2UI system prompts for your agent. |
72
+ | Your agent service | A backend you own. It receives user prompts/actions, calls a model with the A2UI prompt and catalog, validates output, and returns A2UI. |
73
+ | Your transport implementation | Client code that calls your agent service, handles REST or streaming responses, pushes messages into `MessageStore`, and forwards actions. |
74
+
75
+ ## The Three Pieces
76
+
77
+ ```text
78
+ Catalog: what can be rendered
79
+ -> Agent: what should be rendered
80
+ -> Client: render it and send actions back
81
+ ```
82
+
83
+ ### Catalog
84
+
85
+ For a React developer, the catalog is your public component API for AI. It is
86
+ the generated-UI equivalent of exporting a component plus its prop types.
87
+
88
+ The catalog tells the agent:
89
+
90
+ - Component names, such as `Text`, `Column`, `ProductTile`.
91
+ - Prop names and types.
92
+ - Required fields.
93
+ - Allowed enum values.
94
+ - Optional functions for dynamic formatting and validation.
95
+
96
+ The catalog tells the client:
97
+
98
+ - Which ReactLynx component to instantiate for each A2UI component name.
99
+ - Which component names are safe to render.
100
+
101
+ ### Agent
102
+
103
+ The agent is a UI planner. It receives normal chat messages, reads the catalog,
104
+ and returns A2UI JSON messages. Your backend should validate those messages
105
+ before returning them to the client.
106
+
107
+ The important product rule is: the agent designs within your catalog. If a
108
+ component is not in the catalog, it should not appear in the generated UI.
109
+
110
+ ### Client
111
+
112
+ The client owns transport and rendering. It fetches messages from the agent,
113
+ pushes them into `MessageStore`, renders `<A2UI>`, and forwards generated user
114
+ actions back to your backend.
115
+
116
+ If you know `useSyncExternalStore`, the `MessageStore` idea should feel
117
+ familiar: it is an append-only external store of protocol messages. `<A2UI>`
118
+ subscribes to it and updates the rendered surface as messages arrive.
119
+
120
+ ## Quickstart
121
+
122
+ In your ReactLynx app, install the GenUI package. The CLI requires Node.js 22
123
+ or newer and is exposed by the same package.
124
+
125
+ ```sh
126
+ pnpm add @lynx-js/genui
127
+ genui a2ui --help
128
+ ```
129
+
130
+ The rest of the flow is app-local: define catalog-facing component contracts,
131
+ generate catalog artifacts, give the generated prompt to your agent service,
132
+ and render validated A2UI messages in your ReactLynx client.
133
+
134
+ ### 1. Catalog: Turn React Components Into Agent-Visible Components
135
+
136
+ Start with a component contract. This is the part React developers already do
137
+ well: name the props and keep the component predictable.
138
+
139
+ ```tsx
140
+ /**
141
+ * Product tile for commerce recommendations.
142
+ *
143
+ * @a2uiCatalog ProductTile
144
+ */
145
+ export interface ProductTileProps {
146
+ /** Product name shown as the title. */
147
+ title: string;
148
+ /** Price text already localized by the caller. */
149
+ price: string;
150
+ /** Image search query or resolved URL. */
151
+ imageUrl?: string;
152
+ }
153
+
154
+ export function ProductTile(props: ProductTileProps) {
155
+ return (
156
+ <view className='product-tile'>
157
+ {props.imageUrl ? <image src={props.imageUrl} /> : null}
158
+ <text>{props.title}</text>
159
+ <text>{props.price}</text>
160
+ </view>
161
+ );
162
+ }
163
+
164
+ ProductTile.displayName = 'ProductTile';
165
+ ```
166
+
167
+ Generate a schema for the agent:
168
+
169
+ ```sh
170
+ genui a2ui generate catalog --catalog-dir src/catalog --out-dir dist/catalog
171
+ ```
172
+
173
+ Then pair the component with its manifest:
174
+
175
+ ```tsx
176
+ import {
177
+ Button,
178
+ Column,
179
+ Text,
180
+ createMessageStore,
181
+ defineCatalog,
182
+ serializeCatalog,
183
+ } from '@lynx-js/genui/a2ui';
184
+ import buttonManifest from '@lynx-js/genui/a2ui/catalog/Button/catalog.json'
185
+ with { type: 'json' };
186
+ import columnManifest from '@lynx-js/genui/a2ui/catalog/Column/catalog.json'
187
+ with { type: 'json' };
188
+ import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json'
189
+ with { type: 'json' };
190
+ import productTileManifest from './dist/catalog/ProductTile/catalog.json'
191
+ with { type: 'json' };
192
+
193
+ export const uiCatalog = defineCatalog([
194
+ [Text, textManifest],
195
+ [Column, columnManifest],
196
+ [Button, buttonManifest],
197
+ [ProductTile, productTileManifest],
198
+ ]);
199
+
200
+ export const catalogHandshake = serializeCatalog(uiCatalog);
201
+ export const store = createMessageStore();
202
+ ```
203
+
204
+ Use `catalogHandshake` when your own transport or agent consumes the client
205
+ handshake format. If your agent uses a different internal catalog format to
206
+ build prompts, add an explicit backend conversion step so the agent sees the
207
+ same component names that the client has registered.
208
+
209
+ There is intentionally no exported "all built-ins" constant. Importing every
210
+ component makes bundle cost invisible and weakens tree-shaking. Import only the
211
+ built-in components and catalog manifests your generated UI should be allowed to
212
+ use.
213
+
214
+ Production note: minifiers can rewrite function names. Set
215
+ `ProductTile.displayName = 'ProductTile'` or pair custom components with their
216
+ manifest so the protocol name stays stable.
217
+
218
+ ### 2. CLI: Generate Catalogs And Prompts
219
+
220
+ The CLI is the build-time bridge between React source code and the agent. Use
221
+ it when you want repeatable artifacts instead of hand-maintained JSON:
222
+
223
+ - `generate catalog` reads TypeScript catalog contracts and writes
224
+ `dist/catalog/<Component>/catalog.json`.
225
+ - `generate prompt` reads generated catalog artifacts and writes an A2UI system
226
+ prompt for an agent.
227
+
228
+ Run the published CLI package through `npx`:
229
+
230
+ ```sh
231
+ genui a2ui generate catalog \
232
+ --catalog-dir src/catalog \
233
+ --source src/functions \
234
+ --out-dir dist/catalog
235
+
236
+ genui a2ui generate prompt \
237
+ --catalog-dir dist/catalog \
238
+ --catalog-id https://example.com/catalogs/custom/v1/catalog.json \
239
+ --out dist/a2ui-system-prompt.txt
240
+ ```
241
+
242
+ When your build already produces TypeDoc JSON, keep the same
243
+ `genui a2ui` command prefix and pass that file to `generate catalog`:
244
+
245
+ ```sh
246
+ genui a2ui generate catalog \
247
+ --typedoc-json typedoc.json \
248
+ --out-dir dist/catalog
249
+ ```
250
+
251
+ Key options:
252
+
253
+ | Option | Use |
254
+ | ----------------------- | ---------------------------------------------------------------------------- |
255
+ | `--catalog-dir <dir>` | Scan catalog component interfaces, or read generated artifacts for prompts. |
256
+ | `--source <path>` | Add source files or directories, commonly for catalog functions. Repeatable. |
257
+ | `--typedoc-json <file>` | Reuse an existing TypeDoc JSON project instead of running TypeDoc. |
258
+ | `--out-dir <dir>` | Write generated catalog artifacts. Defaults to `dist/catalog`. |
259
+ | `--catalog-id <id>` | Set the catalog id expected in generated `createSurface` messages. |
260
+ | `--out <file>` | Write the generated prompt to a file instead of stdout. |
261
+ | `--appendix <text>` | Add extra agent instructions to the generated prompt. |
262
+
263
+ Catalog authoring details:
264
+
265
+ - Put `@a2uiCatalog` on the props `interface`, not on the component function.
266
+ You can pass the component name explicitly, such as `@a2uiCatalog ProductTile`.
267
+ If the tag is empty, the generator infers the name by removing a trailing
268
+ `Props` or `ComponentProps` from the interface name.
269
+ - TypeDoc comments become schema metadata: summary text and `@remarks` become
270
+ `description`, `@defaultValue` or `@default` become `default`,
271
+ `@deprecated` becomes `deprecated: true`, and optional properties are omitted
272
+ from `required`. For object or array defaults, put the JSON value inside a
273
+ code span, such as `` @defaultValue `{}` ``.
274
+ - Supported prop types include `string`, `number`, `boolean`, string literal
275
+ enums, unions, arrays, inline object types, and `Record<string, T>`.
276
+ - Avoid `any`, `unknown`, `null`, `undefined`, `never`, `void`, nullable
277
+ unions, most imported aliases, referenced external interfaces, and
278
+ non-string `Record` keys. Inline the agent-facing fields directly in the
279
+ marked interface.
280
+ - The scanner accepts `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, and `.cts` files.
281
+ It ignores `.d.ts`, `node_modules`, `dist`, and `.turbo`.
282
+
283
+ Operational notes:
284
+
285
+ - Keep generated catalog artifacts in your package build output and commit API
286
+ reports or generated manifests when the package contract expects them.
287
+ - Regenerate catalog artifacts whenever a catalog-facing props interface or
288
+ `@a2uiFunction` definition changes.
289
+ - `generate prompt` uses the built-in A2UI basic catalog when `--catalog-dir`
290
+ is omitted; pass `--catalog-dir` for custom generated catalogs.
291
+ - The generated prompt and the client catalog must describe the same component
292
+ names and props. A mismatch can pass server validation but render as
293
+ unsupported on the client.
294
+ - `functions` and `theme` are not inferred from component props. Add them
295
+ explicitly through generated function definitions or prompt/catalog helpers.
296
+
297
+ Keep the generated prompt with your backend code and the generated catalog
298
+ artifacts with your app package so agent and client deployments stay in sync.
299
+
300
+ ### 3. Agent: Ask For UI, Receive Validated Messages
301
+
302
+ Your agent service is a backend route in your product, not browser code. It
303
+ should:
304
+
305
+ - Load the A2UI system prompt generated by `genui a2ui generate
306
+ prompt`.
307
+ - Add conversation history, user intent, and any product state the model needs.
308
+ - Call your model provider from the server.
309
+ - Validate or repair model output before returning A2UI messages to the client.
310
+ - Keep provider credentials, base URLs, and model selection out of untrusted
311
+ browser requests.
312
+
313
+ A typical request shape looks like this:
314
+
315
+ ```sh
316
+ curl https://your-domain.example/api/a2ui/chat \
317
+ -H 'Content-Type: application/json' \
318
+ -d '{
319
+ "messages": [
320
+ {
321
+ "role": "user",
322
+ "content": "Create a compact weather card with a photo, temperature, humidity, and a Refresh button."
323
+ }
324
+ ]
325
+ }'
326
+ ```
327
+
328
+ The response contains `messages`. Those are not React elements. They are data
329
+ instructions that the client renderer can process.
330
+
331
+ A tiny A2UI response looks like this:
332
+
333
+ ```json
334
+ [
335
+ {
336
+ "version": "v0.9",
337
+ "createSurface": {
338
+ "surfaceId": "main",
339
+ "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
340
+ }
341
+ },
342
+ {
343
+ "version": "v0.9",
344
+ "updateComponents": {
345
+ "surfaceId": "main",
346
+ "components": [
347
+ {
348
+ "id": "root",
349
+ "component": "Column",
350
+ "children": ["title"]
351
+ },
352
+ {
353
+ "id": "title",
354
+ "component": "Text",
355
+ "text": "Hello from generated UI"
356
+ }
357
+ ]
358
+ }
359
+ }
360
+ ]
361
+ ```
362
+
363
+ You do not need to hand-write this JSON for normal app development. It is
364
+ useful to recognize the structure when debugging.
365
+
366
+ Important endpoints:
367
+
368
+ | Endpoint | Use |
369
+ | ------------------------------ | ----------------------------------------------------------------------------------- |
370
+ | `GET /api/a2ui/health` | Optional health/configuration check for your backend. |
371
+ | `POST /api/a2ui/chat` | Return one validated JSON response. |
372
+ | `POST /api/a2ui/stream` | Stream model deltas as SSE, then emit validated messages in the final `done` event. |
373
+ | `POST /api/a2ui/action` | Convert a client action into the next validated A2UI response. |
374
+ | `POST /api/a2ui/action/stream` | Stream an action response and final validation payload. |
375
+
376
+ Common server-side configuration:
377
+
378
+ | Variable | Purpose |
379
+ | ------------------------- | ------------------------------------------------------------------------ |
380
+ | `OPENAI_API_KEY` | Model credential kept on the server. |
381
+ | `OPENAI_MODEL` | Model id chosen by your backend. |
382
+ | `OPENAI_BASE_URL` | Optional OpenAI-compatible endpoint. |
383
+ | `OPENAI_API_STYLE` | `responses` or `chat`, depending on your provider integration. |
384
+ | `IMAGE_PROVIDER_API_KEY` | Optional image provider credential if your agent resolves image queries. |
385
+ | `A2UI_CORS_ORIGINS` | Comma-separated browser origins allowed by your server. |
386
+ | `A2UI_RATE_LIMIT_PER_MIN` | Per-client request limit for your server. |
387
+
388
+ ### 4. Client: Render Messages Like React State
389
+
390
+ The client fetches agent output and pushes each message into the store.
391
+ `<A2UI>` does the protocol processing and renders the matching ReactLynx
392
+ components.
49
393
 
50
394
  ```tsx
51
- import { A2UI, Button, Text, createMessageStore } from '@lynx-js/genui/a2ui';
395
+ import {
396
+ A2UI,
397
+ Button,
398
+ Column,
399
+ Text,
400
+ createMessageStore,
401
+ } from '@lynx-js/genui/a2ui';
402
+ import type { UserActionPayload } from '@lynx-js/genui/a2ui';
52
403
 
53
404
  const store = createMessageStore();
405
+ const catalogs = [Text, Column, Button];
406
+
407
+ async function sendPrompt(content: string) {
408
+ const response = await fetch('/api/a2ui/chat', {
409
+ method: 'POST',
410
+ headers: { 'Content-Type': 'application/json' },
411
+ body: JSON.stringify({
412
+ messages: [{ role: 'user', content }],
413
+ }),
414
+ });
415
+ const body = await response.json();
416
+ for (const message of body.messages ?? []) {
417
+ store.push(message);
418
+ }
419
+ }
54
420
 
55
- // Your IO module pushes raw v0.9 protocol messages into the store.
56
- // async function streamFromAgent(input: string) {
57
- // for await (const msg of myAgent.stream(input)) store.push(msg);
58
- // }
421
+ async function sendAction(action: UserActionPayload) {
422
+ const response = await fetch('/api/a2ui/action', {
423
+ method: 'POST',
424
+ headers: { 'Content-Type': 'application/json' },
425
+ body: JSON.stringify({
426
+ surfaceId: action.surfaceId,
427
+ action,
428
+ }),
429
+ });
430
+ const body = await response.json();
431
+ for (const message of body.messages ?? []) {
432
+ store.push(message);
433
+ }
434
+ }
59
435
 
60
- export function A2UIScreen(): import('@lynx-js/react').ReactNode {
436
+ export function GeneratedUIScreen(): import('@lynx-js/react').ReactNode {
61
437
  return (
62
438
  <A2UI
63
439
  messageStore={store}
64
- catalogs={[Text, Button]}
65
- className='surface-container'
440
+ catalogs={catalogs}
66
441
  onAction={(action) => {
67
- // Forward to your agent — push the response messages back into
68
- // the same store. Fire-and-forget; the renderer never awaits.
442
+ void sendAction(action);
69
443
  }}
70
- wrapSurface={(c) => <view className='luna-light'>{c}</view>}
444
+ wrapSurface={(children) => <view className='a2ui-light'>{children}</view>}
71
445
  />
72
446
  );
73
447
  }
74
448
  ```
75
449
 
76
- The `<A2UI>` component is intentionally minimal:
450
+ Map this back to React:
451
+
452
+ - `MessageStore` is the external state source.
453
+ - `store.push(message)` is like receiving the next state update from the
454
+ server.
455
+ - `catalogs` is the allowlist of components the generated tree may use.
456
+ - `onAction` is like an event handler, except the event is serialized and sent
457
+ back to the agent.
458
+ - Passing a new React `key` to `<A2UI>` starts a fresh renderer session.
459
+
460
+ ## Transport Layer
461
+
462
+ GenUI does not prescribe one transport. The protocol messages can travel over
463
+ REST, SSE, WebSocket, A2A, AG UI, MCP, or an in-process mock. In a React app,
464
+ the transport layer is the adapter between your product state and
465
+ `MessageStore`.
77
466
 
78
- - It owns its own `MessageProcessor` per mount; passing a different
79
- `messageStore` instance does **not** reset internal state — use a
80
- `key` prop derived from your turn/session id when you want a fresh
81
- session.
82
- - `onAction` is fire-and-forget. The renderer doesn't wait for a
83
- response — your agent pushes follow-up messages back into the same
84
- `messageStore`.
85
- - `className` applies to the surface root view (`surface-${surfaceId}`).
86
- - `wrapSurface` applies an outer wrapper around the rendered surface.
87
- - Both can be used for multi-theme switching; choose the layer that
88
- matches your styling strategy.
467
+ It owns:
89
468
 
90
- ## Catalogs
469
+ - Calling the agent endpoint.
470
+ - Passing conversation history and data-model snapshots.
471
+ - Parsing JSON or streaming SSE responses.
472
+ - Pushing validated A2UI messages into the store in order.
473
+ - Forwarding `onAction` payloads back to the agent.
474
+ - Cancelling stale requests and surfacing errors.
91
475
 
92
- The package intentionally **does not** ship an "all-in-one" aggregate.
93
- Composition is per-component so bundlers can tree-shake what isn't
94
- referenced.
476
+ It should not own:
95
477
 
96
- ### Bare components (renderer-only)
478
+ - Rendering A2UI components directly.
479
+ - Mutating the generated component tree by hand.
480
+ - Trusting arbitrary prose from the model as UI.
481
+ - Letting browser clients override provider credentials in production.
482
+
483
+ ### Interface Best Practices
484
+
485
+ Keep the transport small and explicit:
97
486
 
98
487
  ```ts
99
- import { defineCatalog, Text, Button } from '@lynx-js/genui/a2ui';
488
+ import type { MessageStore, UserActionPayload } from '@lynx-js/genui/a2ui';
489
+
490
+ interface ConversationContext {
491
+ history: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>;
492
+ dataModel: Record<string, unknown>;
493
+ }
100
494
 
101
- const catalog = defineCatalog([Text, Button]);
495
+ interface A2UITransport {
496
+ generate(input: {
497
+ prompt: string;
498
+ conversation?: ConversationContext;
499
+ signal?: AbortSignal;
500
+ }): Promise<unknown[]>;
501
+ respondToAction(input: {
502
+ surfaceId: string;
503
+ action: UserActionPayload;
504
+ conversation?: ConversationContext;
505
+ signal?: AbortSignal;
506
+ }): Promise<unknown[]>;
507
+ }
508
+
509
+ async function applyMessages(
510
+ store: MessageStore,
511
+ messages: unknown[],
512
+ ): Promise<void> {
513
+ for (const message of messages) {
514
+ store.push(message);
515
+ }
516
+ }
102
517
  ```
103
518
 
104
- The protocol name comes from `displayName ?? component.name`.
519
+ This keeps generated UI as data until the last step. The renderer remains the
520
+ only place that interprets A2UI messages.
105
521
 
106
- > ⚠️ Production minifiers rewrite `function` names. For production
107
- > safety, set an explicit `displayName` on every custom component, or
108
- > pair it with its `catalog.json` manifest (the manifest key is
109
- > authoritative).
522
+ ### REST Baseline
110
523
 
111
- ### Paired with manifests (renderer + agent handshake)
524
+ Use routes such as `/api/a2ui/chat` and `/api/a2ui/action` when you want a
525
+ simple request/response implementation:
112
526
 
113
527
  ```ts
114
- import { Text, defineCatalog } from '@lynx-js/genui/a2ui';
115
- import textManifest from '@lynx-js/genui/a2ui/catalog/Text/catalog.json'
116
- with { type: 'json' };
528
+ function extractMessages(payload: unknown): unknown[] {
529
+ if (Array.isArray(payload)) return payload;
530
+ if (typeof payload === 'string') {
531
+ try {
532
+ return extractMessages(JSON.parse(payload));
533
+ } catch {
534
+ return [];
535
+ }
536
+ }
537
+ if (!payload || typeof payload !== 'object') return [];
538
+
539
+ const record = payload as {
540
+ messages?: unknown;
541
+ validation?: { messages?: unknown };
542
+ text?: unknown;
543
+ };
544
+ if (Array.isArray(record.messages)) return record.messages;
545
+ if (Array.isArray(record.validation?.messages)) {
546
+ return record.validation.messages;
547
+ }
548
+ if (typeof record.text === 'string') return extractMessages(record.text);
549
+ return [];
550
+ }
551
+
552
+ async function postA2UI(
553
+ url: string,
554
+ body: unknown,
555
+ signal?: AbortSignal,
556
+ ): Promise<unknown[]> {
557
+ const response = await fetch(url, {
558
+ method: 'POST',
559
+ headers: { 'Content-Type': 'application/json' },
560
+ body: JSON.stringify(body),
561
+ signal,
562
+ });
563
+
564
+ const payload = await response.json().catch(() => ({}));
565
+ if (!response.ok) {
566
+ throw new Error(`A2UI request failed: ${response.status}`);
567
+ }
568
+
569
+ const messages = extractMessages(payload);
570
+ if (messages.length === 0) {
571
+ throw new Error('A2UI response did not include renderable messages');
572
+ }
573
+ return messages;
574
+ }
575
+ ```
576
+
577
+ Then wire it to the renderer:
578
+
579
+ ```ts
580
+ async function generate(prompt: string, signal?: AbortSignal) {
581
+ const messages = await postA2UI(
582
+ '/api/a2ui/chat',
583
+ { messages: [{ role: 'user', content: prompt }] },
584
+ signal,
585
+ );
586
+ await applyMessages(store, messages);
587
+ }
117
588
 
118
- const catalog = defineCatalog([[Text, textManifest]]);
119
- agentChannel.handshake({ catalog: serializeCatalog(catalog) });
589
+ async function respondToAction(
590
+ action: UserActionPayload,
591
+ signal?: AbortSignal,
592
+ ) {
593
+ const messages = await postA2UI(
594
+ '/api/a2ui/action',
595
+ { surfaceId: action.surfaceId, action },
596
+ signal,
597
+ );
598
+ await applyMessages(store, messages);
599
+ }
120
600
  ```
121
601
 
122
- See [`src/catalog/README.md`](src/catalog/README.md) for the full
123
- recipe (including the paste-able "every built-in" snippet).
602
+ ### SSE Streaming
124
603
 
125
- ## Custom Components
604
+ Use routes such as `/api/a2ui/stream` and `/api/a2ui/action/stream` when you
605
+ want to show generation progress. The server emits:
126
606
 
127
- Any function returning a `ReactNode` works. The function's name (or
128
- `displayName`) is the protocol name the agent will use:
607
+ - `delta`: raw model text, useful for an inspector or loading state.
608
+ - `repair`: optional metadata when the server had to repair invalid model
609
+ output.
610
+ - `done`: the final validated payload. Use the messages from this event for
611
+ rendering.
612
+ - `error`: structured failure payload.
129
613
 
130
- ```tsx
131
- function MyChart(props: { data: number[] }) { ... }
132
- MyChart.displayName = 'MyChart';
614
+ ```ts
615
+ interface SseFrame {
616
+ event: string;
617
+ data: unknown;
618
+ }
133
619
 
134
- <A2UI catalogs={[Text, Button, MyChart]} ... />;
135
- // Agent emits `{ component: 'MyChart', data: [...] }` → renders MyChart.
620
+ function parseSseFrame(frame: string): SseFrame | null {
621
+ const lines = frame.split(/\r?\n/u);
622
+ let event = 'message';
623
+ const dataLines: string[] = [];
624
+
625
+ for (const line of lines) {
626
+ if (line.startsWith('event:')) {
627
+ event = line.slice('event:'.length).trim();
628
+ } else if (line.startsWith('data:')) {
629
+ dataLines.push(line.slice('data:'.length).trimStart());
630
+ }
631
+ }
632
+
633
+ if (dataLines.length === 0) return null;
634
+ const raw = dataLines.join('\n');
635
+ try {
636
+ return { event, data: JSON.parse(raw) };
637
+ } catch {
638
+ return { event, data: raw };
639
+ }
640
+ }
641
+
642
+ async function readA2UISse(
643
+ response: Response,
644
+ onDelta?: (text: string) => void,
645
+ ): Promise<unknown[]> {
646
+ const reader = response.body?.getReader();
647
+ if (!reader) return [];
648
+
649
+ const decoder = new TextDecoder();
650
+ let buffer = '';
651
+ let generatedText = '';
652
+
653
+ while (true) {
654
+ const { done, value } = await reader.read();
655
+ buffer += decoder.decode(value, { stream: !done });
656
+
657
+ const frames = buffer.split(/\r?\n\r?\n/u);
658
+ buffer = frames.pop() ?? '';
659
+
660
+ for (const frame of frames) {
661
+ const parsed = parseSseFrame(frame);
662
+ if (!parsed) continue;
663
+
664
+ if (parsed.event === 'delta') {
665
+ const text = (parsed.data as { text?: unknown }).text;
666
+ if (typeof text === 'string') {
667
+ generatedText += text;
668
+ onDelta?.(generatedText);
669
+ }
670
+ continue;
671
+ }
672
+
673
+ if (parsed.event === 'done') {
674
+ const messages = extractMessages(parsed.data);
675
+ if (messages.length === 0) {
676
+ throw new Error('A2UI stream finished without renderable messages');
677
+ }
678
+ return messages;
679
+ }
680
+
681
+ if (parsed.event === 'error') {
682
+ throw new Error(JSON.stringify(parsed.data));
683
+ }
684
+ }
685
+
686
+ if (done) break;
687
+ }
688
+
689
+ return extractMessages(generatedText);
690
+ }
136
691
  ```
137
692
 
138
- If you want schema introspection for a custom component, generate the
139
- manifest with `@lynx-js/genui/a2ui-catalog-extractor` against your interface
140
- and pair it with the component the same way as the built-ins.
693
+ Avoid rendering every `delta` as A2UI. During streaming, the model text may be
694
+ an incomplete JSON array. Render from the final `done` event by default. If you
695
+ choose partial rendering, only publish complete parsed message objects and
696
+ replace them with the final validated messages when `done` arrives.
697
+
698
+ ## Operational Best Practices
699
+
700
+ - Keep one active generation per conversation surface. Abort or ignore older
701
+ requests when a new prompt starts.
702
+ - Use a separate `AbortController` for user actions. An old action response
703
+ should not update the UI after a newer action has started.
704
+ - Render from `done.validation.messages` or `messages`. Treat `delta` as
705
+ progress text for inspectors and loading states.
706
+ - Push messages into `MessageStore` in server order. Do not sort, merge, or
707
+ deduplicate them unless you understand the protocol consequences.
708
+ - Keep conversation history and the current data-model snapshot outside
709
+ `MessageStore`; include them in the next agent request when you need coherent
710
+ multi-turn updates.
711
+ - Send action requests with both `surfaceId` and the full `action` payload.
712
+ Action responses normally update the existing surface rather than creating a
713
+ new one.
714
+ - Normalize all supported response formats: direct arrays, `{ messages }`,
715
+ `{ validation: { messages } }`, and stringified JSON.
716
+ - Check `content-type`. Your endpoints may return JSON or `text/event-stream`
717
+ depending on the route.
718
+ - Parse non-2xx responses as structured JSON when possible, then fall back to a
719
+ status-based error.
720
+ - Keep endpoint allowlists strict. The hosted Playground should only talk to
721
+ trusted GenUI endpoints.
722
+ - Do not pass model API keys, base URLs, or model ids from a browser in
723
+ production. Keep provider selection and credentials on the server.
724
+ - Configure CORS and rate limits on the server before exposing the agent to
725
+ browsers.
726
+ - Version your catalog contract. The agent catalog and client catalog must
727
+ agree on component names and props, or validated output may still render as
728
+ unsupported on the client.
729
+ - Use deterministic mocks for tests. A transport can be an in-process async
730
+ generator that pushes known A2UI messages into the store.
731
+
732
+ Common mistakes:
733
+
734
+ - Rendering raw model prose instead of validated A2UI messages.
735
+ - Reusing one `MessageStore` for unrelated conversations without remounting
736
+ `<A2UI key={...}>`.
737
+ - Dropping `conversation.dataModel`, which makes follow-up actions lose state.
738
+ - Retrying non-idempotent actions automatically, which can apply the same user
739
+ intent twice.
740
+ - Allowing generated image URLs, remote endpoints, or provider overrides from
741
+ untrusted browser input.
742
+
743
+ ## Try The Playground
744
+
745
+ The hosted playground is the fastest way to see the whole loop before
746
+ integrating it into an app:
747
+
748
+ [https://lynx-stack.dev/a2ui/](https://lynx-stack.dev/a2ui/)
749
+
750
+ Use the hosted page to try the demos, inspect generated A2UI JSON, browse the
751
+ catalog, and preview Lynx surfaces.
752
+
753
+ Use the playground to:
754
+
755
+ - Describe UI in natural language and inspect the generated A2UI JSON.
756
+ - Browse the component catalog like a React component library.
757
+ - Preview the generated Lynx surface.
758
+ - Test action flows such as submit, refresh, and selection.
759
+ - Generate preview URLs and QR codes for native Lynx testing.
760
+
761
+ ## Glossary For React Developers
762
+
763
+ | GenUI term | React-friendly meaning |
764
+ | ------------------ | -------------------------------------------------------------------------------------- |
765
+ | A2UI | JSON messages that describe UI changes. Similar to a serialized, constrained UI tree. |
766
+ | Surface | A generated UI root, similar to a mounted app region. |
767
+ | Catalog | The approved component library and prop schema exposed to the agent. |
768
+ | `MessageStore` | Append-only external store that receives protocol messages. |
769
+ | `updateComponents` | "Render these component instances with these props." |
770
+ | `updateDataModel` | "Patch the data used by bound props." Similar to remote state updates. |
771
+ | Action | A generated UI event, similar to `onClick`, sent back to the agent as structured data. |
772
+
773
+ ## Protocol Notes
774
+
775
+ The current A2UI path targets A2UI v0.9.
776
+
777
+ - The model must output a raw JSON array, not Markdown.
778
+ - A fresh response starts with `createSurface`, followed by
779
+ `updateComponents` containing a `root` component.
780
+ - Components form a flat graph. Children are referenced by id rather than
781
+ inlined.
782
+ - Data bindings use JSON Pointer paths and must be populated by
783
+ `updateDataModel`.
784
+ - Interactive components emit action payloads. The client posts those actions
785
+ to the agent, and the agent returns update messages for the existing surface.
786
+
787
+ ## Testing And Quality
788
+
789
+ Use your app's normal test runner. The high-value checks are:
790
+
791
+ - Unit-test catalog registration so every component name in generated messages
792
+ maps to the ReactLynx component you expect.
793
+ - Unit-test transport parsing with deterministic JSON and SSE fixtures,
794
+ including malformed responses and aborts.
795
+ - Replay saved A2UI message arrays through `<A2UI>` so renderer regressions are
796
+ visible without calling a model.
797
+ - E2E-test one prompt flow and one action flow with mocked agent responses
798
+ before adding model-backed tests.
799
+
800
+ ## Product Direction
801
+
802
+ GenUI is designed around a few commitments:
803
+
804
+ - React remains the implementation layer. The agent chooses from components you
805
+ own.
806
+ - The catalog is the product contract. It keeps generated UI aligned with your
807
+ design system and platform constraints.
808
+ - Progressive rendering should make the UI useful before a turn fully
809
+ completes.
810
+ - Transports are replaceable. REST, SSE, WebSocket, A2A, AG UI, or MCP can all
811
+ carry the same A2UI messages.
812
+ - Generated UI should be inspectable, replayable, and judgeable in automated
813
+ workflows.
814
+
815
+ Start with the hosted Playground, then generate a small catalog in your app and
816
+ wire one prompt route plus one action route before expanding to richer
817
+ components.