A2UI surface
ProDraws one A2UI surface with Candor parts. Pass a store (from createA2uiStore) to stream messages in, or a plain state to draw a finished one; either way inputs write to the surface's data model at their bound path. Event actions go to onAction with their context resolved; without it, buttons with actions say they aren't connected. Images, video and audio show as labelled placeholders unless the app opts in, and openUrl shows the link as text to copy instead of opening it.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/a2ui-surface
Brings in: button, card-registry, dialog, input, locale, locked, tabs, textarea, use-copy, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { A2uiSurface, A2UI_BASIC_CATALOG, applyA2uiMessages, emptyA2uiState } from "@/components/a2ui-surface"
// What an agent sends: a surface, its components as a flat list, then its data.
const state = applyA2uiMessages(emptyA2uiState(), [
{ version: "v0.9", createSurface: { surfaceId: "invite", catalogId: A2UI_BASIC_CATALOG } },
{
version: "v0.9",
updateComponents: {
surfaceId: "invite",
components: [
{ id: "root", component: "Card", child: "body" },
{ id: "body", component: "Column", children: ["title", "email", "send"] },
{ id: "title", component: "Text", variant: "h3", text: { call: "formatString", args: { value: "Invite someone to ${/team}" } } },
{ id: "email", component: "TextField", label: "Email", value: { path: "/email" } },
{ id: "send-label", component: "Text", text: "Send invite" },
{ id: "send", component: "Button", variant: "primary", child: "send-label", action: { event: { name: "invite", context: { email: { path: "/email" } } } } },
],
},
},
{ version: "v0.9", updateDataModel: { surfaceId: "invite", value: { team: "Platform", email: "" } } },
])
export default function Example() {
return <A2uiSurface className="max-w-sm" state={state} surfaceId="invite" onAction={(m) => console.log(m.action.name, m.action.context)} />
}<A2uiSurface>
| Prop | Type | Default | Description |
|---|---|---|---|
| staterequired | A2uiStore | A2uiState | — | A store from createA2uiStore, or a state from applyA2uiMessages. |
| surfaceIdrequired | string | — | Which surface to draw. |
| onAction | A2uiActionHandler | undefined | — | Called when a person uses a control with an event action. |
| allowImages | boolean | undefined | false | Load https and data: images the agent points to. Off by default. |
| allowMedia | boolean | undefined | false | Load https and data: video and audio the agent points to. Off by default. |
| className | string | undefined | — | Extra classes for the outer element. |
Functions
parsePointer(pointer: string)Splits a JSON Pointer ("/a/b~1c") into tokens; "/" and "" mean the whole model. Null when it isn't one.
joinPath(base: string, path: string)Joins a binding path to the scope it's read in: absolute paths stand alone, relative ones hang off the scope.
getAtPath(data: unknown, pointer: string)Reads the value at a JSON Pointer, or undefined.
setAtPath(data: unknown, pointer: string, value: unknown)Returns a copy of data with value set at the pointer (undefined removes it). Missing parents are made as objects.
emptyA2uiState()A state with no surfaces.
applyA2uiMessage(state: A2uiState, message: unknown)Applies one A2UI v0.9 server message to a state and returns the new state; the old one is never changed. Accepts the message as an object or a JSON line. A message that can't be applied leaves the surfaces as they were and adds a sentence to problems saying why.
applyA2uiMessages(state: A2uiState, messages: readonly unknown[])Applies messages in order.
setA2uiData(state: A2uiState, surfaceId: string, path: string, value: unknown)Writes a value into a surface's data model, as an input does.
createA2uiStore(initial?: A2uiState | readonly unknown[])Makes a store, optionally starting from a state or a list of messages.
resolveA2uiValue(value: unknown, scope: A2uiScope, depth = 0)Resolves a dynamic value: a literal stays as it is, {path} reads the data model (relative paths from the scope), and {call, args} runs a catalog function. Objects and lists are resolved all the way down.
createA2uiCard(options: { onAction?: A2uiActionHandler | undefined; allowImages?: boolean | undefined; allowMedia?: boolean | undefined } = {})Makes the "a2ui" card for a CardRegistryProvider, with your action handler and media choices: cards={{ ...standardCards, a2ui: createA2uiCard({ onAction }) }}.
Strings
The fixed words, in English by default. Change any of them under a2uiSurface in <LocaleProvider strings={{ … }}>; the table is exported as a2uiSurfaceStrings.
| Key | English |
|---|---|
| partFailed | Can’t show this part: the {type} failed while drawing. |
| partNotId | Can’t show this part: a child reference isn’t an id. |
| partContainsItself | Can’t show this part: it contains itself. |
| partUnknown | Can’t show this part (“{type}” isn’t in this app’s catalog). |
| invalidValue | This value isn’t valid. |
| image | Image |
| video | Video |
| audio | Audio |
| mediaNotShown | {kind} not shown. This app doesn’t load files an agent points to. |
| mediaNotShownLabel | {kind} not shown: {label}. This app doesn’t load files an agent points to. |
| mediaNotShownHost | {kind} not shown (from {host}). This app doesn’t load files an agent points to. |
| mediaNotShownLabelHost | {kind} not shown: {label} (from {host}). This app doesn’t load files an agent points to. |
| text | Text |
| checkbox | Checkbox |
| choose | Choose |
| value | Value |
| time | Time |
| date | Date |
| open | Open |
| details | Details |
| tab | Tab {n} |
| linkRequest | The agent’s view asked to open a link. Links from agents aren’t opened for you; copy it if you trust it. |
| copyLink | Copy link |
| copied | Copied |
| copyFailed | Couldn’t copy |
| dismiss | Dismiss |
| actionsOff | This app hasn’t connected actions for this view. |
| missing | Nothing to show yet: the agent hasn’t made this view. |
| waiting | Waiting for the agent’s layout… |
Types
- A2uiComponent
- One component in a surface's flat list: an id, a type from the catalog, and that type's properties.
{ id: string; component: string; [prop: string]: unknown }- A2uiSurfaceState
- One surface: its components by id and its data model.
{ surfaceId: string catalogId: string theme?: Record<string, unknown> | undefined /** When true, actions from this surface carry the whole data model. */ sendDataModel: boolean components: Record<string, A2uiComponent> dataModel: unknown }- A2uiState
- Every surface the agent has made, plus what couldn't be applied and why.
{ surfaces: Record<string, A2uiSurfaceState> /** Messages that were ignored, in plain words, newest last (at most 20). */ problems: string[] }- A2uiActionMessage
- What the app receives when a person uses a control with an event action (A2UI's client-to-server `action` message).
{ version: "v0.9" action: { name: string surfaceId: string sourceComponentId: string /** ISO 8601. */ timestamp: string /** The action's context with every binding resolved against the data model. */ context: Record<string, unknown> } }- A2uiActionHandler
(message: A2uiActionMessage, extra: { dataModel?: unknown }) => void- A2uiStore
- A small store around the processor, for apps that stream messages in and for inputs that write back.
{ getState: () => A2uiState /** Applies one message or a list of them. */ apply: (message: unknown) => void setData: (surfaceId: string, path: string, value: unknown) => void subscribe: (listener: () => void) => () => void }- A2uiScope
- Where a binding is read: the data model and the base path relative paths hang off (a template item, or "/").
{ dataModel: unknown; base: string }- A2uiCardProps
- A2UI messages carried in a reply card: `{card: "a2ui", props: {messages: [...]}}`.
{ messages: unknown[]; surfaceId?: string | undefined }