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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.68Then 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.68Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/a2ui-surfaceIts 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.68import { 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, asmessagesis: 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 callsonAction, oronHandofffor 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;
