mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 10:05:14 +00:00
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:
parent
7b347291ff
commit
bc670bc63c
17 changed files with 1721 additions and 186 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
112
packages/coding-agent/docs/color-inventory.md
Normal file
112
packages/coding-agent/docs/color-inventory.md
Normal 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.
|
||||
938
packages/coding-agent/docs/design-tokens.md
Normal file
938
packages/coding-agent/docs/design-tokens.md
Normal 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
|
||||
182
packages/coding-agent/docs/theme-colors.md
Normal file
182
packages/coding-agent/docs/theme-colors.md
Normal 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"
|
||||
}
|
||||
```
|
||||
310
packages/coding-agent/docs/themes.md
Normal file
310
packages/coding-agent/docs/themes.md
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue