Theme Reference
This page describes the theme package format, one part after the other. Use it next to the tutorial in Make a Theme. Every name on this page comes from the validator (aivory-theme), the desktop renderer or the JSON Schema of the AIVory Code repository.
- One folder of TOML, JSON, Markdown, fonts and images. No code
- The validator checks the folder with
aivoryd theme dev,theme checkandtheme pack - Catalog version 1: 45 components, 14 data roots, 13 actions
- Desktop first: the desktop app is the only client that draws a theme today. See What is not finished
Folder Layout
| Path | Rule |
|---|---|
theme.toml |
Required. |
layouts/<class>/<screen>.json |
<class> is desktop, phone, fold or watch. The class must be in displays. |
layouts/desktop/ |
All 8 screens are required: warroom, session, inbox, tasks, repos, settings, focus, decision. |
layouts/watch/ |
Only warroom and decision. |
layouts/phone/, layouts/fold/ |
The 8 screen names. A class that is in displays needs at least one file. |
parts/<name>.json |
The name uses [a-z0-9_-]. A part never refers to itself, also not through other parts. |
strings/<lang>.toml |
<lang> is a language tag such as en or pt-BR. If one file exists, strings/en.toml is required. |
*.json |
Only in layouts/ and parts/. |
| Fonts | Only in assets/fonts/. |
| Images | Only in assets/images/ and previews/. |
theme.lock, <file>.upstream |
The daemon writes them when you add or update a theme. Do not make them. |
Files and folders whose name starts with a dot are not part of a theme. The unpacker leaves them out.
The Manifest: theme.toml
The manifest accepts only the keys below. An unknown key is an error.
| Key | Type | Required | Default | Rule |
|---|---|---|---|---|
id |
string | yes | 3 to 40 characters of [a-z0-9-]. It does not start or end with -. |
|
name |
string | yes | 1 to 80 characters. Shown in Settings. | |
description |
string | no | "" |
500 characters or fewer. |
version |
string | yes | SemVer x.y.z, for example 1.0.0 or 1.0.0-rc1. With --tag, it must equal the tag without the v. |
|
author.name |
string | yes | 1 to 80 characters. Write it as the table [author]. |
|
author.url |
string | no | none | Starts with https://. |
license |
string | yes | An SPDX expression such as MIT or MIT OR Apache-2.0. The validator allows letters, digits, ., -, +, spaces and parentheses, up to 120 characters. |
|
min_app |
string | yes | SemVer. The theme is rejected by an app that is older. | |
catalog |
integer | yes | The catalog version that the theme needs. The only version is 1. |
|
displays |
list | yes | Any of desktop, phone, fold, watch. It must contain desktop. No duplicates. |
|
tokens |
list of strings | no | ["tokens.toml"] |
1 to 8 relative .toml files. A later file overrides keys of an earlier file. Tables merge key by key. |
id = "harbor"
name = "Harbor"
description = "A calm dark theme with one decision at a time."
version = "0.1.0"
license = "MIT"
min_app = "2026.1.0"
catalog = 1
displays = ["desktop"]
tokens = ["tokens.toml"]
[author]
name = "Jane Doe"
url = "https://github.com/janedoe"
Tokens
A token file is TOML with the tables below. Layouts use token names, not raw values. The tables accept only the keys that are listed here. Unknown keys are an error.
colors
A color is #rrggbb or #rrggbbaa. All of these are required.
| Token | Use |
|---|---|
colors.bg |
Page background. |
colors.surface |
Background of panels, cards and buttons. |
colors.text |
Text. |
colors.muted |
Secondary text and hairlines. |
colors.accent |
Primary buttons, active tabs and the active nav item. |
colors.analysis |
The analysis panel and the second color of segments charts. |
colors.state.needsYou |
A session that waits for you. |
colors.state.working |
A session that works. |
colors.state.idle |
An idle session. |
colors.state.parked |
A parked session. |
colors.state.error |
A session with an error. |
colors.state.done |
A finished session. |
In a layout you write these names in props such as color, bg, border and tone: bg, surface, text, muted, accent, analysis, state.needsYou or only needsYou.
The app also checks 12 pairs for contrast (for example text on bg and surface, 4.5:1; muted, 3:1). A pair that fails keeps the value of Minimal, and the app notes it. The theme still loads.
fonts
| Token | Type | Required | Value |
|---|---|---|---|
fonts.ui |
string | yes | sans, serif, mono, or the path of a bundled font file, for example assets/fonts/Inter.ttf. |
fonts.mono |
string | yes | The same values. |
serif is not bundled in the desktop app. It keeps the sans face and logs a note.
type
The five text sizes in px. Each is required and must be 6 to 72. Components pick one with the prop size. The app scales them with the interface text size.
| Token | Typical value |
|---|---|
type.xs |
11 |
type.sm |
12 to 13 |
type.md |
14 to 15 |
type.lg |
18 to 20 |
type.xl |
24 to 28 |
space and radii
Both are open tables: you choose the names. A value is a number from 0 to 200.
| Table | Used by | Notes |
|---|---|---|
space.<name> |
The props gap and pad. |
"pad": "edge" reads space.edge. A number works too. An unknown name gives the default. |
radii.<name> |
The prop radius. |
"radius": "card" reads radii.card. A number works too. |
The names radii.button, radii.card, radii.tile, radii.pane and radii.rail_item also set the corners of the screens that the app draws itself. The app caps them at 48.
motion and effects
| Token | Type | Range | Desktop app today |
|---|---|---|---|
motion.easing |
string | linear, ease, easeIn, easeOut, easeInOut |
Not read yet |
motion.durations.<name> |
integer, ms | 0 to 5000 | Not read yet |
effects.glass_alpha |
number | 0 to 1 | Used by panel with variant: "glass" (default 0.6) |
effects.glow |
number | 0 to 1 | Not read yet |
effects.scanlines |
number | 0 to 1 | Not read yet |
effects.blur |
number | 0 to 40 | Not read yet |
motion has only easing and the table durations. A key such as fast_ms directly in [motion] is an error. Write [motion.durations] and fast = 120.
Strings
strings/<lang>.toml holds texts. A layout reads one as $strings.key. A nested table gives a dotted key: [nav] with home = "Home" is $strings.nav.home.
| Rule | Value |
|---|---|
| Key characters | [A-Za-z0-9_] |
| Value | A string of 200 characters or fewer, or a table. |
| Required file | strings/en.toml, when any strings file exists. |
| Every language | A key that a layout uses must exist in every language file. |
The desktop app reads strings/en.toml today. It does not pick a language by locale yet. A missing key shows the key itself.
Layout Files
A layout file is JSON with two keys:
{
"$schema": "https://example.com/optional",
"root": { "type": "panel", "title": "$strings.title", "children": [] }
}
$schema is optional and must be a string. Any other key at the top is an error. Do not put screen or display in the file: the path of the file sets both.
Nodes
A node is an object with type (a component of the catalog), or an object { "use": "<part>" }. A use node accepts only use and when.
| Key | Meaning |
|---|---|
type |
The component name. Required, unless the node is a use node. |
id |
1 to 64 characters of [A-Za-z0-9_-]. It names the state of the node (scroll position, selected tab). Set it on nodes that sit in more than one place. |
children |
A list of nodes. |
fallback |
One node. Required on scenes and effects. |
when |
Show the node only when a test is true. See below. |
on |
Controls: press and key. See Actions. |
role |
"analysis", and only on a panel. |
tiers |
sessions only. |
classes |
responsive only. |
| any other key | A prop of the component. |
A node may sit only in children, fallback, tiers.<tier>.template and classes.<class>. A node inside another prop is an error.
Values
| Form | Example | Meaning |
|---|---|---|
| Literal | "Open", 3, true |
Used as is. A text is 200 characters or fewer. |
| Data path | "$gates.needsYou.count" |
Read from the snapshot. See Data Paths. |
| Literal dollar | "$$5 per hour" |
$$ at the start gives a text with one $. |
| Table pick | { "path": "$item.state", "map": { "needsYou": "alert" }, "else": "plain" } |
Pick a value by the text of the path. else is the value for a key that the table does not have. |
| List | ["a", "b"] |
Used by segments and tabs. |
when
{ "path": "$asks", "is": "empty" }
is |
value |
True when |
|---|---|---|
nonEmpty |
none | The path has a value that is not empty: not null, not "", not 0, not false, not an empty list or object. |
empty |
none | The opposite of nonEmpty. |
eq |
a literal | The value is equal. Numbers compare as numbers. |
ne |
a literal | The value is not equal. |
in |
a list of literals | The value is one of the list. |
A when has one test. It accepts only path, is and value.
Parts
A part is a file parts/<name>.json with the same shape as a layout file. A layout inserts it with { "use": "<name>" }. Use parts for a top bar or a card that many screens share. The validator walks parts like layouts, in the class desktop.
Component Catalog
The catalog has 45 components. The catalog version is 1. In a watch layout only these components are allowed: stack, list, row, text, readout, badge, gauge, decision and composer.
Props that most components accept
| Prop | Type | Meaning |
|---|---|---|
width, height |
number | Size in px. |
grow |
number | A number over 0 fills the free space. |
pad |
space name or number |
Padding. |
bg |
color name | Background. |
radius |
radii name or number |
Corner radius. |
border |
color name | A 1 px border. |
spacer reads size only. card and panel read width, height, grow, pad and radius. Text clips with an ellipsis. A component does this, and a theme cannot turn it off.
Layout
| Component | Props | Notes |
|---|---|---|
stack |
dir (row or column, default column), gap, align (start, center, end), justify (center, end, between), wrap |
The dir prop is dir, not direction. |
grid |
columns (1 to 12, default 2), gap |
Cells have equal width. |
layer |
none | The first child is the base. The other children lie over it. |
scroll |
dir (row or column), gap |
Scrolls its children. |
spacer |
size |
Without size it fills the free space. |
repeat |
items (a list path), limit, dir, gap, wrap |
The children are the template. $item is valid inside. |
responsive |
classes |
See Window width. |
sessions |
tiers, gap, include, item_width |
See Session tiers. |
Frame
| Component | Props | Notes |
|---|---|---|
panel |
variant, title, role, gap |
Variants: flat (default), glass, chamfer, window, monitor. With role: "analysis" the title is “Analysis” and $analysis.* is valid inside. |
frame |
segments (a list of labels), title |
A panel with square corners and a row of segment labels on top. |
nav |
dir (row or column), variant (pill or plain), gap, size, radius |
The app lists the screens: War room, Inbox, Tasks, Repos, Settings. You pick the variant and the place. |
button |
label, variant (primary), composer, size, radius, on |
composer is the id of the composer whose text a dispatch or newSession sends. |
segment_button |
label, selected, composer, on |
A button in pill shape. A true selected fills it. |
switch |
label, value, size, on |
A toggle that shows value. |
tabs |
items (a list of labels), size |
The children are the pages. The first child belongs to the first label. |
look_switch |
size |
The app lists the looks. The user can always return to Minimal. |
key_hint |
keys, label |
A key cap with a label. It shows text only. |
Content
| Component | Props | Notes |
|---|---|---|
text |
value, size, color, weight (bold), mono, align (center, end), lines |
lines sets the number of lines (default 1). |
readout |
label, value, unit, color, dir (row) |
A label with a value. |
badge |
value, state, tone |
With state, the badge uses the state color and shows the state name when value is empty. Else tone picks the color. |
image |
none | Draws its fallback, or nothing. Not yet supported. |
list |
items (a list path), limit, gap, empty (a text), size |
The children are the template. $item is valid inside. |
row |
title, sub, meta, state, on, radius, size |
One line. A press fires on.press. |
card |
title, sub, state, gap, on |
The children go below the title. A press fires on.press. |
log |
items, field (default text), limit (default 50), size |
A list of lines in mono type. |
files |
label, added, removed |
A file name with two bars for added and removed lines. |
diff |
value |
A text. A line that starts with + is green and one that starts with - is red. |
Session components
| Component | Props | Notes |
|---|---|---|
decision |
session, free_text |
The question, the reason, the risk flag, the steps and the option buttons of one ask. The app owns this content. session defaults to $item, then the open dialog, then the selected session, then the oldest ask. |
decision_queue |
limit, free_text, gap |
All open asks as a list of decisions. |
transcript |
session, size |
The chat lines of a session. session defaults to $item.id, else $selected.id. |
terminal |
session, size |
The same lines in terminal style. |
composer |
mode, session, codebase, placeholder |
A text box with the Send button of the app. mode is send, dispatch or newSession. Without mode: send when a session is known, else dispatch. |
settings_section |
none | The app draws the Settings screen here. |
A settings layout without a settings_section is not used. Settings > Display must stay reachable, so the app then shows its own Settings screen.
Charts
| Component | Props | Notes |
|---|---|---|
chart |
kind, items (a list path), field (default pct), max, height (default 60), color |
Kinds that the desktop app draws: segments, stacked (reads active, waiting, idle of each item, for example $events), strip, area, bar. Any other kind, for example line or spark, draws thin columns. |
gauge |
label, value, max (default 100), unit, color, kind |
The desktop app draws every kind as a bar. |
ruler |
steps (2 to 60, default 10) |
A scale with ticks. |
Effects and scenes
| Component | Parameters | Validator rule |
|---|---|---|
rain |
speed: "activity" or a number from 0.1 to 10 |
Needs fallback. |
scramble, scanlines, rings |
none checked | Need fallback. |
city3d |
height: tokens, cost or turns |
Needs fallback. |
fsn3d |
none checked | Needs fallback. |
radar |
sweep_s: a number from 2 to 20. rings_min: a list of up to 6 numbers. |
Needs fallback. |
bloom |
strand: tokensHour or tokensTotal |
Needs fallback. |
camera_wall |
none checked | Needs fallback. |
The desktop app draws the fallback node of every effect and scene. The scenes and effects themselves are not yet supported. Build a theme that looks right with the fallback.
Window width
responsive holds a node for each width class under classes:
| Class | Window width |
|---|---|
compact |
below 960 px |
regular |
960 to 1439 px |
wide |
1440 px and more |
A class without a node uses the next smaller class. On the phone class only compact is allowed. On fold: compact and regular.
Session tiers
sessions draws one template for each count of live sessions. A live session is not parked and not done.
{ "type": "sessions", "gap": "gap", "tiers": {
"1": { "layout": "grid", "template": { "type": "card", "title": "$item.title" } },
"13-30": { "layout": "list", "template": { "type": "row", "title": "$item.title" } }
} }
| Tier | Live sessions |
|---|---|
1 |
0 or 1 |
2-4 |
2 to 4 |
5-12 |
5 to 12 |
13-30 |
13 to 30 (required) |
31+ |
31 and more |
| Rule | Detail |
|---|---|
| Keys of a tier | layout and template. Both are required. |
layout |
list, grid or scene. The tiers 13-30 and 31+ must use list. |
| Missing tier | A tier without a template uses the next smaller tier. If none is smaller, the first tier that exists. |
list |
Groups the sessions by codebase. In 31+ the app closes groups that have no decision and no error. |
grid, scene |
Items flow in rows. item_width (default 240 px) sets the least width of an item. |
include |
"all" also shows parked and done sessions. |
| Order | The model sets the order: live sessions first, an open ask first, then the longest wait. |
Data Paths
A text that starts with $ is a data path. Segments use [A-Za-z0-9_]. A number segment is a list index: $sessions.0.title. A path that leads nowhere gives an empty text. Layouts have no formulas. The app computes all counts, orders and formats.
| Root | Type | Fields |
|---|---|---|
$sessions |
list | id, title, codebase, part, provider, model, state, since (ms), cost (USD), tokens, controlled, contextPct, prompt, ask, subagents, linesTotal, composer, task |
$asks |
list | id, title, codebase, ask. One item for each session with an open ask, longest wait first. |
$gates |
object | needsYou, working, blocked, idle. Each has count and ids. Also asks (a list of session ids). |
$codebases |
list | id, name, path, branch, worktrees, daemon, languages (a list), parts (a list of name and paths) |
$repos |
object | count, items. Each item: id, name, path, branch, worktrees, sessions (live), languages (one text) |
$usage |
list | provider, label, pct, used, resetsAt, estimate |
$events |
list | at, active, waiting, idle, text. One item for each minute of the last hour. |
$inbox |
object | open: the number of open inbox items |
$tasks |
object | queued: the number of queued tasks |
$daemon |
object | version, name, demo, host |
$intent |
object | mode (alert, monitor or idle), asks, errors |
$selected |
object | The same fields as one item of $sessions. The session of the Session or Focus screen, or of the open decision dialog. |
$display |
object | class (desktop), width (px), tier (1, 2-4, 5-12, 13-30 or 31+) |
$analysis |
list | source, label, value, unit. Valid only inside a panel with "role": "analysis". |
$item |
any | The current element. Valid only inside repeat, list and sessions templates. |
$strings |
text | $strings.key. See Strings. |
Values of a session:
| Field | Values |
|---|---|
state |
needsYou, error, working, idle, parked, done |
ask |
null, or an object with id, kind (permission, plan, question, error), text, options, and sometimes steps, reason, risky |
composer |
mode (plan, edit, auto), provider, model, effort (low, medium, high, max) |
subagents |
A list of id, name, state, task |
A session has no transcript tail in the snapshot. Use transcript or terminal to show the lines. When a session is selected and $intent.mode is not alert, $intent.mode reads one.
Actions
A control calls one action of the app, and nothing else:
{ "type": "button", "label": "Open session",
"on": { "press": { "action": "focus", "id": "$item.id" } } }
on accepts only press and key. There are no timers and no load events. A key event needs a combo such as "Ctrl+K" (modifiers Ctrl or Cmd, Alt, Shift, plus one key). The combos Ctrl+Shift+L, Esc and Escape belong to the app.
An argument name uses [a-z][A-Za-z0-9]*. An argument is a data path or a literal.
| Action | Arguments | What the app does | Daemon method |
|---|---|---|---|
focus |
id |
Opens the Focus screen of a session. | none (a screen change) |
open |
screen: warroom, inbox, tasks, repos, settings or decision (with id) |
Opens a screen. decision opens the decision layout as a dialog with a Close button. |
none (a screen change) |
dispatch |
codebase (optional) |
Starts work from the text of a composer that the button names in composer. |
session/dispatch |
newSession |
codebase |
Starts a session with the text and the mode, model and effort of that composer. | session/new |
send |
none | Sent by the composer component with its own Send button. |
session/send |
answer |
none | Answered by the decision component with its own option buttons. |
session/answer |
pause |
id |
Interrupts the current turn. | session/pause |
park |
id |
Stops the session and keeps it resumable. | session/park |
resume |
id |
Resumes a session. | session/resume |
stop |
id |
Shows a confirm that the app owns, then kills the agent process. | session/stop |
autopilot |
on: true or false |
Shows a confirm that the app owns, then sets the autopilot. | autopilot/set |
permissionMode |
id, mode: plan, edit or auto |
Sets the permission mode. | session/mode |
setLook |
none | The validator accepts the name. Use the look_switch component: on another node the app logs an error and does nothing. |
settings/set |
composer that the user types into. An argument named text, prompt or message on send, dispatch, newSession or answer is a validator error. A theme cannot put a fixed prompt on a button, answer a decision itself, or change the label of an option. The texts of the stop and autopilot confirms are fixed.
Assets and Limits
| Item | Limit |
|---|---|
Theme archive (.aivory-theme.zip) |
10 MB. The unpacked size is at most 100 times the archive size. |
| All files unpacked | 20 MB |
| Files | 500 |
| One asset (font or image) | 4 MB |
| Image size | 4096 x 4096 px |
| One layout or part file | 512 KB, 4,000 nodes, depth 40 |
| Every other file | 1 MB |
| Font families | 8 |
| One text in a layout or in a strings file | 200 characters |
| Tokens files | 1 to 8 |
| Path | 240 bytes, no backslash, none of the characters < > : " ? * $ or the pipe, no Windows device names, no part that ends with a dot or a space |
| Asset rule | Detail |
|---|---|
| File types | TOML, JSON, Markdown, LICENSE, TTF, OTF, PNG, JPG, WebP, SVG. All other files are rejected. |
| Fonts | The validator parses each font and reads its family name. A font is used when fonts.ui or fonts.mono holds its path. |
| Images | The validator reads the header of PNG, JPG and WebP files. |
| SVG | Must be static: no script, foreignObject, iframe, embed, object, audio, video, animate, set, entities, javascript:, @import or external href. |
| Licenses | assets/fonts/ and assets/images/ each need a file LICENSE, LICENSE.txt or LICENSE.md next to the assets. |
| Links | Links and special files are not allowed. |
The image component does not draw files yet, so no layout can show a file of assets/images/ today. Images in previews/ are for the Settings screen and for the registry.
Validator
Run the validator in one of three ways:
| Command | Use |
|---|---|
aivoryd theme dev <DIR> |
Checks on each save and prints the errors. --once checks once and ends. --interval-ms sets the poll time (default 1000). |
aivoryd theme check <DIR> |
Checks once. --tag v1.0.0 also checks that version equals the tag without the v. |
aivoryd theme pack <DIR> |
Checks, then writes <id>-<version>.aivory-theme.zip and prints its SHA-256. --out <DIR> picks the folder. |
A good theme prints ok: <id> <version>. A bad theme prints one line for each error and the count. The exit code is 1.
An error has the form <file> <JSON pointer>: <rule>. A pointer such as /root/children/2/on/press/action leads to the key.
Common errors
| Mistake | Message |
|---|---|
| A desktop screen is missing | layouts/desktop/tasks.json: required screen is missing |
| A component name is wrong | layouts/desktop/tasks.json /root/type: unknown component "panl" |
| A data root is wrong | parts/topbar.json /root/children/2/value: unknown data root "gate"; use codebases, sessions, asks, gates, usage, events, inbox, tasks, repos, daemon, analysis, intent, selected, display |
| An action name is wrong | layouts/desktop/warroom.json /root/children/1/children/0/tiers/1/template/on/press/action: unknown action "delete" |
| Text on a button for an agent | layouts/desktop/warroom.json .../on/press/text: text for an agent comes only from a composer that the user types into |
An event other than press or key |
.../on/hover: an action fires on a user event only: "press" or "key" (no timers, no load events) |
$item outside a template |
layouts/desktop/repos.json .../title: $item is only valid inside repeat, list and sessions |
$analysis outside an analysis panel |
layouts/desktop/session.json /root/children/1/title: $analysis is only valid inside a panel with "role": "analysis" |
| A scene without a fallback | layouts/desktop/tasks.json /root: "city3d" needs a "fallback" node |
| A strings key is missing | layouts/desktop/inbox.json .../value: unknown strings key "nope" (every language needs it) |
| No strings file at all | parts/topbar.json .../label: the package has no strings table (strings/<lang>.toml) |
| An unknown part | layouts/desktop/tasks.json /root/children/0/use: unknown part "top" |
| A tier name is wrong | layouts/desktop/warroom.json .../tiers/12-30: unknown tier; use 1, 2-4, 5-12, 13-30, 31+ |
The tier 13-30 is missing |
layouts/desktop/warroom.json .../tiers: tier "13-30" is required (from 13 sessions the theme shows rows) |
A grid for 13-30 |
layouts/desktop/warroom.json .../tiers/13-30/layout: tiers 13-30 and 31+ must use layout "list" |
item instead of template |
.../tiers/13-30/item: unknown key of a tier and .../tiers/13-30: a tier needs a "template" node |
| Extra key in a layout file | layouts/desktop/tasks.json /screen: unknown key; a layout file has "$schema" and "root" |
| Bad JSON | layouts/desktop/tasks.json: not valid JSON: key must be a string at line 1 column 3 |
| A color is not a color | tokens.toml /colors/accent: "blue" is not a color (#rrggbb or #rrggbbaa) |
| A text size is out of range | tokens.toml /type/md: text size must be 6 to 72 px |
| An unknown token key | tokens.toml: unknown field `warn`, expected one of `bg`, `surface`, `text`, `muted`, `accent`, `analysis`, `state` in `colors` |
fast_ms in [motion] |
tokens.toml: unknown field `fast_ms`, expected `durations` or `easing` in `motion` |
| A font is not in the package | tokens.toml /fonts/ui: "assets/fonts/x.ttf" is not sans, serif, mono or a font file of the package |
| An effect is out of range | tokens.toml /effects/glass_alpha: value must be 0 to 1 |
| A bad id | theme.toml /id: id "Harbor" must be 3 to 40 characters of [a-z0-9-] |
| A bad version | theme.toml /version: "1.0" is not a SemVer version (x.y.z) |
| The version differs from the tag | theme.toml /version: version "0.1.0" must equal the tag "v0.2.0" without the v |
min_app is too high |
theme.toml /min_app: the theme needs app 99.0.0 or newer |
| An unknown catalog | theme.toml /catalog: catalog 2 is not supported (this app has 1) |
desktop is not in displays |
theme.toml /displays: displays must include "desktop" |
| An unknown manifest key | theme.toml: TOML parse error ... unknown field `extra`, expected one of `id`, `name`, `description`, `version`, `author`, `license`, `min_app`, `catalog`, `displays`, `tokens` |
| A file type is not allowed | run.sh: file type not allowed (a theme has TOML, JSON, Markdown, fonts and images only) |
| An image is not an image | assets/images/a.png: the image header is not valid |
| An asset folder has no license | assets/fonts: the folder needs a LICENSE file next to its assets |
A Complete Minimal Theme
aivoryd theme init harbor writes this theme. It passes aivoryd theme dev harbor --once and prints ok: harbor 0.1.0. It has the 8 required screens. Each screen is a panel with the daemon version. Replace the panels to build your theme in the tutorial.
theme.toml
id = "harbor"
name = "harbor"
description = "A starter theme."
version = "0.1.0"
license = "MIT"
min_app = "2026.1.0"
catalog = 1
displays = ["desktop"]
tokens = ["tokens.toml"]
[author]
name = "Your Name"
tokens.toml
[colors]
bg = "#ffffff"
surface = "#f4f5f7"
text = "#15161b"
muted = "#6b7280"
accent = "#2563eb"
analysis = "#7c3aed"
[colors.state]
needsYou = "#d97706"
working = "#2563eb"
idle = "#6b7280"
parked = "#9ca3af"
error = "#dc2626"
done = "#16a34a"
[fonts]
ui = "sans"
mono = "mono"
[type]
xs = 11
sm = 12
md = 14
lg = 18
xl = 24
[space]
sm = 8
md = 12
[radii]
card = 8
[motion]
easing = "easeOut"
[motion.durations]
fast = 120
strings/en.toml
title = "Fleet"
layouts/desktop/warroom.json
{
"root": { "type": "panel", "id": "warroom", "title": "$strings.title", "children": [
{ "type": "text", "value": "$daemon.version" }
] }
}
The other seven files (session, inbox, tasks, repos, settings, focus, decision) have the same content with another id. The folder also holds LICENSE and README.md.
What Is Not Finished
These parts exist in the format or in the code, but they do not work yet. The desktop renderer is apps/desktop of the AIVory Code repository.
| Part | State |
|---|---|
Scenes (city3d, fsn3d, radar, bloom, camera_wall) |
Validated. The desktop app draws the fallback. |
Effects (rain, scramble, scanlines, rings) |
Validated. The desktop app draws the fallback. |
image |
Has no props. It draws its fallback, or nothing. |
gauge kind |
The desktop app draws a bar for every kind. |
chart kind |
The desktop app draws columns: no smooth line, spark or arc. |
motion, effects.glow, effects.scanlines, effects.blur |
Validated and stored. The desktop app does not read them. |
phone, fold and watch layouts |
Validated and served by theme/get (see the API). No client draws them yet. |
| Language by locale | The desktop app reads strings/en.toml only. |
| Built-in looks as packages | The eight looks are compiled into the app. theme init --from <id> works for installed themes only. |
| Reload and error panel in the app | theme dev prints errors in the terminal. It does not reload the app and shows no panel. |
Install from @registry/name |
Not supported. theme add takes a local folder, github:<owner>/<repo>[@tag] or a GitHub link. |
Theme blocks (block add) |
Not built. Copy a file by hand. |