Release v0.7.22

This commit is contained in:
Mario Zechner 2025-11-19 01:50:45 +01:00
parent 1b28780155
commit 1f68d6eb40
16 changed files with 649 additions and 1587 deletions

View file

@ -1,112 +0,0 @@
# Color Usage Inventory
## Complete list of all semantic color uses in the codebase
### UI Chrome & Structure
- **border** - cyan - Borders around sections (changelog, selectors)
- **borderSubtle** - blue - Borders in selectors (model, session, thinking)
- **borderHorizontal** - gray - Horizontal separator in editor
### Text Hierarchy
- **textPrimary** - default/none - Main content text
- **textSecondary** - gray - Metadata, timestamps, descriptions
- **textDim** - dim - De-emphasized content, placeholder text, "..." indicators
- **textBold** - bold - Emphasis (note: this is styling, not color)
### Interactive/Selection
- **selectionCursor** - blue - "" cursor in selection lists
- **selectionText** - bold+blue - Selected item text in session selector
- **selectionInfo** - gray - Scroll info "(1/10)" in selectors
- **checkmark** - green - "✓" checkmark for current model
- **providerBadge** - gray - "[anthropic]" provider labels
### Feedback/Status
- **error** - red - Error messages
- **errorAborted** - red - "Aborted" message
- **success** - green - Success messages (stdout)
- **warning** - yellow - Warning messages
- **info** - cyan - Info messages
### Tool Execution
- **toolCommand** - bold - "$ command" in tool execution
- **toolPath** - cyan - File paths in read tool
- **stdout** - green - Standard output lines
- **stderr** - red - Standard error lines
- **stdoutDim** - dim - Truncated stdout lines
- **stderrDim** - dim - Truncated stderr lines
### Footer/Stats
- **footerText** - gray - All footer content (pwd and stats)
### Logo/Branding
- **logoBrand** - bold+cyan - "pi" logo text
- **logoVersion** - dim - Version number
- **instructionsKey** - dim - Keyboard shortcut keys (esc, ctrl+c, etc.)
- **instructionsText** - gray - Instruction text ("to interrupt", etc.)
### Markdown - Headings
- **markdownH1** - bold+underline+yellow - Level 1 headings
- **markdownH2** - bold+yellow - Level 2 headings
- **markdownH3** - bold - Level 3+ headings (uses bold modifier only)
### Markdown - Emphasis
- **markdownBold** - bold - **bold** text
- **markdownItalic** - italic - *italic* text (also used for thinking text)
- **markdownStrikethrough** - strikethrough - ~~strikethrough~~ text
### Markdown - Code
- **markdownCodeBlock** - green - Code block content
- **markdownCodeBlockIndent** - dim - " " indent before code
- **markdownCodeDelimiter** - gray - "```" delimiters
- **markdownInlineCode** - cyan - `inline code` content
- **markdownInlineCodeDelimiter** - gray - "`" backticks
### Markdown - Links
- **markdownLinkText** - underline+blue - Link text
- **markdownLinkUrl** - gray - " (url)" when text != url
### Markdown - Lists
- **markdownListBullet** - cyan - "- " or "1. " bullets
### Markdown - Quotes
- **markdownQuoteText** - italic - Quoted text
- **markdownQuoteBorder** - gray - "│ " quote border
### Markdown - Other
- **markdownHr** - gray - "─────" horizontal rules
- **markdownTableHeader** - bold - Table header cells
### Loader/Spinner
- **spinnerFrame** - cyan - Spinner animation frame
- **spinnerMessage** - dim - Loading message text
## Summary Statistics
**Total semantic color uses: ~45**
### By Color
- gray: 15 uses (metadata, borders, delimiters, dim text)
- cyan: 9 uses (brand, borders, code, bullets)
- blue: 6 uses (selection, links, borders)
- red: 5 uses (errors, stderr)
- green: 4 uses (success, stdout, code blocks)
- yellow: 3 uses (headings, warnings)
- bold: 8 uses (emphasis, headings, commands)
- dim: 8 uses (de-emphasis, placeholders)
- italic: 3 uses (quotes, thinking, emphasis)
- underline: 2 uses (headings, links)
### By Category
- Markdown: 18 colors
- UI Chrome/Structure: 3 colors
- Text Hierarchy: 4 colors
- Interactive: 5 colors
- Feedback: 4 colors
- Tool Execution: 7 colors
- Footer: 1 color
- Logo/Instructions: 4 colors
- Loader: 2 colors
## Recommendation
We need approximately **35-40 distinct color values** for a complete theme, organized by semantic purpose. Some will be the same color (e.g., multiple uses of "gray"), but they should have separate semantic names so they can be customized independently.

View file

