Skip to content

Commit fd58d77

Browse files
committed
docs: refresh render and a2ui references
1 parent 7472553 commit fd58d77

8 files changed

Lines changed: 96 additions & 20 deletions

File tree

apps/website/content/docs/a2ui/api/api-docs.json

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@
460460
{
461461
"name": "push",
462462
"signature": "push(chunk: string): A2uiMessage[]",
463-
"description": "",
463+
"description": "Push a JSONL stream chunk and return every complete A2UI envelope parsed from it.",
464464
"params": [
465465
{
466466
"name": "chunk",
@@ -881,19 +881,21 @@
881881
{
882882
"name": "createA2uiMessageParser",
883883
"kind": "function",
884-
"description": "",
884+
"description": "Creates a stateful parser for newline-delimited A2UI message streams.\n\nThe parser buffers incomplete lines, skips malformed JSON, and returns only\nrecognized A2UI envelopes: `surfaceUpdate`, `dataModelUpdate`,\n`beginRendering`, and `deleteSurface`.",
885885
"signature": "createA2uiMessageParser(): A2uiMessageParser",
886886
"params": [],
887887
"returns": {
888888
"type": "A2uiMessageParser",
889889
"description": ""
890890
},
891-
"examples": []
891+
"examples": [
892+
"```ts\nconst parser = createA2uiMessageParser();\nconst messages = parser.push('{\"beginRendering\":{\"surfaceId\":\"s1\",\"root\":\"root\"}}\\n');\n```"
893+
]
892894
},
893895
{
894896
"name": "deleteByPointer",
895897
"kind": "function",
896-
"description": "",
898+
"description": "Returns a new model with the key at `pointer` removed.\n\nEmpty string and `/` clear the whole model. Missing parent paths leave the\ninput model unchanged.",
897899
"signature": "deleteByPointer(model: Record<string, unknown>, pointer: string): Record<string, unknown>",
898900
"params": [
899901
{
@@ -913,12 +915,14 @@
913915
"type": "Record<string, unknown>",
914916
"description": ""
915917
},
916-
"examples": []
918+
"examples": [
919+
"```ts\nconst next = deleteByPointer({ user: { name: 'Ada' } }, '/user/name');\n```"
920+
]
917921
},
918922
{
919923
"name": "getByPointer",
920924
"kind": "function",
921-
"description": "",
925+
"description": "Reads a value from a model using the package's simple slash-separated pointer syntax.\n\nEmpty string and `/` target the root. Segments are not RFC 6901-unescaped;\nnormalize keys before using pointers that would require `~0` or `~1`.",
922926
"signature": "getByPointer(model: Record<string, unknown>, pointer: string): unknown",
923927
"params": [
924928
{
@@ -938,12 +942,14 @@
938942
"type": "unknown",
939943
"description": ""
940944
},
941-
"examples": []
945+
"examples": [
946+
"```ts\ngetByPointer({ user: { name: 'Ada' } }, '/user/name'); // 'Ada'\n```"
947+
]
942948
},
943949
{
944950
"name": "isLiteralBoolean",
945951
"kind": "function",
946-
"description": "",
952+
"description": "Returns true when `value` is an A2UI boolean literal wrapper.",
947953
"signature": "isLiteralBoolean(value: unknown): value is { literalBoolean: boolean }",
948954
"params": [
949955
{
@@ -962,7 +968,7 @@
962968
{
963969
"name": "isLiteralNumber",
964970
"kind": "function",
965-
"description": "",
971+
"description": "Returns true when `value` is an A2UI number literal wrapper.",
966972
"signature": "isLiteralNumber(value: unknown): value is { literalNumber: number }",
967973
"params": [
968974
{
@@ -981,7 +987,7 @@
981987
{
982988
"name": "isLiteralString",
983989
"kind": "function",
984-
"description": "",
990+
"description": "Returns true when `value` is an A2UI string literal wrapper.",
985991
"signature": "isLiteralString(value: unknown): value is { literalString: string }",
986992
"params": [
987993
{
@@ -1000,7 +1006,7 @@
10001006
{
10011007
"name": "isPathRef",
10021008
"kind": "function",
1003-
"description": "",
1009+
"description": "Returns true when `value` is an A2UI dynamic path reference.",
10041010
"signature": "isPathRef(value: unknown): value is { path: string }",
10051011
"params": [
10061012
{
@@ -1019,7 +1025,7 @@
10191025
{
10201026
"name": "resolveDynamic",
10211027
"kind": "function",
1022-
"description": "",
1028+
"description": "Resolves an A2UI dynamic value against a client data model.\n\nLiteral wrappers unwrap to their inner values, `{ path }` references read\nfrom the model by JSON-pointer path, arrays resolve recursively, and\nunrecognized plain values pass through unchanged.",
10231029
"signature": "resolveDynamic(value: unknown, model: Record<string, unknown>, scope: A2uiScope): unknown",
10241030
"params": [
10251031
{
@@ -1045,12 +1051,14 @@
10451051
"type": "unknown",
10461052
"description": ""
10471053
},
1048-
"examples": []
1054+
"examples": [
1055+
"```ts\nconst model = { customer: { name: 'Ada' } };\nresolveDynamic({ path: '/customer/name' }, model); // 'Ada'\nresolveDynamic({ literalString: 'Checkout' }, model); // 'Checkout'\n```"
1056+
]
10491057
},
10501058
{
10511059
"name": "setByPointer",
10521060
"kind": "function",
1053-
"description": "",
1061+
"description": "Returns a new model with `value` written at `pointer`.\n\nThe input model is not mutated. Missing intermediate objects are created.",
10541062
"signature": "setByPointer(model: Record<string, unknown>, pointer: string, value: unknown): Record<string, unknown>",
10551063
"params": [
10561064
{
@@ -1076,6 +1084,8 @@
10761084
"type": "Record<string, unknown>",
10771085
"description": ""
10781086
},
1079-
"examples": []
1087+
"examples": [
1088+
"```ts\nconst next = setByPointer({}, '/user/name', 'Ada');\n```"
1089+
]
10801090
}
10811091
]

apps/website/content/docs/a2ui/getting-started/quickstart.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Let's start with a real stream. An agent emits A2UI as newline-delimited JSON
2828
```text
2929
---a2ui_JSON---
3030
{"dataModelUpdate":{"surfaceId":"booking","contents":[{"key":"origin","valueString":"LAX"},{"key":"dest","valueString":"JFK"},{"key":"passengers","valueNumber":1}]}}
31-
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":"Book a flight","usageHint":"h2"}}},{"id":"origin","component":{"MultipleChoice":{"label":"Origin","options":[{"label":"LAX","value":"LAX"},{"label":"JFK","value":"JFK"}],"selections":{"path":"/origin"},"maxAllowedSelections":1}}},{"id":"submit_label","component":{"Text":{"text":"Search flights"}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"bookingSubmit","context":[{"key":"origin","value":{"path":"/origin"}},{"key":"dest","value":{"path":"/dest"}}]}}}}]}}
31+
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Book a flight"},"usageHint":"h2"}}},{"id":"origin","component":{"MultipleChoice":{"label":{"literalString":"Origin"},"options":[{"label":{"literalString":"LAX"},"value":"LAX"},{"label":{"literalString":"JFK"},"value":"JFK"}],"selections":{"path":"/origin"},"maxAllowedSelections":1}}},{"id":"submit_label","component":{"Text":{"text":{"literalString":"Search flights"}}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"bookingSubmit","context":[{"key":"origin","value":{"path":"/origin"}},{"key":"dest","value":{"path":"/dest"}}]}}}}]}}
3232
{"beginRendering":{"surfaceId":"booking","root":"root"}}
3333
```
3434

apps/website/content/docs/a2ui/guides/message-protocol.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Every envelope carries that `surfaceId`. A `dataModelUpdate` for `"booking"` onl
1515
As an **id-keyed adjacency list**. A `surfaceUpdate` carries a flat `components` array. Each entry has an `id` and a single `component` definition. Parent-child links are by id reference, not by nesting.
1616

1717
```json
18-
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":"Book a flight","usageHint":"h2"}}}]}}
18+
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Book a flight"},"usageHint":"h2"}}}]}}
1919
```
2020

2121
The `component` value is a **keyed union**: a single-key object where the key names the component type and the value holds its props — `{ "<Name>": { props } }`. `{ "Text": { ... } }` is a Text, `{ "Column": { ... } }` is a Column. There's no separate `type` field; the key *is* the type.
@@ -69,7 +69,7 @@ The stream is a sequence of single-key envelope objects. The parser recognizes e
6969
Defines (or replaces) the components for a surface.
7070

7171
```json
72-
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"submit_label","component":{"Text":{"text":"Search flights"}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"bookingSubmit","context":[{"key":"origin","value":{"path":"/origin"}},{"key":"dest","value":{"path":"/dest"}}]}}}}]}}
72+
{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"submit_label","component":{"Text":{"text":{"literalString":"Search flights"}}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"bookingSubmit","context":[{"key":"origin","value":{"path":"/origin"}},{"key":"dest","value":{"path":"/dest"}}]}}}}]}}
7373
```
7474

7575
### `dataModelUpdate`

libs/a2ui/src/lib/guards.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
// SPDX-License-Identifier: MIT
22

3+
/** Returns true when `value` is an A2UI dynamic path reference. */
34
export function isPathRef(value: unknown): value is { path: string } {
45
return typeof value === 'object' && value !== null
56
&& 'path' in value && typeof (value as { path: unknown }).path === 'string';
67
}
78

9+
/** Returns true when `value` is an A2UI string literal wrapper. */
810
export function isLiteralString(value: unknown): value is { literalString: string } {
911
return typeof value === 'object' && value !== null && 'literalString' in value;
1012
}
1113

14+
/** Returns true when `value` is an A2UI number literal wrapper. */
1215
export function isLiteralNumber(value: unknown): value is { literalNumber: number } {
1316
return typeof value === 'object' && value !== null && 'literalNumber' in value;
1417
}
1518

19+
/** Returns true when `value` is an A2UI boolean literal wrapper. */
1620
export function isLiteralBoolean(value: unknown): value is { literalBoolean: boolean } {
1721
return typeof value === 'object' && value !== null && 'literalBoolean' in value;
1822
}

libs/a2ui/src/lib/parser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@ import type { A2uiMessage } from './types.js';
44
const ENVELOPE_KEYS = ['surfaceUpdate', 'dataModelUpdate', 'beginRendering', 'deleteSurface'] as const;
55

66
export interface A2uiMessageParser {
7+
/** Push a JSONL stream chunk and return every complete A2UI envelope parsed from it. */
78
push(chunk: string): A2uiMessage[];
89
}
910

11+
/**
12+
* Creates a stateful parser for newline-delimited A2UI message streams.
13+
*
14+
* The parser buffers incomplete lines, skips malformed JSON, and returns only
15+
* recognized A2UI envelopes: `surfaceUpdate`, `dataModelUpdate`,
16+
* `beginRendering`, and `deleteSurface`.
17+
*
18+
* @example
19+
* ```ts
20+
* const parser = createA2uiMessageParser();
21+
* const messages = parser.push('{"beginRendering":{"surfaceId":"s1","root":"root"}}\n');
22+
* ```
23+
*/
1024
export function createA2uiMessageParser(): A2uiMessageParser {
1125
let buffer = '';
1226

libs/a2ui/src/lib/pointer.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ function parsePointer(pointer: string): string[] {
55
return pointer.split('/').filter(Boolean);
66
}
77

8+
/**
9+
* Reads a value from a model using the package's simple slash-separated pointer syntax.
10+
*
11+
* Empty string and `/` target the root. Segments are not RFC 6901-unescaped;
12+
* normalize keys before using pointers that would require `~0` or `~1`.
13+
*
14+
* @example
15+
* ```ts
16+
* getByPointer({ user: { name: 'Ada' } }, '/user/name'); // 'Ada'
17+
* ```
18+
*/
819
export function getByPointer(model: Record<string, unknown>, pointer: string): unknown {
920
const segments = parsePointer(pointer);
1021
let current: unknown = model;
@@ -15,6 +26,16 @@ export function getByPointer(model: Record<string, unknown>, pointer: string): u
1526
return current;
1627
}
1728

29+
/**
30+
* Returns a new model with `value` written at `pointer`.
31+
*
32+
* The input model is not mutated. Missing intermediate objects are created.
33+
*
34+
* @example
35+
* ```ts
36+
* const next = setByPointer({}, '/user/name', 'Ada');
37+
* ```
38+
*/
1839
export function setByPointer(
1940
model: Record<string, unknown>,
2041
pointer: string,
@@ -40,6 +61,17 @@ export function setByPointer(
4061
return clone(model, segments, value) as Record<string, unknown>;
4162
}
4263

64+
/**
65+
* Returns a new model with the key at `pointer` removed.
66+
*
67+
* Empty string and `/` clear the whole model. Missing parent paths leave the
68+
* input model unchanged.
69+
*
70+
* @example
71+
* ```ts
72+
* const next = deleteByPointer({ user: { name: 'Ada' } }, '/user/name');
73+
* ```
74+
*/
4375
export function deleteByPointer(
4476
model: Record<string, unknown>,
4577
pointer: string,

libs/a2ui/src/lib/resolve.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ function resolvePathRef(ref: PathRef, model: Record<string, unknown>, scope?: A2
3535
return getByPointer(model, '/' + path);
3636
}
3737

38+
/**
39+
* Resolves an A2UI dynamic value against a client data model.
40+
*
41+
* Literal wrappers unwrap to their inner values, `{ path }` references read
42+
* from the model by JSON-pointer path, arrays resolve recursively, and
43+
* unrecognized plain values pass through unchanged.
44+
*
45+
* @example
46+
* ```ts
47+
* const model = { customer: { name: 'Ada' } };
48+
* resolveDynamic({ path: '/customer/name' }, model); // 'Ada'
49+
* resolveDynamic({ literalString: 'Checkout' }, model); // 'Checkout'
50+
* ```
51+
*/
3852
export function resolveDynamic(
3953
value: unknown,
4054
model: Record<string, unknown>,

libs/render/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
## What it does
1818

1919
- Renders a JSON spec tree to Angular components via a named view registry (`<render-spec>`) or a single node (`<render-element>`).
20-
- Registry composition utilities (`views`, `withViews`, `withoutViews`) let you build, extend, and trim registries without mutation.
20+
- Registry composition utilities (`views`, `withViews`, `overrideViews`, `withoutViews`) let you build, extend, replace, and trim registries without mutation.
2121
- Signal-based state store (`signalStateStore`) and per-component fallback support keep UI consistent during streaming.
2222

2323
## Install
2424

2525
```bash
26-
npm install @threadplane/render
26+
npm install @threadplane/render @json-render/core
2727
```
2828

2929
**Peer dependencies:** `@angular/core ^20.0.0 || ^21.0.0`, `@angular/common ^20.0.0 || ^21.0.0`, `@json-render/core ^0.16.0`
3030

31+
`@json-render/core` supplies the spec types and evaluation engine that `@threadplane/render` adapts to Angular.
32+
3133
## Quick start
3234

3335
**1. Define your view registry and provide it.**

0 commit comments

Comments
 (0)