Agora
Generative UI

Builtin components

The ready-made component definitions in @adonis-agora/agent/genui/builtins — BUILTIN_COMPONENTS (DataTable, Chart, KpiCards, …) and LAYOUT_COMPONENTS (Stack, Card, Text, …): what each is for, its props, its text fallback, and how to combine, pick or override them in your catalog.

@adonis-agora/agent/genui/builtins ships component definitions for the things most chat apps end up showing — a table, a chart, a few KPIs, sources, a checklist. Each one is an ordinary ComponentDefinition: a name, a title, a description the model reads, a JSON Schema for its props, and a fallbackText for channels that cannot draw it.

Builtins are definitions only — no visuals. The package does not ship a React DataTable or Chart. Your app renders each one with its own components, keyed by name in the client registry, exactly like any other component in your catalog. A builtin the client has no renderer for falls back to its fallbackText.

The two lists

The module exports two arrays plus every definition on its own:

ExportContentsUse
BUILTIN_COMPONENTSDataTable, Chart, KpiCards, SourceCards, Checklist, Timeline, CodeBlock, Diff, CalloutContent: data a model shows.
LAYOUT_COMPONENTSStack, Card, Heading, Text, Badge, Link, ImageLayout and text primitives for composing trees in tree mode, the default (Stack and Card take children).
DataTable, Chart, …Each definition individuallyPick only the ones you want.

Neither list is added for you: a catalog contains exactly what you pass to defineCatalog. Nothing is marked internal and none has a version, so every builtin is offered to the model and is stamped as version 1.

Using them in a catalog

Spread them next to your own components:

app/genui/catalog.ts
import { defineCatalog, defineComponent } from '@adonis-agora/agent/genui'
import { BUILTIN_COMPONENTS, LAYOUT_COMPONENTS } from '@adonis-agora/agent/genui/builtins'
import { z } from 'zod'

const OrderCard = defineComponent({
  name: 'OrderCard',
  title: 'Order card',
  description: 'One order at a glance.',
  props: z.object({ id: z.number(), status: z.string() }),
})

export const catalog = defineCatalog([...BUILTIN_COMPONENTS, ...LAYOUT_COMPONENTS, OrderCard])
config/agent.ts
import { genui } from '@adonis-agora/agent/genui'
import { catalog } from '#genui/catalog'

export default defineConfig({ model, genui: genui({ catalog }) })

In tree mode (the default) the builtins are listed, with a one-line props signature each, in the description of the single ui__render tool, Stack and Card marked "takes children", and its input schema carries each one's props schema below as a variant of the node union. A tree of one builtin ({ "type": "DataTable", "props": { … } }) is pushed as that component.

With mode: 'per-component', each standalone builtin becomes a tool named ui__show_<snake_case name> (DataTable → ui__show_data_table, KpiCards → ui__show_kpi_cards). Its description is Show the user a <title> (<name>). <description> and its input schema is the props schema below.

Stack and Card only make sense in a tree, where they wrap children: in per-component mode they get no tool (an empty container is nothing to show), and the leaf primitives (Heading, Text, Badge, Link, Image) each get their own.

Picking, excluding and overriding

There is no flag to switch builtins on or off — you choose what goes in the array:

import { defineCatalog } from '@adonis-agora/agent/genui'
import { BUILTIN_COMPONENTS, Callout, Chart, DataTable } from '@adonis-agora/agent/genui/builtins'

// Only some of them
const small = defineCatalog([DataTable, Chart, Callout])

// All but one
const noDiff = defineCatalog(BUILTIN_COMPONENTS.filter((c) => c.name !== 'Diff'))

defineCatalog rejects two definitions with the same name (genui: component "DataTable" is defined twice in one catalog). To replace a builtin with your own definition of the same name — a stricter schema, another description, a version — use catalog.extend, where a later definition replaces an earlier one:

import { defineCatalog, defineComponent } from '@adonis-agora/agent/genui'
import { BUILTIN_COMPONENTS, DataTable } from '@adonis-agora/agent/genui/builtins'

