Theming Reference
Theming in the coding agent: schema, loading, runtime behavior, and failure modes.
What the theme system controls
The theme system drives:
- foreground/background color tokens used across the TUI
- markdown styling adapters (
getMarkdownTheme()) - selector/editor/settings list adapters (
getSelectListTheme(),getEditorTheme(),getSettingsListTheme()) - symbol preset + symbol overrides (
unicode,nerd,ascii) - syntax highlighting colors used by native highlighter (
@veyyon/natives) - status line segment colors
Primary implementation: src/modes/theme/theme.ts.
Theme JSON shape
Theme files are JSON objects validated against the runtime schema in theme.ts (themeJsonSchema) and mirrored by src/modes/theme/theme-schema.json.
Top-level fields:
name(required)colors(required; all color tokens required)vars(optional; reusable color variables)export(optional; HTML export colors)symbols(optional)preset(optional:unicode | nerd | ascii)overrides(optional: key/value overrides forSymbolKey)
Color values accept:
- hex string (
"#RRGGBB") - 256-color index (
0..255) - variable reference string (resolved through
vars) - empty string (
"") meaning terminal default (\x1b[39mfg,\x1b[49mbg)
Required color tokens (current)
All tokens below are required in colors.
Core text and borders (11)
accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText
Background blocks (7)
selectedBg, userMessageBg, customMessageBg, toolPendingBg, toolSuccessBg, toolErrorBg, statusLineBg
Message/tool text (5)
userMessageText, customMessageText, customMessageLabel, toolTitle, toolOutput
Markdown (10)
mdHeading, mdLink, mdLinkUrl, mdCode, mdCodeBlock, mdCodeBlockBorder, mdQuote, mdQuoteBorder, mdHr, mdListBullet
Tool diff + syntax highlighting (12)
toolDiffAdded, toolDiffRemoved, toolDiffContext,
syntaxComment, syntaxKeyword, syntaxFunction, syntaxVariable, syntaxString, syntaxNumber, syntaxType, syntaxOperator, syntaxPunctuation
Mode/thinking borders (8)
thinkingOff, thinkingMinimal, thinkingLow, thinkingMedium, thinkingHigh, thinkingXhigh, bashMode, pythonMode
Status line segment colors (13)
statusLineSep, statusLineModel, statusLinePath, statusLineGitClean, statusLineGitDirty, statusLineContext, statusLineSpend, statusLineStaged, statusLineDirty, statusLineUntracked, statusLineOutput, statusLineCost, statusLineSubagents
Optional tokens
Purpose accents (5, optional)
sessionAccent, modeAccent, shareAccent, infoAccent, matchHighlight
These color the “cool arc” of the design language: session identity segments use sessionAccent, mode labels (plan, vibe, goal, loop) use modeAccent, share and collab segments use shareAccent, informational callouts such as the welcome tip label use infoAccent, and fuzzy-filter hit characters in select lists use matchHighlight. A theme that omits them still loads: each falls back to a sensible existing token (sessionAccent and modeAccent to accent, shareAccent to link then accent, infoAccent to muted, matchHighlight to warning). The built-in titanium theme binds them to its Daybreak palette (teal, violet, indigo, rose, gold).
composerBg (optional)
The tonal ground painted under the composer’s input rows and the padding rows above and below them (the “quiet card”). The padding rows carry the same ground so the card has a visible body rather than a single tinted line.
A theme that omits composerBg gets an unpainted composer: the input area renders directly on the terminal’s own background. Inheriting statusLineBg here used to render the composer band as a gray slab on mismatched terminals, so the default is no paint. A theme that wants a painted composer card must set composerBg explicitly, and an explicit value is painted.
export section (optional)
Used for HTML export theming helpers:
export.pageBgexport.cardBgexport.infoBg
If omitted, export code derives defaults from resolved theme colors.
symbols section (optional)
symbols.presetsets a theme-level default symbol set.symbols.overridescan override individualSymbolKeyvalues.symbols.spinnerFramesoverrides the loading spinner frames. Accepts either a flatstring[](applied to thestatusandactivityspinners) or an object{ "status"?: string[], "activity"?: string[], "thinking"?: string[] }to override each type independently. Any type not specified falls back to the symbol preset’s default frames.statusdrives the ~12.5fps spinner used by loaders and tool-execution indicators;activitydrives the ~30fps spinner used by markdown progress bars and similar high-frequency UI;thinkingdrives the starburst pulse shown in place of hidden reasoning while a model thinks (a single-frame array renders it static, with no animation timer).
Runtime precedence:
- settings
symbolPresetoverride (if set) - theme JSON
symbols.preset - fallback
"unicode"
Invalid override keys are ignored and logged (logger.debug).
Box-drawing borders
All outlined chrome, tool-result frames, overlays, code fences, the editor, the welcome banner, and markdown tables draw with the boxSharp.* tokens (┌┐└┘─│├┤┬┴┼).
Override behavior follows from that:
boxSharp.{topLeft,topRight,bottomLeft,bottomRight}restyle corners everywhere, markdown tables included.boxSharp.{horizontal,vertical}restyle edges and rules.boxSharp.{cross,teeDown,teeUp,teeRight,teeLeft}restyle dividers and junctions.- The
boxRound.*tokens (rounded corners╭╮╰╯) remain in the symbol schema, but no shipped surface consumes them today; setting them changes nothing.
Built-in vs custom theme sources
Theme lookup order (loadThemeJson):
- built-in embedded themes (
dark.json,light.json, and alldefaults/*.jsoncompiled intodefaultThemes) - custom theme file:
<customThemesDir>/<name>.json
Custom themes directory comes from getCustomThemesDir():
- default:
~/.veyyon/profiles/default/agent/themes: profile-aware: under a named profile (--profile <name>/VEYYON_PROFILE) this resolves to~/.veyyon/profiles/<name>/agent/themes - overridden by
VEYYON_CODING_AGENT_DIR:$VEYYON_CODING_AGENT_DIR/themes
getAvailableThemes() returns merged built-in + custom names, sorted, with built-ins taking precedence on name collision.
Loading, validation, and resolution
For custom theme files:
- read JSON
- parse JSON
- validate against
themeJsonSchema - resolve
varsreferences recursively - convert resolved values to ANSI by terminal capability mode
Validation behavior:
- missing required color tokens: explicit grouped error message
- bad token types/values: validation errors with JSON path
- unknown theme file:
Theme not found: <name>
Var reference behavior:
- supports nested references
- throws on missing variable reference
- throws on circular references
Terminal color mode behavior
Color mode detection (detectColorMode):
COLORTERM=truecolor|24bit=> truecolorWT_SESSION=> truecolorTERMindumb,linux, or empty => 256color- otherwise => truecolor
Conversion behavior:
- hex ->
Bun.color(..., "ansi-16m" | "ansi-256") - numeric ->
38;5/48;5ANSI ""-> default fg/bg reset
Runtime switching behavior
Initial theme (initTheme)
main.ts initializes theme with settings:
symbolPresetcolorBlindModetheme.darktheme.light
Auto theme slot selection uses terminal appearance in this order:
- terminal-reported OSC 11 background luminance, unless the macOS/Zellij fallback path is active
COLORFGBGbackground index (< 8=> dark,>= 8=> light)- macOS appearance fallback only for the known-broken macOS/Zellij OSC 11 path
- dark slot fallback
Current defaults from settings schema:
theme.dark = "titanium"theme.light = "light"symbolPreset = "unicode"colorBlindMode = false
Explicit switching (setTheme)
- loads selected theme
- updates global
themesingleton - optionally starts watcher
- triggers
onThemeChangecallback
On failure:
- falls back to built-in
dark - returns
{ success: false, error }
Preview switching (previewTheme)
- applies temporary preview theme to global
theme - does not change persisted settings by itself
- returns success/error without fallback replacement
Settings UI uses this for live preview and restores prior theme on cancel.
Watchers and live reload
When watcher is enabled (setTheme(..., true) / interactive init):
- watches
<customThemesDir>/<currentTheme>.jsononly when that file exists - built-ins are effectively not watched; built-in theme lookup also takes precedence over same-name custom files
- matching file changes schedule a debounced reload; reload errors or temporary file absence keep the last successfully loaded theme
- the watcher does not perform a delete/rename fallback; it waits for a future successful reload or explicit theme switch
Auto mode also reevaluates dark/light slot mapping from terminal appearance changes, SIGWINCH, and the macOS fallback observer when active.
Color-blind mode behavior
colorBlindMode changes only one token at runtime:
toolDiffAddedis HSV-adjusted (green shifted toward blue)- adjustment is applied only when resolved value is a hex string
Other tokens are unchanged.
Where theme settings are persisted
Theme-related settings are persisted by Settings to global config YAML:
- path:
<agentDir>/config.yml - default agent dir:
~/.veyyon/profiles/default/agent(profile-aware:~/.veyyon/profiles/<name>/agentunder a named profile) - effective default file:
~/.veyyon/profiles/default/agent/config.yml
Persisted keys:
theme.darktheme.lightsymbolPresetcolorBlindMode
Legacy migration exists: old flat theme: "name" is migrated to nested theme.dark or theme.light based on luminance detection.
Creating a custom theme (practical)
- Create file in custom themes dir, e.g.
~/.veyyon/profiles/default/agent/themes/my-theme.json. - Include
name, optionalvars, and all requiredcolorstokens. - Optionally include
symbolsandexport. - Select the theme in Settings (
Appearance -> Dark ThemeorAppearance -> Light Theme) depending on which auto slot you want.
Minimal skeleton:
{
"name": "my-theme",
"vars": {
"accent": "#7aa2f7",
"muted": 244
},
"colors": {
"accent": "accent",
"border": "#4c566a",
"borderAccent": "accent",
"borderMuted": "muted",
"success": "#9ece6a",
"error": "#f7768e",
"warning": "#e0af68",
"muted": "muted",
"dim": 240,
"text": "",
"thinkingText": "muted",
"selectedBg": "#2a2f45",
"userMessageBg": "#1f2335",
"userMessageText": "",
"customMessageBg": "#24283b",
"customMessageText": "",
"customMessageLabel": "accent",
"toolPendingBg": "#1f2335",
"toolSuccessBg": "#1f2d2a",
"toolErrorBg": "#2d1f2a",
"toolTitle": "",
"toolOutput": "muted",
"mdHeading": "accent",
"mdLink": "accent",
"mdLinkUrl": "muted",
"mdCode": "#c0caf5",
"mdCodeBlock": "#c0caf5",
"mdCodeBlockBorder": "muted",
"mdQuote": "muted",
"mdQuoteBorder": "muted",
"mdHr": "muted",
"mdListBullet": "accent",
"toolDiffAdded": "#9ece6a",
"toolDiffRemoved": "#f7768e",
"toolDiffContext": "muted",
"syntaxComment": "#565f89",
"syntaxKeyword": "#bb9af7",
"syntaxFunction": "#7aa2f7",
"syntaxVariable": "#c0caf5",
"syntaxString": "#9ece6a",
"syntaxNumber": "#ff9e64",
"syntaxType": "#2ac3de",
"syntaxOperator": "#89ddff",
"syntaxPunctuation": "#9aa5ce",
"thinkingOff": 240,
"thinkingMinimal": 244,
"thinkingLow": "#7aa2f7",
"thinkingMedium": "#2ac3de",
"thinkingHigh": "#bb9af7",
"thinkingXhigh": "#f7768e",
"bashMode": "#2ac3de",
"pythonMode": "#bb9af7",
"statusLineBg": "#16161e",
"statusLineSep": 240,
"statusLineModel": "#bb9af7",
"statusLinePath": "#7aa2f7",
"statusLineGitClean": "#9ece6a",
"statusLineGitDirty": "#e0af68",
"statusLineContext": "#2ac3de",
"statusLineSpend": "#7dcfff",
"statusLineStaged": "#9ece6a",
"statusLineDirty": "#e0af68",
"statusLineUntracked": "#f7768e",
"statusLineOutput": "#c0caf5",
"statusLineCost": "#ff9e64",
"statusLineSubagents": "#bb9af7"
}
}
Testing custom themes
Use this workflow:
- Start interactive mode (watcher enabled from startup).
- Open settings and preview theme values (live
previewTheme). - For custom theme files, edit the JSON while running and confirm auto-reload on save.
- Exercise critical surfaces:
- markdown rendering
- tool blocks (pending/success/error)
- diff rendering (added/removed/context)
- status line readability
- thinking level border changes
- bash/python mode border colors
- Validate both symbol presets if your theme depends on glyph width/appearance.
Real constraints and caveats
- All
colorstokens are required for custom themes. exportandsymbolsare optional.$schemain theme JSON is informational; runtime validation is enforced by an arktype schema in code.setThemefailure falls back todark;previewThemefailure does not replace current theme.- File watcher reload errors or temporary missing files keep the current loaded theme until a successful reload or explicit theme switch.