Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b34e2e6
feat(ai-components): agent conversations, reply steps, approvals and …
oliverlaz Nov 3, 2026
ba27cf2
fix(ai-components): address review findings for agent conversations
oliverlaz Nov 3, 2026
a608769
feat(ai-components): show the UI an agent writes for a reply
oliverlaz Nov 3, 2026
af8624f
feat(a2ui): render A2UI replies on the official A2UI packages
oliverlaz Nov 3, 2026
9b2129d
fix(ai-components): show a reply's UI where no frame is painted
oliverlaz Nov 3, 2026
867f136
fix(a2ui): follow links by the host's policy, open Modals as dialogs
oliverlaz Nov 3, 2026
99f451d
feat(a2ui): draw the basic catalog's main parts as Stream's components
oliverlaz Nov 3, 2026
3ea354a
fix(ai-components): set agent replies in the SCR type and spacing scale
oliverlaz Nov 3, 2026
be4790a
fix(ai-components): type an agent's answer as fast as it arrives
oliverlaz Nov 3, 2026
45431d0
feat(ai-components): type ai_ui and ai_ui_action on Stream messages
oliverlaz Nov 3, 2026
3f49689
test(a2ui): render A2UI replies under StrictMode
oliverlaz Nov 3, 2026
8e5b5c6
feat(openui): render OpenUI replies on the official OpenUI packages
oliverlaz Nov 3, 2026
88d27db
feat(a2ui): write an agent's A2UI instructions and check its A2UI
oliverlaz Nov 3, 2026
f9799a1
Merge feat/ai-components-juno into feat/ai-components-generative-ui
oliverlaz Nov 3, 2026
187e3a6
fix(ai-components): show a reply's text when its generated UI can't show
oliverlaz Nov 3, 2026
8329238
feat(ai-components): honour A2UI accessibility and describe instructions
oliverlaz Nov 3, 2026
5c85d45
Merge release-v14 into feat/ai-components-generative-ui
oliverlaz Nov 4, 2026
ee97b69
chore(ai-components): take patch releases of the A2UI and OpenUI peers
oliverlaz Nov 4, 2026
3c42c09
fix(ai-components): keep a reply's generated UI under what covers it
oliverlaz Nov 4, 2026
6d04c2f
fix(a2ui): draw a borderless Button in the accent colour, as a link
oliverlaz Nov 4, 2026
4e81b88
fix(ai-components): contain failing generated UI and trim the public API
oliverlaz Nov 4, 2026
e5970df
feat(ai-components): draw A2UI and OpenUI replies as StreamChatOpenUI…
oliverlaz Nov 4, 2026
a0722d2
docs(ai-components): say which release generated UI needs
oliverlaz Nov 4, 2026
2626e32
fix(ai-components): draw only a Column's borderless buttons as follow…
oliverlaz Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Mock modules with `vi.mock('../../EmptyStateIndicator', () => ({ … }))`; use `
`yarn build` = `yarn clean` + 4 steps in parallel via `concurrently`, each writing to a separate `dist/` subdirectory:

1. **`build-translations`** — `i18next-cli extract` pulls `t()` calls from source into `src/i18n/*.json`
2. **`vite build`** — bundles 5 entry points as ESM (`dist/es/*.mjs`) + CJS (`dist/cjs/*.js`)
2. **`vite build`** — bundles 6 entry points as ESM (`dist/es/*.mjs`) + CJS (`dist/cjs/*.js`)
3. **`tsc -p tsconfig.lib.json`** — `.d.ts` only → `dist/types/`
4. **`build-styling`** — Sass → `dist/css/index.css`, `emoji-replacement.css`, `emoji-picker.css`, `channel-detail.css`, `ai-components.css`, plus `cp -r src/styling/assets dist/css/assets`

Expand All @@ -326,6 +326,8 @@ Mock modules with `vi.mock('../../EmptyStateIndicator', () => ({ … }))`; use `
| `stream-chat-react/channel-detail` | `src/plugins/ChannelDetail/` |
| `stream-chat-react/emojis` | `src/plugins/Emojis/` |
| `stream-chat-react/ai-components` | `src/plugins/AIComponents/` |
| `stream-chat-react/a2ui` | `src/plugins/A2UI/` |
| `stream-chat-react/openui` | `src/plugins/OpenUI/` |
| `stream-chat-react/mp3-encoder` | `src/plugins/encoders/mp3.ts` |
| `stream-chat-react/css/*` | `dist/css/*` |

Expand Down
44 changes: 42 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@
"require": "./dist/cjs/ai-components.js",
"default": "./dist/cjs/ai-components.js"
},
"./a2ui": {
"types": "./dist/types/plugins/A2UI/index.d.ts",
"import": "./dist/es/a2ui.mjs",
"require": "./dist/cjs/a2ui-require.js",
"default": "./dist/es/a2ui.mjs"
},
"./openui": {
"types": "./dist/types/plugins/OpenUI/index.d.ts",
"import": "./dist/es/openui.mjs",
"require": "./dist/cjs/openui.js",
"default": "./dist/es/openui.mjs"
},
"./dist/css/*": {
"default": "./dist/css/*"
},
Expand All @@ -66,6 +78,12 @@
],
"ai-components": [
"./dist/types/plugins/AIComponents/index.d.ts"
],
"a2ui": [
"./dist/types/plugins/A2UI/index.d.ts"
],
"openui": [
"./dist/types/plugins/OpenUI/index.d.ts"
]
}
},
Expand Down Expand Up @@ -116,27 +134,43 @@
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.1.0",
"use-sync-external-store": "^1.6.0",
"zod": "^4.1.12"
"zod": "^4.2.0"
},
"optionalDependencies": {
"@stream-io/transliterate": "^1.5.5"
},
"peerDependencies": {
"@a2ui/react": "~0.15.0",
"@a2ui/web_core": "~0.15.0",
"@breezystack/lamejs": "^1.2.7",
"@emoji-mart/data": "^1.1.0",
"@openuidev/react-lang": "~1.0.2",
"@openuidev/react-ui": "~0.18.1",
"emoji-mart": "^5.4.0",
"modern-normalize": "^3.0.1",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
"stream-chat": "^9.54.0"
},
"peerDependenciesMeta": {
"@a2ui/react": {
"optional": true
},
"@a2ui/web_core": {
"optional": true
},
"@breezystack/lamejs": {
"optional": true
},
"@emoji-mart/data": {
"optional": true
},
"@openuidev/react-lang": {
"optional": true
},
"@openuidev/react-ui": {
"optional": true
},
"emoji-mart": {
"optional": true
},
Expand All @@ -150,11 +184,16 @@
"README.md"
],
"devDependencies": {
"@a2ui/react": "0.15.0",
"@a2ui/web_core": "0.15.0",
"@breezystack/lamejs": "^1.2.7",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@emoji-mart/data": "^1.2.1",
"@eslint/js": "^9.39.4",
"@openuidev/react-headless": "0.18.1",
"@openuidev/react-lang": "1.0.2",
"@openuidev/react-ui": "0.18.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.8",
Expand Down Expand Up @@ -199,7 +238,8 @@
"typescript-eslint": "^8.59.4",
"vite": "^8.1.3",
"vitest": "^4.1.9",
"vitest-axe": "^0.1.0"
"vitest-axe": "^0.1.0",
"zustand": "4.5.7"
},
"scripts": {
"clean": "rm -rf dist",
Expand Down
8 changes: 8 additions & 0 deletions scripts/validate-cjs-node-bundle.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@

require('../dist/cjs/index.js');
require('../dist/cjs/ai-components.js');

// The A2UI renderer is an ES module only, and requiring it says so.
try {
require('stream-chat-react/a2ui');
throw new Error('stream-chat-react/a2ui loaded with require');
} catch (error) {
if (!/an ES module only/.test(error.message)) throw error;
}
116 changes: 116 additions & 0 deletions src/plugins/A2UI/A2UIInstructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// What an agent is told to write A2UI with, and what checks it, without the A2UI packages: they
// load when the instructions are asked for.
import type { AIUIComponent } from '../AIComponents/GenerativeUI/AIUIComponent';
import type { AIUIComponentDefinition } from '../AIComponents/GenerativeUI/AIUIComponentDefinition';
import { isRecord } from '../AIComponents/GenerativeUI/AIUIHelpers';
import { A2UI_VERSION } from './A2UIStream';

/**
* What an agent is told to write replies' UI in A2UI with: the basic catalog, or the part of it
* `keep` names, with the app's components, and what the app adds. The same as the
* `prompt.json` StreamChatA2UI's instructions are written from.
*/
export type A2UIInstructionsConfig = {
/** The id of the catalog with the app's components, as the renderer names it. */
catalogId?: string;
/** The app's components, or their portable definitions, such as a `UIComponents.json`. */
components?: (AIUIComponent | AIUIComponentDefinition)[];
/**
* Replies with their UI, as the agent should write them. Each block must render with the
* catalog, or no instructions are written: a broken example teaches the model broken UI.
*/
examples?: string[];
/** The info string of a reply's UI block, as the agents router reads it. */
fence?: string;
/** The basic catalog's components and functions to describe; all of them when absent. */
keep?: string[];
/** Guidance on using the app's components. */
notes?: string[];
/** What the model is told first, such as which answers to write as UI. */
preamble?: string;
/** Rules added after the protocol's. */
rules?: string[];
};