@ -1,938 +0,0 @@
# Design Tokens System
## Overview
A minimal design tokens system for terminal UI theming. Uses a two-layer approach:
1. **Primitive tokens** - Raw color values
2. **Semantic tokens** - Purpose-based mappings that reference primitives
## Architecture
### Primitive Tokens (Colors)
These are the raw chalk color functions - the "palette":
```typescript
interface ColorPrimitives {
// Grays
gray50: ChalkFunction; // Lightest gray
gray100: ChalkFunction;
gray200: ChalkFunction;
gray300: ChalkFunction;
gray400: ChalkFunction;
gray500: ChalkFunction; // Mid gray
gray600: ChalkFunction;
gray700: ChalkFunction;
gray800: ChalkFunction;
gray900: ChalkFunction; // Darkest gray
// Colors
blue: ChalkFunction;
cyan: ChalkFunction;
green: ChalkFunction;
yellow: ChalkFunction;
red: ChalkFunction;
magenta: ChalkFunction;
// Modifiers
bold: ChalkFunction;
dim: ChalkFunction;
italic: ChalkFunction;
underline: ChalkFunction;
strikethrough: ChalkFunction;
// Special
none: ChalkFunction; // Pass-through, no styling
}
type ChalkFunction = (str: string) => string;
```
### Semantic Tokens (Design Decisions)
These map primitives to purposes:
```typescript
interface SemanticTokens {
// Text hierarchy
text: {
primary: ChalkFunction; // Main content text
secondary: ChalkFunction; // Supporting text
tertiary: ChalkFunction; // De-emphasized text
disabled: ChalkFunction; // Inactive/disabled text
};
// Interactive elements
interactive: {
default: ChalkFunction; // Default interactive elements
hover: ChalkFunction; // Hovered/selected state
active: ChalkFunction; // Active/current state
};
// Feedback
feedback: {
error: ChalkFunction;
warning: ChalkFunction;
success: ChalkFunction;
info: ChalkFunction;
};
// Borders & dividers
border: {
default: ChalkFunction;
subtle: ChalkFunction;
emphasis: ChalkFunction;
};
// Code
code: {
text: ChalkFunction;
keyword: ChalkFunction;
string: ChalkFunction;
comment: ChalkFunction;
delimiter: ChalkFunction;
};
// Markdown specific
markdown: {
heading: {
h1: ChalkFunction;
h2: ChalkFunction;
h3: ChalkFunction;
};
emphasis: {
bold: ChalkFunction;
italic: ChalkFunction;
strikethrough: ChalkFunction;
};
link: {
text: ChalkFunction;
url: ChalkFunction;
};
quote: {
text: ChalkFunction;
border: ChalkFunction;
};
list: {
bullet: ChalkFunction;
};
code: {
inline: ChalkFunction;
inlineDelimiter: ChalkFunction;
block: ChalkFunction;
blockDelimiter: ChalkFunction;
};
};
// Output streams
output: {
stdout: ChalkFunction;
stderr: ChalkFunction;
neutral: ChalkFunction;
};
}
```
### Theme Structure
A theme combines primitives with semantic mappings:
```typescript
interface Theme {
name: string;
primitives: ColorPrimitives;
tokens: SemanticTokens;
}
```
## Built-in Themes
### Dark Theme
```typescript
const darkPrimitives: ColorPrimitives = {
// Grays - for dark backgrounds, lighter = more prominent
gray50: chalk.white,
gray100: (s) => s, // No color = terminal default
gray200: chalk.white,
gray300: (s) => s,
gray400: chalk.gray,
gray500: chalk.gray,
gray600: chalk.gray,
gray700: chalk.dim,
gray800: chalk.dim,
gray900: chalk.black,
// Colors
blue: chalk.blue,
cyan: chalk.cyan,
green: chalk.green,
yellow: chalk.yellow,
red: chalk.red,
magenta: chalk.magenta,
// Modifiers
bold: chalk.bold,
dim: chalk.dim,
italic: chalk.italic,
underline: chalk.underline,
strikethrough: chalk.strikethrough,
// Special
none: (s) => s,
};
const darkTheme: Theme = {
name: "dark",
primitives: darkPrimitives,
tokens: {
text: {
primary: darkPrimitives.gray100,
secondary: darkPrimitives.gray400,
tertiary: darkPrimitives.gray700,
disabled: darkPrimitives.dim,
},
interactive: {
default: darkPrimitives.blue,
hover: darkPrimitives.blue,
active: (s) => darkPrimitives.bold(darkPrimitives.blue(s)),
},
feedback: {
error: darkPrimitives.red,
warning: darkPrimitives.yellow,
success: darkPrimitives.green,
info: darkPrimitives.cyan,
},
border: {
default: darkPrimitives.blue,
subtle: darkPrimitives.gray600,
emphasis: darkPrimitives.cyan,
},
code: {
text: darkPrimitives.green,
keyword: darkPrimitives.cyan,
string: darkPrimitives.green,
comment: darkPrimitives.gray600,
delimiter: darkPrimitives.gray600,
},
markdown: {
heading: {
h1: (s) => darkPrimitives.underline(darkPrimitives.bold(darkPrimitives.yellow(s))),
h2: (s) => darkPrimitives.bold(darkPrimitives.yellow(s)),
h3: darkPrimitives.bold,
},
emphasis: {
bold: darkPrimitives.bold,
italic: darkPrimitives.italic,
strikethrough: darkPrimitives.strikethrough,
},
link: {
text: (s) => darkPrimitives.underline(darkPrimitives.blue(s)),
url: darkPrimitives.gray600,
},
quote: {
text: darkPrimitives.italic,
border: darkPrimitives.gray600,
},
list: {
bullet: darkPrimitives.cyan,
},
code: {
inline: darkPrimitives.cyan,
inlineDelimiter: darkPrimitives.gray600,
block: darkPrimitives.green,
blockDelimiter: darkPrimitives.gray600,
},
},
output: {
stdout: darkPrimitives.green,
stderr: darkPrimitives.red,
neutral: darkPrimitives.gray600,
},
},
};
```
### Light Theme
```typescript
const lightPrimitives: ColorPrimitives = {
// Grays - for light backgrounds, darker = more prominent
gray50: chalk.black,
gray100: (s) => s, // No color = terminal default
gray200: chalk.black,
gray300: (s) => s,
gray400: chalk.gray, // Use actual gray, not dim
gray500: chalk.gray,
gray600: chalk.gray,
gray700: chalk.gray,
gray800: chalk.gray,
gray900: chalk.white,
// Colors - use bold variants for better visibility on light bg
blue: (s) => chalk.bold(chalk.blue(s)),
cyan: (s) => chalk.bold(chalk.cyan(s)),
green: (s) => chalk.bold(chalk.green(s)),
yellow: (s) => chalk.bold(chalk.yellow(s)),
red: (s) => chalk.bold(chalk.red(s)),
magenta: (s) => chalk.bold(chalk.magenta(s)),
// Modifiers
bold: chalk.bold,
dim: chalk.gray, // Don't use chalk.dim on light bg!
italic: chalk.italic,
underline: chalk.underline,
strikethrough: chalk.strikethrough,
// Special
none: (s) => s,
};
const lightTheme: Theme = {
name: "light",
primitives: lightPrimitives,
tokens: {
text: {
primary: lightPrimitives.gray100,
secondary: lightPrimitives.gray400,
tertiary: lightPrimitives.gray600,
disabled: lightPrimitives.dim,
},
interactive: {
default: lightPrimitives.blue,
hover: lightPrimitives.blue,
active: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
},
feedback: {
error: lightPrimitives.red,
warning: (s) => chalk.bold(chalk.yellow(s)), // Yellow needs extra bold
success: lightPrimitives.green,
info: lightPrimitives.cyan,
},
border: {
default: lightPrimitives.blue,
subtle: lightPrimitives.gray400,
emphasis: lightPrimitives.cyan,
},
code: {
text: lightPrimitives.green,
keyword: lightPrimitives.cyan,
string: lightPrimitives.green,
comment: lightPrimitives.gray600,
delimiter: lightPrimitives.gray600,
},
markdown: {
heading: {
h1: (s) => lightPrimitives.underline(lightPrimitives.bold(lightPrimitives.blue(s))),
h2: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
h3: lightPrimitives.bold,
},
emphasis: {
bold: lightPrimitives.bold,
italic: lightPrimitives.italic,
strikethrough: lightPrimitives.strikethrough,
},
link: {
text: (s) => lightPrimitives.underline(lightPrimitives.blue(s)),
url: lightPrimitives.blue,
},
quote: {
text: lightPrimitives.italic,
border: lightPrimitives.gray600,
},
list: {
bullet: lightPrimitives.blue,
},
code: {
inline: lightPrimitives.blue,
inlineDelimiter: lightPrimitives.gray600,
block: lightPrimitives.green,
blockDelimiter: lightPrimitives.gray600,
},
},
output: {
stdout: lightPrimitives.green,
stderr: lightPrimitives.red,
neutral: lightPrimitives.gray600,
},
},
};
```
## Usage Examples
### Simple Text Styling
```typescript
const theme = getTheme();
// Before
console.log(chalk.gray("Secondary text"));
// After
console.log(theme.tokens.text.secondary("Secondary text"));
```
### Interactive Elements
```typescript
const theme = getTheme();
// Before
const cursor = chalk.blue(" ");
// After
const cursor = theme.tokens.interactive.default(" ");
```
### Error Messages
```typescript
const theme = getTheme();
// Before
this.contentContainer.addChild(new Text(chalk.red("Error: " + errorMsg)));
// After
this.contentContainer.addChild(new Text(theme.tokens.feedback.error("Error: " + errorMsg)));
```
### Markdown Headings
```typescript
const theme = getTheme();
// Before
lines.push(chalk.bold.yellow(headingText));
// After
lines.push(theme.tokens.markdown.heading.h2(headingText));
```
### Borders
```typescript
const theme = getTheme();
// Before
this.addChild(new Text(chalk.blue("─".repeat(80))));
// After
this.addChild(new Text(theme.tokens.border.default("─".repeat(80))));
```
## User Configuration
### Theme File Format
Themes can be defined in JSON files that users can customize. The system will load themes from:
1. Built-in themes (dark, light) - hardcoded in the app
2. User themes in `~/.pi/agent/themes/` directory
**Example: `~/.pi/agent/themes/my-theme.json`**
```json
{
"name": "my-theme",
"extends": "dark",
"primitives": {
"blue": "blueBright",
"cyan": "cyanBright",
"green": "greenBright"
},
"tokens": {
"text": {
"primary": "white"
},
"interactive": {
"default": ["bold", "blue"]
},
"markdown": {
"heading": {
"h1": ["bold", "underline", "magenta"],
"h2": ["bold", "magenta"]
}
}
}
}
```
### JSON Schema
Themes in JSON can reference:
1. **Chalk color names**: `"red"`, `"blue"`, `"gray"`, `"white"`, `"black"`, etc.
2. **Chalk bright colors**: `"redBright"`, `"blueBright"`, etc.
3. **Chalk modifiers**: `"bold"`, `"dim"`, `"italic"`, `"underline"`, `"strikethrough"`
4. **Combinations**: `["bold", "blue"]` or `["underline", "bold", "cyan"]`
5. **Primitive references**: `"$gray400"` to reference another primitive
6. **None/passthrough**: `"none"` or `""` for no styling
### Supported Chalk Values
```typescript
type ChalkColorName =
// Basic colors
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
// Bright variants
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
// Modifiers
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
// Special
| "none";
type ChalkValue = ChalkColorName | ChalkColorName[] | string; // string allows "$primitive" refs
```
### Theme Extension
Themes can extend other themes using `"extends": "dark"` or `"extends": "light"`. Only the overridden values need to be specified.
**Example: Minimal override**
```json
{
"name": "solarized-dark",
"extends": "dark",
"tokens": {
"feedback": {
"error": "magenta",
"warning": "yellow"
},
"markdown": {
"heading": {
"h1": ["bold", "cyan"],
"h2": ["bold", "blue"]
}
}
}
}
```
### Loading Order
1. Load built-in themes (dark, light)
2. Scan `~/.pi/agent/themes/*.json`
3. Parse and validate each JSON theme
4. Build theme by:
- Start with base theme (if extends specified)
- Apply primitive overrides
- Apply token overrides
- Convert JSON values to chalk functions
## Implementation
### Theme Module Structure
**Location:** `packages/tui/src/theme/`
```
theme/
├── index.ts # Public API
├── types.ts # Type definitions
├── primitives.ts # Color primitives for each theme
├── tokens.ts # Semantic token mappings
├── themes.ts # Built-in theme definitions
├── registry.ts # Theme management (current, set, get)
├── loader.ts # JSON theme loader
└── parser.ts # JSON to ChalkFunction converter
```
### Public API
```typescript
// packages/tui/src/theme/index.ts
export { type Theme, type SemanticTokens, type ColorPrimitives } from './types.js';
export { darkTheme, lightTheme } from './themes.js';
export { getTheme, setTheme, getThemeNames } from './registry.js';
```
### Theme Registry
```typescript
// packages/tui/src/theme/registry.ts
import { darkTheme, lightTheme } from './themes.js';
import type { Theme } from './types.js';
const themes = new Map<string, Theme>([
['dark', darkTheme],
['light', lightTheme],
]);
let currentTheme: Theme = darkTheme;
export function getTheme(): Theme {
return currentTheme;
}
export function setTheme(name: string): void {
const theme = themes.get(name);
if (!theme) {
throw new Error(`Theme "${name}" not found`);
}
currentTheme = theme;
}
export function getThemeNames(): string[] {
return Array.from(themes.keys());
}
export function registerTheme(theme: Theme): void {
themes.set(theme.name, theme);
}
export function getThemeByName(name: string): Theme | undefined {
return themes.get(name);
}
```
### JSON Theme Parser
```typescript
// packages/tui/src/theme/parser.ts
import chalk from 'chalk';
import type { ChalkFunction } from './types.js';
type ChalkColorName =
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
| "none";
type JsonThemeValue = ChalkColorName | ChalkColorName[] | string;
interface JsonTheme {
name: string;
extends?: string;
primitives?: Record<string, JsonThemeValue>;
tokens?: any; // Partial<SemanticTokens> but with JsonThemeValue instead of ChalkFunction
}
// Map chalk color names to actual chalk functions
const chalkMap: Record<ChalkColorName, any> = {
black: chalk.black,
red: chalk.red,
green: chalk.green,
yellow: chalk.yellow,
blue: chalk.blue,
magenta: chalk.magenta,
cyan: chalk.cyan,
white: chalk.white,
gray: chalk.gray,
blackBright: chalk.blackBright,
redBright: chalk.redBright,
greenBright: chalk.greenBright,
yellowBright: chalk.yellowBright,
blueBright: chalk.blueBright,
magentaBright: chalk.magentaBright,
cyanBright: chalk.cyanBright,
whiteBright: chalk.whiteBright,
bold: chalk.bold,
dim: chalk.dim,
italic: chalk.italic,
underline: chalk.underline,
strikethrough: chalk.strikethrough,
inverse: chalk.inverse,
none: (s: string) => s,
};
export function parseThemeValue(
value: JsonThemeValue,
primitives?: Record<string, ChalkFunction>
): ChalkFunction {
// Handle primitive reference: "$gray400"
if (typeof value === 'string' && value.startsWith('
## Migration Strategy
### Phase 1: Infrastructure
1. Create theme module with types, primitives, and built-in themes
2. Export from `@mariozechner/pi-tui`
3. Add tests for theme functions
### Phase 2: Component Migration (Priority Order)
1. **Markdown** (biggest impact, 50+ color calls)
2. **ToolExecution** (stdout/stderr readability)
3. **SelectList** (used everywhere)
4. **Footer** (always visible)
5. **TuiRenderer** (logo, instructions)
6. Other components
### Phase 3: Persistence & UI
1. Add theme to SettingsManager
2. Create ThemeSelector component
3. Add `/theme` slash command
4. Initialize theme on startup
### Example Migration
**Before:**
```typescript
// markdown.ts
if (headingLevel === 1) {
lines.push(chalk.bold.underline.yellow(headingText));
} else if (headingLevel === 2) {
lines.push(chalk.bold.yellow(headingText));
} else {
lines.push(chalk.bold(headingPrefix + headingText));
}
```
**After:**
```typescript
// markdown.ts
import { getTheme } from '@mariozechner/pi-tui/theme';
const theme = getTheme();
if (headingLevel === 1) {
lines.push(theme.tokens.markdown.heading.h1(headingText));
} else if (headingLevel === 2) {
lines.push(theme.tokens.markdown.heading.h2(headingText));
} else {
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
}
```
## Benefits of This Approach
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
2. **Maintainable**: Change all headings by editing one token mapping
3. **Extensible**: Easy to add new themes without touching components
4. **Type-safe**: Full TypeScript support
5. **Testable**: Can test themes independently
6. **Minimal**: Only what we need, no over-engineering
7. **Composable**: Can chain primitives (bold + underline + color)
## Key Differences from Themes.md
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
- **Composability**: Can combine primitive modifiers
- **Better light theme**: Properly handles chalk.dim and color visibility issues
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
- **Easier to extend**: Add new token without changing primitives
- **Better for sharing**: Could export just primitives for custom themes
)) {
const primitiveName = value.slice(1);
if (primitives && primitives[primitiveName]) {
return primitives[primitiveName];
}
throw new Error(`Primitive reference "${value}" not found`);
}
// Handle array of chalk names (composition): ["bold", "blue"]
if (Array.isArray(value)) {
return (str: string) => {
let result = str;
for (const name of value) {
const chalkFn = chalkMap[name as ChalkColorName];
if (!chalkFn) {
throw new Error(`Unknown chalk function: ${name}`);
}
result = chalkFn(result);
}
return result;
};
}
// Handle single chalk name: "blue"
if (typeof value === 'string') {
const chalkFn = chalkMap[value as ChalkColorName];
if (!chalkFn) {
throw new Error(`Unknown chalk function: ${value}`);
}
return chalkFn;
}
throw new Error(`Invalid theme value: ${JSON.stringify(value)}`);
}
// Deep merge objects, used for extending themes
function deepMerge(target: any, source: any): any {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
export function parseJsonTheme(json: JsonTheme, baseTheme?: Theme): Theme {
// Start with base theme if extending
let primitives: Record<string, ChalkFunction> = {};
let tokens: any = {};
if (json.extends && baseTheme) {
// Copy base theme primitives and tokens
primitives = { ...baseTheme.primitives };
tokens = deepMerge({}, baseTheme.tokens);
}
// Parse and override primitives
if (json.primitives) {
for (const [key, value] of Object.entries(json.primitives)) {
primitives[key] = parseThemeValue(value, primitives);
}
}
// Parse and override tokens (recursive)
if (json.tokens) {
const parsedTokens = parseTokens(json.tokens, primitives);
tokens = deepMerge(tokens, parsedTokens);
}
return {
name: json.name,
primitives,
tokens,
};
}
function parseTokens(obj: any, primitives: Record<string, ChalkFunction>): any {
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
// Nested object, recurse
result[key] = parseTokens(value, primitives);
} else {
// Leaf value, parse it
result[key] = parseThemeValue(value as JsonThemeValue, primitives);
}
}
return result;
}
```
### JSON Theme Loader
```typescript
// packages/tui/src/theme/loader.ts
import { existsSync, readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { parseJsonTheme } from './parser.js';
import { getThemeByName, registerTheme } from './registry.js';
import type { Theme } from './types.js';
export function loadUserThemes(themesDir: string): Theme[] {
const themes: Theme[] = [];
if (!existsSync(themesDir)) {
return themes;
}
const files = readdirSync(themesDir).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const content = readFileSync(join(themesDir, file), 'utf-8');
const json = JSON.parse(content);
// Get base theme if extending
let baseTheme: Theme | undefined;
if (json.extends) {
baseTheme = getThemeByName(json.extends);
if (!baseTheme) {
console.warn(`Theme ${json.name} extends unknown theme "${json.extends}", skipping`);
continue;
}
}
const theme = parseJsonTheme(json, baseTheme);
registerTheme(theme);
themes.push(theme);
} catch (error) {
console.error(`Failed to load theme from ${file}:`, error);
}
}
return themes;
}
```
## Migration Strategy
### Phase 1: Infrastructure
1. Create theme module with types, primitives, and built-in themes
2. Export from `@mariozechner/pi-tui`
3. Add tests for theme functions
### Phase 2: Component Migration (Priority Order)
1. **Markdown** (biggest impact, 50+ color calls)
2. **ToolExecution** (stdout/stderr readability)
3. **SelectList** (used everywhere)
4. **Footer** (always visible)
5. **TuiRenderer** (logo, instructions)
6. Other components
### Phase 3: Persistence & UI
1. Add theme to SettingsManager
2. Create ThemeSelector component
3. Add `/theme` slash command
4. Initialize theme on startup
### Example Migration
**Before:**
```typescript
// markdown.ts
if (headingLevel === 1) {
lines.push(chalk.bold.underline.yellow(headingText));
} else if (headingLevel === 2) {
lines.push(chalk.bold.yellow(headingText));
} else {
lines.push(chalk.bold(headingPrefix + headingText));
}
```
**After:**
```typescript
// markdown.ts
import { getTheme } from '@mariozechner/pi-tui/theme';
const theme = getTheme();
if (headingLevel === 1) {
lines.push(theme.tokens.markdown.heading.h1(headingText));
} else if (headingLevel === 2) {
lines.push(theme.tokens.markdown.heading.h2(headingText));
} else {
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
}
```
## Benefits of This Approach
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
2. **Maintainable**: Change all headings by editing one token mapping
3. **Extensible**: Easy to add new themes without touching components
4. **Type-safe**: Full TypeScript support
5. **Testable**: Can test themes independently
6. **Minimal**: Only what we need, no over-engineering
7. **Composable**: Can chain primitives (bold + underline + color)
## Key Differences from Themes.md
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
- **Composability**: Can combine primitive modifiers
- **Better light theme**: Properly handles chalk.dim and color visibility issues
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
- **Easier to extend**: Add new token without changing primitives
- **Better for sharing**: Could export just primitives for custom themes

