- Home
- Documentation
- tui
- Theming Reference
Theming Reference
Theming Reference
Section titled “Theming Reference”This document describes how theming works in the coding-agent today: schema, loading, runtime behavior, and failure modes.
What the theme system controls
Section titled “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 (
@f5xc-salesdemos/pi-natives) - status line segment colors
Primary implementation: src/modes/theme/theme.ts.
Theme JSON shape
Section titled “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)
Section titled “Required color tokens (current)”All tokens below are required in colors.
Core text and borders (11)
Section titled “Core text and borders (11)”accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText
Background blocks (7)
Section titled “Background blocks (7)”selectedBg, userMessageBg, customMessageBg, toolPendingBg, toolSuccessBg, toolErrorBg, statusLineBg
Message/tool text (5)
Section titled “Message/tool text (5)”userMessageText, customMessageText, customMessageLabel, toolTitle, toolOutput
Markdown (10)
Section titled “Markdown (10)”mdHeading, mdLink, mdLinkUrl, mdCode, mdCodeBlock, mdCodeBlockBorder, mdQuote, mdQuoteBorder, mdHr, mdListBullet
Tool diff + syntax highlighting (12)
Section titled “Tool diff + syntax highlighting (12)”toolDiffAdded, toolDiffRemoved, toolDiffContext,
syntaxComment, syntaxKeyword, syntaxFunction, syntaxVariable, syntaxString, syntaxNumber, syntaxType, syntaxOperator, syntaxPunctuation
Mode/thinking borders (8)
Section titled “Mode/thinking borders (8)”thinkingOff, thinkingMinimal, thinkingLow, thinkingMedium, thinkingHigh, thinkingXhigh, bashMode, pythonMode
Status line segment colors (14)
Section titled “Status line segment colors (14)”statusLineSep, statusLineModel, statusLinePath, statusLineGitClean, statusLineGitDirty, statusLineContext, statusLineSpend, statusLineStaged, statusLineDirty, statusLineUntracked, statusLineOutput, statusLineCost, statusLineSubagents
Optional tokens
Section titled “Optional tokens”export section (optional)
Section titled “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)
Section titled “symbols section (optional)”symbols.presetsets a theme-level default symbol set.symbols.overridescan override individualSymbolKeyvalues.
Runtime precedence:
- settings
symbolPresetoverride (if set) - theme JSON
symbols.preset - fallback
"unicode"
Invalid override keys are ignored and logged (logger.debug).
Built-in vs custom theme sources
Section titled “Built-in vs custom theme sources”Theme lookup order (loadThemeJson):
- built-in embedded themes (
defaults/xcsh-dark.jsonanddefaults/xcsh-light.jsoncompiled intodefaultThemes) - custom theme file:
<customThemesDir>/<name>.json
Custom themes directory comes from getCustomThemesDir():
- default:
~/.xcsh/agent/themes - overridden by
PI_CODING_AGENT_DIR($PI_CODING_AGENT_DIR/themes)
getAvailableThemes() returns merged built-in + custom names, sorted, with built-ins taking precedence on name collision.
Loading, validation, and resolution
Section titled “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
Section titled “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
Section titled “Runtime switching behavior”Initial theme (initTheme)
Section titled “Initial theme (initTheme)”main.ts initializes theme with settings:
symbolPresetcolorBlindModetheme.darktheme.light
Auto theme slot selection uses COLORFGBG background detection:
- parse background index from
COLORFGBG < 8=> dark slot (theme.dark)>= 8=> light slot (theme.light)- parse failure => dark slot
Current defaults from settings schema:
theme.dark = "xcsh-dark"theme.light = "xcsh-light"symbolPreset = "unicode"colorBlindMode = false
Explicit switching (setTheme)
Section titled “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)
Section titled “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
Section titled “Watchers and live reload”When watcher is enabled (setTheme(..., true) / interactive init):
- only watches custom file path
<customThemesDir>/<currentTheme>.json - built-ins are effectively not watched
- file
change: attempts reload (debounced) - file
rename/delete: falls back todark, closes watcher
Auto mode also installs a SIGWINCH listener and can re-evaluate dark/light slot mapping when terminal state changes.
Color-blind mode behavior
Section titled “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
Section titled “Where theme settings are persisted”Theme-related settings are persisted by Settings to global config YAML:
- path:
<agentDir>/config.yml - default agent dir:
~/.xcsh/agent - effective default file:
~/.xcsh/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)
Section titled “Creating a custom theme (practical)”- Create file in custom themes dir, e.g.
~/.xcsh/agent/themes/my-theme.json. - Include
name, optionalvars, and all requiredcolorstokens. - Optionally include
symbolsandexport. - Select the theme in Settings (
Display -> Dark themeorDisplay -> Light theme) depending on which auto slot you want.
Minimal skeleton. Every key in colors is required — the runtime validator
(additionalProperties: false) rejects both missing keys and unknown keys.
For the shipped reference implementations see
packages/coding-agent/src/modes/theme/defaults/xcsh-dark.json
and xcsh-light.json.
The status line has two parallel color systems documented in issue #242:
- Hex text colors (
statusLinePath,statusLineGitClean,statusLineGitDirty,statusLineStaged,statusLineDirty,statusLineUntracked) drive non-powerline rendering. - 256-color palette indices (
statusLine<Segment>Bg/statusLine<Segment>Fg) drive powerline segment fills. They are independent of the hex keys above — both must be set.
{ "name": "my-theme", "vars": { "accent": "#7aa2f7", "muted": 244 }, "colors": { "accent": "accent", "chromeAccent": "accent", "spinnerAccent": "accent", "contentAccent": "muted", "border": "#4c566a", "borderAccent": "accent", "borderMuted": "muted", "success": "#9ece6a", "error": "#f7768e", "warning": "#e0af68", "muted": "muted", "dim": 240, "gutterSuccess": "#7dcfff", "gutterWarning": "#e0af68", "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", "syntaxControl": "#bb9af7",
"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",
"statusLineOsIconBg": 7, "statusLineOsIconFg": 232, "statusLinePathBg": 4, "statusLinePathFg": 254, "statusLineGitCleanBg": 2, "statusLineGitCleanFg": 0, "statusLineGitDirtyBg": 3, "statusLineGitDirtyFg": 0, "statusLineGitStagedBg": 64, "statusLineGitStagedFg": 0, "statusLineGitUntrackedBg": 39, "statusLineGitUntrackedFg": 0, "statusLineGitConflictBg": 1, "statusLineGitConflictFg": 7, "statusLinePlanModeBg": 236, "statusLinePlanModeFg": 117, "statusLineProfileF5xcBg": "accent", "statusLineProfileF5xcFg": 231 }}Testing custom themes
Section titled “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
Section titled “Real constraints and caveats”- All
colorstokens are required for custom themes. exportandsymbolsare optional.$schemain theme JSON is informational; runtime validation is enforced by compiled TypeBox schema in code.setThemefailure falls back todark;previewThemefailure does not replace current theme.- File watcher reload errors keep the current loaded theme until a successful reload or fallback path is triggered.