MusePi

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

The theme system drives:

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:

Color values accept:

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

export section (optional)

Used for HTML export theming helpers:

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

symbols section (optional)

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).

Box-drawing borders

All outlined chrome — tool-result frames, overlays, code fences, the editor, the welcome banner — draws with the boxRound.* tokens: rounded corners (╭╮╰╯) plus tee/cross junctions (├┤┬┴┼, which have no rounded Unicode form, so they are sourced from the boxSharp.* tokens). Markdown tables are the sole exception and keep the fully sharp boxSharp.* set (┌┐└┘).

Override behavior follows from that split:

Built-in vs custom theme sources

Theme lookup order (loadThemeJson):

  1. built-in embedded themes (dark.json, light.json, and all defaults/*.json compiled into defaultThemes)
  2. custom theme file: <customThemesDir>/<name>.json

Custom themes directory comes from getCustomThemesDir():

getAvailableThemes() returns merged built-in + custom names, sorted, with built-ins taking precedence on name collision.

Loading, validation, and resolution

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:

Var reference behavior:

Terminal color mode behavior

Color mode detection (detectColorMode):

Conversion behavior:

Runtime switching behavior

Initial theme (initTheme)

main.ts initializes theme with settings:

Auto theme slot selection uses terminal appearance in this order:

  1. terminal-reported OSC 11 background luminance, unless the macOS/Zellij fallback path is active
  2. COLORFGBG background index (< 8 => dark, >= 8 => light)
  3. macOS appearance fallback only for the known-broken macOS/Zellij OSC 11 path
  4. dark slot fallback

Current defaults from settings schema:

Explicit switching (setTheme)

On failure:

Preview switching (previewTheme)

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):

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:

Other tokens are unchanged.

Where theme settings are persisted

Theme-related settings are persisted by Settings to global config YAML:

Persisted keys:

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)

  1. Create file in custom themes dir, e.g. ~/.musepi/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 (Appearance -> Dark Theme or Appearance -> 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:

  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.

Real constraints and caveats