/** The A2UI an example holds that would not render, and why. */
export class A2UIInstructionsError extends Error {
readonly problems: string[];

constructor(problems: string[]) {
super(`An example's A2UI would not render:\n${problems.join('\n')}`);
this.name = 'A2UIInstructionsError';
this.problems = problems;
}
}

/** A description of the A2UI v1.0 messages and the rules that keep what they make valid. */
export const A2UI_PROTOCOL_OVERVIEW = `# Generating user interfaces with A2UI v1.0

You render user interfaces by emitting A2UI messages. Every message is a JSON object with a \`"version": "1.0"\` field plus exactly one of these keys:

- \`createSurface\`: creates a surface. Requires \`surfaceId\`, and accepts \`catalogId\`, \`sendDataModel\`, \`components\` and \`dataModel\` so a complete UI can be sent in one message.
- \`updateComponents\`: adds or replaces components of an existing surface. Requires \`surfaceId\` and \`components\`.
- \`updateDataModel\`: writes to the surface data model. Requires \`surfaceId\` and \`value\`, and accepts a JSON Pointer \`path\`. Setting \`value\` to \`null\` deletes the entry at \`path\`.
- \`deleteSurface\`: removes a surface. Requires \`surfaceId\`.
- \`callRendererFunction\`: asks the renderer to run one of its functions.
- \`agentFunctionResponse\`: answers a \`callAgentFunction\` request from the renderer.

Rules:

1. Components are a flat adjacency list. Each component has a unique \`id\`, a \`component\` type, and its properties inline. Exactly one component must have the id \`root\`. An id is an identifier: letters, digits and underscores, not starting with a digit, such as \`saturday_plan\` (never \`saturday-plan\`).
2. Children are referenced by id, never nested inline. Use an array of ids, or \`{"componentId": "...", "path": "/items"}\` to repeat a template for every item of a data model list.
3. Any property may be a literal, a data binding \`{"@path": "/a/b"}\` or a function call \`{"@call": "name", "args": {...}}\`. Inside a list template, paths without a leading slash resolve relative to the current item, and \`{"@call": "@index"}\` returns the item's position. A literal object key that starts with \`@\` is written with \`@@\`, so \`"@@type"\` means \`"@type"\`.
4. Put content that changes in the data model and bind to it, so you can update values with \`updateDataModel\` instead of resending components.
5. Interactive components declare an \`action\`, which either dispatches an event to you (\`{"event": {"name": "...", "context": {...}}}\`) or runs a renderer function (\`{"functionCall": {"@call": "openUrl", "args": {"url": "..."}}}\`).
6. Surface ids must be unique for the lifetime of the renderer. Creating a surface that already exists is an error: delete it first, or send \`updateComponents\` and \`updateDataModel\` to change it.`;