View file

@ -1,182 +0,0 @@
# Minimal Theme Color Set
## Complete list of required theme colors
Based on analysis of all color usage in the codebase.
### Text Hierarchy (3 colors)
- **textPrimary** - Main content text (default terminal color)
- **textSecondary** - Metadata, supporting text
- **textTertiary** - De-emphasized text (dimmed/muted)
### UI Chrome (4 colors)
- **border** - Primary borders (around changelog, selectors)
- **borderSubtle** - Subtle borders/separators
- **uiBackground** - General UI background elements
- **scrollInfo** - Scroll position indicators like "(1/10)"
### Interactive Elements (4 colors)
- **interactionDefault** - Default interactive state (unselected)
- **interactionHover** - Hovered/focused state
- **interactionActive** - Currently active/selected item
- **interactionSuccess** - Success indicator (checkmarks)
### Feedback/Status (4 colors)
- **feedbackError** - Errors, failures
- **feedbackSuccess** - Success, completed
- **feedbackWarning** - Warnings, cautions
- **feedbackInfo** - Informational messages
### Branding (2 colors)
- **brandPrimary** - Logo, primary brand color
- **brandSecondary** - Secondary brand elements
### Tool Execution (6 colors + 3 backgrounds)
- **toolCommand** - Command text in tool headers
- **toolPath** - File paths
- **toolStdout** - Standard output
- **toolStderr** - Standard error
- **toolDimmed** - Truncated/hidden lines
- **toolNeutral** - Neutral tool output
- **toolBgPending** - Background for pending tool execution
- **toolBgSuccess** - Background for successful tool execution
- **toolBgError** - Background for failed tool execution
### Markdown - Structure (5 colors)
- **mdHeading1** - H1 headings
- **mdHeading2** - H2 headings
- **mdHeading3** - H3+ headings
- **mdHr** - Horizontal rules
- **mdTable** - Table borders and structure
### Markdown - Code (4 colors)
- **mdCodeBlock** - Code block content
- **mdCodeBlockDelimiter** - Code block ``` delimiters
- **mdCodeInline** - Inline `code` content
- **mdCodeInlineDelimiter** - Inline code ` backticks
### Markdown - Lists & Quotes (3 colors)
- **mdListBullet** - List bullets (- or 1.)
- **mdQuoteText** - Blockquote text
- **mdQuoteBorder** - Blockquote border (│)
### Markdown - Links (2 colors)
- **mdLinkText** - Link text
- **mdLinkUrl** - Link URL in parentheses
### Backgrounds (2 colors)
- **bgUserMessage** - Background for user messages
- **bgDefault** - Default/transparent background
### Special/Optional (2 colors)
- **spinner** - Loading spinner animation
- **thinking** - Thinking/reasoning text
## Total: 44 colors
### Grouped by Common Values
Many of these will share the same value. Typical groupings:
**"Secondary" family** (gray-ish):
- textSecondary
- textTertiary
- borderSubtle
- scrollInfo
- toolDimmed
- mdHr
- mdCodeBlockDelimiter
- mdCodeInlineDelimiter
- mdQuoteBorder
- mdLinkUrl
**"Primary accent" family** (blue-ish):
- border
- interactionDefault
- interactionHover
- interactionActive
- brandPrimary
- mdLinkText
**"Success" family** (green-ish):
- feedbackSuccess
- interactionSuccess
- toolStdout
- mdCodeBlock
**"Error" family** (red-ish):
- feedbackError
- toolStderr
**"Code/Tech" family** (cyan-ish):
- brandPrimary
- mdCodeInline
- mdListBullet
- spinner
**"Emphasis" family** (yellow-ish):
- mdHeading1
- mdHeading2
- feedbackWarning
## Simplified Minimal Set (Alternative)
If we want to reduce further, we could consolidate to ~25 colors by using more shared values:
### Core Colors (8)
- **text** - Primary text
- **textMuted** - Secondary/dimmed text
- **accent** - Primary accent (blue)
- **accentSubtle** - Subtle accent
- **success** - Green
- **error** - Red
- **warning** - Yellow
- **info** - Cyan
### Backgrounds (4)
- **bgDefault** - Transparent/default
- **bgUserMessage** - User message background
- **bgSuccess** - Success state background
- **bgError** - Error state background
### Specialized (13)
- **border** - Primary borders
- **borderSubtle** - Subtle borders
- **selection** - Selected items
- **brand** - Brand/logo color
- **mdHeading** - All headings (or separate h1/h2)
- **mdCode** - All code (blocks + inline)
- **mdCodeDelimiter** - Code delimiters
- **mdList** - List bullets
- **mdLink** - Links
- **mdQuote** - Quotes
- **toolCommand** - Command text
- **toolPath** - File paths
- **spinner** - Loading indicator
**Total: 25 colors** (vs 44 in the detailed version)
## Recommendation
Start with the **44-color detailed set** because:
1. Gives maximum flexibility for theming
2. Each has a clear semantic purpose
3. Themes can set many to the same value if desired
4. Easier to add granular control than to split apart later
Users creating themes can start by setting common values and override specific ones:
```json
{
"name": "my-theme",
"_comment": "Set common values first",
"textSecondary": "gray",
"textTertiary": "gray",
"borderSubtle": "gray",
"mdCodeBlockDelimiter": "gray",
"_comment": "Then override specific ones",
"mdHeading1": "yellow",
"error": "red"
}
```

