Fix lockstep versioning and improve documentation

- Sync all packages to version 0.7.7
- Rewrite sync-versions.js to handle ALL inter-package dependencies automatically
- Fix web-ui dependency on pi-ai (was 0.6.0, now 0.7.7)
- Move agent fix changelog entry to coding-agent CHANGELOG
- Remove redundant agent CHANGELOG.md
- Improve README.md with clearer lockstep versioning docs
- Add /changelog command to display full changelog in TUI (newest last)
- Fix changelog description (not a scrollable viewer, just displays in chat)
- Update CHANGELOG for 0.7.7 release
This commit is contained in:
Mario Zechner 2025-11-13 23:37:43 +01:00
parent 7b347291ff
commit bc670bc63c
17 changed files with 1721 additions and 186 deletions

View file

@ -1,10 +1,13 @@
# Changelog
## [0.7.7] - Unreleased
## [Unreleased]
## [0.7.7] - 2025-11-13
### Added
- Automatic changelog viewer on startup in interactive mode. When starting a new session (not continuing/resuming), the agent will display all changelog entries since the last version you used in a scrollable markdown viewer. The last shown version is tracked in `~/.pi/agent/settings.json`.
- Automatic changelog display on startup in interactive mode. When starting a new session (not continuing/resuming), the agent will display all changelog entries since the last version you used. The last shown version is tracked in `~/.pi/agent/settings.json`.
- `/changelog` command to display the changelog in the TUI
- OpenRouter Auto Router model support ([#5](https://github.com/badlogic/pi-mono/pull/5))
- Windows Git Bash support with automatic detection and process tree termination ([#1](https://github.com/badlogic/pi-mono/pull/1))
@ -18,6 +21,7 @@
- Fixed markdown list rendering bug where bullets were not displayed when list items contained inline code with cyan color formatting
- Fixed context percentage showing 0% in footer when last assistant message was aborted ([#12](https://github.com/badlogic/pi-mono/issues/12))
- Fixed error message loss when `turn_end` event contains an error. Previously, errors in `turn_end` events (e.g., "Provider returned error" from OpenRouter Auto Router) were not captured in `agent.state.error`, making it appear as if the agent completed successfully. ([#6](https://github.com/badlogic/pi-mono/issues/6))
## [0.7.6] - 2025-11-13

View file

@ -0,0 +1,112 @@
# 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

@ -0,0 +1,938 @@
# 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

@ -0,0 +1,182 @@
# 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,310 @@
# 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

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-coding-agent",
"version": "0.7.6",
"version": "0.7.7",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"bin": {
@ -21,8 +21,8 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-agent": "^0.7.5",
"@mariozechner/pi-ai": "^0.7.5",
"@mariozechner/pi-agent": "^0.7.7",
"@mariozechner/pi-ai": "^0.7.7",
"chalk": "^5.5.0",
"diff": "^8.0.2",
"glob": "^11.0.3"

View file

@ -12,8 +12,10 @@ import {
TUI,
} from "@mariozechner/pi-tui";
import chalk from "chalk";
import { getChangelogPath, getNewEntries, parseChangelog } from "../changelog.js";
import { exportSessionToHtml } from "../export-html.js";
import type { SessionManager } from "../session-manager.js";
import { SettingsManager } from "../settings-manager.js";
import { AssistantMessageComponent } from "./assistant-message.js";
import { CustomEditor } from "./custom-editor.js";
import { DynamicBorder } from "./dynamic-border.js";
@ -92,9 +94,14 @@ export class TuiRenderer {
description: "Show session info and stats",
};
const changelogCommand: SlashCommand = {
name: "changelog",
description: "Show changelog entries",
};
// Setup autocomplete for file paths and slash commands
const autocompleteProvider = new CombinedAutocompleteProvider(
[thinkingCommand, modelCommand, exportCommand, sessionCommand],
[thinkingCommand, modelCommand, exportCommand, sessionCommand, changelogCommand],
process.cwd(),
);
this.editor.setAutocompleteProvider(autocompleteProvider);
@ -194,6 +201,13 @@ export class TuiRenderer {
return;
}
// Check for /changelog command
if (text === "/changelog") {
this.handleChangelogCommand();
this.editor.setText("");
return;
}
if (this.onInputCallback) {
this.onInputCallback(text);
}
@ -648,6 +662,25 @@ export class TuiRenderer {
this.ui.requestRender();
}
private handleChangelogCommand(): void {
const changelogPath = getChangelogPath();
const allEntries = parseChangelog(changelogPath);
// Show all entries in reverse order (oldest first, newest last)
const changelogMarkdown =
allEntries.length > 0
? allEntries
.reverse()
.map((e) => e.content)
.join("\n\n")
: "No changelog entries found.";
// Display in chat
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Markdown(changelogMarkdown));
this.ui.requestRender();
}
stop(): void {
if (this.loadingAnimation) {
this.loadingAnimation.stop();