export const writing = (fence: string, catalogId: string) => `## Writing UI in A2UI

Write a reply's UI in one fenced block whose info string is \`${fence}\`, after one short sentence saying what it shows, which is what people read wherever the UI can't show. In the block, write A2UI messages, one JSON object per line, and nothing else:

1. First a \`createSurface\` with a \`surfaceId\` new to this reply, the \`catalogId\` \`${catalogId}\`, and the \`dataModel\` the UI's inputs start from.
2. Then \`updateComponents\` messages for that surface with a few components each, \`root\` first, so the UI shows as you write it.

A reply's UI is shown as you wrote it: never write \`callRendererFunction\` or \`agentFunctionResponse\`, and never change an earlier reply's UI, but write a new one. What the person does in it comes back as their next message. Give every event action a \`userMessage\`, the words of that message, such as "Update the plan", and a \`context\` with what you need to know, bound to the data model.`;

// A JSON value with its object keys sorted, so the same catalog always reads the same.
export const sorted = (value: unknown): unknown =>
Array.isArray(value)
? value.map(sorted)
: isRecord(value)
? Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, sorted(value[key])]),
)
: value;

export const fenceOf = (fence?: string) => fence ?? `ui:a2ui@${A2UI_VERSION}`;

// The A2UI blocks of an example reply.
export const blocksIn = (example: string, fence: string) =>
example
.split(`\`\`\`${fence}\n`)
.slice(1)
.map((block) => block.split('\n```')[0]);

/**
* The instructions that teach an agent's model to write A2UI with the basic catalog and the
* app's components, in Markdown, from the A2UI core's own catalog: the counterpart of
* StreamChatA2UI's `A2UIInstructions`, from the same configuration as its `prompt.json`. Give
* them to the agent's backend, as the part of its instructions about UI. It runs on a server or
* in a build too, and rejects with an `A2UIInstructionsError` when an example would not render.
*/
export const a2uiInstructions = async (config: A2UIInstructionsConfig = {}) =>
(await import('./instructions')).a2uiInstructionsText(config);

/**
* What is wrong with the A2UI messages of `text`, such as an agent's example, for the catalog
* `config` makes, or nothing when they render as written: messages the renderer can't read,
* components it doesn't have or that don't fit their schemas, and references that lead nowhere.
* The counterpart of StreamChatA2UI's `A2UIProgram.problems`.
*/
export const a2uiProblems = async (
text: string,
config: Pick<A2UIInstructionsConfig, 'catalogId' | 'components' | 'keep'> = {},
) => (await import('./instructions')).a2uiProblemsIn(text, config);
8 changes: 8 additions & 0 deletions src/plugins/A2UI/A2UIPlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';

/** What shows while a reply's A2UI loads, or while its first surface is being written. */
export const A2UIPlaceholder = () => (
<div aria-busy='true' className='str-chat__a2ui str-chat__a2ui--loading'>
<div className='str-chat__a2ui__placeholder' />
</div>
);
Loading
Loading