View file

@ -0,0 +1,563 @@
# 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 (9 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 |
| `text` | Default text color | Main content (usually `""`) |
### Backgrounds & Content Text (6 colors)
| Token | Purpose |
|-------|---------|
| `userMessageBg` | User message background |
| `userMessageText` | User message text color |
| `toolPendingBg` | Tool execution box (pending state) |
| `toolSuccessBg` | Tool execution box (success state) |
| `toolErrorBg` | Tool execution box (error state) |
| `toolText` | Tool execution box text color (all states) |
### Markdown (9 colors)
| Token | Purpose |
|-------|---------|
| `mdHeading` | Heading text (`#`, `##`, etc) |
| `mdLink` | Link text and URLs |
| `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) |
**Total: 36 color tokens** (all required)
## Theme Format
Themes are defined in JSON files with the following structure:
```json
{
"$schema": "https://pi.mariozechner.at/theme-schema.json",
"name": "my-theme",
"vars": {
"blue": "#0066cc",
"gray": 242,
"brightCyan": 51
},
"colors": {
"accent": "blue",
"muted": "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://pi.mariozechner.at/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"
}
}
```
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://pi.mariozechner.at/theme-schema.json
```
Add to your theme file for auto-completion and validation:
```json
{
"$schema": "https://pi.mariozechner.at/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
dim(text: string): string
italic(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;
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;
}
export class Markdown {
constructor(
text: string,
paddingX: number,
paddingY: number,
defaultTextStyle?: DefaultTextStyle,
theme?: MarkdownTheme // Optional theme functions
)
// Usage in component
renderHeading(text: string) {
return this.theme.heading(text); // Applies color
}
}
```
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),
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),
};
}
// 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.dim(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.

