Slate catalog
What slate_catalog answers, and wsp slate catalog prints: the index with no name, and one entry for each piece, source and chapter named below. A slate is written in this kit with slate_write.
The index
Slate kit wsp/2, JSX-like text. Pieces (attributes, ! required, (a|b) the only values; <items>):
column: gap(tight|normal|loose) align; children
grid: columns! gap(tight|normal|loose) align; children
row: gap(tight|normal|loose) align wrap; children
section: title! note collapsible open align onChange; children
text: value! tone emphasis mono size(small|normal|large) lines placeholder
heading: value! level(title|section|label)
markdown: value!
number: label! value! format unit tone note size(normal|large) trend
meter: label! value! max note tone format(percent|value|fraction|tokens|bytes|none)
bars: label! items! key name! value! tone max format(percent|value|none)
chart: label! items! x value format unit tone height(small|normal|large) stack; <series label! value! tone>
diagram: value! label
timeline: label! items! name! start end state(done|running|failed|waiting) group now
treemap: label! items! name! value! group format
donut: label! items! name! value! format unit
image: src! caption
images: items! src! caption layout(strip|gallery|compare)
sparkline: label! values! tone
table: items! key empty rows; <col title! value! tone emphasis mono align width(fit|fill)> <action label! onPress>
checklist: items! key title! done! state(pending|working|done) note editable empty onChange
facts: layout(line|grid); <fact label! value! tone emphasis mono>
status: value! tone
chip: value!
ring: label! value! max format(percent|value|fraction) tone note
output: run! label lines wrap
button: label! variant(default|primary|quiet|danger) held note size(normal|small) onPress
input: label! value! lines kind(text|number|password) placeholder held mono submit onChange onSubmit
select: label! value! placeholder held onChange; <option value! label>
choices: label! value! answer held onChange; <option value! label note>
toggle: label! value! note held onChange
empty: title! body
Every piece: id, when={cond}; items take when too. tone: default muted good warning bad info accent. emphasis: normal strong quiet. format, where a line lists none: plain tokens bytes percent usd inr duration integer.
section, column, grid: pad none tight normal loose; surface="inset" sets the card ground; align start center end, else children fill the width.
bars compare categories; time is a chart, x in ms or ISO; shares a donut, sizes a treemap, spans on a clock a timeline; a flow is a diagram; a screenshot or plot is an image from its path on the thread's computer, or an https address the person allows.
Sources, read only:
thread: id title agent model status turns lastTurn cost tokens context changes plan waitingOn subagents[] ...
usage: account windows[] session week status note ...
cost: rateUsdPerHour accruedUsd
time: now today zone
git: branch head ahead behind changed ...
pr: number url state draft branch headSubject mergeable review checks[] word ...
Declarations: <value name start> <secret name> <derived name value> <run name cmd env args stdin on timeout every always once confirm then tool resource> <file name> <when change={$path} or done={$run} do={steps}>
Steps: set toggle start cancel send steer queue fill open copy pane. Functions: percent pct tokens bytes usd number duration ago until date time weekday plural word upper lower trim short round floor ceil abs min max clamp str num bool concat json lines split replace contains startsWith endsWith orElse exists isNull coalesce if len count sum avg first last at append pluck join. ago(t) "30s ago", until(t) "in 4m".
After |: where sortBy groupBy take skip count sum min max avg first last pick map distinct flatten join format. $run reads state exit out err json; a secret only .set .len.
Rules:
1. One <slate>, one root piece, declarations beside it. Give an id to a piece you will change.
2. prop="text" is literal; prop={formula} reads live data or $values with operators, a ? b : c, [lists], {records} and functions, never methods or =>. A text child with {holes} fills a sentence.
3. Props are meaning, never style. held is a sentence that disables: bind it to a condition; "" or false enables.
4. A list binds to items; item and index read the row.
5. A press reaches you only through send("literal text", $path); <when> chains run without you.
6. Small edit: patch by id, <props id="price" value={$spot.json.v} />; bigger: resend the whole slate with if_version.
7. Simple and airy unless asked for more: few pieces, one idea per section, short labels, no emoji. Separate things by layout, never by ·, • or |.
8. One heading per section: its title or a heading, in sentence case, never bold text.
9. Status and last-checked lines small and muted, beside their subject.
10. Actions at the end of their row; one primary per section.
11. mono only for figures, ids, times and paths.
12. Every live number says its window and unit. Show a run's json fields, never its raw out; times through date(), time() or ago().
13. Nothing centered but a lone figure or card.
14. Use the slate tools, never wsp from a shell.
15. A secret the person types goes in a <secret> input, never a file or the chat. One in a file stays there for the run to read, never you.
16. A button asked for where the project has no UI goes here: the slate is its UI, and the reply says so.
<slate title="Issue">
<value name="id" start="" />
<run name="check" cmd='gh api "repos/{owner}/{repo}/issues/$ID"' env={{ ID: $id }} />
<when change={$id} do={start($check)} />
<column>
<input label="Issue" value={$id} />
<text when={$check.exit == 0}>{$check.json.title}</text>
<button label="Fix it" onPress={send("Fix this issue.", $id)} />
</column>
</slate>
More: slate_catalog <piece, source, runs, patch, functions, steps, handlers or examples>.
Pieces
column
column (core): Stacks children top to bottom.
gap: tight|normal|loose
align: start|center|end|stretch
pad: none|tight|normal|loose
surface: plain|inset
events: none; holds children
sketch: [column-2 column gap=tight]
example: <column gap="tight"><text>One</text><text>Two</text></column>
grid
grid (core): Children in 2 to 4 equal columns, one column under 360 px.
columns: integer, required, 2 to 4
gap: tight|normal|loose
align: start|center|end
pad: none|tight|normal|loose
surface: plain|inset
events: none; holds children
sketch: grid of 3 [grid-1 grid]
example: <grid columns={3}><number label="Open" value={3} /><number label="Done" value={9} /><number label="Failed" value={0} /></grid>
row
row (core): Places children side by side.
gap: tight|normal|loose
align: start|center|end|between
wrap: boolean
events: none; holds children
sketch: [row-1 row align=between]
example: <row align="between"><text>Left</text><text>Right</text></row>
section
section (core): A titled group, optionally collapsible.
title: string, required, formula
note: string, formula
collapsible: boolean
open: boolean, two-way $value or a literal start; open={false} starts it shut; open={$open} also keeps it
align: start|center|end
pad: none|tight|normal|loose
surface: plain|inset
events: onChange; holds children
sketch: Files changed [section-1 section collapsible]
example: <section title="Files changed" collapsible><text>None yet</text></section>
text
text (core): A line or a paragraph; a text child with {holes} fills a sentence.
value: text or number, required, formula
tone: tone, formula
emphasis: normal|strong|quiet, formula
mono: boolean
size: small|normal|large
lines: integer, 1 to 20
placeholder: string, formula
events: none; a text child fills value
sketch: {tokens(thread.context.free)} free [text-1 text muted]
example: <text tone="muted">{tokens(thread.context.free)} free</text>
heading
heading (core): A heading on the app's type ladder: title, section, or label in small caps.
value: text or number, required, formula
level: title|section|label
events: none; a text child fills value
sketch: # Gold price [heading-1 heading]
example: <heading level="title">Gold price</heading>
markdown
markdown (core): Rich text, sanitised; images are not fetched.
value: string, required, formula
events: none; a text child fills value
sketch: **Why.** The host keeps the last 5,000 events. [markdown-1 markdown]
example: <markdown>**Why.** The host keeps the last 5,000 events.</markdown>
number
number (core): A figure with a label.
label: string, required, formula
value: text or number, required, formula
format: plain|tokens|bytes|percent|usd|inr|duration|integer
unit: string, formula
tone: tone, formula
note: string, formula
size: normal|large
trend: list, formula; read in the sketch alone; the panel's stat cell draws its figure alone, so a trend the person sees is a sparkline beside it
events: none
sketch: Spent {thread.cost.usd} [number-1 number]
example: <number label="Spent" value={thread.cost.usd} format="usd" />
meter
meter (core): One value against a maximum.
label: string, required, formula
value: number, required, formula
max: number, formula
note: string, formula
tone: default|good|warning|bad, formula
format: percent|value|fraction|tokens|bytes|none
events: none
sketch: Weekly [{usage.week.percent} of 100] resets {until(usage.week.resetsAt)} [week meter]
example: <meter id="week" label="Weekly" value={usage.week.percent} note={`resets ${until(usage.week.resetsAt)}`} />
bars
bars (core): Categories compared as rows of name, track and figure; never a series over time.
label: string, required, formula
items: list, required, formula
key: any, per row
name: string, required, per row
value: number, required, per row
tone: tone, per row
max: number, formula
format: percent|value|none
events: none
sketch: Busiest [bars-1 bars]
example: <bars label="Busiest" items={processes.list | take(5)} name={item.name} value={item.cpu} />
chart
chart (core): Lines over a list, oldest first; stack piles the <series>.
label: string, required, formula
items: list, required, formula
x: any, per row; a time on x (ISO or ms) labels the axis by clock, in this computer's time zone; a number by its value
value: number, per row
format: plain|tokens|bytes|percent|usd|inr|duration|integer
unit: string, formula
tone: tone, formula
height: small|normal|large
stack: boolean
<series> label! value! tone when (item and index read the row)
events: none
sketch: Gold line of {$hist}, x {item.at} [chart-1 chart]
example: <chart label="Gold" items={$hist} x={item.at} value={item.v} />
diagram
diagram (core): A Mermaid diagram: a flow, a sequence or a state machine; value={`...${$step}`} shows the live step.
value: string, required, formula; Mermaid source; a text child is taken as written, braces and all. Labels a few words, decisions short questions; detail goes in a text beside it. Under about 15 nodes; past that, split it or summarize
label: string, formula
events: none; a text child fills value
sketch: Deploy flowchart LR (+3 lines) [diagram-1 diagram]
example: <diagram label="Deploy" value={`flowchart LR
build --> test --> ship
classDef now stroke-width:3px
class ${$step} now`} />
timeline
timeline (core): Spans on a clock, Gantt style, such as a run's steps or a deploy's phases.
label: string, required, formula
items: list, required, formula
name: string, required, per row
start: any, per row; a time, ISO or ms; none waits
end: any, per row; none while it runs
state: done|running|failed|waiting, per row
group: string, per row; rows of one group sit under its name
now: any, formula; where a running span ends; the clock if unset
events: none
sketch: CI run spans of {$steps} [timeline-1 timeline]
example: <timeline label="CI run" items={$steps} name={item.name} start={item.started} end={item.ended} group={item.job} />
treemap
treemap (core): Sizes of one whole's parts as nested boxes, such as a bundle by module within package.
label: string, required, formula
items: list, required, formula
name: string, required, per row
value: number, required, per row
group: string, per row
format: plain|tokens|bytes|percent|usd|inr|duration|integer
events: none
sketch: Bundle boxes of {$stats.json.modules} [treemap-1 treemap format=bytes]
example: <treemap label="Bundle" items={$stats.json.modules} name={item.name} value={item.bytes} group={item.package} format="bytes" />
donut
donut (core): Shares of one whole, five parts at most with the rest as Other; never a series over time.
label: string, required, formula
items: list, required, formula
name: string, required, per row
value: number, required, per row
format: plain|tokens|bytes|percent|usd|inr|duration|integer
unit: string, formula
events: none
sketch: By country shares of {$countries} [donut-1 donut format=integer]
example: <donut label="By country" items={$countries} name={item.name} value={item.n} format="integer" />
image
image (core): A picture from a file on the thread's computer, such as a screenshot or a saved plot, or from an https address once the person allows its domain; opens large to zoom.
src: string, required, formula; a path on the thread's computer, relative to its folder or whole, or an http(s) address; PNG, JPEG, GIF or WebP up to 10 MB
caption: string, formula
events: none
sketch: [image /tmp/login-failed.png] Login, after the redirect [image-1 image]
example: <image src="/docs/tmp/login-failed.png" caption="Login, after the redirect" />
images
images (core): Several pictures, as image takes them: a row that scrolls, a grid, or a before and after under a slider.
items: list, required, formula
src: string, required, per row
caption: string, per row
layout: strip|gallery|compare
events: none
sketch: compare of images [images-1 images]
example: <images layout="compare" items={[{ p: "shots/before.png", c: "Before" }, { p: "shots/after.png", c: "After" }]} src={item.p} caption={item.c} />
sparkline
sparkline (core): A small line beside text, the way to show a figure's trend.
label: string, required, formula
values: list, required, formula
tone: tone, formula
events: none
sketch: Load no points yet [sparkline-1 sparkline]
example: <sparkline label="Load" values={pluck($hist, 'v')} />
table
table (core): Rows with columns from a bound list: <col> per column, <action> per row button.
items: list, required, formula
key: any, per row
empty: string, formula
rows: integer, 1 to 5000
<col> title! value! tone emphasis mono align width when (item and index read the row)
<action> label! when onPress (item and index read the row, when too)
events: none
sketch: | Check | [checks table]
example: <table id="checks" items={pr.checks} key={item.name}><col title="Check" value={item.name} /><action label="Fix" when={item.state == 'fail'} onPress={send("Fix this check.", item.name)} /></table>
checklist
checklist (core): Rows with a tick; editable writes the tick back to the value.
items: list, required, formula
key: any, per row
title: string, required, per row
done: boolean, required, per row
state: pending|working|done, per row
note: string, per row
editable: boolean
empty: string, formula
events: onChange
sketch: rows of {$steps} [checklist-1 checklist editable]
example: <checklist items={$steps} title={item.title} done={item.done} editable />
facts
facts (core): Label and value pairs: <fact> per pair.
layout: line|grid
<fact> label! value! tone emphasis mono when
events: none
sketch: State: {pr.word} Review: {word(pr.review)} [facts-1 facts]
example: <facts><fact label="State" value={pr.word} /><fact label="Review" value={word(pr.review)} /></facts>
status
status (core): A dot in a tone with a word: Live, Down, Waiting.
value: text or number, required, formula
tone: default|muted|good|warning|bad|info, formula
events: none; a text child fills value
sketch: ({$up ? 'good' : 'bad'}) {$up ? 'Live' : 'Down'} [status-1 status]
example: <status tone={$up ? 'good' : 'bad'}>{$up ? 'Live' : 'Down'}</status>
chip
chip (core): A short fact in a small framed chip.
value: text or number, required, formula
events: none; a text child fills value
sketch: [{git.branch}] [chip-1 chip]
example: <chip>{git.branch}</chip>
ring
ring (core): Progress as a ring with its figure inside.
label: string, required, formula
value: number, required, formula
max: number, formula
format: percent|value|fraction
tone: default|good|warning|bad, formula
note: string, formula
events: none
sketch: Done ({$done}) [ring-1 ring format=fraction]
example: <ring label="Done" value={$done} max={len($steps)} format="fraction" />
output
output (core): A run's output as it streams, with its state and a Cancel.
run: $run, required
label: string, formula
lines: integer, 3 to 40; how many of the newest lines show, out then err, 8 by default
wrap: boolean
events: none
sketch: output $tests: not run yet [output-1 output lines=12]
example: <output run={$tests} lines={12} />
button
button (core): An action the person presses. held is a sentence that disables it: bind it to a condition.
label: string, required, formula
variant: default|primary|quiet|danger
held: string, formula
note: string, formula
size: normal|small
events: onPress; a text child fills label
sketch: [ Next ] (held: {$ok ? null : 'Check the id first'}) [button-1 button primary]
example: <button label="Next" variant="primary" held={$ok ? null : 'Check the id first'} onPress={set($step, 2)} />
input
input (core): Text in, written to its $value as the person types; a secret's input is a password field.
label: string, required, formula
value: text or number, required, two-way $value
lines: integer, 1 to 20
kind: text|number|password
placeholder: string, formula
held: string, formula
mono: boolean
submit: string, formula
events: onChange, onSubmit
sketch: Issue number: "{$id}" [input-1 input mono]
example: <input label="Issue number" value={$id} mono />
select
select (core): One choice from options, written to its $value.
label: string, required, formula
value: string, required, two-way $value
placeholder: string, formula
held: string, formula
<option> value! label when
events: onChange
sketch: Hide: {$hide} (of Done, Canceled) [select-1 select]
example: <select label="Hide" value={$hide}><option value="Done" /><option value="Canceled" /></select>
choices
choices (core): Large options to pick one, written to its $value; answer marks the right one once picked.
label: string, required, formula
value: any, required, two-way $value
answer: any, formula
held: string, formula
<option> value! label note when
events: onChange
sketch: Which layer routes packets?: {$pick} [choices-1 choices]
example: <choices label="Which layer routes packets?" value={$pick} options={$q.options} answer={$q.answer} />
toggle
toggle (core): An on/off switch written to its boolean $value.
label: string, required, formula
value: boolean, required, two-way $value
note: string, formula
held: string, formula
events: onChange
sketch: Show done: off [toggle-1 toggle]
example: <toggle label="Show done" value={$showDone} />
empty
empty (core): An empty state, with at most one button under it.
title: string, required, formula
body: string, formula
events: none; a text child fills body; holds children
sketch: No pull request yet. This branch has none. [empty-1 empty]
example: <empty when={pr.number == null} title="No pull request yet">This branch has none.</empty>
Sources
thread
thread (core): This thread: status, last turn, context, cost, tokens, changes, plan.
thread.turns: number
thread.id thread.title thread.agent thread.model thread.effort thread.access thread.computer thread.project thread.folder thread.waitingOn: string
thread.status: starting|working|needs-you|failed|done|resting
thread.lastTurn: { status, durationMs, waitedMs, startedAt, endedAt, model }
thread.cost: { usd }
thread.tokens: { input, output, cached, cacheWrite, reasoning }
thread.context: { used, window, free, percent }
thread.changes: { files: { path, kind, additions, deletions }[], from, to, moved: any[], shared }
thread.plan: { steps: { text, state: pending|working|done }[], text }
thread.subagents: { id, title, state, startedAt, endedAt }[]
thread.messages: { recent: { who, at, text }[], last }
update: push; scope: the slate's own thread; cost: none beyond today
note thread.context: the latest turn that reports it; null on an agent that does not
note thread.changes.files: the latest session.changes
example: {percent(thread.context.used / thread.context.window)}
tree
tree (working): The thread's root and every thread under it.
tree.root: { id, title, status, agent, computer }
tree.parent: { id, title, status, agent, computer }
tree.children: { id, title, status, agent, computer, costUsd, startedAt, endedAt }[]
tree.all: { id, title, status, agent, computer, depth }[]
tree.counts: { working, needsYou, done, failed, resting }
update: push; scope: the thread's tree; cost: none beyond today
example: {tree.counts.working}
usage
usage (core): The plan windows of the account the thread runs on.
usage.readAt: time
usage.note: string
usage.account: { key, label, agent, plan, address, computers: string[] }
usage.windows: { kind: session|week|week_opus|week_sonnet|month|overage, usedPercent, resetsAt }[]
usage.session: { percent, resetsAt }
usage.week: { percent, resetsAt }
usage.status: ok|warning|reached
usage.burn: { tokensPerMinute, threads }
usage.credits: { count, nextExpiresAt }
update: push; scope: the account the thread runs on; cost: one event per turn end
note usage.windows: Claude Code and Codex report them; a keyed sign-in has none and usage.note says why
example: {pct(usage.week.percent)}
cost
cost (core): What the thread's computer costs: the rate now and what it has run up.
cost.rateUsdPerHour cost.accruedUsd: number
update: push; scope: the computer the thread runs on; cost: none beyond today
note cost.rateUsdPerHour: 0 on this computer, which usd() reads as free
example: {usd(cost.accruedUsd)}
time
time (core): The clock, so a countdown or an age moves on its own.
time.today time.zone: string
time.now: number
update: tick; scope: none; cost: one re-evaluation a tick
note time.now: ms epoch; ticks each second while a visible piece reads it
example: {until(usage.week.resetsAt)}
git
git (core): The thread's checkout: branch, head, counts against upstream.
git.readAt: time
git.editsUnread git.countsUnknown: boolean
git.ahead git.behind git.changed git.stashes: number
git.branch git.head: string
git.status: { entries: { xy, path, origPath }[] }
update: push; scope: the thread's folder; cost: none for the checkout; status entries poll 15 s while bound
note git.branch: null on a folder that is not a checkout
example: {git.branch}
pr
pr (core): The pull request of the thread's branch and its checks.
pr.readAt: time
pr.draft: boolean
pr.url pr.base pr.branch pr.headOid pr.headSubject pr.author pr.word pr.unread: string
pr.number pr.additions pr.deletions pr.changedFiles pr.commits pr.behindBase: number
pr.state: open|merged|closed
pr.mergeable: mergeable|conflicting|unknown
pr.review: none|approved|changes_asked|required
pr.checks: { name, workflow, state: pass|fail|pending|skipped|cancelled, link, description, startedAt, completedAt }[]
update: push; scope: the thread's folder; cost: one gh read every 3 min; every 30 s while a bound check is pending
note pr.number: null while the branch has no pull request
note pr.headSubject: the head commit's subject; there is no pr.title
example: {pr.checks | where(item.state == 'fail') | count}
machine
machine (working): The thread's computer: load, memory, disk and their last minute.
machine.at: time
machine.cpu machine.load1: number
machine.name: string
machine.mem: { used, total }
machine.disk: { used, total }
machine.history: { cpu: { x, y }[], load1: { x, y }[], mem: { x, y }[] }
update: subscription; scope: the thread's computer; cost: one sample a second while bound
example: {bytes(machine.mem.used)}
processes
processes (working): The thread's computer's processes.
processes.at: time
processes.total: number
processes.list: { pid, name, cpu, mem, cmd }[]
update: subscription; scope: the thread's computer; cost: one snapshot every 2 s while bound
example: {processes.list | sortBy(item.cpu, 'desc') | take(5)}
ports
ports (working): Ports open on the thread's computer.
ports.list: { port, process, url }[]
update: push; scope: the thread's computer; cost: none beyond today
example: {ports.list}
notifications
notifications (working): The last 20 notices for this thread and its account.
notifications.recent: { kind, text, at }[]
update: push; scope: this thread and its account; cost: none beyond today
example: {notifications.recent | first}
mcp
mcp (working): The thread's agent's MCP servers: consent state, tools, resources.
mcp.<server>: { state: unasked|asked|allowed|refused|down, tools: { name, title, description, inputSchema: any, annotations: any, ui }[], resources: { uri, name, mimeType, description }[] }
update: push; scope: servers configured for the thread's agent; cost: a listing on connect
example: {mcp.linear.state}
Chapters
runs
runs: a command the slate starts with no turn of yours.
<run name="check" cmd='gh api "repos/$REPO/issues/$ID"' env={{ REPO: $repo, ID: $id }} every={60} />
<run name="py">{`python3 -c "print('both quotes')"`}</run>
Each attribute:
name: the run is read as $name.
cmd: the literal command, run by bash -c in the thread's folder. Single quotes outside double ones; the block form above takes any text.
env={{ ID: $id }}: values it reads as $ID. args={[$a]}: as $1. stdin={$x}: on standard input. A secret goes only in env or stdin; one in a file, the command reads itself, never through you.
every={60}: starts it again every 60 seconds, at least 10, while the Slate tab is on screen; at once when it comes back.
always: with every, ticks while the tab is not on screen too.
once: a start while it still runs is skipped; without once it stops and starts again.
stream: fills lines as it prints, the last 500, so an output piece shows them live.
timeout={60}: seconds before it is stopped, at most 600.
on="host": runs on the host's computer, not the thread's.
confirm="Stop it?": asks the person every start; a formula works too.
then='python3 x.py': pipes its raw result to that command; its stdout as JSON becomes json, out stays raw.
tool="server.tool" args={{ id: $id }}, or resource="server:uri": calls an MCP tool instead; slate_catalog <server> lists its tools.
start($run) in a handler or <when> starts one. The person allows each command once, on the slate, when it first wants to start: at the write for an every= run, at the first press or <when> for the rest. Until then it reads held; allowing it starts it at once. slate_state start runs only what they allowed "Always in this thread".
$run reads state why exit out err json lines startedAt endedAt ms runs cut stale refreshing text; state is idle (never started) held running done failed cancelled.
json is out parsed as JSON. Before a first result or after a failure, out and json are empty and len of them 0: show a figure with when={$run.exit == 0}. stale: the command changed since this result. Output is scrubbed of secrets.
<value> state and each run's last result outlive an app or host restart.
Chain: <when done={$check} do={set($ok, $check.exit == 0)} />; done fires on any outcome.
Code only the slate uses goes in <file name="x.py"> and runs as $SLATE_DIR/x.py; project code runs where it is.
A tool's json is its structured result, else its text parsed; out is its text; one marked text only keeps json empty unless its text is JSON. The person allows a server once per thread, a destructive tool every start. A secret in args returns as [secret:name].
functions
Functions (pure; a null argument gives null unless the function takes one). Times read in this computer's zone: date(t) "Mon 5 Oct", time(t) "14:05", ago(t) "30s ago", until(t) "in 4m".
percent(fraction, places?): percent(thread.context.used / thread.context.window)
pct(points, places?): pct(usage.week.percent)
tokens(n): tokens(thread.context.free)
bytes(n)
usd(n, places?): usd(thread.cost.usd)
number(x, places?)
duration(ms, style?): duration(thread.lastTurn.durationMs)
ago(t): ago(pr.readAt)
until(t): until(usage.week.resetsAt)
date(t, style?): date(usage.week.resetsAt)
time(t)
weekday(t)
plural(n, one, many?): plural(len(pr.checks), 'check')
word(x): word(item.state)
upper(s)
lower(s)
trim(s)
short(s, n): short(pr.headSubject, 40)
round(x, places?)
floor(x)
ceil(x)
abs(x)
min(a, b, ...) or min(list)
max(a, b, ...) or max(list)
clamp(x, lo, hi)
str(x)
num(x): num(item.completedAt)
bool(x)
concat(a, b, ...)
json(s): json($check.out).title
lines(s)
split(s, sep)
replace(s, from, to)
contains(list or text, value): contains(pluck(pr.checks, 'state'), 'fail')
startsWith(s, p)
endsWith(s, p)
orElse(x, fallback): orElse(pr.word, 'no pull request')
exists(x)
isNull(x)
coalesce(a, b, ...)
if(cond, a, b)
len(x): len(pr.checks)
count(list)
sum(list, field?)
avg(list, field?)
first(list, n?): first(pr.checks).name
last(list, n?)
at(list, i)
append(list, x)
pluck(list, field): pluck(pr.checks, 'name')
join(list, sep)
steps
List steps, after | (item and index read the element):
where(cond): keeps the elements where cond is true. pr.checks | where(item.state == 'fail')
sortBy(expr, 'asc'|'desc'): stable sort; nulls last. processes.list | sortBy(item.cpu, 'desc')
groupBy(expr): { key, items, count } per group in first-seen order. pr.checks | groupBy(item.state)
take(n): the first n; n is a literal. pr.checks | take(5)
skip(n): all but the first n. pr.checks | skip(1)
count: the length. $steps | where(item.done) | count
sum(expr): the total of expr; nulls skipped. thread.changes.files | sum(item.additions)
min(expr): the least expr. pr.checks | min(num(item.startedAt))
max(expr): the greatest expr. processes.list | max(item.cpu)
avg(expr): the mean of expr. processes.list | avg(item.mem)
first: the first element or null. thread.plan.steps | where(item.state == 'working') | first
last: the last element or null. thread.plan.steps | last
pick(field, ...): keeps the named fields. processes.list | pick(name, cpu)
map(name: expr, ...): adds or replaces fields. pr.checks | map(took: num(item.completedAt) - num(item.startedAt))
distinct(expr?): drops later repeats. pr.checks | distinct(item.workflow)
flatten(field?): joins inner lists. tree.all | flatten(children)
join(other, key, otherKey?): attaches the matching element of other. thread.subagents | join(tree.children, id)
format(expr): expr as text per element. pr.checks | format(`${item.name}: ${word(item.state)}`)
handlers
Handler steps, in onPress, onSubmit, onChange and <when do>; one step or [a, b], at most 6:
set($path, value): writes a value; later steps read the new value. Consent: none. set($step, 2)
toggle($path): flips a boolean value. Consent: none. toggle($showDone)
start($run): starts a run; does not wait; its end fires <when done>. Consent: the person approves the command once. start($check)
cancel($run): stops a running run. Consent: none. cancel($tests)
send("text", $path, ...): a message to the agent: the text, then slate: {...} with the paths as data. Consent: a press is the consent; from a reaction, one approval per slate. send("Fix the failing check.", item.name)
steer("text", $path, ...): as send, into the running turn. Consent: as send. steer("Look at the failing check first.")
queue("text", $path, ...): as send, after the running turn (a send until the flag lands). Consent: as send. queue("Then open the pull request.")
fill("text", $path, ...): puts the text in the composer, not sent. Window only, on a press. Consent: none; a press only. fill("About this branch:", git.branch)
open(url): opens an http, https or mailto link. Window only, on a press. Consent: once per domain; a press only. open(pr.url)
copy(text): copies to the clipboard. Window only, on a press. Consent: none; a press only. copy(git.head)
pane("kind"): opens a right panel tab. Window only, on a press. Consent: none; a press only. pane("pr")
patch
patch: elements without <slate>, sent as text to slate_write; one write may hold several.
<props id="price" tone="warning" />: merges these props into the piece.
<text id="eta">Ready</text>: a piece with an id that exists replaces it whole, children too.
<add under="list" at={0}><text id="new">New</text></add>: adds pieces under a piece, at an index from 0, else last.
<move id="new" under="other" at={1} />: moves a piece.
<remove id="new" />: removes a piece and what it holds. <remove name="hist" /> removes a declaration.
<value name="hist" start={[]} />, <run ...>, <derived ...>, <file name="x.py">...</file>: adds the declaration or replaces the one of that name.
<clear /> empties the slate; <undo /> goes back one write. Each stands alone in its write.
For anything bigger, resend the whole <slate> with if_version set to the version you read.
version, which every write and read answers, counts the slate's edits and is what if_version takes; wsp/2 and schema 2 name the format.
check with press walks a press and its <when> chain on a copy: nothing runs and nothing is sent, and it says what would start.
check with values {"$run.json": {...}, "$run.exit": 0} previews the panel for a result the run has not had yet.
examples
pull request panel:
<slate title="Pull request">
<derived name="failing" value={pr.checks | where(item.state == 'fail')} />
<run name="merge" cmd='gh pr merge --squash --auto "$PR"' env={{ PR: pr.number }} timeout={30}
confirm="Enable auto-merge on this pull request? It merges when the checks pass." />
<column>
<empty when={pr.number == null} title="No pull request yet">This thread's branch has no pull request.</empty>
<facts id="head" when={pr.number != null}>
<fact label="State" value={pr.word} />
<fact label="Review" value={word(pr.review)} />
<fact label="Mergeable" value={word(pr.mergeable)} tone={pr.mergeable == 'mergeable' ? 'good' : pr.mergeable == 'conflicting' ? 'warning' : 'muted'} />
</facts>
<table id="checks" when={pr.number != null} items={pr.checks} key={item.name} empty="No checks yet">
<col title="Check" value={item.name} />
<col title="State" value={word(item.state)} tone={item.state == 'fail' ? 'bad' : item.state == 'pass' ? 'good' : 'muted'} />
<col title="Took" value={item.completedAt ? duration(num(item.completedAt) - num(item.startedAt)) : 'running'} mono align="end" />
<action label="Send to agent" when={item.state == 'fail'} onPress={send("A check failed. Read its log and fix it.", item.name, item.link)} />
</table>
<text id="summary" when={len($failing) > 0} tone="muted">{len($failing)} failing of {len(pr.checks)}</text>
<row when={pr.number != null}>
<button label="Open on GitHub" onPress={open(pr.url)} />
<button label="Checks and comments" variant="quiet" onPress={pane("pr")} />
<button id="automerge" label="Auto-merge" when={pr.mergeable == 'mergeable' and pr.review == 'approved' and $merge.state != 'done'} onPress={start($merge)} />
</row>
<text when={$merge.state == 'done'} tone="good">Auto-merge is on</text>
<text when={$merge.state == 'failed'} tone="bad">gh refused: {short($merge.err, 120)}</text>
</column>
</slate>
a long task's progress:
<slate title="Cart rounding fix">
<value name="steps" start={[
{ title: "Find where the cart rounds totals", done: true },
{ title: "Round once at the end", done: true },
{ title: "Fix the two failing tests", done: false },
{ title: "Open a pull request", done: false }
]} />
<derived name="done" value={$steps | where(item.done) | count} />
<run name="tests" cmd="pnpm exec vitest run --minWorkers=1 --maxWorkers=2 test/cart.test.ts" stream timeout={300} once />
<when done={$tests} do={set($steps[2].done, $tests.exit == 0)} />
<column>
<meter id="progress" label="Steps" value={$done} max={len($steps)} format="fraction" />
<checklist id="todo" items={$steps} key={item.title} title={item.title} done={item.done} editable />
<section title="Files changed" collapsible>
<table items={thread.changes.files} key={item.path} empty="Nothing changed yet">
<col title="File" value={item.path} mono />
<col title="Added" value={item.additions} tone="good" align="end" />
<col title="Removed" value={item.deletions} tone="bad" align="end" />
</table>
</section>
<row>
<button label="Run the cart tests" when={$tests.state != 'running'} onPress={start($tests)} />
<button id="next" label="Go on to the next step" variant="primary"
when={$done < len($steps) and thread.status != 'working'}
onPress={send("Go on to the next unchecked step on the slate.", $steps)} />
</row>
<output run={$tests} lines={10} />
</column>
</slate>
a setup the person performs, with a secret:
<slate title="Deploy setup">
<value name="step" start={1} />
<value name="repo" start="acme/landing" />
<value name="project" start="" />
<secret name="vercelToken" />
<run name="check" cmd='vercel project inspect "$PROJECT" 2>&1 | head -c 4000'
env={{ PROJECT: $project, VERCEL_TOKEN: $vercelToken }} on="host" timeout={30} />
<run name="env" cmd='grep -v "^VERCEL_TOKEN=" .env > .env.tmp 2>/dev/null; printf "VERCEL_TOKEN=%s\n" "$VERCEL_TOKEN" >> .env.tmp; mv .env.tmp .env'
env={{ VERCEL_TOKEN: $vercelToken }} on="host" timeout={10} />
<run name="ci" cmd='gh secret set VERCEL_TOKEN --repo "$REPO"' stdin={$vercelToken}
env={{ REPO: $repo }} on="host" timeout={60} />
<derived name="checked" value={$check.state == 'done' and contains($check.out, $project)} />
<derived name="written" value={$env.state == 'done' and $ci.state == 'done'} />
<when change={$project} do={start($check)} />
<when change={$checked} do={set($step, $checked ? 3 : 2)} />
<when change={$written} do={set($step, $written ? 4 : 3)} />
<column>
<text emphasis="strong">Step {$step} of 4</text>
<section title="1. The Vercel token" when={$step == 1}>
<markdown>Open the Vercel dashboard, make a token with the project's scope, and paste it here. It never reaches the agent.</markdown>
<button label="Open Vercel tokens" onPress={open("https://vercel.com/account/tokens")} />
<input label="Vercel token" value={$vercelToken} kind="password" />
<button label="Next" variant="primary" when={$vercelToken.set} onPress={set($step, 2)} />
</section>
<section title="2. The project" when={$step == 2 or $step == 3}>
<input id="project-name" label="Project name on Vercel" value={$project} mono />
<text when={$check.state == 'held'} tone="muted">Approve the check to go on</text>
<text when={$check.state == 'running'} tone="muted">Checking</text>
<text when={$check.state == 'failed'} tone="bad">Vercel did not find it: {short($check.err, 160)}</text>
<text when={$checked} tone="good">Found {$project}</text>
</section>
<section title="3. Write it down" when={$step == 3}>
<facts>
<fact label="Repo" value={$repo} mono />
<fact label="Project" value={$project} mono />
</facts>
<row>
<button label="Write .env" when={$env.state != 'done'} onPress={start($env)} />
<button label="Set the CI secret" when={$ci.state != 'done'} onPress={start($ci)} />
</row>
<text when={$env.state == 'done'} tone="good">.env written</text>
<text when={$ci.state == 'done'} tone="good">CI secret set</text>
<text when={$env.state == 'failed' or $ci.state == 'failed'} tone="bad">{short(concat($env.err, $ci.err), 200)}</text>
</section>
<section title="4. Done" when={$step == 4}>
<text tone="good">The token is in .env and in the repo's secrets.</text>
<button id="write" label="Tell the agent" variant="primary"
onPress={send("The Vercel setup is done on the slate; wire the deploy hook next.", $repo, $project, $vercelToken)} />
</section>
</column>
</slate>
a live figure with an hour of history:
<slate title="Gold">
<value name="hist" start={[]} />
<run name="spot" cmd="curl -s https://api.gold-api.com/price/XAU" every={60} />
<when done={$spot} do={set($hist, if($spot.exit == 0, last(append($hist, { at: time.now, v: $spot.json.price }), 60), $hist))} />
<column>
<heading level="title">Gold</heading>
<grid columns={3}>
<number label="Per ounce" value={$spot.json.price} format="usd" trend={pluck($hist, 'v')} />
<number label="Low" value={min(pluck($hist, 'v'))} format="usd" />
<number label="High" value={max(pluck($hist, 'v'))} format="usd" />
</grid>
<section title="Feed" surface="inset" pad="normal">
<row>
<status tone={$spot.exit == 0 ? 'good' : 'muted'}>{$spot.exit == 0 ? 'Live' : 'Waiting'}</status>
<chip>every minute</chip>
</row>
<ring label="Last hour" value={len($hist)} max={60} format="fraction" />
</section>
<chart label="Per ounce, last hour" items={$hist} x={item.at} value={item.v} format="usd" />
</column>
</slate>
a quiz that logs each answer:
<slate title="Networking quiz">
<value name="qs" start={[
{ q: "Which layer routes packets between networks?", options: ["Link", "Network", "Transport"], answer: "Network" },
{ q: "Which port does HTTPS use by default?", options: ["80", "443", "8080"], answer: "443" }
]} />
<value name="i" start={0} />
<value name="pick" start={null} />
<value name="log" start={[]} />
<column>
<heading level="label">Question {$i + 1} of {len($qs)}</heading>
<choices id="answer" label={$qs[$i].q} value={$pick} options={$qs[$i].options} answer={$qs[$i].answer}
held={$pick == null ? null : 'Answered'}
onChange={set($log, append($log, { q: $qs[$i].q, pick: $pick, right: $pick == $qs[$i].answer }))} />
<button label="Next question" when={$i + 1 < len($qs)} held={$pick == null ? 'Pick an answer first' : null}
onPress={[set($i, $i + 1), set($pick, null)]} />
<number label="Score" value={len($log | where(item.right))} unit={`of ${len($qs)}`} />
<table items={$log} empty="No answers yet">
<col title="Question" value={item.q} />
<col title="Picked" value={item.pick} />
<col title="Result" value={item.right ? 'Right' : 'Wrong'} tone={item.right ? 'good' : 'bad'} />
</table>
</column>
</slate>
Last updated