From fd58d77cf03d373c415076873c3df68e323db06f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Jul 2026 18:29:02 -0700 Subject: [PATCH] docs: refresh render and a2ui references --- .../content/docs/a2ui/api/api-docs.json | 40 ++++++++++++------- .../docs/a2ui/getting-started/quickstart.mdx | 2 +- .../docs/a2ui/guides/message-protocol.mdx | 4 +- libs/a2ui/src/lib/guards.ts | 4 ++ libs/a2ui/src/lib/parser.ts | 14 +++++++ libs/a2ui/src/lib/pointer.ts | 32 +++++++++++++++ libs/a2ui/src/lib/resolve.ts | 14 +++++++ libs/render/README.md | 6 ++- 8 files changed, 96 insertions(+), 20 deletions(-) diff --git a/apps/website/content/docs/a2ui/api/api-docs.json b/apps/website/content/docs/a2ui/api/api-docs.json index 65a24400b..7f76b6fd1 100644 --- a/apps/website/content/docs/a2ui/api/api-docs.json +++ b/apps/website/content/docs/a2ui/api/api-docs.json @@ -460,7 +460,7 @@ { "name": "push", "signature": "push(chunk: string): A2uiMessage[]", - "description": "", + "description": "Push a JSONL stream chunk and return every complete A2UI envelope parsed from it.", "params": [ { "name": "chunk", @@ -881,19 +881,21 @@ { "name": "createA2uiMessageParser", "kind": "function", - "description": "", + "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`.", "signature": "createA2uiMessageParser(): A2uiMessageParser", "params": [], "returns": { "type": "A2uiMessageParser", "description": "" }, - "examples": [] + "examples": [ + "```ts\nconst parser = createA2uiMessageParser();\nconst messages = parser.push('{\"beginRendering\":{\"surfaceId\":\"s1\",\"root\":\"root\"}}\\n');\n```" + ] }, { "name": "deleteByPointer", "kind": "function", - "description": "", + "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.", "signature": "deleteByPointer(model: Record, pointer: string): Record", "params": [ { @@ -913,12 +915,14 @@ "type": "Record", "description": "" }, - "examples": [] + "examples": [ + "```ts\nconst next = deleteByPointer({ user: { name: 'Ada' } }, '/user/name');\n```" + ] }, { "name": "getByPointer", "kind": "function", - "description": "", + "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`.", "signature": "getByPointer(model: Record, pointer: string): unknown", "params": [ { @@ -938,12 +942,14 @@ "type": "unknown", "description": "" }, - "examples": [] + "examples": [ + "```ts\ngetByPointer({ user: { name: 'Ada' } }, '/user/name'); // 'Ada'\n```" + ] }, { "name": "isLiteralBoolean", "kind": "function", - "description": "", + "description": "Returns true when `value` is an A2UI boolean literal wrapper.", "signature": "isLiteralBoolean(value: unknown): value is { literalBoolean: boolean }", "params": [ { @@ -962,7 +968,7 @@ { "name": "isLiteralNumber", "kind": "function", - "description": "", + "description": "Returns true when `value` is an A2UI number literal wrapper.", "signature": "isLiteralNumber(value: unknown): value is { literalNumber: number }", "params": [ { @@ -981,7 +987,7 @@ { "name": "isLiteralString", "kind": "function", - "description": "", + "description": "Returns true when `value` is an A2UI string literal wrapper.", "signature": "isLiteralString(value: unknown): value is { literalString: string }", "params": [ { @@ -1000,7 +1006,7 @@ { "name": "isPathRef", "kind": "function", - "description": "", + "description": "Returns true when `value` is an A2UI dynamic path reference.", "signature": "isPathRef(value: unknown): value is { path: string }", "params": [ { @@ -1019,7 +1025,7 @@ { "name": "resolveDynamic", "kind": "function", - "description": "", + "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.", "signature": "resolveDynamic(value: unknown, model: Record, scope: A2uiScope): unknown", "params": [ { @@ -1045,12 +1051,14 @@ "type": "unknown", "description": "" }, - "examples": [] + "examples": [ + "```ts\nconst model = { customer: { name: 'Ada' } };\nresolveDynamic({ path: '/customer/name' }, model); // 'Ada'\nresolveDynamic({ literalString: 'Checkout' }, model); // 'Checkout'\n```" + ] }, { "name": "setByPointer", "kind": "function", - "description": "", + "description": "Returns a new model with `value` written at `pointer`.\n\nThe input model is not mutated. Missing intermediate objects are created.", "signature": "setByPointer(model: Record, pointer: string, value: unknown): Record", "params": [ { @@ -1076,6 +1084,8 @@ "type": "Record", "description": "" }, - "examples": [] + "examples": [ + "```ts\nconst next = setByPointer({}, '/user/name', 'Ada');\n```" + ] } ] \ No newline at end of file diff --git a/apps/website/content/docs/a2ui/getting-started/quickstart.mdx b/apps/website/content/docs/a2ui/getting-started/quickstart.mdx index 321ff8db0..ce6bfd1a8 100644 --- a/apps/website/content/docs/a2ui/getting-started/quickstart.mdx +++ b/apps/website/content/docs/a2ui/getting-started/quickstart.mdx @@ -28,7 +28,7 @@ Let's start with a real stream. An agent emits A2UI as newline-delimited JSON ```text ---a2ui_JSON--- {"dataModelUpdate":{"surfaceId":"booking","contents":[{"key":"origin","valueString":"LAX"},{"key":"dest","valueString":"JFK"},{"key":"passengers","valueNumber":1}]}} -{"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"}}]}}}}]}} +{"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"}}]}}}}]}} {"beginRendering":{"surfaceId":"booking","root":"root"}} ``` diff --git a/apps/website/content/docs/a2ui/guides/message-protocol.mdx b/apps/website/content/docs/a2ui/guides/message-protocol.mdx index 478751660..185825752 100644 --- a/apps/website/content/docs/a2ui/guides/message-protocol.mdx +++ b/apps/website/content/docs/a2ui/guides/message-protocol.mdx @@ -15,7 +15,7 @@ Every envelope carries that `surfaceId`. A `dataModelUpdate` for `"booking"` onl 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. ```json -{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":"Book a flight","usageHint":"h2"}}}]}} +{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","origin","submit"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Book a flight"},"usageHint":"h2"}}}]}} ``` The `component` value is a **keyed union**: a single-key object where the key names the component type and the value holds its props — `{ "": { 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 Defines (or replaces) the components for a surface. ```json -{"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"}}]}}}}]}} +{"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"}}]}}}}]}} ``` ### `dataModelUpdate` diff --git a/libs/a2ui/src/lib/guards.ts b/libs/a2ui/src/lib/guards.ts index edfca3b06..11aa46168 100644 --- a/libs/a2ui/src/lib/guards.ts +++ b/libs/a2ui/src/lib/guards.ts @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT +/** Returns true when `value` is an A2UI dynamic path reference. */ export function isPathRef(value: unknown): value is { path: string } { return typeof value === 'object' && value !== null && 'path' in value && typeof (value as { path: unknown }).path === 'string'; } +/** Returns true when `value` is an A2UI string literal wrapper. */ export function isLiteralString(value: unknown): value is { literalString: string } { return typeof value === 'object' && value !== null && 'literalString' in value; } +/** Returns true when `value` is an A2UI number literal wrapper. */ export function isLiteralNumber(value: unknown): value is { literalNumber: number } { return typeof value === 'object' && value !== null && 'literalNumber' in value; } +/** Returns true when `value` is an A2UI boolean literal wrapper. */ export function isLiteralBoolean(value: unknown): value is { literalBoolean: boolean } { return typeof value === 'object' && value !== null && 'literalBoolean' in value; } diff --git a/libs/a2ui/src/lib/parser.ts b/libs/a2ui/src/lib/parser.ts index 51e43bc39..0f3793d02 100644 --- a/libs/a2ui/src/lib/parser.ts +++ b/libs/a2ui/src/lib/parser.ts @@ -4,9 +4,23 @@ import type { A2uiMessage } from './types.js'; const ENVELOPE_KEYS = ['surfaceUpdate', 'dataModelUpdate', 'beginRendering', 'deleteSurface'] as const; export interface A2uiMessageParser { + /** Push a JSONL stream chunk and return every complete A2UI envelope parsed from it. */ push(chunk: string): A2uiMessage[]; } +/** + * Creates a stateful parser for newline-delimited A2UI message streams. + * + * The parser buffers incomplete lines, skips malformed JSON, and returns only + * recognized A2UI envelopes: `surfaceUpdate`, `dataModelUpdate`, + * `beginRendering`, and `deleteSurface`. + * + * @example + * ```ts + * const parser = createA2uiMessageParser(); + * const messages = parser.push('{"beginRendering":{"surfaceId":"s1","root":"root"}}\n'); + * ``` + */ export function createA2uiMessageParser(): A2uiMessageParser { let buffer = ''; diff --git a/libs/a2ui/src/lib/pointer.ts b/libs/a2ui/src/lib/pointer.ts index adbe53bb0..f908f9760 100644 --- a/libs/a2ui/src/lib/pointer.ts +++ b/libs/a2ui/src/lib/pointer.ts @@ -5,6 +5,17 @@ function parsePointer(pointer: string): string[] { return pointer.split('/').filter(Boolean); } +/** + * Reads a value from a model using the package's simple slash-separated pointer syntax. + * + * Empty string and `/` target the root. Segments are not RFC 6901-unescaped; + * normalize keys before using pointers that would require `~0` or `~1`. + * + * @example + * ```ts + * getByPointer({ user: { name: 'Ada' } }, '/user/name'); // 'Ada' + * ``` + */ export function getByPointer(model: Record, pointer: string): unknown { const segments = parsePointer(pointer); let current: unknown = model; @@ -15,6 +26,16 @@ export function getByPointer(model: Record, pointer: string): u return current; } +/** + * Returns a new model with `value` written at `pointer`. + * + * The input model is not mutated. Missing intermediate objects are created. + * + * @example + * ```ts + * const next = setByPointer({}, '/user/name', 'Ada'); + * ``` + */ export function setByPointer( model: Record, pointer: string, @@ -40,6 +61,17 @@ export function setByPointer( return clone(model, segments, value) as Record; } +/** + * Returns a new model with the key at `pointer` removed. + * + * Empty string and `/` clear the whole model. Missing parent paths leave the + * input model unchanged. + * + * @example + * ```ts + * const next = deleteByPointer({ user: { name: 'Ada' } }, '/user/name'); + * ``` + */ export function deleteByPointer( model: Record, pointer: string, diff --git a/libs/a2ui/src/lib/resolve.ts b/libs/a2ui/src/lib/resolve.ts index 1668d5b55..70a889894 100644 --- a/libs/a2ui/src/lib/resolve.ts +++ b/libs/a2ui/src/lib/resolve.ts @@ -35,6 +35,20 @@ function resolvePathRef(ref: PathRef, model: Record, scope?: A2 return getByPointer(model, '/' + path); } +/** + * Resolves an A2UI dynamic value against a client data model. + * + * Literal wrappers unwrap to their inner values, `{ path }` references read + * from the model by JSON-pointer path, arrays resolve recursively, and + * unrecognized plain values pass through unchanged. + * + * @example + * ```ts + * const model = { customer: { name: 'Ada' } }; + * resolveDynamic({ path: '/customer/name' }, model); // 'Ada' + * resolveDynamic({ literalString: 'Checkout' }, model); // 'Checkout' + * ``` + */ export function resolveDynamic( value: unknown, model: Record, diff --git a/libs/render/README.md b/libs/render/README.md index 5c2f815f2..bc723731b 100644 --- a/libs/render/README.md +++ b/libs/render/README.md @@ -17,17 +17,19 @@ ## What it does - Renders a JSON spec tree to Angular components via a named view registry (``) or a single node (``). -- Registry composition utilities (`views`, `withViews`, `withoutViews`) let you build, extend, and trim registries without mutation. +- Registry composition utilities (`views`, `withViews`, `overrideViews`, `withoutViews`) let you build, extend, replace, and trim registries without mutation. - Signal-based state store (`signalStateStore`) and per-component fallback support keep UI consistent during streaming. ## Install ```bash -npm install @threadplane/render +npm install @threadplane/render @json-render/core ``` **Peer dependencies:** `@angular/core ^20.0.0 || ^21.0.0`, `@angular/common ^20.0.0 || ^21.0.0`, `@json-render/core ^0.16.0` +`@json-render/core` supplies the spec types and evaluation engine that `@threadplane/render` adapts to Angular. + ## Quick start **1. Define your view registry and provide it.**