View file

@ -1,310 +0,0 @@
# Theme System Analysis
## Problem Statement
Issue #7: In terminals with light backgrounds, some outputs use dark colors that are hard to read. We need a theme system that allows users to choose between light and dark themes.
## Current Color Usage Analysis
### Color Usage Statistics
Total chalk color calls: 132 across 14 files
Most frequent colors:
- `chalk.dim` (48 occurrences) - Used for secondary text
- `chalk.gray` (28 occurrences) - Used for borders, metadata, dimmed content
- `chalk.bold` (20 occurrences) - Used for emphasis
- `chalk.blue` (12 occurrences) - Used for selections, borders, links
- `chalk.cyan` (9 occurrences) - Used for primary UI elements (logo, list bullets, code)
- `chalk.red` (7 occurrences) - Used for errors, stderr output
- `chalk.green` (6 occurrences) - Used for success, stdout output
- `chalk.yellow` (3 occurrences) - Used for headings in markdown
- `chalk.bgRgb` (6 occurrences) - Used for custom backgrounds in Text/Markdown
### Files Using Colors
#### coding-agent Package
1. **main.ts** - CLI output messages
2. **tui/assistant-message.ts** - Thinking text (gray italic), errors (red), aborted (red)
3. **tui/dynamic-border.ts** - Configurable border color (default blue)
4. **tui/footer.ts** - Stats and pwd (gray)
5. **tui/model-selector.ts** - Borders (blue), selection arrow (blue), provider badge (gray), checkmark (green)
6. **tui/session-selector.ts** - Border (blue), selection cursor (blue), metadata (dim)
7. **tui/thinking-selector.ts** - Border (blue)
8. **tui/tool-execution.ts** - stdout (green), stderr (red), dim lines (dim), line numbers
9. **tui/tui-renderer.ts** - Logo (bold cyan), instructions (dim/gray)
#### tui Package
1. **components/editor.ts** - Horizontal border (gray)
2. **components/loader.ts** - Spinner (cyan), message (dim)
3. **components/markdown.ts** - Complex color system:
- H1 headings: bold.underline.yellow
- H2 headings: bold.yellow
- H3+ headings: bold
- Code blocks: gray (delimiters), dim (indent), green (code)
- List bullets: cyan
- Blockquotes: gray (pipe), italic (text)
- Horizontal rules: gray
- Inline code: gray (backticks), cyan (code)
- Links: underline.blue (text), gray (URL)
- Strikethrough: strikethrough
- Tables: bold (headers)
4. **components/select-list.ts** - No matches (gray), selection arrow (blue), selected item (blue), description (gray)
5. **components/text.ts** - Custom bgRgb support
### Color System Architecture
#### Current Implementation
- Colors are hardcoded using `chalk` directly
- No centralized theme management
- No way to switch themes at runtime
- Some components accept color parameters (e.g., DynamicBorder, Text, Markdown)
#### Markdown Component Color System
The Markdown component has a `Color` type enum:
```typescript
type Color = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" |
"bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray"
```
It accepts optional `bgColor` and `fgColor` parameters, plus `customBgRgb`.
## Proposed Solution
### Theme Structure
Create a centralized theme system with semantic color names:
```typescript
interface Theme {
name: string;
// UI Chrome
border: ChalkFunction;
selection: ChalkFunction;
selectionText: ChalkFunction;
// Text hierarchy
primary: ChalkFunction;
secondary: ChalkFunction;
dim: ChalkFunction;
// Semantic colors
error: ChalkFunction;
success: ChalkFunction;
warning: ChalkFunction;
info: ChalkFunction;
// Code/output
code: ChalkFunction;
codeDelimiter: ChalkFunction;
stdout: ChalkFunction;
stderr: ChalkFunction;
// Markdown specific
heading1: ChalkFunction;
heading2: ChalkFunction;
heading3: ChalkFunction;
link: ChalkFunction;
linkUrl: ChalkFunction;
listBullet: ChalkFunction;
blockquote: ChalkFunction;
blockquotePipe: ChalkFunction;
inlineCode: ChalkFunction;
inlineCodeDelimiter: ChalkFunction;
// Backgrounds (optional, for components like Text/Markdown)
backgroundRgb?: { r: number; g: number; b: number };
}
type ChalkFunction = (str: string) => string;
```
### Built-in Themes
#### Dark Theme (current default)
```typescript
const darkTheme: Theme = {
name: "dark",
border: chalk.blue,
selection: chalk.blue,
selectionText: chalk.blue,
primary: (s) => s, // no color
secondary: chalk.gray,
dim: chalk.dim,
error: chalk.red,
success: chalk.green,
warning: chalk.yellow,
info: chalk.cyan,
code: chalk.green,
codeDelimiter: chalk.gray,
stdout: chalk.green,
stderr: chalk.red,
heading1: chalk.bold.underline.yellow,
heading2: chalk.bold.yellow,
heading3: chalk.bold,
link: chalk.underline.blue,
linkUrl: chalk.gray,
listBullet: chalk.cyan,
blockquote: chalk.italic,
blockquotePipe: chalk.gray,
inlineCode: chalk.cyan,
inlineCodeDelimiter: chalk.gray,
};
```
#### Light Theme
```typescript
const lightTheme: Theme = {
name: "light",
border: chalk.blue,
selection: chalk.blue,
selectionText: chalk.blue.bold,
primary: (s) => s,
secondary: chalk.gray,
dim: chalk.gray, // Don't use chalk.dim on light backgrounds
error: chalk.red.bold,
success: chalk.green.bold,
warning: chalk.yellow.bold,
info: chalk.cyan.bold,
code: chalk.green.bold,
codeDelimiter: chalk.gray,
stdout: chalk.green.bold,
stderr: chalk.red.bold,
heading1: chalk.bold.underline.blue,
heading2: chalk.bold.blue,
heading3: chalk.bold,
link: chalk.underline.blue,
linkUrl: chalk.blue,
listBullet: chalk.blue.bold,
blockquote: chalk.italic,
blockquotePipe: chalk.gray,
inlineCode: chalk.blue.bold,
inlineCodeDelimiter: chalk.gray,
};
```
### Implementation Plan
#### 1. Create Theme Module
**Location:** `packages/tui/src/theme.ts`
```typescript
export interface Theme { ... }
export const darkTheme: Theme = { ... };
export const lightTheme: Theme = { ... };
export const themes = { dark: darkTheme, light: lightTheme };
let currentTheme: Theme = darkTheme;
export function setTheme(theme: Theme): void {
currentTheme = theme;
}
export function getTheme(): Theme {
return currentTheme;
}
```
#### 2. Update Settings Manager
**Location:** `packages/coding-agent/src/settings-manager.ts`
Add `theme` field to Settings interface:
```typescript
export interface Settings {
lastChangelogVersion?: string;
theme?: "dark" | "light";
}
```
#### 3. Create Theme Selector Component
**Location:** `packages/coding-agent/src/tui/theme-selector.ts`
Similar to ModelSelector and ThinkingSelector, create a TUI component for selecting themes.
#### 4. Refactor Color Usage
Replace all hardcoded `chalk.*` calls with `theme.*`:
**Example - Before:**
```typescript
lines.push(chalk.blue("─".repeat(width)));
const cursor = chalk.blue(" ");
```
**Example - After:**
```typescript
const theme = getTheme();
lines.push(theme.border("─".repeat(width)));
const cursor = theme.selection(" ");
```
#### 5. Update Components
##### High Priority (User-facing content issues)
1. **markdown.ts** - Update all color calls to use theme
2. **tool-execution.ts** - stdout/stderr colors
3. **assistant-message.ts** - Error messages
4. **tui-renderer.ts** - Logo and instructions
5. **footer.ts** - Stats display
##### Medium Priority (UI chrome)
6. **dynamic-border.ts** - Accept theme parameter
7. **model-selector.ts** - Selection colors
8. **session-selector.ts** - Selection colors
9. **thinking-selector.ts** - Border colors
10. **select-list.ts** - Selection colors
11. **loader.ts** - Spinner color
12. **editor.ts** - Border color
##### Low Priority (CLI output)
13. **main.ts** - CLI messages
#### 6. Add Theme Slash Command
**Location:** `packages/coding-agent/src/tui/tui-renderer.ts`
Add `/theme` command similar to `/model` and `/thinking`.
#### 7. Initialize Theme on Startup
**Location:** `packages/coding-agent/src/main.ts`
```typescript
// Load theme from settings
const settingsManager = new SettingsManager();
const themeName = settingsManager.getTheme() || "dark";
const theme = themes[themeName] || darkTheme;
setTheme(theme);
```
### Migration Strategy
1. **Phase 1:** Create theme infrastructure (theme.ts, types, built-in themes)
2. **Phase 2:** Update TUI package components (markdown, text, loader, editor, select-list)
3. **Phase 3:** Update coding-agent TUI components (all tui/*.ts files)
4. **Phase 4:** Add theme selector and persistence
5. **Phase 5:** Update CLI output in main.ts (optional, low priority)
### Testing Plan
1. Test both themes in terminals with light backgrounds
2. Test both themes in terminals with dark backgrounds
3. Verify theme switching works at runtime via `/theme`
4. Verify theme persists across sessions via settings.json
5. Test all components for readability in both themes
### Open Questions
1. Should we support custom user themes loaded from a JSON file?
2. Should we auto-detect terminal background color and choose theme automatically?
3. Should theme apply to background colors used in Text/Markdown components?
4. Do we need more than two themes initially?
### Breaking Changes
None - the default theme will remain "dark" matching current behavior.
### Performance Considerations
- Theme getter is called frequently (on every render)
- Should be a simple variable access, not a function call chain
- Consider caching theme functions if performance becomes an issue