co-mono/packages/coding-agent/docs/theme.md
Mario Zechner 506e63a969 Add thinkingText theme token, fix streaming toggle bug
- Add configurable thinkingText color for thinking blocks (defaults to muted)
- Make 'Thinking...' label italic when collapsed
- Fix Ctrl+T during streaming hiding the current message
- Track streamingMessage to properly re-render on toggle

Based on #366 by @paulbettner
2026-01-01 00:25:04 +01:00

16 KiB
Raw Blame History

pi can create themes. Ask it to build one for your use case.

Pi Coding Agent Themes

Themes allow you to customize the colors used throughout the coding agent TUI.

Color Tokens

Every theme must define all color tokens. There are no optional colors.

Core UI (10 colors)

Token Purpose Examples
accent Primary accent color Logo, selected items, cursor ()
border Normal borders Selector borders, horizontal lines
borderAccent Highlighted borders Changelog borders, special panels
borderMuted Subtle borders Editor borders, secondary separators
success Success states Success messages, diff additions
error Error states Error messages, diff deletions
warning Warning states Warning messages
muted Secondary/dimmed text Metadata, descriptions, output
dim Very dimmed text Less important info, placeholders
text Default text color Main content (usually "")
thinkingText Thinking block text Assistant reasoning traces

Backgrounds & Content Text (11 colors)

Token Purpose
selectedBg Selected/active line background (e.g., tree selector)
userMessageBg User message background
userMessageText User message text color
customMessageBg Hook custom message background
customMessageText Hook custom message text color
customMessageLabel Hook custom message label/type text
toolPendingBg Tool execution box (pending state)
toolSuccessBg Tool execution box (success state)
toolErrorBg Tool execution box (error state)
toolTitle Tool execution title/heading (e.g., $ command, read file.txt)
toolOutput Tool execution output text

Markdown (10 colors)

