Skip to main content

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"). Calling render_ui with 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):

TypeDescriptionKey fields
cardTitled panel with a black header bartitle, children
stackVertical or horizontal groupchildren, gap (sm/md/lg), direction
columnsSide-by-side columnschildren, ratios (list of flex weights)

Leaf types:

TypeDescriptionKey fields
textParagraph or headingcontent ⚠️, variant (body/heading/subheading/caption/code)
badgeStatus chiplabel ⚠️, color (default/success/warning/error/info)
buttonClickable actionlabel ⚠️, action ⚠️, variant (primary/secondary/danger), context, disabled
tableData gridcolumns (list of {key, label}), rows (list of dicts)
formInput formaction ⚠️, submit_label, fields (list of {name, label, type, required, placeholder, options, default})
listBullet or numbered listitems (list of strings), ordered
progressProgress barvalue (0–100), label
dividerHorizontal rule

⚠️ Common mistake: Do not use "text" as a field name — it is not valid for any component. Use:

  • "content" for text components
  • "label" for badge and button components

Always set "action" on buttons and forms so the agent can identify which interaction was triggered.

Targets

TargetBehaviour
"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

ShapeRenders as
Single question + options, no multi_selectInline button list
Multiple questions or multi_select=TruePaged form (one question per page)
No optionsFree-text textarea

QuestionItem Fields

FieldTypeDescription
questionstrThe question text
optionslist[str] | NoneSelectable choices
multi_selectboolAllow multiple selections (checkbox list)
allow_free_textboolAdd "Type something else…" as the last option
field_namestrResponse key (auto-slugged from question if omitted)
requiredboolMark 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

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.