# A2UI surface

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.

- Registry name: `a2ui-surface` (`@candor/a2ui-surface`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/a2ui-surface

## Install

```bash
npx candor-ui add a2ui-surface
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/a2ui-surface`.
It brings in: button, card-registry, dialog, input, locale, locked, tabs, textarea, use-copy, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `state: A2uiStore | A2uiState`: A store from createA2uiStore, or a state from applyA2uiMessages.
- `surfaceId: string`: Which surface to draw.
- `onAction?: A2uiActionHandler | undefined`: Called when a person uses a control with an event action.
- `allowImages?: boolean | undefined` (default `false`): Load https and data: images the agent points to. Off by default.
- `allowMedia?: boolean | undefined` (default `false`): Load https and data: video and audio the agent points to. Off by default.
- `className?: string | undefined`

## Functions

### parsePointer

```ts
parsePointer(pointer: string)
```

Splits a JSON Pointer ("/a/b~1c") into tokens; "/" and "" mean the whole model. Null when it isn't one.

### joinPath

```ts
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

```ts
getAtPath(data: unknown, pointer: string)
```

Reads the value at a JSON Pointer, or undefined.

### setAtPath

```ts
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

```ts
emptyA2uiState()
```

A state with no surfaces.

### applyA2uiMessage

```ts
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

```ts
applyA2uiMessages(state: A2uiState, messages: readonly unknown[])
```

Applies messages in order.

### setA2uiData

```ts
setA2uiData(state: A2uiState, surfaceId: string, path: string, value: unknown)
```

Writes a value into a surface's data model, as an input does.

### createA2uiStore

```ts
createA2uiStore(initial?: A2uiState | readonly unknown[])
```

Makes a store, optionally starting from a state or a list of messages.

### resolveA2uiValue

```ts
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

```ts
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 (`a2uiSurface`)

The fixed words, English by default. Override any of them under `a2uiSurface` in `<LocaleProvider strings={{ a2uiSurface: { … } }}>`; the table is exported as `a2uiSurfaceStrings`.

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

```ts
{ id: string; component: string; [prop: string]: unknown }
```

### A2uiSurfaceState

One surface: its components by id and its data model.

```ts
{ 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.

```ts
{ 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).

```ts
{ 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

```ts
(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.

```ts
{ 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 "/").

```ts
{ dataModel: unknown; base: string }
```

### A2uiCardProps

A2UI messages carried in a reply card: `{card: "a2ui", props: {messages: [...]}}`.

```ts
{ messages: unknown[]; surfaceId?: string | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
