Skip to content

Theming Reference

This document describes how theming works in the coding-agent today: schema, loading, runtime behavior, and failure modes.

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 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 for SymbolKey)

Color values accept:

  • hex string ("#RRGGBB")
  • 256-color index (0..255)
  • variable reference string (resolved through vars)
  • empty string ("") meaning terminal default (\x1b[39m fg, \x1b[49m bg)

All tokens below are required in colors.

accent, border, borderAccent, borderMuted, success, error, warning, muted, dim, text, thinkingText

selectedBg, userMessageBg, customMessageBg, toolPendingBg, toolSuccessBg, toolErrorBg, statusLineBg

userMessageText, customMessageText, customMessageLabel, toolTitle, toolOutput

mdHeading, mdLink, mdLinkUrl, mdCode, mdCodeBlock, mdCodeBlockBorder, mdQuote, mdQuoteBorder, mdHr, mdListBullet

toolDiffAdded, toolDiffRemoved, toolDiffContext, syntaxComment, syntaxKeyword, syntaxFunction, syntaxVariable, syntaxString, syntaxNumber, syntaxType, syntaxOperator, syntaxPunctuation

thinkingOff, thinkingMinimal, thinkingLow, thinkingMedium, thinkingHigh, thinkingXhigh, bashMode, pythonMode

statusLineSep, statusLineModel, statusLinePath, statusLineGitClean, statusLineGitDirty, statusLineContext, statusLineSpend, statusLineStaged, statusLineDirty, statusLineUntracked, statusLineOutput, statusLineCost, statusLineSubagents

Used for HTML export theming helpers:

  • export.pageBg
  • export.cardBg
  • export.infoBg

If omitted, export code derives defaults from resolved theme colors.

  • symbols.preset sets a theme-level default symbol set.
  • symbols.overrides can override individual SymbolKey values.

Runtime precedence:

  1. settings symbolPreset override (if set)
  2. theme JSON symbols.preset
  3. fallback "unicode"

Invalid override keys are ignored and logged (logger.debug).

Theme lookup order (loadThemeJson):

  1. built-in embedded themes (defaults/xcsh-dark.json and defaults/xcsh-light.json compiled into defaultThemes)
  2. 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.

For custom theme files:

  1. read JSON
  2. parse JSON
  3. validate against ThemeJsonSchema
  4. resolve vars references recursively
  5. 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

Color mode detection (detectColorMode):

  • COLORTERM=truecolor|24bit => truecolor
  • WT_SESSION => truecolor
  • TERM in dumb, linux, or empty => 256color
  • otherwise => truecolor

Conversion behavior:

  • hex -> Bun.color(..., "ansi-16m" | "ansi-256")
  • numeric -> 38;5 / 48;5 ANSI
  • "" -> default fg/bg reset

main.ts initializes theme with settings:

  • symbolPreset
  • colorBlindMode
  • theme.dark
  • theme.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
  • loads selected theme
  • updates global theme singleton
  • optionally starts watcher
  • triggers onThemeChange callback

On failure:

  • falls back to built-in dark
  • returns { success: false, error }
  • 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.

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 to dark, closes watcher

Auto mode also installs a SIGWINCH listener and can re-evaluate dark/light slot mapping when terminal state changes.

colorBlindMode changes only one token at runtime:

  • toolDiffAdded is HSV-adjusted (green shifted toward blue)
  • adjustment is applied only when resolved value is a hex string

Other tokens are unchanged.

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.dark
  • theme.light
  • symbolPreset
  • colorBlindMode

Legacy migration exists: old flat theme: "name" is migrated to nested theme.dark or theme.light based on luminance detection.

  1. Create file in custom themes dir, e.g. ~/.xcsh/agent/themes/my-theme.json.
  2. Include name, optional vars, and all required colors tokens.
  3. Optionally include symbols and export.
  4. Select the theme in Settings (Display -> Dark theme or Display -> 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
}
}

Use this workflow:

  1. Start interactive mode (watcher enabled from startup).
  2. Open settings and preview theme values (live previewTheme).
  3. For custom theme files, edit the JSON while running and confirm auto-reload on save.
  4. 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
  5. Validate both symbol presets if your theme depends on glyph width/appearance.
  • All colors tokens are required for custom themes.
  • export and symbols are optional.
  • $schema in theme JSON is informational; runtime validation is enforced by compiled TypeBox schema in code.
  • setTheme failure falls back to dark; previewTheme failure does not replace current theme.
  • File watcher reload errors keep the current loaded theme until a successful reload or fallback path is triggered.