The catalog orrery/1
Edit on GitHubThe 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:
| Prop | Meaning |
|---|---|
key | A stable handle for the client's local state (the selected tab, open, the cursor). Without one, the node's path in the bound tree. |
weight | Inside a row or column: this node's share of the space. |
visible | A 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.
| Prop | Type | Meaning |
|---|---|---|
children | nodes | The contents: an array, or {"$data": …, "each": <node>}. |
gap | integer | Cells between children. |
wrap | bool | A row wraps onto the next line when narrow: what keeps 40 columns readable. |
maxRows | integer | The 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.
| Prop | Type | Meaning |
|---|---|---|
tabs | [{key, label, children}] | key unique among the tabs (two alike are refused). An array or an each. |
selected | string | The 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
| Prop | Type | Meaning |
|---|---|---|
value | string | The text. |
style | plain · muted · emphasis · code · error · success · warning | How 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.
link
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
| Prop | Type | Meaning |
|---|---|---|
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": [ … ]}. |
rowAction | string | What choosing a row sends. |
empty · loading | As 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
| Prop | Type | Meaning |
|---|---|---|
items | [{value, label, detail, disabled}] | detail is a dimmer second part; a disabled item cannot be chosen. |
style | bullet · number · select | |
action | string | What choosing sends; the value is the item's value. |
selected | string | The value the cursor starts on. |
empty · loading | As 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
| Prop | Type | Meaning |
|---|---|---|
fields | array | Each {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. |
submit | string | The submit row's label. |
action | string | What 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 unknowntype, a heading level outside 1–3, a neutral callout, two tabs with one key, a customkindwithout a namespace, an image or raster over its caps or withoutalt, a wrong prop type. - Text props take text. A number bound into
value,labelor akvvalue is refused: useformator${…}. stack(the old name) still parses, but is not one of the 32: sendroworcolumn.- A table row without a
keysends its index: chosen after the data reorders, it names the row now at that index. Give rows akey. - 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 freequestion. Buttons, lists, tables, trees, files, tabs and choices work. - A form's
patternis 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
selectchoice cycles in place with ← →: there is no open drop-down.align: centerdraws left in the TUI. Aquestion'sdeadline_msis not counted down by the client. - A
column'swrapdoes 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
tableorlist, or usecustomwith a fallback. - A client says which catalog it draws. A node it lacks falls back to the sender's fallback, then the text form.