Canvas (A2UI)
The Canvas is a structured UI layer that lets the agent render interactive components — tables, forms, cards, buttons, badges — directly in the Suzent interface, alongside the chat.
The agent calls render_ui to push a surface (a component tree) to the frontend. The user can interact with it (click buttons, submit forms), and those interactions are sent back to the agent as messages, creating a conversational UI loop.
Concepts
Surface
A surface is a named, renderable UI panel. It has:
surface_id— a stable slug (e.g."search_results"). Callingrender_uiwith the same id replaces the surface in place (upsert semantics).component— the root of a component tree (see below).title— optional display name shown in the canvas tab strip.target— where to render:"canvas"(sidebar, default) or"inline"(inside the chat message).
Component Tree
Components are plain dicts with a "type" field. They nest recursively via "children" lists.
Container types (have children):
| Type | Description | Key fields |
|---|---|---|
card | Titled panel with a black header bar | title, children |
stack | Vertical or horizontal group | children, gap (sm/md/lg), direction |
columns | Side-by-side columns | children, ratios (list of flex weights) |
Leaf types:
| Type | Description | Key fields |
|---|---|---|
text | Paragraph or heading | content ⚠️, variant (body/heading/subheading/caption/code) |
badge | Status chip | label ⚠️, color (default/success/warning/error/info) |
button | Clickable action | label ⚠️, action ⚠️, variant (primary/secondary/danger), context, disabled |
table | Data grid | columns (list of {key, label}), rows (list of dicts) |
form | Input form | action ⚠️, submit_label, fields (list of {name, label, type, required, placeholder, options, default}) |
list | Bullet or numbered list | items (list of strings), ordered |
progress | Progress bar | value (0–100), label |
divider | Horizontal rule | — |
⚠️ Common mistake: Do not use
"text"as a field name — it is not valid for any component. Use:
"content"fortextcomponents"label"forbadgeandbuttoncomponentsAlways set
"action"on buttons and forms so the agent can identify which interaction was triggered.
Targets
| Target | Behaviour |
|---|---|
"canvas" | Renders in the sidebar Canvas tab. Persists across the session. Auto-opens the sidebar on first render. |
"inline" | Embeds the component tree directly inside the current chat message. Useful for compact, one-shot panels. Persists in message history. |
Interaction Callbacks
When the user interacts with a surface, the action is sent back to the agent as a user message:
[canvas: <action>] "<button_label>" # button click
[canvas: <action>] {"field": "value", ...} # form submit
The action string is whatever you set in the component's "action" field. Always give buttons and forms distinct, descriptive action names.
Usage
render_ui(
surface_id="results",
title="Search Results",
component={...},
target="canvas", # optional, default
)
Example — card with status badges and buttons
render_ui(
surface_id="status",
title="Analysis Status",
component={
"type": "card",
"title": "WorldReasoner Results",
"children": [
{"type": "text", "content": "Evaluation complete."},
{"type": "badge", "label": "92% Accuracy", "color": "success"},
{"type": "badge", "label": "High Confidence", "color": "info"},
{"type": "button", "label": "View Details", "action": "view_details"},
{"type": "button", "label": "Export CSV", "action": "export_csv", "variant": "secondary"},
],
}
)
Example — data table
render_ui(
surface_id="llm_scores",
title="LLM Benchmark Scores",
component={
"type": "table",
"columns": [
{"key": "model", "label": "Model"},
{"key": "score", "label": "Score"},
{"key": "category", "label": "Category"},
],
"rows": [
{"model": "Claude 3.5", "score": "92%", "category": "Reasoning"},
{"model": "GPT-4o", "score": "88%", "category": "Reasoning"},
],
}
)
Example — input form
render_ui(
surface_id="booking",
title="Book a Table",
component={
"type": "form",
"action": "confirm_booking",
"submit_label": "Confirm",
"fields": [
{"name": "date", "label": "Date", "type": "text", "required": True},
{"name": "guests", "label": "Guests", "type": "number"},
{"name": "notes", "label": "Notes", "type": "textarea"},
],
}
)
# When submitted, agent receives: [canvas: confirm_booking] {"date": "...", "guests": 2, "notes": "..."}
Example — inline quick-action panel
render_ui(
surface_id="quick_actions",
target="inline",
component={
"type": "stack",
"children": [
{"type": "text", "content": "Choose an action:", "variant": "subheading"},
{"type": "button", "label": "Run Deep Analysis", "action": "deep_analysis"},
{"type": "button", "label": "Skip", "action": "skip", "variant": "secondary"},
],
}
)
Example — multi-column layout
render_ui(
surface_id="dashboard",
title="Dashboard",
component={
"type": "columns",
"ratios": [2, 1],
"children": [
{
"type": "card",
"title": "Progress",
"children": [
{"type": "progress", "label": "Data collection", "value": 80},
{"type": "progress", "label": "Analysis", "value": 45},
],
},
{
"type": "stack",
"children": [
{"type": "badge", "label": "Running", "color": "warning"},
{"type": "button", "label": "Stop", "action": "stop_job", "variant": "danger"},
],
},
],
}
)
Ask Question Tool
For collecting user input during a task, prefer ask_question over render_ui. It blocks the agent until the user responds, then returns the answers as structured data.
ask_question(questions=[
QuestionItem(question="What's your goal?", options=["Build a feature", "Fix a bug", "Refactor"], required=True),
QuestionItem(question="Any additional context?"),
])
# Returns: User answered: {"what_s_your_goal": "Fix a bug", "any_additional_context": "..."}
Rendering Behaviour
| Shape | Renders as |
|---|---|
Single question + options, no multi_select | Inline button list |
Multiple questions or multi_select=True | Paged form (one question per page) |
No options | Free-text textarea |
QuestionItem Fields
| Field | Type | Description |
|---|---|---|
question | str | The question text |
options | list[str] | None | Selectable choices |
multi_select | bool | Allow multiple selections (checkbox list) |
allow_free_text | bool | Add "Type something else…" as the last option |
field_name | str | Response key (auto-slugged from question if omitted) |
required | bool | Mark the field required |
Paged Form UX
When rendered as a form, ask_question shows one question at a time with Back, Skip, and Next/Submit navigation. The user can skip optional questions; Next is only enabled once the current field has a value.
For select and multiselect fields with allow_free_text=True, a "Type something else…" item appears inline as the last option — clicking it expands a text input on the same page.
Callbacks
- Button click →
[canvas: choose_option] "<label>" - Form submit →
[canvas: submit_question] {"field": "value", ...}- Single-select →
string; multi-select →list[str]; textarea →string
- Single-select →
Canvas Persistence
Canvas surfaces are persisted to localStorage keyed by chat ID. They survive page reloads and are restored when switching back to a conversation.
Upsert Semantics
Calling render_ui with the same surface_id replaces the existing surface in place. There is no remove_ui — to clear a surface, replace it with an empty stack or stop sending updates.