export const catalog = defineCatalog(BUILTIN_COMPONENTS).extend([
  defineComponent({
    ...DataTable,
    description: 'Orders, invoices or customers as a table. Always include the id column.',
    version: 2,
  }),
])

A definition is a plain object, so spreading it keeps the builtin's schema and fallbackText while you change the rest. A per-request resolver can do the same per tenant with catalog.extend(...).

While the model writes a tree. No builtin sets streaming, so each follows the tree's default (genui({ streaming }), 'complete' unless you choose 'partial' — see Drawing a tree while the model writes it). With 'partial', a builtin is drawn as its props arrive, its node flagged incomplete. To hold one back until its whole subtree has arrived — a Chart whose renderer cannot draw half a series — override it the same way:

export const catalog = defineCatalog([...BUILTIN_COMPONENTS, ...LAYOUT_COMPONENTS]).extend([
  { ...Chart, streaming: 'complete' }, // a placeholder until the chart's JSON has closed
])

Rendering them on the client

Register a renderer under each builtin's name, as for any component:

import type { GenuiRegistry } from '@adonis-agora/agent/react/genui'
import { DataTable, Chart, KpiCards, Callout } from '~/components/genui'

const registry: GenuiRegistry = { DataTable, Chart, KpiCards, Callout }

Each React component receives the validated props described below — except, with streaming: 'partial', while the model is still writing its node: then the props are whatever has arrived (a missing rows, a half-written last item), and useGenuiNode()?.incomplete is true, so default what you read and draw a skeleton until it fills in (Loading states and skeletons). Passing the same catalog to <AgentProvider genui={{ registry, catalog }}> validates props before drawing. If you send UI capabilities, declare only the builtins you actually render (at version 1, or your override's version); the rest are hidden from the model for that turn and streamed as their text fallback.

The table and chart factories from @adonis-agora/agent/genui (see Component registries and server rendering) produce presentations of the builtin DataTable and Chart, so a tool that presents with them needs those two definitions in its catalog and renderers registered under those names. The React server registry's default pagination splits DataTable rows across pages.

Content components — BUILTIN_COMPONENTS

Every props object is closed (additionalProperties: false): a prop not listed below is refused. "Row" means an object whose values are each a string, number, boolean or null.

DataTable

Tabular data (records with the same fields) as a sortable table; the model is told to prefer it over markdown tables for more than 3 rows.

PropTypeRequiredNotes
titlestringnoShort caption.
columns{ key, label, align?, format? }[]yes1–12 columns. key: field name in each row; label: header. align: 'left' | 'right' | 'center'. format: 'text' | 'number' | 'currency' | 'percent' | 'date' | 'link'.
rowsrow[]yesUp to 500 rows.

Fallback: the title in bold, then a fixed-width text table in a code block (columns capped at 28 characters, at most 15 rows, then … N more rows).

Chart

A bar chart to compare categories or a line chart for a trend over time.

PropTypeRequiredNotes
type'bar' | 'line'yes
titlestringno
xKeystringyesField of each point used for the x axis.
series{ key, label? }[]yes1–6 series; key names a numeric field of each point.
datarow[]yes1–200 points.
unitstringnoShown after values, e.g. KB or %.

Fallback: a text bar chart of the first series (up to 20 points), whatever the type.

KpiCards

A few key numbers (metrics, totals, counts) as cards.

PropTypeRequiredNotes
titlestringno
items{ label, value, delta?, trend?, hint? }[]yes1–8 items. label: string; value: string | number; delta: change, e.g. +12%; trend: 'up' | 'down' | 'flat'; hint: string.

Fallback: one bullet per item, • *label:* value (delta).

SourceCards

The documents, pages or links an answer is based on, as cards.

PropTypeRequiredNotes
titlestringno
items{ title, url?, snippet?, source? }[]yes1–12 items. source: where it came from, e.g. Wiki, GitHub.

Fallback: one bullet per item, the title linked to url (Slack <url\|title> syntax) followed by — source.

Checklist

Steps or a to-do list with done/not-done state.

PropTypeRequiredNotes
titlestringno
items{ label, done?, note? }[]yes1–50 items. done: boolean.

Fallback: ☑ / ☐ per item, with the note in italics.

Timeline

Events in chronological order — the history of a project, an incident, a deal.

PropTypeRequiredNotes
titlestringno
events{ date?, title, description? }[]yes1–50 events; date is a free-form string.

Fallback: one bullet per event, the date in bold.

CodeBlock

A code snippet or file excerpt with syntax highlighting.

PropTypeRequiredNotes
titlestringno
languagestringno
codestringyes

Fallback: the code in a code block.

Diff

A before/after change to a text or code file, as a line diff.

PropTypeRequiredNotes
titlestringno
languagestringno
beforestringyes
afterstringyes

The model sends both versions, not a patch. Fallback: a unified-style +/- line diff in a code block. The diff function is exported as lineDiff(before, after) (returning { op: 'same' | 'add' | 'del', text }[]), so a renderer can draw the same diff.

Callout

A short note, warning or success message, emphasized.

PropTypeRequiredNotes
tone'info' | 'success' | 'warning' | 'danger'no
titlestringno
textstringyes

Fallback: :warning: for warning/danger, :information_source: otherwise, then the title in bold and the text.

Layout components — LAYOUT_COMPONENTS

ComponentPurposeProps (required in bold)Children
StackLays its children out vertically, or horizontally with direction: 'row'.direction?: 'column' | 'row', gap?: numberyes
CardA box with an optional title around its children.title?: string, subtitle?: stringyes
HeadingA heading.text: stringno
TextA paragraph of plain text (no markdown).text: string, muted?: booleanno
BadgeA small label.text: string, tone?: 'neutral' | 'brand' | 'success' | 'warning' | 'danger'no
LinkA link to an http(s) URL.text: string, url: string (must match ^https?://)no
ImageAn image from an https URL.url: string (must match ^https://), alt?: stringno

Text fallbacks: Card prints its title in bold (and — subtitle), then its children's text; Heading prints bold text, Text the text, Badge [text], Link <url|text>, Image the URL. Stack — and a Card without a title — has an empty fallback, so the generic rendering takes over and prints its props as a JSON code block ({} for a bare Stack) before its children's text.

What the model sends

A single node takes the props under props — in tree mode ui__render with { "type": "KpiCards", "props": … }, or with mode: 'per-component' the props directly, as ui__show_kpi_cards:

{
  "title": "This month",
  "items": [
    { "label": "Revenue", "value": "$48.2k", "delta": "+12%", "trend": "up" },
    { "label": "New customers", "value": 132 }
  ]
}

The client renders your KpiCards component with those props; a text-only channel receives:

*This month*
• *Revenue:* $48.2k (+12%)
• *New customers:* 132

Composed, ui__render takes one nested element, mixing content and layout builtins:

{
  "type": "Card",
  "props": { "title": "Deploy 1.4.0" },
  "children": [
    { "type": "Callout", "props": { "tone": "warning", "text": "Migrations run before the switch-over." } },
    {
      "type": "Checklist",
      "props": {
        "items": [
          { "label": "Build image", "done": true },
          { "label": "Run migrations" }
        ]
      }
    }
  ]
}

It is pushed as one genui:tree component; giving children to a component that does not take them (Callout, Checklist, …) is refused and reaches the model as a tool error it can fix.

Using the text helpers

Besides the definitions, the module exports the helpers their fallbacks use:

  • textTable(columns, rows, max = 15) — the fixed-width code-block table DataTable falls back to.
  • lineDiff(before, after) — the LCS line diff behind Diff (inputs larger than 250,000 line pairs degrade to "all removed, all added").

See also

On this page

Partner

Your product as the official integration for Agent, right here.

Become a partner →