co-mono/packages/coding-agent/docs/theme.md
Mario Zechner 6267720660 Add theme-configurable HTML export colors (from PR #387)
- Add optional 'export' section to theme JSON with pageBg, cardBg, infoBg
- If not specified, colors are auto-derived from userMessageBg
- Add export colors to dark.json and light.json
- Update theme-schema.json and TypeBox schema
- Add documentation to docs/theme.md
- Add margin-top back to tool-output for spacing between header and content
2026-01-01 22:21:40 +01:00

617 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

> 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)
### HTML Export Colors (optional)
The `export` section is optional and controls colors used when exporting sessions to HTML via `/export`. If not specified, these colors are automatically derived from `userMessageBg` based on luminance detection.
| Token | Purpose |
|-------|---------|
| `pageBg` | Page background color |
| `cardBg` | Card/container background (headers, stats boxes) |
| `infoBg` | Info sections background (system prompt, notices, compaction) |
Example:
```json
{
"export": {
"pageBg": "#18181e",
"cardBg": "#1e1e24",
"infoBg": "#3c3728"
}
}
```
## Theme Format
Themes are defined in JSON files with the following structure:
```json
{
"$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:
```json
{
"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:
```json
{
"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`):
```json
{
"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:**
```bash
mkdir -p ~/.pi/agent/themes
```
2. **Create theme file:**
```bash
vim ~/.pi/agent/themes/my-theme.json
```
3. **Define all colors:**
```json
{
"$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:
```json
{
"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:
```bash
echo $COLORTERM # Should output "truecolor" or "24bit"
```
## Example Themes
See the built-in themes for complete examples:
- [Dark theme](../src/themes/dark.json)
- [Light theme](../src/themes/light.json)
## Schema Validation
Themes are validated on load using [TypeBox](https://github.com/sinclairzx81/typebox) + [Ajv](https://ajv.js.org/).
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:
```json
{
"$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:
```typescript
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`:
```typescript
// 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:
```typescript
// 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:
```typescript
// 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:**
```typescript
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:**
```json
{
"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:**
```typescript
// 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.