Skip to main content

Display

A2UISurface

Shows the UI an agent composed in A2UI, with Ouro HX. Every button is one of your app's declared actions: read and write run, a consequential one asks the person to confirm what the agent prepared, and a person-only one takes them to your app's own screen. Messages that fail core's validator show a short alert, and onProblems gets the details for the agent.

A shallow pan of golden orzo with peas and charred lemon halves

Charred Lemon Orzo

Orzo toasted in brown butter, finished with charred lemon, peas and a lot of parmesan. A weeknight pan dinner.

VegetarianOne pan
Time
25 min
Servings
4
Difficulty
Easy

Ingredients

1½ cups

orzo

3 tbsp

butter

2

lemons, halved

1 cup

frozen peas

¾ cup

grated parmesan

3 cups

vegetable stock

Press a button.

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.68

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/a2ui-surface#v1.3.68

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/a2ui-surface

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/a2ui-surface#v1.3.68 ourostack/ouro-ui/a2ui-surface-tests#v1.3.68
import { A2UISurface } from "@/components/ouro/a2ui-surface/a2ui-surface";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<A2UISurface
  catalog={recipeCatalog}
  messages={messages}
  onAction={(message) => callTool(message.action.name, message.action.context)}
  onHandoff={({ action }) => navigate(`/recipes/${id}?do=${action}`)}
/>

Use instead when

  • Use HX's components directly instead when your own code, not an agent, decides the layout.

Props

Type: A2UISurfaceProps.

catalog (required)
Type: unknown
The app's A2UI catalog, generated from its actions by core's ouro-a2ui-catalog.mts catalog.
className
Type: string
Layout only: margins, width (max-w-md).
label
Type: ((context: LabelContext) => string | undefined)
Names the ids in a confirm sheet's rows from the app's own data, such as a recipe's title for rcp_lemon_orzo. Return undefined to keep the name the agent prepared, or the id. The name wins over what the agent prepared, because the agent's text is the one thing the person can't check. Keep it stable, as messages is: a new function on each render resolves the messages again.
messages (required)
Type: readonly unknown[]
The agent's A2UI messages, in order. The surface shows where they leave it.
onAction (required)
Type: (message: A2UIActionMessage) => void | Promise<void>
Runs a pressed read or write button, and a consequential one once the person confirms. Send the message to the agent, or call the action's own tool with message.action.context. Return a promise to show the button loading until it settles.
onHandoff (required)
Type: (handoff: A2UIHandoff) => void
A person-only button never runs from composed UI: take the person to your app's own screen for it.
onProblems
Type: ((problems: readonly A2UIProblem[]) => void)
The validator's problems, when the messages fail it; the person sees a short alert instead. Hand them to the agent to correct.
surfaceId
Type: string
Default: the first one the messages create
Which surface to show.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • onPress: Not a prop. A pressed button calls onAction, or onHandoff for a person-only one: <A2UISurface onAction={send} onHandoff={openApp}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

A2UIActionMessage

The A2UI action message a pressed button sends: named after the action's id, with the prepared input as its context.

interface A2UIActionMessage {
  version: string;
  action: {
    /** The action's id, which is also its MCP tool's name. */
    name: string;
    surfaceId: string;
    sourceComponentId: string;
    timestamp: string;
    /** The button's label, as the person read it. */
    userMessage: string;
    /** The input the agent prepared, by field name. */
    context: Record<string, unknown>;
    /** That the person pressed it, its consent class, and for a consequential action that the person confirmed. */
    metadata: { extensions: { bot_ouroboros_action: { consent: string; by: "person"; confirmed?: true } } };
  };
}

A2UIHandoff

Where a person-only button takes the person: the action's id and the declaration's how.

interface A2UIHandoff {
  action: string;
  how: string;
}

A2UIProblem

A problem the validator found in the agent's messages, in A2UI's validation error format.

type A2UIProblem = Problem;