A2UI surface

Pro

Draws 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.

Markdown

Preview

Install

With the Candor CLI

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

TSX · The example above
"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>

PropTypeDefaultDescription
staterequiredA2uiStore | A2uiState—A store from createA2uiStore, or a state from applyA2uiMessages.
surfaceIdrequiredstring—Which surface to draw.
onActionA2uiActionHandler | undefined—Called when a person uses a control with an event action.
allowImagesboolean | undefinedfalseLoad https and data: images the agent points to. Off by default.
allowMediaboolean | undefinedfalseLoad https and data: video and audio the agent points to. Off by default.
classNamestring | 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.

KeyEnglish
partFailedCan’t show this part: the {type} failed while drawing.
partNotIdCan’t show this part: a child reference isn’t an id.
partContainsItselfCan’t show this part: it contains itself.
partUnknownCan’t show this part (“{type}” isn’t in this app’s catalog).
invalidValueThis value isn’t valid.
imageImage
videoVideo
audioAudio
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.
textText
checkboxCheckbox
chooseChoose
valueValue
timeTime
dateDate
openOpen
detailsDetails
tabTab {n}
linkRequestThe agent’s view asked to open a link. Links from agents aren’t opened for you; copy it if you trust it.
copyLinkCopy link
copiedCopied
copyFailedCouldn’t copy
dismissDismiss
actionsOffThis app hasn’t connected actions for this view.
missingNothing to show yet: the agent hasn’t made this view.
waitingWaiting 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 }