The catalog orrery/1
Docs/Extend/The catalog orrery/1

The catalog orrery/1

Edit on GitHub
docs-site/extend/catalog.md

The catalog is every component type an extension, or the agent, can put in a structure (Show things: components). An extension composes these and never draws: a type the catalog lacks lives in a person's own client, sent as custom with a fallback.

Every node is {"type": "<id>", …props}, and may also carry:

PropMeaning
keyA stable handle for the client's local state (the selected tab, open, the cursor). Without one, the node's path in the bound tree.
weightInside a row or column: this node's share of the space.
visibleA condition. False drops the node.

Props are camelCase. Shapes reused from the older surface kinds keep their snake_case names (deadline_ms, a hunk's old_start), so old payloads still parse. Every prop can be a binding.

Text form is what orrery run -p --json, session/read, a stock AG-UI client and a client that draws nothing show: plain text, no colour. Keys are those a focused component takes in the TUI. F6 / Shift+F6 move focus between the prompt, panes and band; inside a component, Tab / Shift+Tab move between its controls (also ↑/↓ where the control does not use them), and Esc leaves typing, then dismisses the pane. h j k l work as arrows. A component never takes focus when it arrives.

empty and loading, on the data kinds marked below: with nothing to show, loading: true draws a loading state (text form loading...), else empty is drawn (else the plain form).

The 32 ids: row, column, box, tabs, collapsible, divider, text, heading, link, badge, callout, kv, code, markdown, table, list, tree, stream, chart, stat, task, button, form, textarea, question, progress, image, raster, files, diff, command, custom.

Layout

row · column

Children left to right, or top to bottom.

PropTypeMeaning
childrennodesThe contents: an array, or {"$data": …, "each": <node>}.
gapintegerCells between children.
wrapboolA row wraps onto the next line when narrow: what keeps 40 columns readable.
maxRowsintegerThe most lines it takes.

Text form: each child's, one per line. Keys: none of its own.

box

A titled border around children. Props: title, children. Text form: the title, then the children's.

tabs

Several views in one place. The selected tab is each client's own, never sent.

PropTypeMeaning
tabs[{key, label, children}]key unique among the tabs (two alike are refused). An array or an each.
selectedstringThe tab shown first, by key.

Text form: every tab's label, then its children. Keys: ← → switch tabs.

collapsible

A titled group that opens and closes, locally. Props: title (required), open (starts open), children. Text form: the title and the children. Keys: Enter or Space toggles.

divider

One line. Prop: label. Text form: ---, or --- <label> ---.

Text

text

PropTypeMeaning
valuestringThe text.
styleplain · muted · emphasis · code · error · success · warningHow it is meant, never a colour.
spans[{text, style}]Inline runs, replacing value. style: bold, dim, code, kbd, file (a file reference, opened only through the host), elapsed.

Text form: the value, or the spans joined.

heading

Props: text, level (1, 2 or 3; another is refused). Text form: the text.

Props: label, href. Text form: label (href). Keys: Enter sends open, which the host handles: it opens only an http(s):// URL or a path inside the workspace that the bound tree shows, and says what it opened (Links). It never reaches the owner.

badge

Props: text, tone (neutral, info, success, warning, error). Text form: [text].

callout

Props: tone (required: info, warning, error or success; neutral is refused, use a text), title, text. Text form: <tone>: <title> then the text, or <tone>: <text>.

kv

Key-value pairs that reflow at 40 columns. Props: items ([{label, value}], both strings), empty, loading. Text form: label: value per line.

code

Props: code, language, lineNumbers, start (the first line's number), highlight (line numbers). Text form: the code.

markdown

Props: value, complete (false while it streams; absent is complete). Text form: the source.

Data

table

PropTypeMeaning
columns["Title"] or [{title, align, priority}]align: left, right, center. Lower priority columns drop first when narrow; the first column never drops.
rows[[cell]]A cell is a string or {text, style}. An array, or {"$data": "/runs", "each": [ … ]}.
rowActionstringWhat choosing a row sends.
empty · loadingAs above.

A row is either [cell…] or {key, cells}. The value sent with rowAction is the row's key when it has one, so the owner knows which item was chosen even after the rows move; a row without a key sends its index.

Text form: the titles joined by |, then one line per row. Keys: ↑ ↓ move the cursor, Enter sends rowAction.

list

PropTypeMeaning
items[{value, label, detail, disabled}]detail is a dimmer second part; a disabled item cannot be chosen.
stylebullet · number · select
actionstringWhat choosing sends; the value is the item's value.
selectedstringThe value the cursor starts on.
empty · loadingAs above.

Text form: - label (detail) or 1. label per line. Keys: ↑ ↓ (skipping disabled items), Enter sends action.

tree

Props: nodes ([{label, id, expanded, children}]), action (the value is the node's id, else its label), empty, loading. Text form: the root labels. Keys: ↑ ↓ move, → opens, ← closes, Enter sends action.

stream

Log lines. Props: id (the channel), lines ([{text, level, ts}]: level debug/info/warn/error, ts unix ms), empty, loading. Text form: [level] text per line. Clients show ts as local time, HH:MM:SS.

chart

Props: kind (bars, columns, stacked, line, area, scatter, sparkline, donut, gauge), title, unit, series ([{label, values}]), labels, empty, loading. Text form: its values. See Surface kinds.

stat

The number a panel opens on. Props: label, value (a string: format a number), change (+12%, drawn with ▲ or ▼ so its direction survives no colour), tone, empty, loading. Text form: label: value (change).

task

A checklist or a vertical pipeline. Props: items ([{id, label, status}], status pending/running/done/failed/cancelled), style (list or pipeline), empty, loading. Text form: [Status] label per line.

Inputs

An input takes keys only while focused, and gives them back on Esc.

button

Props: label, action (required), tone (default, primary, danger), disabled. Text form: [label]. Keys: Enter or Space sends action, with a null value.

form

PropTypeMeaning
fieldsarrayEach {name, label, kind, required, default} plus the options below. kind is {"t": "text"}, secret, number, bool or {"t": "choice", "choices": [{value, label}]}. required must be written.
submitstringThe submit row's label.
actionstringWhat submitting sends; submit when absent.

Field options: placeholder, pattern (a regex-lite regex matched against the whole value: no look-around, backreferences or Unicode classes), error (shown under the field), min / max / step (number), multi (choice), style (select or radio for a choice, check or toggle for a bool).

The value sent is {name: value} for every field; numbers go as numbers, a multi choice as an array. A required field left empty stops the submit and moves to it.

Text form: N fields, submit <label>. Keys: ↑ ↓ between fields and the submit row; Enter or a character starts typing in a text field; ← → step a number or cycle a choice; Space or Enter toggles a bool; Enter on the submit row submits.

textarea

Props: name (the key its value goes under), label, placeholder, value, rows, action (required). The value sent is {<name>: text}. Text form: label: value. Keys: Enter or a character starts typing; while typing, Enter is a new line and the client's submit chord sends.

question

Confirm, or pick one. Props: prompt, choices ([{value, label}]), multi, free (an answer outside the choices), default, deadline_ms. Sends answer with the chosen value (an array when multi, the typed text with no choices). Text form: prompt [a, b]. Keys: ↑ ↓, Space picks in a multi, Enter answers.

progress

Props: label, done, total (no total is a spinner). Text form: label done/total, or the label.

Media

image

Props: mime, data (base64), alt (required), title, w, h. Text form: the alt. Size caps as Surface kinds.

raster

A grid of colours. Props: w, h (cells), cells (w*h colours 0xRRGGBB, row by row), labels ({rows, cols}), legend ([{colour, label}]), alt (required). Text form: the alt.

Agent and dev

files

Changed files. Props: files ([{path, status, add, del}], status added/modified/deleted/renamed), action (the value is the path), empty, loading. Text form: M path +3 -1 per line. Keys: ↑ ↓, Enter sends action.

diff

Props: path, hunks ([{old_start, old_lines, new_start, new_lines, lines: [{kind, text}]}], kind context/add/remove). Text form: path (N hunks).

command

Props: line, output, exit, status. Text form: $ line, the output, exit N.

custom

A type the catalog lacks, drawn only by a client that knows it. Props: kind (<ext>.<name>), props (anything), fallback (a catalog node). Text form: the fallback's, else its props as text.

Limits

  • Every refusal names the JSON path and the component, such as `heading` at `/children/0`: heading level 4 is not 1, 2 or 3. Refused: an unknown type, a heading level outside 1–3, a neutral callout, two tabs with one key, a custom kind without a namespace, an image or raster over its caps or without alt, a wrong prop type.
  • Text props take text. A number bound into value, label or a kv value is refused: use format or ${…}.
  • stack (the old name) still parses, but is not one of the 32: send row or column.
  • A table row without a key sends its index: chosen after the data reorders, it names the row now at that index. Give rows a key.
  • Typing in the TUI is not wired yet. The keys are defined, but ratatui does not yet pass characters or a submit chord to a focused textarea, form text field or free question. Buttons, lists, tables, trees, files, tabs and choices work.
  • A form's pattern is checked by every first-party client before it sends, with the one Rust check (decision 0095). The owner still checks what it receives: a third-party client may not check.
  • A select choice cycles in place with ← →: there is no open drop-down. align: center draws left in the TUI. A question's deadline_ms is not counted down by the client.
  • A column's wrap does nothing (a column already stacks).
  • Scroll, focus, the selected tab, open or closed, the cursor and text not yet submitted stay in each client and are never sent or shared between attached clients.
  • v1 only. Grid, menu, combobox, pagination, timeline, slider, date pickers and the rest wait for v2, each by its own decision. Until then, compute into a table or list, or use custom with a fallback.
  • A client says which catalog it draws. A node it lacks falls back to the sender's fallback, then the text form.