Themes and the footer

A theme changes colors only, never behavior. The footer changes the status line. Both come from files; both warn and fall back instead of refusing startup.

Choosing a theme#

Type /theme <name> to switch now. Radian writes the name back to tui.theme in the config file (configuration.md holds the path), so the next session starts there. A read-only config applies for the session but is never written.

Three names are built in: dark, light and mono. Any other name is a file in ~/.radian/themes/, named by its stem: /theme tokyo-night reads tokyo-night.toml. Both .toml and .json files read; a .toml file wins when both exist for one stem. The .json shape is the old format and loads with a startup hint to convert it. An unknown name warns and keeps the current theme.

dark is the original palette. light re-picks the same roles for a light background. mono uses no color at all: bold, underline and dim only. A non-empty NO_COLOR environment variable forces mono: /theme then does nothing and says so.

Set the startup theme in the config file:

[tui.theme]
tui.theme = "dark"

The theme file format#

A theme file is TOML:

name = "tokyo-night"
appearance = "dark"   # dark | light; default dark

[vars]
accent = "#7aa2f7"
bg_raised = "okhsl(229 5% 24%)"

[roles]
text = "#c0caf5"
user = "$accent"                 # one color: the foreground only
selection = { bg = "#283457" }
frame = { fg = "$accent", dim = true }

name is informational; the configured name decides which file loads. A missing name is tolerated. appearance picks which built-in palette the roles you do not set keep: dark or light, default dark. An unknown value warns and assumes dark. There is no inheritance beyond this: a file starts from one built-in palette, then sets roles.

[vars] holds reusable colors. A role value of $name uses the entry, and entries may reference each other with $other: chains resolve, and a loop only breaks the roles that use it. A missing variable warns once per name.

A [roles] key is one of the 82 role names below. A value is one color for the foreground, or a table with fg, bg, bold, italic, dim and underline. An absent field leaves the built-in value alone; bold = false turns off a bold the built-in sets. The same value shape overrides single roles from the config file:

[tui.colors]
user = "#0af"
notice = { fg = "oklch(62% 0.1 200)", bold = true }
md_heading = 203

A color is one of: #rgb, #rrggbb, oklch(L C H), okhsl(H S L), a 0–255 palette index, $var, or "" for the terminal default. The lightness in oklch() reads as a percentage or a plain 0–1 number; the hue reads in degrees with an optional deg suffix. Out-of-gamut oklch clips by reducing chroma and keeping the hue. With truecolor (COLORTERM is truecolor or 24bit) colors render as RGB; otherwise each renders as the nearest 256-color entry.

Validation is forgiving on purpose. An unknown role name warns once and is ignored. A bad value, a missing variable or a loop warns once and only the affected roles fall back to the built-in palette chosen by appearance. A file that does not parse, has no table at the top, or has no [roles] table falls back as a whole. Nothing here blocks startup.

Every role#

Roles are the same keys [tui.colors] accepts. A missing role keeps the built-in value.

Transcript text:

Role What it paints
text The main text level.
user Your messages.
reasoning The body of the thinking channel.
interrupted The [interrupted] marker and the cut-off body text.
tool_call The tool name and title in a tool head line.
tool_name The tool name inside the head line.
tool_result A tool result body.
tool_result_error A failed tool result.
notice Transcript notices.
notice_label The label: half of a notice line.
notice_body The value half of a notice line.
hint Hints: the placeholder, the question-form hint row, the picker help.
trust_hint The project trust line under the wordmark. Only this line uses the role.
completion The completion menu.
find The in-transcript search match highlight.
selection The transcript selection. A background only: the text keeps its own foreground.
truncation Truncation markers.
note Notes such as the startup Tip line.
turn_idle The idle turn marker.
turn_running The running turn marker.
turn_cancelled The cancelled turn marker.
approval_border The approval prompt border.
frame Structural rules: the │ rails, the ╭─/╰── open frames, table rules, the compaction rule.
diff_line_number A diff's gutter line numbers.
status_ok A tool head's ✓.
status_warn A tool head's warning marker.
status_error A tool head's ×.
auto The AUTO status marker.