Token Purpose
mdHeading Heading text (#, ##, etc)
mdLink Link text
mdLinkUrl Link URL (in parentheses)
mdCode Inline code (backticks)
mdCodeBlock Code block content
mdCodeBlockBorder Code block fences (```)
mdQuote Blockquote text
mdQuoteBorder Blockquote border ()
mdHr Horizontal rule (---)
mdListBullet List bullets/numbers

Tool Diffs (3 colors)

Token Purpose
toolDiffAdded Added lines in tool diffs
toolDiffRemoved Removed lines in tool diffs
toolDiffContext Context lines in tool diffs

Note: Diff colors are specific to tool execution boxes and must work with tool background colors.

Syntax Highlighting (9 colors)

Future-proofing for syntax highlighting support:

Token Purpose
syntaxComment Comments
syntaxKeyword Keywords (if, function, etc)
syntaxFunction Function names
syntaxVariable Variable names
syntaxString String literals
syntaxNumber Number literals
syntaxType Type names
syntaxOperator Operators (+, -, etc)
syntaxPunctuation Punctuation (;, ,, etc)

Thinking Level Borders (6 colors)

Editor border colors that indicate the current thinking/reasoning level:

Token Purpose
thinkingOff Border when thinking is off (most subtle)
thinkingMinimal Border for minimal thinking
thinkingLow Border for low thinking
thinkingMedium Border for medium thinking
thinkingHigh Border for high thinking
thinkingXhigh Border for xhigh thinking (most prominent, OpenAI codex-max only)

These create a visual hierarchy: off → minimal → low → medium → high → xhigh

Bash Mode (1 color)

Token Purpose
bashMode Editor border color when in bash mode (! prefix)

Total: 50 color tokens (all required)

Theme Format

Themes are defined in JSON files with the following structure:

{
  "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
  "name": "my-theme",
  "vars": {
    "blue": "#0066cc",
    "gray": 242,
    "brightCyan": 51
  },
  "colors": {
    "accent": "blue",
    "muted": "gray",
    "thinkingText": "gray",
    "text": "",
    ...
  }
}

Color Values

Four formats are supported:

  1. Hex colors: "#ff0000" (6-digit hex RGB)
  2. 256-color palette: 39 (number 0-255, xterm 256-color palette)
  3. Color references: "blue" (must be defined in vars)
  4. Terminal default: "" (empty string, uses terminal's default color)

The vars Section

The optional vars section allows you to define reusable colors:

{
  "vars": {
    "nord0": "#2E3440",
    "nord1": "#3B4252",
    "nord8": "#88C0D0",
    "brightBlue": 39
  },
  "colors": {
    "accent": "nord8",
    "muted": "nord1",
    "mdLink": "brightBlue"
  }
}

Benefits:

  • Reuse colors across multiple tokens
  • Easier to maintain theme consistency
  • Can reference standard color palettes

Variables can be hex colors ("#ff0000"), 256-color indices (42), or references to other variables.

Terminal Default (empty string)

Use "" (empty string) to inherit the terminal's default foreground/background color:

{
  "colors": {
    "text": ""  // Uses terminal's default text color
  }
}

This is useful for:

  • Main text color (adapts to user's terminal theme)
  • Creating themes that blend with terminal appearance

Built-in Themes

Pi comes with two built-in themes:

dark (default)

Optimized for dark terminal backgrounds with bright, saturated colors.

light

Optimized for light terminal backgrounds with darker, muted colors.

Selecting a Theme

Themes are configured in the settings (accessible via /settings):

{
  "theme": "dark"
}

Or use the /theme command interactively.

On first run, Pi detects your terminal's background and sets a sensible default (dark or light).

Custom Themes

Theme Locations

Custom themes are loaded from ~/.pi/agent/themes/*.json.

Creating a Custom Theme

  1. Create theme directory:

    mkdir -p ~/.pi/agent/themes
    
  2. Create theme file:

    vim ~/.pi/agent/themes/my-theme.json
    
  3. Define all colors:

    {
      "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
      "name": "my-theme",
      "vars": {
        "primary": "#00aaff",
        "secondary": 242,
        "brightGreen": 46
      },
      "colors": {
        "accent": "primary",
        "border": "primary",
        "borderAccent": "#00ffff",
        "borderMuted": "secondary",
        "success": "brightGreen",
        "error": "#ff0000",
        "warning": "#ffff00",
        "muted": "secondary",
        "text": "",
    
        "userMessageBg": "#2d2d30",
        "userMessageText": "",
        "toolPendingBg": "#1e1e2e",
        "toolSuccessBg": "#1e2e1e",
        "toolErrorBg": "#2e1e1e",
        "toolText": "",
    
        "mdHeading": "#ffaa00",
        "mdLink": "primary",
        "mdCode": "#00ffff",
        "mdCodeBlock": "#00ff00",
        "mdCodeBlockBorder": "secondary",
        "mdQuote": "secondary",
        "mdQuoteBorder": "secondary",
        "mdHr": "secondary",
        "mdListBullet": "#00ffff",
    
        "toolDiffAdded": "#00ff00",
        "toolDiffRemoved": "#ff0000",
        "toolDiffContext": "secondary",
    
        "syntaxComment": "secondary",
        "syntaxKeyword": "primary",
        "syntaxFunction": "#00aaff",
        "syntaxVariable": "#ffaa00",
        "syntaxString": "#00ff00",
        "syntaxNumber": "#ff00ff",
        "syntaxType": "#00aaff",
        "syntaxOperator": "primary",
        "syntaxPunctuation": "secondary",
    
        "thinkingOff": "secondary",
        "thinkingMinimal": "primary",
        "thinkingLow": "#00aaff",
        "thinkingMedium": "#00ffff",
        "thinkingHigh": "#ff00ff"
      }
    }
    
  4. Select your theme:

    • Use /settings command and set "theme": "my-theme"
    • Or use /theme command interactively

Tips

Light vs Dark Themes

For dark terminals:

  • Use bright, saturated colors
  • Higher contrast
  • Example: #00ffff (bright cyan)

For light terminals:

  • Use darker, muted colors
  • Lower contrast to avoid eye strain
  • Example: #008888 (dark cyan)

Color Harmony

  • Start with a base palette (e.g., Nord, Gruvbox, Tokyo Night)
  • Define your palette in defs
  • Reference colors consistently

Testing

Test your theme with:

  • Different message types (user, assistant, errors)
  • Tool executions (success and error states)
  • Markdown content (headings, code, lists, etc)
  • Long text that wraps

Color Format Reference

Hex Colors

Standard 6-digit hex format:

  • "#ff0000" - Red
  • "#00ff00" - Green
  • "#0000ff" - Blue
  • "#808080" - Gray
  • "#ffffff" - White
  • "#000000" - Black

RGB values: #RRGGBB where each component is 00-ff (0-255)

256-Color Palette

Use numeric indices (0-255) to reference the xterm 256-color palette:

Colors 0-15: Basic ANSI colors (terminal-dependent, may be themed)

  • 0 - Black
  • 1 - Red
  • 2 - Green
  • 3 - Yellow
  • 4 - Blue
  • 5 - Magenta
  • 6 - Cyan
  • 7 - White
  • 8-15 - Bright variants

Colors 16-231: 6×6×6 RGB cube (standardized)

  • Formula: 16 + 36×R + 6×G + B where R, G, B are 0-5
  • Example: 39 = bright cyan, 196 = bright red

Colors 232-255: Grayscale ramp (standardized)

  • 232 - Darkest gray
  • 255 - Near white

Example usage:

{
  "vars": {
    "gray": 242,
    "brightCyan": 51,
    "darkBlue": 18
  },
  "colors": {
    "muted": "gray",
    "accent": "brightCyan"
  }
}

Benefits:

  • Works everywhere (TERM=xterm-256color)
  • No truecolor detection needed
  • Standardized RGB cube (16-231) looks the same on all terminals

Terminal Compatibility

Pi uses 24-bit RGB colors (\x1b[38;2;R;G;Bm). Most modern terminals support this:

  • iTerm2, Alacritty, Kitty, WezTerm
  • Windows Terminal
  • VS Code integrated terminal
  • Modern GNOME Terminal, Konsole

For older terminals with only 256-color support, Pi automatically falls back to the nearest 256-color approximation.

To check if your terminal supports truecolor:

echo $COLORTERM  # Should output "truecolor" or "24bit"

Example Themes

See the built-in themes for complete examples:

Schema Validation

Themes are validated on load using TypeBox + Ajv.

Invalid themes will show an error with details about what's wrong:

Error loading theme 'my-theme':
  - colors.accent: must be string or number
  - colors.mdHeading: required property missing

For editor support, the JSON schema is available at:

https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json

Add to your theme file for auto-completion and validation:

{
  "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
  ...
}

Implementation

Theme Class

Themes are loaded and converted to a Theme class that provides type-safe color methods:

class Theme {
  // Apply foreground color
  fg(color: ThemeColor, text: string): string
  
  // Apply background color
  bg(color: ThemeBg, text: string): string
  
  // Text attributes (preserve current colors)
  bold(text: string): string
  italic(text: string): string
  underline(text: string): string
}

Global Theme Instance

The active theme is available as a global singleton in coding-agent:

// theme.ts
export let theme: Theme;

export function setTheme(name: string) {
  theme = loadTheme(name);
}

// Usage throughout coding-agent
import { theme } from './theme.js';

theme.fg('accent', 'Selected')
theme.bg('userMessageBg', content)

TUI Component Theming

TUI components (like Markdown, SelectList, Editor) are in the @mariozechner/pi-tui package and don't have direct access to the theme. Instead, they define interfaces for the colors they need:

// In @mariozechner/pi-tui
export interface MarkdownTheme {
  heading: (text: string) => string;
  link: (text: string) => string;
  linkUrl: (text: string) => string;
  code: (text: string) => string;
  codeBlock: (text: string) => string;
  codeBlockBorder: (text: string) => string;
  quote: (text: string) => string;
  quoteBorder: (text: string) => string;
  hr: (text: string) => string;
  listBullet: (text: string) => string;
  bold: (text: string) => string;
  italic: (text: string) => string;
  strikethrough: (text: string) => string;
  underline: (text: string) => string;
}

The coding-agent provides themed functions when creating components:

// In coding-agent
import { theme } from './theme.js';
import { Markdown } from '@mariozechner/pi-tui';

// Helper to create markdown theme functions
function getMarkdownTheme(): MarkdownTheme {
  return {
    heading: (text) => theme.fg('mdHeading', text),
    link: (text) => theme.fg('mdLink', text),
    linkUrl: (text) => theme.fg('mdLinkUrl', text),
    code: (text) => theme.fg('mdCode', text),
    codeBlock: (text) => theme.fg('mdCodeBlock', text),
    codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text),
    quote: (text) => theme.fg('mdQuote', text),
    quoteBorder: (text) => theme.fg('mdQuoteBorder', text),
    hr: (text) => theme.fg('mdHr', text),
    listBullet: (text) => theme.fg('mdListBullet', text),
    bold: (text) => theme.bold(text),
    italic: (text) => theme.italic(text),
    underline: (text) => theme.underline(text),
    strikethrough: (text) => chalk.strikethrough(text),
  };
}

// Create markdown with theme
const md = new Markdown(
  text,
  1, 1,
  { bgColor: theme.bg('userMessageBg') },
  getMarkdownTheme()
);

This approach:

  • Keeps TUI components theme-agnostic (reusable in other projects)
  • Maintains type safety via interfaces
  • Allows components to have sensible defaults if no theme provided
  • Centralizes theme access in coding-agent

Example usage:

const theme = loadTheme('dark');

// Apply foreground colors
theme.fg('accent', 'Selected')
theme.fg('success', '✓ Done')
theme.fg('error', 'Failed')

// Apply background colors
theme.bg('userMessageBg', content)
theme.bg('toolSuccessBg', output)

// Combine styles
theme.bold(theme.fg('accent', 'Title'))
theme.italic(theme.fg('muted', 'metadata'))

// Nested foreground + background
const userMsg = theme.bg('userMessageBg',
  theme.fg('userMessageText', 'Hello')
)

Color resolution:

  1. Detect terminal capabilities:

    • Check $COLORTERM env var (truecolor or 24bit → truecolor support)
    • Check $TERM env var (*-256color → 256-color support)
    • Fallback to 256-color mode if detection fails
  2. Load JSON theme file

  3. Resolve vars references recursively:

    {
      "vars": {
        "primary": "#0066cc",
        "accent": "primary"
      },
      "colors": {
        "accent": "accent"  // → "primary" → "#0066cc"
      }
    }
    
  4. Convert colors to ANSI codes based on terminal capability:

    Truecolor mode (24-bit):

    • Hex ("#ff0000") → \x1b[38;2;255;0;0m
    • 256-color (42) → \x1b[38;5;42m (keep as-is)
    • Empty string ("") → \x1b[39m

    256-color mode:

    • Hex ("#ff0000") → convert to nearest RGB cube color → \x1b[38;5;196m
    • 256-color (42) → \x1b[38;5;42m (keep as-is)
    • Empty string ("") → \x1b[39m

    Hex to 256-color conversion:

    // Convert RGB to 6x6x6 cube (colors 16-231)
    r_index = Math.round(r / 255 * 5)
    g_index = Math.round(g / 255 * 5)
    b_index = Math.round(b / 255 * 5)
    color_index = 16 + 36 * r_index + 6 * g_index + b_index
    
  5. Cache as Theme instance

This ensures themes work correctly regardless of terminal capabilities, with graceful degradation from truecolor to 256-color.