Per-level thinking colors (the footer's reasoning item paints one marker per level so levels read apart at a glance):

Role What it paints
reasoning_off The off level marker.
reasoning_minimal The minimal level marker.
reasoning_low The low level marker.
reasoning_medium The medium level marker.
reasoning_high The high level marker.
reasoning_xhigh The xhigh level marker.
reasoning_max The max level marker.

Tool block backgrounds by state:

Role What it paints
tool_pending_bg A running tool block's background.
tool_success_bg A finished tool block's background.
tool_error_bg A failed tool block's background.

The /context grid categories (one cell color per estimated category, plus the free space and the compaction buffer):

Role What it paints
ctx_system The system prompt cell.
ctx_files The context files cell.
ctx_memory The memory cell.
ctx_tools The built-in tools cell.
ctx_skills The skills cell.
ctx_mcp The MCP tools cell.
ctx_user The user messages cell.
ctx_assistant The assistant messages cell.
ctx_reasoning The thinking cell.
ctx_results The tool results cell.
ctx_free The free space.
ctx_buffer The compaction buffer.

Footer:

Role What it paints
model The model item.
tier The tier item.
usage The usage item.
mcp The MCP item.
budget The context share.
budget_over The over-budget highlight.
footer_separator The separator between items, weaker than the item text.

Diff lines:

Role What it paints
diff_added Added lines.
diff_removed Removed lines.
diff_context Context lines.
diff_hunk File headers and @@ markers.

Rendered diagrams:

Role What it paints
mermaid_node Diagram nodes.
mermaid_edge Diagram edges.
mermaid_text Diagram text.

Assistant Markdown:

Role What it paints
md_heading Heading text.
md_heading_mark The # mark, one level weaker than the heading text.
md_link Link text.
md_link_url The URL.
md_code Inline code.
md_quote Quoted text.
md_quote_border The quote rail.
md_hr Horizontal rules.
md_list_bullet List bullets.

Code-block syntax:

Role What it paints
syntax_comment Comments.
syntax_keyword Keywords.
syntax_function Function names.
syntax_variable Variables.
syntax_string Strings.
syntax_number Numbers.
syntax_type Types.
syntax_operator Operators.
syntax_punctuation Punctuation.

Light versus dark terminals#

The file never names a terminal background. appearance decides the starting palette: dark for dark terminals, light for light ones. Set every role you care about and let appearance carry the rest. mono is not an appearance value; it is a full built-in theme for terminals without color.

Importing a pi theme#

A pi theme JSON converts once, by code. It never runs as the theme format.

radian theme import ~/.pi/agent/themes/kanagawa-wave.json [--name <name>] [--force]

It writes ~/.radian/themes/<name>.toml: the file's own name, else --name, else the input file stem. It prints the written path and one report line per mapped value, naming every pi key it drops and every role that keeps the built-in value. It refuses to overwrite an existing file without --force. A missing or bad input file exits with code 1, as does an existing output without --force.

What converts: pi's text levels and semantic colors map onto the matching roles (user message text to user, thinking text to reasoning, tool title to tool_call). The import report names every mapped value, every pi key it drops, and every role that keeps the built-in value: read it for the full mapping of your file. What does not convert: pi roles nothing consumes yet (panel roles and several Markdown, diff and syntax families) are dropped and reported. The result is a starting point: tune it with [tui.colors] or by editing the file.

There is no command that exports a built-in theme. Start a hand-written file from the worked example below, or import a pi theme and edit the result.

Validation errors#

Every problem warns once on the startup line and never blocks startup. The shapes: an unknown theme name (no theme "x": built-ins are dark, light, mono; theme files live in …), a broken file (uses built-in dark), a top level that is not a table, a missing [roles] table, an unknown role (unknown role(s) … ignored), a bad role value (names the role and uses the built-in color), a missing variable, and a variable loop. NO_COLOR set is not an error: the theme is fixed to mono.

A complete theme from scratch#

This file builds a full theme on the dark palette, overriding only the roles that carry its accent. Copy it to ~/.radian/themes/harbor.toml and run /theme harbor.

name = "harbor"
appearance = "dark"

[vars]
accent = "#7aa2f7"
good = "#9ece6a"
bad = "#f7768e"
weak = "#565f89"

[roles]
# Text levels
text = "#c0caf5"
user = "$accent"
notice = "#c0caf5"
notice_label = "$accent"
notice_body = "#9aa5ce"
hint = "$weak"
truncation = "$weak"
interrupted = "#e0af68"

# Structure
frame = { fg = "$weak" }
selection = { bg = "#283457" }
completion = { fg = "#c0caf5", bg = "#283457" }
find = { bg = "#283457" }

# Thinking
reasoning = "#9aa5ce"
reasoning_high = "$accent"
reasoning_max = "$accent"

# Tools
tool_call = "#c0caf5"
tool_name = "#7aa2f7"
tool_result = "#9aa5ce"
tool_result_error = "$bad"
status_ok = "$good"
status_warn = "#e0af68"
status_error = "$bad"

# Markdown and code
md_heading = "$accent"
md_heading_mark = "$weak"
md_link = "$accent"
md_link_url = "$weak"
md_code = "#9ece6a"
md_quote = "#9aa5ce"
md_quote_border = "$weak"
md_hr = "$weak"
md_list_bullet = "$accent"
syntax_comment = "$weak"
syntax_keyword = "$accent"
syntax_string = "$good"

# Diff
diff_added = "$good"
diff_removed = "$bad"
diff_hunk = "$accent"

# Footer
model = "$accent"
tier = "#9aa5ce"
usage = "#9aa5ce"
mcp = "$accent"
budget = "#9aa5ce"
budget_over = "#e0af68"
auto = "#e0af68"
footer_separator = "$weak"
approval_border = "$accent"
turn_running = "#e0af68"
turn_idle = "$good"
turn_cancelled = "$bad"

Every role not named here keeps the dark built-in. Type /theme harbor to apply it, then adjust one role at a time with [tui.colors] before editing the file: the config override and the file share the value shape, so a tuned override pastes straight into [roles].

The footer is the status line under the input box. It follows the theme roles above and is configured separately: footer.md lists every preset, row, item and command item, including the loop item, each item's own parameters, the per-preset rows, and tui.widgets.

A [tui.colors] key, a theme file role, and a footer item style all name one slot each in the same table the renderer uses, so the footer follows the theme.