mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 08:00:59 +00:00
Release v0.7.22
This commit is contained in:
parent
1b28780155
commit
1f68d6eb40
16 changed files with 649 additions and 1587 deletions
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -3195,11 +3195,11 @@
|
||||||
},
|
},
|
||||||
"packages/agent": {
|
"packages/agent": {
|
||||||
"name": "@mariozechner/pi-agent",
|
"name": "@mariozechner/pi-agent",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.7.20",
|
"@mariozechner/pi-ai": "^0.7.21",
|
||||||
"@mariozechner/pi-tui": "^0.7.20"
|
"@mariozechner/pi-tui": "^0.7.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
|
@ -3225,7 +3225,7 @@
|
||||||
},
|
},
|
||||||
"packages/ai": {
|
"packages/ai": {
|
||||||
"name": "@mariozechner/pi-ai",
|
"name": "@mariozechner/pi-ai",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.61.0",
|
"@anthropic-ai/sdk": "^0.61.0",
|
||||||
|
|
@ -3272,11 +3272,11 @@
|
||||||
},
|
},
|
||||||
"packages/coding-agent": {
|
"packages/coding-agent": {
|
||||||
"name": "@mariozechner/pi-coding-agent",
|
"name": "@mariozechner/pi-coding-agent",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.20",
|
"@mariozechner/pi-agent": "^0.7.21",
|
||||||
"@mariozechner/pi-ai": "^0.7.20",
|
"@mariozechner/pi-ai": "^0.7.21",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^11.0.3"
|
"glob": "^11.0.3"
|
||||||
|
|
@ -3319,10 +3319,10 @@
|
||||||
},
|
},
|
||||||
"packages/pods": {
|
"packages/pods": {
|
||||||
"name": "@mariozechner/pi",
|
"name": "@mariozechner/pi",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.20",
|
"@mariozechner/pi-agent": "^0.7.21",
|
||||||
"chalk": "^5.5.0"
|
"chalk": "^5.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -3345,7 +3345,7 @@
|
||||||
},
|
},
|
||||||
"packages/proxy": {
|
"packages/proxy": {
|
||||||
"name": "@mariozechner/pi-proxy",
|
"name": "@mariozechner/pi-proxy",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.14.0",
|
"@hono/node-server": "^1.14.0",
|
||||||
"hono": "^4.6.16"
|
"hono": "^4.6.16"
|
||||||
|
|
@ -3361,7 +3361,7 @@
|
||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@mariozechner/pi-tui",
|
"name": "@mariozechner/pi-tui",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
|
|
@ -3400,12 +3400,12 @@
|
||||||
},
|
},
|
||||||
"packages/web-ui": {
|
"packages/web-ui": {
|
||||||
"name": "@mariozechner/pi-web-ui",
|
"name": "@mariozechner/pi-web-ui",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
"@mariozechner/pi-ai": "^0.7.20",
|
"@mariozechner/pi-ai": "^0.7.21",
|
||||||
"@mariozechner/pi-tui": "^0.7.20",
|
"@mariozechner/pi-tui": "^0.7.21",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.544.0",
|
"lucide": "^0.544.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-agent",
|
"name": "@mariozechner/pi-agent",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.7.21",
|
"@mariozechner/pi-ai": "^0.7.22",
|
||||||
"@mariozechner/pi-tui": "^0.7.21"
|
"@mariozechner/pi-tui": "^0.7.22"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-ai",
|
"name": "@mariozechner/pi-ai",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -3572,23 +3572,6 @@ export const MODELS = {
|
||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
maxTokens: 40000,
|
maxTokens: 40000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"google/gemini-2.5-flash-lite-preview-06-17": {
|
|
||||||
id: "google/gemini-2.5-flash-lite-preview-06-17",
|
|
||||||
name: "Google: Gemini 2.5 Flash Lite Preview 06-17",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text", "image"],
|
|
||||||
cost: {
|
|
||||||
input: 0.09999999999999999,
|
|
||||||
output: 0.39999999999999997,
|
|
||||||
cacheRead: 0.024999999999999998,
|
|
||||||
cacheWrite: 0.18330000000000002,
|
|
||||||
},
|
|
||||||
contextWindow: 1048576,
|
|
||||||
maxTokens: 65535,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"google/gemini-2.5-flash": {
|
"google/gemini-2.5-flash": {
|
||||||
id: "google/gemini-2.5-flash",
|
id: "google/gemini-2.5-flash",
|
||||||
name: "Google: Gemini 2.5 Flash",
|
name: "Google: Gemini 2.5 Flash",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.7.22] - 2025-11-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Long Line Wrapping**: Fixed crash when rendering long lines without spaces (e.g., file paths). Long words now break character-by-character to fit within terminal width.
|
||||||
|
|
||||||
## [0.7.21] - 2025-11-19
|
## [0.7.21] - 2025-11-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
# Color Usage Inventory
|
|
||||||
|
|
||||||
## Complete list of all semantic color uses in the codebase
|
|
||||||
|
|
||||||
### UI Chrome & Structure
|
|
||||||
- **border** - cyan - Borders around sections (changelog, selectors)
|
|
||||||
- **borderSubtle** - blue - Borders in selectors (model, session, thinking)
|
|
||||||
- **borderHorizontal** - gray - Horizontal separator in editor
|
|
||||||
|
|
||||||
### Text Hierarchy
|
|
||||||
- **textPrimary** - default/none - Main content text
|
|
||||||
- **textSecondary** - gray - Metadata, timestamps, descriptions
|
|
||||||
- **textDim** - dim - De-emphasized content, placeholder text, "..." indicators
|
|
||||||
- **textBold** - bold - Emphasis (note: this is styling, not color)
|
|
||||||
|
|
||||||
### Interactive/Selection
|
|
||||||
- **selectionCursor** - blue - "›" cursor in selection lists
|
|
||||||
- **selectionText** - bold+blue - Selected item text in session selector
|
|
||||||
- **selectionInfo** - gray - Scroll info "(1/10)" in selectors
|
|
||||||
- **checkmark** - green - "✓" checkmark for current model
|
|
||||||
- **providerBadge** - gray - "[anthropic]" provider labels
|
|
||||||
|
|
||||||
### Feedback/Status
|
|
||||||
- **error** - red - Error messages
|
|
||||||
- **errorAborted** - red - "Aborted" message
|
|
||||||
- **success** - green - Success messages (stdout)
|
|
||||||
- **warning** - yellow - Warning messages
|
|
||||||
- **info** - cyan - Info messages
|
|
||||||
|
|
||||||
### Tool Execution
|
|
||||||
- **toolCommand** - bold - "$ command" in tool execution
|
|
||||||
- **toolPath** - cyan - File paths in read tool
|
|
||||||
- **stdout** - green - Standard output lines
|
|
||||||
- **stderr** - red - Standard error lines
|
|
||||||
- **stdoutDim** - dim - Truncated stdout lines
|
|
||||||
- **stderrDim** - dim - Truncated stderr lines
|
|
||||||
|
|
||||||
### Footer/Stats
|
|
||||||
- **footerText** - gray - All footer content (pwd and stats)
|
|
||||||
|
|
||||||
### Logo/Branding
|
|
||||||
- **logoBrand** - bold+cyan - "pi" logo text
|
|
||||||
- **logoVersion** - dim - Version number
|
|
||||||
- **instructionsKey** - dim - Keyboard shortcut keys (esc, ctrl+c, etc.)
|
|
||||||
- **instructionsText** - gray - Instruction text ("to interrupt", etc.)
|
|
||||||
|
|
||||||
### Markdown - Headings
|
|
||||||
- **markdownH1** - bold+underline+yellow - Level 1 headings
|
|
||||||
- **markdownH2** - bold+yellow - Level 2 headings
|
|
||||||
- **markdownH3** - bold - Level 3+ headings (uses bold modifier only)
|
|
||||||
|
|
||||||
### Markdown - Emphasis
|
|
||||||
- **markdownBold** - bold - **bold** text
|
|
||||||
- **markdownItalic** - italic - *italic* text (also used for thinking text)
|
|
||||||
- **markdownStrikethrough** - strikethrough - ~~strikethrough~~ text
|
|
||||||
|
|
||||||
### Markdown - Code
|
|
||||||
- **markdownCodeBlock** - green - Code block content
|
|
||||||
- **markdownCodeBlockIndent** - dim - " " indent before code
|
|
||||||
- **markdownCodeDelimiter** - gray - "```" delimiters
|
|
||||||
- **markdownInlineCode** - cyan - `inline code` content
|
|
||||||
- **markdownInlineCodeDelimiter** - gray - "`" backticks
|
|
||||||
|
|
||||||
### Markdown - Links
|
|
||||||
- **markdownLinkText** - underline+blue - Link text
|
|
||||||
- **markdownLinkUrl** - gray - " (url)" when text != url
|
|
||||||
|
|
||||||
### Markdown - Lists
|
|
||||||
- **markdownListBullet** - cyan - "- " or "1. " bullets
|
|
||||||
|
|
||||||
### Markdown - Quotes
|
|
||||||
- **markdownQuoteText** - italic - Quoted text
|
|
||||||
- **markdownQuoteBorder** - gray - "│ " quote border
|
|
||||||
|
|
||||||
### Markdown - Other
|
|
||||||
- **markdownHr** - gray - "─────" horizontal rules
|
|
||||||
- **markdownTableHeader** - bold - Table header cells
|
|
||||||
|
|
||||||
### Loader/Spinner
|
|
||||||
- **spinnerFrame** - cyan - Spinner animation frame
|
|
||||||
- **spinnerMessage** - dim - Loading message text
|
|
||||||
|
|
||||||
## Summary Statistics
|
|
||||||
|
|
||||||
**Total semantic color uses: ~45**
|
|
||||||
|
|
||||||
### By Color
|
|
||||||
- gray: 15 uses (metadata, borders, delimiters, dim text)
|
|
||||||
- cyan: 9 uses (brand, borders, code, bullets)
|
|
||||||
- blue: 6 uses (selection, links, borders)
|
|
||||||
- red: 5 uses (errors, stderr)
|
|
||||||
- green: 4 uses (success, stdout, code blocks)
|
|
||||||
- yellow: 3 uses (headings, warnings)
|
|
||||||
- bold: 8 uses (emphasis, headings, commands)
|
|
||||||
- dim: 8 uses (de-emphasis, placeholders)
|
|
||||||
- italic: 3 uses (quotes, thinking, emphasis)
|
|
||||||
- underline: 2 uses (headings, links)
|
|
||||||
|
|
||||||
### By Category
|
|
||||||
- Markdown: 18 colors
|
|
||||||
- UI Chrome/Structure: 3 colors
|
|
||||||
- Text Hierarchy: 4 colors
|
|
||||||
- Interactive: 5 colors
|
|
||||||
- Feedback: 4 colors
|
|
||||||
- Tool Execution: 7 colors
|
|
||||||
- Footer: 1 color
|
|
||||||
- Logo/Instructions: 4 colors
|
|
||||||
- Loader: 2 colors
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
We need approximately **35-40 distinct color values** for a complete theme, organized by semantic purpose. Some will be the same color (e.g., multiple uses of "gray"), but they should have separate semantic names so they can be customized independently.
|
|
||||||
|
|
@ -1,938 +0,0 @@
|
||||||
# Design Tokens System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A minimal design tokens system for terminal UI theming. Uses a two-layer approach:
|
|
||||||
1. **Primitive tokens** - Raw color values
|
|
||||||
2. **Semantic tokens** - Purpose-based mappings that reference primitives
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Primitive Tokens (Colors)
|
|
||||||
|
|
||||||
These are the raw chalk color functions - the "palette":
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ColorPrimitives {
|
|
||||||
// Grays
|
|
||||||
gray50: ChalkFunction; // Lightest gray
|
|
||||||
gray100: ChalkFunction;
|
|
||||||
gray200: ChalkFunction;
|
|
||||||
gray300: ChalkFunction;
|
|
||||||
gray400: ChalkFunction;
|
|
||||||
gray500: ChalkFunction; // Mid gray
|
|
||||||
gray600: ChalkFunction;
|
|
||||||
gray700: ChalkFunction;
|
|
||||||
gray800: ChalkFunction;
|
|
||||||
gray900: ChalkFunction; // Darkest gray
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
blue: ChalkFunction;
|
|
||||||
cyan: ChalkFunction;
|
|
||||||
green: ChalkFunction;
|
|
||||||
yellow: ChalkFunction;
|
|
||||||
red: ChalkFunction;
|
|
||||||
magenta: ChalkFunction;
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
bold: ChalkFunction;
|
|
||||||
dim: ChalkFunction;
|
|
||||||
italic: ChalkFunction;
|
|
||||||
underline: ChalkFunction;
|
|
||||||
strikethrough: ChalkFunction;
|
|
||||||
|
|
||||||
// Special
|
|
||||||
none: ChalkFunction; // Pass-through, no styling
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChalkFunction = (str: string) => string;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Semantic Tokens (Design Decisions)
|
|
||||||
|
|
||||||
These map primitives to purposes:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface SemanticTokens {
|
|
||||||
// Text hierarchy
|
|
||||||
text: {
|
|
||||||
primary: ChalkFunction; // Main content text
|
|
||||||
secondary: ChalkFunction; // Supporting text
|
|
||||||
tertiary: ChalkFunction; // De-emphasized text
|
|
||||||
disabled: ChalkFunction; // Inactive/disabled text
|
|
||||||
};
|
|
||||||
|
|
||||||
// Interactive elements
|
|
||||||
interactive: {
|
|
||||||
default: ChalkFunction; // Default interactive elements
|
|
||||||
hover: ChalkFunction; // Hovered/selected state
|
|
||||||
active: ChalkFunction; // Active/current state
|
|
||||||
};
|
|
||||||
|
|
||||||
// Feedback
|
|
||||||
feedback: {
|
|
||||||
error: ChalkFunction;
|
|
||||||
warning: ChalkFunction;
|
|
||||||
success: ChalkFunction;
|
|
||||||
info: ChalkFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Borders & dividers
|
|
||||||
border: {
|
|
||||||
default: ChalkFunction;
|
|
||||||
subtle: ChalkFunction;
|
|
||||||
emphasis: ChalkFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Code
|
|
||||||
code: {
|
|
||||||
text: ChalkFunction;
|
|
||||||
keyword: ChalkFunction;
|
|
||||||
string: ChalkFunction;
|
|
||||||
comment: ChalkFunction;
|
|
||||||
delimiter: ChalkFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Markdown specific
|
|
||||||
markdown: {
|
|
||||||
heading: {
|
|
||||||
h1: ChalkFunction;
|
|
||||||
h2: ChalkFunction;
|
|
||||||
h3: ChalkFunction;
|
|
||||||
};
|
|
||||||
emphasis: {
|
|
||||||
bold: ChalkFunction;
|
|
||||||
italic: ChalkFunction;
|
|
||||||
strikethrough: ChalkFunction;
|
|
||||||
};
|
|
||||||
link: {
|
|
||||||
text: ChalkFunction;
|
|
||||||
url: ChalkFunction;
|
|
||||||
};
|
|
||||||
quote: {
|
|
||||||
text: ChalkFunction;
|
|
||||||
border: ChalkFunction;
|
|
||||||
};
|
|
||||||
list: {
|
|
||||||
bullet: ChalkFunction;
|
|
||||||
};
|
|
||||||
code: {
|
|
||||||
inline: ChalkFunction;
|
|
||||||
inlineDelimiter: ChalkFunction;
|
|
||||||
block: ChalkFunction;
|
|
||||||
blockDelimiter: ChalkFunction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Output streams
|
|
||||||
output: {
|
|
||||||
stdout: ChalkFunction;
|
|
||||||
stderr: ChalkFunction;
|
|
||||||
neutral: ChalkFunction;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Theme Structure
|
|
||||||
|
|
||||||
A theme combines primitives with semantic mappings:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Theme {
|
|
||||||
name: string;
|
|
||||||
primitives: ColorPrimitives;
|
|
||||||
tokens: SemanticTokens;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Built-in Themes
|
|
||||||
|
|
||||||
### Dark Theme
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const darkPrimitives: ColorPrimitives = {
|
|
||||||
// Grays - for dark backgrounds, lighter = more prominent
|
|
||||||
gray50: chalk.white,
|
|
||||||
gray100: (s) => s, // No color = terminal default
|
|
||||||
gray200: chalk.white,
|
|
||||||
gray300: (s) => s,
|
|
||||||
gray400: chalk.gray,
|
|
||||||
gray500: chalk.gray,
|
|
||||||
gray600: chalk.gray,
|
|
||||||
gray700: chalk.dim,
|
|
||||||
gray800: chalk.dim,
|
|
||||||
gray900: chalk.black,
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
blue: chalk.blue,
|
|
||||||
cyan: chalk.cyan,
|
|
||||||
green: chalk.green,
|
|
||||||
yellow: chalk.yellow,
|
|
||||||
red: chalk.red,
|
|
||||||
magenta: chalk.magenta,
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
bold: chalk.bold,
|
|
||||||
dim: chalk.dim,
|
|
||||||
italic: chalk.italic,
|
|
||||||
underline: chalk.underline,
|
|
||||||
strikethrough: chalk.strikethrough,
|
|
||||||
|
|
||||||
// Special
|
|
||||||
none: (s) => s,
|
|
||||||
};
|
|
||||||
|
|
||||||
const darkTheme: Theme = {
|
|
||||||
name: "dark",
|
|
||||||
primitives: darkPrimitives,
|
|
||||||
tokens: {
|
|
||||||
text: {
|
|
||||||
primary: darkPrimitives.gray100,
|
|
||||||
secondary: darkPrimitives.gray400,
|
|
||||||
tertiary: darkPrimitives.gray700,
|
|
||||||
disabled: darkPrimitives.dim,
|
|
||||||
},
|
|
||||||
|
|
||||||
interactive: {
|
|
||||||
default: darkPrimitives.blue,
|
|
||||||
hover: darkPrimitives.blue,
|
|
||||||
active: (s) => darkPrimitives.bold(darkPrimitives.blue(s)),
|
|
||||||
},
|
|
||||||
|
|
||||||
feedback: {
|
|
||||||
error: darkPrimitives.red,
|
|
||||||
warning: darkPrimitives.yellow,
|
|
||||||
success: darkPrimitives.green,
|
|
||||||
info: darkPrimitives.cyan,
|
|
||||||
},
|
|
||||||
|
|
||||||
border: {
|
|
||||||
default: darkPrimitives.blue,
|
|
||||||
subtle: darkPrimitives.gray600,
|
|
||||||
emphasis: darkPrimitives.cyan,
|
|
||||||
},
|
|
||||||
|
|
||||||
code: {
|
|
||||||
text: darkPrimitives.green,
|
|
||||||
keyword: darkPrimitives.cyan,
|
|
||||||
string: darkPrimitives.green,
|
|
||||||
comment: darkPrimitives.gray600,
|
|
||||||
delimiter: darkPrimitives.gray600,
|
|
||||||
},
|
|
||||||
|
|
||||||
markdown: {
|
|
||||||
heading: {
|
|
||||||
h1: (s) => darkPrimitives.underline(darkPrimitives.bold(darkPrimitives.yellow(s))),
|
|
||||||
h2: (s) => darkPrimitives.bold(darkPrimitives.yellow(s)),
|
|
||||||
h3: darkPrimitives.bold,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
bold: darkPrimitives.bold,
|
|
||||||
italic: darkPrimitives.italic,
|
|
||||||
strikethrough: darkPrimitives.strikethrough,
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
text: (s) => darkPrimitives.underline(darkPrimitives.blue(s)),
|
|
||||||
url: darkPrimitives.gray600,
|
|
||||||
},
|
|
||||||
quote: {
|
|
||||||
text: darkPrimitives.italic,
|
|
||||||
border: darkPrimitives.gray600,
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
bullet: darkPrimitives.cyan,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
inline: darkPrimitives.cyan,
|
|
||||||
inlineDelimiter: darkPrimitives.gray600,
|
|
||||||
block: darkPrimitives.green,
|
|
||||||
blockDelimiter: darkPrimitives.gray600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
stdout: darkPrimitives.green,
|
|
||||||
stderr: darkPrimitives.red,
|
|
||||||
neutral: darkPrimitives.gray600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Light Theme
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const lightPrimitives: ColorPrimitives = {
|
|
||||||
// Grays - for light backgrounds, darker = more prominent
|
|
||||||
gray50: chalk.black,
|
|
||||||
gray100: (s) => s, // No color = terminal default
|
|
||||||
gray200: chalk.black,
|
|
||||||
gray300: (s) => s,
|
|
||||||
gray400: chalk.gray, // Use actual gray, not dim
|
|
||||||
gray500: chalk.gray,
|
|
||||||
gray600: chalk.gray,
|
|
||||||
gray700: chalk.gray,
|
|
||||||
gray800: chalk.gray,
|
|
||||||
gray900: chalk.white,
|
|
||||||
|
|
||||||
// Colors - use bold variants for better visibility on light bg
|
|
||||||
blue: (s) => chalk.bold(chalk.blue(s)),
|
|
||||||
cyan: (s) => chalk.bold(chalk.cyan(s)),
|
|
||||||
green: (s) => chalk.bold(chalk.green(s)),
|
|
||||||
yellow: (s) => chalk.bold(chalk.yellow(s)),
|
|
||||||
red: (s) => chalk.bold(chalk.red(s)),
|
|
||||||
magenta: (s) => chalk.bold(chalk.magenta(s)),
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
bold: chalk.bold,
|
|
||||||
dim: chalk.gray, // Don't use chalk.dim on light bg!
|
|
||||||
italic: chalk.italic,
|
|
||||||
underline: chalk.underline,
|
|
||||||
strikethrough: chalk.strikethrough,
|
|
||||||
|
|
||||||
// Special
|
|
||||||
none: (s) => s,
|
|
||||||
};
|
|
||||||
|
|
||||||
const lightTheme: Theme = {
|
|
||||||
name: "light",
|
|
||||||
primitives: lightPrimitives,
|
|
||||||
tokens: {
|
|
||||||
text: {
|
|
||||||
primary: lightPrimitives.gray100,
|
|
||||||
secondary: lightPrimitives.gray400,
|
|
||||||
tertiary: lightPrimitives.gray600,
|
|
||||||
disabled: lightPrimitives.dim,
|
|
||||||
},
|
|
||||||
|
|
||||||
interactive: {
|
|
||||||
default: lightPrimitives.blue,
|
|
||||||
hover: lightPrimitives.blue,
|
|
||||||
active: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
|
|
||||||
},
|
|
||||||
|
|
||||||
feedback: {
|
|
||||||
error: lightPrimitives.red,
|
|
||||||
warning: (s) => chalk.bold(chalk.yellow(s)), // Yellow needs extra bold
|
|
||||||
success: lightPrimitives.green,
|
|
||||||
info: lightPrimitives.cyan,
|
|
||||||
},
|
|
||||||
|
|
||||||
border: {
|
|
||||||
default: lightPrimitives.blue,
|
|
||||||
subtle: lightPrimitives.gray400,
|
|
||||||
emphasis: lightPrimitives.cyan,
|
|
||||||
},
|
|
||||||
|
|
||||||
code: {
|
|
||||||
text: lightPrimitives.green,
|
|
||||||
keyword: lightPrimitives.cyan,
|
|
||||||
string: lightPrimitives.green,
|
|
||||||
comment: lightPrimitives.gray600,
|
|
||||||
delimiter: lightPrimitives.gray600,
|
|
||||||
},
|
|
||||||
|
|
||||||
markdown: {
|
|
||||||
heading: {
|
|
||||||
h1: (s) => lightPrimitives.underline(lightPrimitives.bold(lightPrimitives.blue(s))),
|
|
||||||
h2: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
|
|
||||||
h3: lightPrimitives.bold,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
bold: lightPrimitives.bold,
|
|
||||||
italic: lightPrimitives.italic,
|
|
||||||
strikethrough: lightPrimitives.strikethrough,
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
text: (s) => lightPrimitives.underline(lightPrimitives.blue(s)),
|
|
||||||
url: lightPrimitives.blue,
|
|
||||||
},
|
|
||||||
quote: {
|
|
||||||
text: lightPrimitives.italic,
|
|
||||||
border: lightPrimitives.gray600,
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
bullet: lightPrimitives.blue,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
inline: lightPrimitives.blue,
|
|
||||||
inlineDelimiter: lightPrimitives.gray600,
|
|
||||||
block: lightPrimitives.green,
|
|
||||||
blockDelimiter: lightPrimitives.gray600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
stdout: lightPrimitives.green,
|
|
||||||
stderr: lightPrimitives.red,
|
|
||||||
neutral: lightPrimitives.gray600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Simple Text Styling
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
// Before
|
|
||||||
console.log(chalk.gray("Secondary text"));
|
|
||||||
|
|
||||||
// After
|
|
||||||
console.log(theme.tokens.text.secondary("Secondary text"));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interactive Elements
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
// Before
|
|
||||||
const cursor = chalk.blue("› ");
|
|
||||||
|
|
||||||
// After
|
|
||||||
const cursor = theme.tokens.interactive.default("› ");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Messages
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
// Before
|
|
||||||
this.contentContainer.addChild(new Text(chalk.red("Error: " + errorMsg)));
|
|
||||||
|
|
||||||
// After
|
|
||||||
this.contentContainer.addChild(new Text(theme.tokens.feedback.error("Error: " + errorMsg)));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Markdown Headings
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
// Before
|
|
||||||
lines.push(chalk.bold.yellow(headingText));
|
|
||||||
|
|
||||||
// After
|
|
||||||
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Borders
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
|
|
||||||
// Before
|
|
||||||
this.addChild(new Text(chalk.blue("─".repeat(80))));
|
|
||||||
|
|
||||||
// After
|
|
||||||
this.addChild(new Text(theme.tokens.border.default("─".repeat(80))));
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Configuration
|
|
||||||
|
|
||||||
### Theme File Format
|
|
||||||
|
|
||||||
Themes can be defined in JSON files that users can customize. The system will load themes from:
|
|
||||||
1. Built-in themes (dark, light) - hardcoded in the app
|
|
||||||
2. User themes in `~/.pi/agent/themes/` directory
|
|
||||||
|
|
||||||
**Example: `~/.pi/agent/themes/my-theme.json`**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "my-theme",
|
|
||||||
"extends": "dark",
|
|
||||||
"primitives": {
|
|
||||||
"blue": "blueBright",
|
|
||||||
"cyan": "cyanBright",
|
|
||||||
"green": "greenBright"
|
|
||||||
},
|
|
||||||
"tokens": {
|
|
||||||
"text": {
|
|
||||||
"primary": "white"
|
|
||||||
},
|
|
||||||
"interactive": {
|
|
||||||
"default": ["bold", "blue"]
|
|
||||||
},
|
|
||||||
"markdown": {
|
|
||||||
"heading": {
|
|
||||||
"h1": ["bold", "underline", "magenta"],
|
|
||||||
"h2": ["bold", "magenta"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Schema
|
|
||||||
|
|
||||||
Themes in JSON can reference:
|
|
||||||
1. **Chalk color names**: `"red"`, `"blue"`, `"gray"`, `"white"`, `"black"`, etc.
|
|
||||||
2. **Chalk bright colors**: `"redBright"`, `"blueBright"`, etc.
|
|
||||||
3. **Chalk modifiers**: `"bold"`, `"dim"`, `"italic"`, `"underline"`, `"strikethrough"`
|
|
||||||
4. **Combinations**: `["bold", "blue"]` or `["underline", "bold", "cyan"]`
|
|
||||||
5. **Primitive references**: `"$gray400"` to reference another primitive
|
|
||||||
6. **None/passthrough**: `"none"` or `""` for no styling
|
|
||||||
|
|
||||||
### Supported Chalk Values
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type ChalkColorName =
|
|
||||||
// Basic colors
|
|
||||||
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
|
|
||||||
// Bright variants
|
|
||||||
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
|
|
||||||
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
|
|
||||||
// Modifiers
|
|
||||||
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
|
|
||||||
// Special
|
|
||||||
| "none";
|
|
||||||
|
|
||||||
type ChalkValue = ChalkColorName | ChalkColorName[] | string; // string allows "$primitive" refs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Theme Extension
|
|
||||||
|
|
||||||
Themes can extend other themes using `"extends": "dark"` or `"extends": "light"`. Only the overridden values need to be specified.
|
|
||||||
|
|
||||||
**Example: Minimal override**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "solarized-dark",
|
|
||||||
"extends": "dark",
|
|
||||||
"tokens": {
|
|
||||||
"feedback": {
|
|
||||||
"error": "magenta",
|
|
||||||
"warning": "yellow"
|
|
||||||
},
|
|
||||||
"markdown": {
|
|
||||||
"heading": {
|
|
||||||
"h1": ["bold", "cyan"],
|
|
||||||
"h2": ["bold", "blue"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Loading Order
|
|
||||||
|
|
||||||
1. Load built-in themes (dark, light)
|
|
||||||
2. Scan `~/.pi/agent/themes/*.json`
|
|
||||||
3. Parse and validate each JSON theme
|
|
||||||
4. Build theme by:
|
|
||||||
- Start with base theme (if extends specified)
|
|
||||||
- Apply primitive overrides
|
|
||||||
- Apply token overrides
|
|
||||||
- Convert JSON values to chalk functions
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
### Theme Module Structure
|
|
||||||
|
|
||||||
**Location:** `packages/tui/src/theme/`
|
|
||||||
|
|
||||||
```
|
|
||||||
theme/
|
|
||||||
├── index.ts # Public API
|
|
||||||
├── types.ts # Type definitions
|
|
||||||
├── primitives.ts # Color primitives for each theme
|
|
||||||
├── tokens.ts # Semantic token mappings
|
|
||||||
├── themes.ts # Built-in theme definitions
|
|
||||||
├── registry.ts # Theme management (current, set, get)
|
|
||||||
├── loader.ts # JSON theme loader
|
|
||||||
└── parser.ts # JSON to ChalkFunction converter
|
|
||||||
```
|
|
||||||
|
|
||||||
### Public API
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/tui/src/theme/index.ts
|
|
||||||
export { type Theme, type SemanticTokens, type ColorPrimitives } from './types.js';
|
|
||||||
export { darkTheme, lightTheme } from './themes.js';
|
|
||||||
export { getTheme, setTheme, getThemeNames } from './registry.js';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Theme Registry
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/tui/src/theme/registry.ts
|
|
||||||
import { darkTheme, lightTheme } from './themes.js';
|
|
||||||
import type { Theme } from './types.js';
|
|
||||||
|
|
||||||
const themes = new Map<string, Theme>([
|
|
||||||
['dark', darkTheme],
|
|
||||||
['light', lightTheme],
|
|
||||||
]);
|
|
||||||
|
|
||||||
let currentTheme: Theme = darkTheme;
|
|
||||||
|
|
||||||
export function getTheme(): Theme {
|
|
||||||
return currentTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setTheme(name: string): void {
|
|
||||||
const theme = themes.get(name);
|
|
||||||
if (!theme) {
|
|
||||||
throw new Error(`Theme "${name}" not found`);
|
|
||||||
}
|
|
||||||
currentTheme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getThemeNames(): string[] {
|
|
||||||
return Array.from(themes.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerTheme(theme: Theme): void {
|
|
||||||
themes.set(theme.name, theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getThemeByName(name: string): Theme | undefined {
|
|
||||||
return themes.get(name);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Theme Parser
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/tui/src/theme/parser.ts
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import type { ChalkFunction } from './types.js';
|
|
||||||
|
|
||||||
type ChalkColorName =
|
|
||||||
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
|
|
||||||
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
|
|
||||||
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
|
|
||||||
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
|
|
||||||
| "none";
|
|
||||||
|
|
||||||
type JsonThemeValue = ChalkColorName | ChalkColorName[] | string;
|
|
||||||
|
|
||||||
interface JsonTheme {
|
|
||||||
name: string;
|
|
||||||
extends?: string;
|
|
||||||
primitives?: Record<string, JsonThemeValue>;
|
|
||||||
tokens?: any; // Partial<SemanticTokens> but with JsonThemeValue instead of ChalkFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map chalk color names to actual chalk functions
|
|
||||||
const chalkMap: Record<ChalkColorName, any> = {
|
|
||||||
black: chalk.black,
|
|
||||||
red: chalk.red,
|
|
||||||
green: chalk.green,
|
|
||||||
yellow: chalk.yellow,
|
|
||||||
blue: chalk.blue,
|
|
||||||
magenta: chalk.magenta,
|
|
||||||
cyan: chalk.cyan,
|
|
||||||
white: chalk.white,
|
|
||||||
gray: chalk.gray,
|
|
||||||
blackBright: chalk.blackBright,
|
|
||||||
redBright: chalk.redBright,
|
|
||||||
greenBright: chalk.greenBright,
|
|
||||||
yellowBright: chalk.yellowBright,
|
|
||||||
blueBright: chalk.blueBright,
|
|
||||||
magentaBright: chalk.magentaBright,
|
|
||||||
cyanBright: chalk.cyanBright,
|
|
||||||
whiteBright: chalk.whiteBright,
|
|
||||||
bold: chalk.bold,
|
|
||||||
dim: chalk.dim,
|
|
||||||
italic: chalk.italic,
|
|
||||||
underline: chalk.underline,
|
|
||||||
strikethrough: chalk.strikethrough,
|
|
||||||
inverse: chalk.inverse,
|
|
||||||
none: (s: string) => s,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parseThemeValue(
|
|
||||||
value: JsonThemeValue,
|
|
||||||
primitives?: Record<string, ChalkFunction>
|
|
||||||
): ChalkFunction {
|
|
||||||
// Handle primitive reference: "$gray400"
|
|
||||||
if (typeof value === 'string' && value.startsWith('
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Phase 1: Infrastructure
|
|
||||||
1. Create theme module with types, primitives, and built-in themes
|
|
||||||
2. Export from `@mariozechner/pi-tui`
|
|
||||||
3. Add tests for theme functions
|
|
||||||
|
|
||||||
### Phase 2: Component Migration (Priority Order)
|
|
||||||
1. **Markdown** (biggest impact, 50+ color calls)
|
|
||||||
2. **ToolExecution** (stdout/stderr readability)
|
|
||||||
3. **SelectList** (used everywhere)
|
|
||||||
4. **Footer** (always visible)
|
|
||||||
5. **TuiRenderer** (logo, instructions)
|
|
||||||
6. Other components
|
|
||||||
|
|
||||||
### Phase 3: Persistence & UI
|
|
||||||
1. Add theme to SettingsManager
|
|
||||||
2. Create ThemeSelector component
|
|
||||||
3. Add `/theme` slash command
|
|
||||||
4. Initialize theme on startup
|
|
||||||
|
|
||||||
### Example Migration
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```typescript
|
|
||||||
// markdown.ts
|
|
||||||
if (headingLevel === 1) {
|
|
||||||
lines.push(chalk.bold.underline.yellow(headingText));
|
|
||||||
} else if (headingLevel === 2) {
|
|
||||||
lines.push(chalk.bold.yellow(headingText));
|
|
||||||
} else {
|
|
||||||
lines.push(chalk.bold(headingPrefix + headingText));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```typescript
|
|
||||||
// markdown.ts
|
|
||||||
import { getTheme } from '@mariozechner/pi-tui/theme';
|
|
||||||
|
|
||||||
const theme = getTheme();
|
|
||||||
if (headingLevel === 1) {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h1(headingText));
|
|
||||||
} else if (headingLevel === 2) {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
|
||||||
} else {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits of This Approach
|
|
||||||
|
|
||||||
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
|
|
||||||
2. **Maintainable**: Change all headings by editing one token mapping
|
|
||||||
3. **Extensible**: Easy to add new themes without touching components
|
|
||||||
4. **Type-safe**: Full TypeScript support
|
|
||||||
5. **Testable**: Can test themes independently
|
|
||||||
6. **Minimal**: Only what we need, no over-engineering
|
|
||||||
7. **Composable**: Can chain primitives (bold + underline + color)
|
|
||||||
|
|
||||||
## Key Differences from Themes.md
|
|
||||||
|
|
||||||
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
|
|
||||||
- **Composability**: Can combine primitive modifiers
|
|
||||||
- **Better light theme**: Properly handles chalk.dim and color visibility issues
|
|
||||||
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
|
|
||||||
- **Easier to extend**: Add new token without changing primitives
|
|
||||||
- **Better for sharing**: Could export just primitives for custom themes
|
|
||||||
)) {
|
|
||||||
const primitiveName = value.slice(1);
|
|
||||||
if (primitives && primitives[primitiveName]) {
|
|
||||||
return primitives[primitiveName];
|
|
||||||
}
|
|
||||||
throw new Error(`Primitive reference "${value}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle array of chalk names (composition): ["bold", "blue"]
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return (str: string) => {
|
|
||||||
let result = str;
|
|
||||||
for (const name of value) {
|
|
||||||
const chalkFn = chalkMap[name as ChalkColorName];
|
|
||||||
if (!chalkFn) {
|
|
||||||
throw new Error(`Unknown chalk function: ${name}`);
|
|
||||||
}
|
|
||||||
result = chalkFn(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle single chalk name: "blue"
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const chalkFn = chalkMap[value as ChalkColorName];
|
|
||||||
if (!chalkFn) {
|
|
||||||
throw new Error(`Unknown chalk function: ${value}`);
|
|
||||||
}
|
|
||||||
return chalkFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Invalid theme value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deep merge objects, used for extending themes
|
|
||||||
function deepMerge(target: any, source: any): any {
|
|
||||||
const result = { ...target };
|
|
||||||
|
|
||||||
for (const key in source) {
|
|
||||||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
||||||
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
||||||
} else {
|
|
||||||
result[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseJsonTheme(json: JsonTheme, baseTheme?: Theme): Theme {
|
|
||||||
// Start with base theme if extending
|
|
||||||
let primitives: Record<string, ChalkFunction> = {};
|
|
||||||
let tokens: any = {};
|
|
||||||
|
|
||||||
if (json.extends && baseTheme) {
|
|
||||||
// Copy base theme primitives and tokens
|
|
||||||
primitives = { ...baseTheme.primitives };
|
|
||||||
tokens = deepMerge({}, baseTheme.tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and override primitives
|
|
||||||
if (json.primitives) {
|
|
||||||
for (const [key, value] of Object.entries(json.primitives)) {
|
|
||||||
primitives[key] = parseThemeValue(value, primitives);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and override tokens (recursive)
|
|
||||||
if (json.tokens) {
|
|
||||||
const parsedTokens = parseTokens(json.tokens, primitives);
|
|
||||||
tokens = deepMerge(tokens, parsedTokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: json.name,
|
|
||||||
primitives,
|
|
||||||
tokens,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTokens(obj: any, primitives: Record<string, ChalkFunction>): any {
|
|
||||||
const result: any = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
||||||
// Nested object, recurse
|
|
||||||
result[key] = parseTokens(value, primitives);
|
|
||||||
} else {
|
|
||||||
// Leaf value, parse it
|
|
||||||
result[key] = parseThemeValue(value as JsonThemeValue, primitives);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON Theme Loader
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// packages/tui/src/theme/loader.ts
|
|
||||||
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { parseJsonTheme } from './parser.js';
|
|
||||||
import { getThemeByName, registerTheme } from './registry.js';
|
|
||||||
import type { Theme } from './types.js';
|
|
||||||
|
|
||||||
export function loadUserThemes(themesDir: string): Theme[] {
|
|
||||||
const themes: Theme[] = [];
|
|
||||||
|
|
||||||
if (!existsSync(themesDir)) {
|
|
||||||
return themes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = readdirSync(themesDir).filter(f => f.endsWith('.json'));
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
const content = readFileSync(join(themesDir, file), 'utf-8');
|
|
||||||
const json = JSON.parse(content);
|
|
||||||
|
|
||||||
// Get base theme if extending
|
|
||||||
let baseTheme: Theme | undefined;
|
|
||||||
if (json.extends) {
|
|
||||||
baseTheme = getThemeByName(json.extends);
|
|
||||||
if (!baseTheme) {
|
|
||||||
console.warn(`Theme ${json.name} extends unknown theme "${json.extends}", skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = parseJsonTheme(json, baseTheme);
|
|
||||||
registerTheme(theme);
|
|
||||||
themes.push(theme);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load theme from ${file}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return themes;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Phase 1: Infrastructure
|
|
||||||
1. Create theme module with types, primitives, and built-in themes
|
|
||||||
2. Export from `@mariozechner/pi-tui`
|
|
||||||
3. Add tests for theme functions
|
|
||||||
|
|
||||||
### Phase 2: Component Migration (Priority Order)
|
|
||||||
1. **Markdown** (biggest impact, 50+ color calls)
|
|
||||||
2. **ToolExecution** (stdout/stderr readability)
|
|
||||||
3. **SelectList** (used everywhere)
|
|
||||||
4. **Footer** (always visible)
|
|
||||||
5. **TuiRenderer** (logo, instructions)
|
|
||||||
6. Other components
|
|
||||||
|
|
||||||
### Phase 3: Persistence & UI
|
|
||||||
1. Add theme to SettingsManager
|
|
||||||
2. Create ThemeSelector component
|
|
||||||
3. Add `/theme` slash command
|
|
||||||
4. Initialize theme on startup
|
|
||||||
|
|
||||||
### Example Migration
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```typescript
|
|
||||||
// markdown.ts
|
|
||||||
if (headingLevel === 1) {
|
|
||||||
lines.push(chalk.bold.underline.yellow(headingText));
|
|
||||||
} else if (headingLevel === 2) {
|
|
||||||
lines.push(chalk.bold.yellow(headingText));
|
|
||||||
} else {
|
|
||||||
lines.push(chalk.bold(headingPrefix + headingText));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```typescript
|
|
||||||
// markdown.ts
|
|
||||||
import { getTheme } from '@mariozechner/pi-tui/theme';
|
|
||||||
|
|
||||||
const theme = getTheme();
|
|
||||||
if (headingLevel === 1) {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h1(headingText));
|
|
||||||
} else if (headingLevel === 2) {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
|
||||||
} else {
|
|
||||||
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits of This Approach
|
|
||||||
|
|
||||||
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
|
|
||||||
2. **Maintainable**: Change all headings by editing one token mapping
|
|
||||||
3. **Extensible**: Easy to add new themes without touching components
|
|
||||||
4. **Type-safe**: Full TypeScript support
|
|
||||||
5. **Testable**: Can test themes independently
|
|
||||||
6. **Minimal**: Only what we need, no over-engineering
|
|
||||||
7. **Composable**: Can chain primitives (bold + underline + color)
|
|
||||||
|
|
||||||
## Key Differences from Themes.md
|
|
||||||
|
|
||||||
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
|
|
||||||
- **Composability**: Can combine primitive modifiers
|
|
||||||
- **Better light theme**: Properly handles chalk.dim and color visibility issues
|
|
||||||
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
|
|
||||||
- **Easier to extend**: Add new token without changing primitives
|
|
||||||
- **Better for sharing**: Could export just primitives for custom themes
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
# Minimal Theme Color Set
|
|
||||||
|
|
||||||
## Complete list of required theme colors
|
|
||||||
|
|
||||||
Based on analysis of all color usage in the codebase.
|
|
||||||
|
|
||||||
### Text Hierarchy (3 colors)
|
|
||||||
- **textPrimary** - Main content text (default terminal color)
|
|
||||||
- **textSecondary** - Metadata, supporting text
|
|
||||||
- **textTertiary** - De-emphasized text (dimmed/muted)
|
|
||||||
|
|
||||||
### UI Chrome (4 colors)
|
|
||||||
- **border** - Primary borders (around changelog, selectors)
|
|
||||||
- **borderSubtle** - Subtle borders/separators
|
|
||||||
- **uiBackground** - General UI background elements
|
|
||||||
- **scrollInfo** - Scroll position indicators like "(1/10)"
|
|
||||||
|
|
||||||
### Interactive Elements (4 colors)
|
|
||||||
- **interactionDefault** - Default interactive state (unselected)
|
|
||||||
- **interactionHover** - Hovered/focused state
|
|
||||||
- **interactionActive** - Currently active/selected item
|
|
||||||
- **interactionSuccess** - Success indicator (checkmarks)
|
|
||||||
|
|
||||||
### Feedback/Status (4 colors)
|
|
||||||
- **feedbackError** - Errors, failures
|
|
||||||
- **feedbackSuccess** - Success, completed
|
|
||||||
- **feedbackWarning** - Warnings, cautions
|
|
||||||
- **feedbackInfo** - Informational messages
|
|
||||||
|
|
||||||
### Branding (2 colors)
|
|
||||||
- **brandPrimary** - Logo, primary brand color
|
|
||||||
- **brandSecondary** - Secondary brand elements
|
|
||||||
|
|
||||||
### Tool Execution (6 colors + 3 backgrounds)
|
|
||||||
- **toolCommand** - Command text in tool headers
|
|
||||||
- **toolPath** - File paths
|
|
||||||
- **toolStdout** - Standard output
|
|
||||||
- **toolStderr** - Standard error
|
|
||||||
- **toolDimmed** - Truncated/hidden lines
|
|
||||||
- **toolNeutral** - Neutral tool output
|
|
||||||
- **toolBgPending** - Background for pending tool execution
|
|
||||||
- **toolBgSuccess** - Background for successful tool execution
|
|
||||||
- **toolBgError** - Background for failed tool execution
|
|
||||||
|
|
||||||
### Markdown - Structure (5 colors)
|
|
||||||
- **mdHeading1** - H1 headings
|
|
||||||
- **mdHeading2** - H2 headings
|
|
||||||
- **mdHeading3** - H3+ headings
|
|
||||||
- **mdHr** - Horizontal rules
|
|
||||||
- **mdTable** - Table borders and structure
|
|
||||||
|
|
||||||
### Markdown - Code (4 colors)
|
|
||||||
- **mdCodeBlock** - Code block content
|
|
||||||
- **mdCodeBlockDelimiter** - Code block ``` delimiters
|
|
||||||
- **mdCodeInline** - Inline `code` content
|
|
||||||
- **mdCodeInlineDelimiter** - Inline code ` backticks
|
|
||||||
|
|
||||||
### Markdown - Lists & Quotes (3 colors)
|
|
||||||
- **mdListBullet** - List bullets (- or 1.)
|
|
||||||
- **mdQuoteText** - Blockquote text
|
|
||||||
- **mdQuoteBorder** - Blockquote border (│)
|
|
||||||
|
|
||||||
### Markdown - Links (2 colors)
|
|
||||||
- **mdLinkText** - Link text
|
|
||||||
- **mdLinkUrl** - Link URL in parentheses
|
|
||||||
|
|
||||||
### Backgrounds (2 colors)
|
|
||||||
- **bgUserMessage** - Background for user messages
|
|
||||||
- **bgDefault** - Default/transparent background
|
|
||||||
|
|
||||||
### Special/Optional (2 colors)
|
|
||||||
- **spinner** - Loading spinner animation
|
|
||||||
- **thinking** - Thinking/reasoning text
|
|
||||||
|
|
||||||
## Total: 44 colors
|
|
||||||
|
|
||||||
### Grouped by Common Values
|
|
||||||
|
|
||||||
Many of these will share the same value. Typical groupings:
|
|
||||||
|
|
||||||
**"Secondary" family** (gray-ish):
|
|
||||||
- textSecondary
|
|
||||||
- textTertiary
|
|
||||||
- borderSubtle
|
|
||||||
- scrollInfo
|
|
||||||
- toolDimmed
|
|
||||||
- mdHr
|
|
||||||
- mdCodeBlockDelimiter
|
|
||||||
- mdCodeInlineDelimiter
|
|
||||||
- mdQuoteBorder
|
|
||||||
- mdLinkUrl
|
|
||||||
|
|
||||||
**"Primary accent" family** (blue-ish):
|
|
||||||
- border
|
|
||||||
- interactionDefault
|
|
||||||
- interactionHover
|
|
||||||
- interactionActive
|
|
||||||
- brandPrimary
|
|
||||||
- mdLinkText
|
|
||||||
|
|
||||||
**"Success" family** (green-ish):
|
|
||||||
- feedbackSuccess
|
|
||||||
- interactionSuccess
|
|
||||||
- toolStdout
|
|
||||||
- mdCodeBlock
|
|
||||||
|
|
||||||
**"Error" family** (red-ish):
|
|
||||||
- feedbackError
|
|
||||||
- toolStderr
|
|
||||||
|
|
||||||
**"Code/Tech" family** (cyan-ish):
|
|
||||||
- brandPrimary
|
|
||||||
- mdCodeInline
|
|
||||||
- mdListBullet
|
|
||||||
- spinner
|
|
||||||
|
|
||||||
**"Emphasis" family** (yellow-ish):
|
|
||||||
- mdHeading1
|
|
||||||
- mdHeading2
|
|
||||||
- feedbackWarning
|
|
||||||
|
|
||||||
## Simplified Minimal Set (Alternative)
|
|
||||||
|
|
||||||
If we want to reduce further, we could consolidate to ~25 colors by using more shared values:
|
|
||||||
|
|
||||||
### Core Colors (8)
|
|
||||||
- **text** - Primary text
|
|
||||||
- **textMuted** - Secondary/dimmed text
|
|
||||||
- **accent** - Primary accent (blue)
|
|
||||||
- **accentSubtle** - Subtle accent
|
|
||||||
- **success** - Green
|
|
||||||
- **error** - Red
|
|
||||||
- **warning** - Yellow
|
|
||||||
- **info** - Cyan
|
|
||||||
|
|
||||||
### Backgrounds (4)
|
|
||||||
- **bgDefault** - Transparent/default
|
|
||||||
- **bgUserMessage** - User message background
|
|
||||||
- **bgSuccess** - Success state background
|
|
||||||
- **bgError** - Error state background
|
|
||||||
|
|
||||||
### Specialized (13)
|
|
||||||
- **border** - Primary borders
|
|
||||||
- **borderSubtle** - Subtle borders
|
|
||||||
- **selection** - Selected items
|
|
||||||
- **brand** - Brand/logo color
|
|
||||||
- **mdHeading** - All headings (or separate h1/h2)
|
|
||||||
- **mdCode** - All code (blocks + inline)
|
|
||||||
- **mdCodeDelimiter** - Code delimiters
|
|
||||||
- **mdList** - List bullets
|
|
||||||
- **mdLink** - Links
|
|
||||||
- **mdQuote** - Quotes
|
|
||||||
- **toolCommand** - Command text
|
|
||||||
- **toolPath** - File paths
|
|
||||||
- **spinner** - Loading indicator
|
|
||||||
|
|
||||||
**Total: 25 colors** (vs 44 in the detailed version)
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
Start with the **44-color detailed set** because:
|
|
||||||
1. Gives maximum flexibility for theming
|
|
||||||
2. Each has a clear semantic purpose
|
|
||||||
3. Themes can set many to the same value if desired
|
|
||||||
4. Easier to add granular control than to split apart later
|
|
||||||
|
|
||||||
Users creating themes can start by setting common values and override specific ones:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "my-theme",
|
|
||||||
"_comment": "Set common values first",
|
|
||||||
"textSecondary": "gray",
|
|
||||||
"textTertiary": "gray",
|
|
||||||
"borderSubtle": "gray",
|
|
||||||
"mdCodeBlockDelimiter": "gray",
|
|
||||||
|
|
||||||
"_comment": "Then override specific ones",
|
|
||||||
"mdHeading1": "yellow",
|
|
||||||
"error": "red"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
563
packages/coding-agent/docs/theme.md
Normal file
563
packages/coding-agent/docs/theme.md
Normal file
|
|
@ -0,0 +1,563 @@
|
||||||
|
# Pi Coding Agent Themes
|
||||||
|
|
||||||
|
Themes allow you to customize the colors used throughout the coding agent TUI.
|
||||||
|
|
||||||
|
## Color Tokens
|
||||||
|
|
||||||
|
Every theme must define all color tokens. There are no optional colors.
|
||||||
|
|
||||||
|
### Core UI (9 colors)
|
||||||
|
|
||||||
|
| Token | Purpose | Examples |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| `accent` | Primary accent color | Logo, selected items, cursor (›) |
|
||||||
|
| `border` | Normal borders | Selector borders, horizontal lines |
|
||||||
|
| `borderAccent` | Highlighted borders | Changelog borders, special panels |
|
||||||
|
| `borderMuted` | Subtle borders | Editor borders, secondary separators |
|
||||||
|
| `success` | Success states | Success messages, diff additions |
|
||||||
|
| `error` | Error states | Error messages, diff deletions |
|
||||||
|
| `warning` | Warning states | Warning messages |
|
||||||
|
| `muted` | Secondary/dimmed text | Metadata, descriptions, output |
|
||||||
|
| `text` | Default text color | Main content (usually `""`) |
|
||||||
|
|
||||||
|
### Backgrounds & Content Text (6 colors)
|
||||||
|
|
||||||
|
| Token | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `userMessageBg` | User message background |
|
||||||
|
| `userMessageText` | User message text color |
|
||||||
|
| `toolPendingBg` | Tool execution box (pending state) |
|
||||||
|
| `toolSuccessBg` | Tool execution box (success state) |
|
||||||
|
| `toolErrorBg` | Tool execution box (error state) |
|
||||||
|
| `toolText` | Tool execution box text color (all states) |
|
||||||
|
|
||||||
|
### Markdown (9 colors)
|
||||||
|
|
||||||
|
| Token | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `mdHeading` | Heading text (`#`, `##`, etc) |
|
||||||
|
| `mdLink` | Link text and URLs |
|
||||||
|
| `mdCode` | Inline code (backticks) |
|
||||||
|
| `mdCodeBlock` | Code block content |
|
||||||
|
| `mdCodeBlockBorder` | Code block fences (```) |
|
||||||
|
| `mdQuote` | Blockquote text |
|
||||||
|
| `mdQuoteBorder` | Blockquote border (`│`) |
|
||||||
|
| `mdHr` | Horizontal rule (`---`) |
|
||||||
|
| `mdListBullet` | List bullets/numbers |
|
||||||
|
|
||||||
|
### Tool Diffs (3 colors)
|
||||||
|
|
||||||
|
| Token | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `toolDiffAdded` | Added lines in tool diffs |
|
||||||
|
| `toolDiffRemoved` | Removed lines in tool diffs |
|
||||||
|
| `toolDiffContext` | Context lines in tool diffs |
|
||||||
|
|
||||||
|
Note: Diff colors are specific to tool execution boxes and must work with tool background colors.
|
||||||
|
|
||||||
|
### Syntax Highlighting (9 colors)
|
||||||
|
|
||||||
|
Future-proofing for syntax highlighting support:
|
||||||
|
|
||||||
|
| Token | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `syntaxComment` | Comments |
|
||||||
|
| `syntaxKeyword` | Keywords (`if`, `function`, etc) |
|
||||||
|
| `syntaxFunction` | Function names |
|
||||||
|
| `syntaxVariable` | Variable names |
|
||||||
|
| `syntaxString` | String literals |
|
||||||
|
| `syntaxNumber` | Number literals |
|
||||||
|
| `syntaxType` | Type names |
|
||||||
|
| `syntaxOperator` | Operators (`+`, `-`, etc) |
|
||||||
|
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
|
||||||
|
|
||||||
|
**Total: 36 color tokens** (all required)
|
||||||
|
|
||||||
|
## Theme Format
|
||||||
|
|
||||||
|
Themes are defined in JSON files with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://pi.mariozechner.at/theme-schema.json",
|
||||||
|
"name": "my-theme",
|
||||||
|
"vars": {
|
||||||
|
"blue": "#0066cc",
|
||||||
|
"gray": 242,
|
||||||
|
"brightCyan": 51
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"accent": "blue",
|
||||||
|
"muted": "gray",
|
||||||
|
"text": "",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Values
|
||||||
|
|
||||||
|
Four formats are supported:
|
||||||
|
|
||||||
|
1. **Hex colors**: `"#ff0000"` (6-digit hex RGB)
|
||||||
|
2. **256-color palette**: `39` (number 0-255, xterm 256-color palette)
|
||||||
|
3. **Color references**: `"blue"` (must be defined in `vars`)
|
||||||
|
4. **Terminal default**: `""` (empty string, uses terminal's default color)
|
||||||
|
|
||||||
|
### The `vars` Section
|
||||||
|
|
||||||
|
The optional `vars` section allows you to define reusable colors:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"nord0": "#2E3440",
|
||||||
|
"nord1": "#3B4252",
|
||||||
|
"nord8": "#88C0D0",
|
||||||
|
"brightBlue": 39
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"accent": "nord8",
|
||||||
|
"muted": "nord1",
|
||||||
|
"mdLink": "brightBlue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Reuse colors across multiple tokens
|
||||||
|
- Easier to maintain theme consistency
|
||||||
|
- Can reference standard color palettes
|
||||||
|
|
||||||
|
Variables can be hex colors (`"#ff0000"`), 256-color indices (`42`), or references to other variables.
|
||||||
|
|
||||||
|
### Terminal Default (empty string)
|
||||||
|
|
||||||
|
Use `""` (empty string) to inherit the terminal's default foreground/background color:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"colors": {
|
||||||
|
"text": "" // Uses terminal's default text color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Main text color (adapts to user's terminal theme)
|
||||||
|
- Creating themes that blend with terminal appearance
|
||||||
|
|
||||||
|
## Built-in Themes
|
||||||
|
|
||||||
|
Pi comes with two built-in themes:
|
||||||
|
|
||||||
|
### `dark` (default)
|
||||||
|
|
||||||
|
Optimized for dark terminal backgrounds with bright, saturated colors.
|
||||||
|
|
||||||
|
### `light`
|
||||||
|
|
||||||
|
Optimized for light terminal backgrounds with darker, muted colors.
|
||||||
|
|
||||||
|
## Selecting a Theme
|
||||||
|
|
||||||
|
Themes are configured in the settings (accessible via `/settings`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"theme": "dark"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the `/theme` command interactively.
|
||||||
|
|
||||||
|
On first run, Pi detects your terminal's background and sets a sensible default (`dark` or `light`).
|
||||||
|
|
||||||
|
## Custom Themes
|
||||||
|
|
||||||
|
### Theme Locations
|
||||||
|
|
||||||
|
Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
||||||
|
|
||||||
|
### Creating a Custom Theme
|
||||||
|
|
||||||
|
1. **Create theme directory:**
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.pi/agent/themes
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create theme file:**
|
||||||
|
```bash
|
||||||
|
vim ~/.pi/agent/themes/my-theme.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Define all colors:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://pi.mariozechner.at/theme-schema.json",
|
||||||
|
"name": "my-theme",
|
||||||
|
"vars": {
|
||||||
|
"primary": "#00aaff",
|
||||||
|
"secondary": 242,
|
||||||
|
"brightGreen": 46
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"accent": "primary",
|
||||||
|
"border": "primary",
|
||||||
|
"borderAccent": "#00ffff",
|
||||||
|
"borderMuted": "secondary",
|
||||||
|
"success": "brightGreen",
|
||||||
|
"error": "#ff0000",
|
||||||
|
"warning": "#ffff00",
|
||||||
|
"muted": "secondary",
|
||||||
|
"text": "",
|
||||||
|
|
||||||
|
"userMessageBg": "#2d2d30",
|
||||||
|
"userMessageText": "",
|
||||||
|
"toolPendingBg": "#1e1e2e",
|
||||||
|
"toolSuccessBg": "#1e2e1e",
|
||||||
|
"toolErrorBg": "#2e1e1e",
|
||||||
|
"toolText": "",
|
||||||
|
|
||||||
|
"mdHeading": "#ffaa00",
|
||||||
|
"mdLink": "primary",
|
||||||
|
"mdCode": "#00ffff",
|
||||||
|
"mdCodeBlock": "#00ff00",
|
||||||
|
"mdCodeBlockBorder": "secondary",
|
||||||
|
"mdQuote": "secondary",
|
||||||
|
"mdQuoteBorder": "secondary",
|
||||||
|
"mdHr": "secondary",
|
||||||
|
"mdListBullet": "#00ffff",
|
||||||
|
|
||||||
|
"toolDiffAdded": "#00ff00",
|
||||||
|
"toolDiffRemoved": "#ff0000",
|
||||||
|
"toolDiffContext": "secondary",
|
||||||
|
|
||||||
|
"syntaxComment": "secondary",
|
||||||
|
"syntaxKeyword": "primary",
|
||||||
|
"syntaxFunction": "#00aaff",
|
||||||
|
"syntaxVariable": "#ffaa00",
|
||||||
|
"syntaxString": "#00ff00",
|
||||||
|
"syntaxNumber": "#ff00ff",
|
||||||
|
"syntaxType": "#00aaff",
|
||||||
|
"syntaxOperator": "primary",
|
||||||
|
"syntaxPunctuation": "secondary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Select your theme:**
|
||||||
|
- Use `/settings` command and set `"theme": "my-theme"`
|
||||||
|
- Or use `/theme` command interactively
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
### Light vs Dark Themes
|
||||||
|
|
||||||
|
**For dark terminals:**
|
||||||
|
- Use bright, saturated colors
|
||||||
|
- Higher contrast
|
||||||
|
- Example: `#00ffff` (bright cyan)
|
||||||
|
|
||||||
|
**For light terminals:**
|
||||||
|
- Use darker, muted colors
|
||||||
|
- Lower contrast to avoid eye strain
|
||||||
|
- Example: `#008888` (dark cyan)
|
||||||
|
|
||||||
|
### Color Harmony
|
||||||
|
|
||||||
|
- Start with a base palette (e.g., Nord, Gruvbox, Tokyo Night)
|
||||||
|
- Define your palette in `defs`
|
||||||
|
- Reference colors consistently
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Test your theme with:
|
||||||
|
- Different message types (user, assistant, errors)
|
||||||
|
- Tool executions (success and error states)
|
||||||
|
- Markdown content (headings, code, lists, etc)
|
||||||
|
- Long text that wraps
|
||||||
|
|
||||||
|
## Color Format Reference
|
||||||
|
|
||||||
|
### Hex Colors
|
||||||
|
|
||||||
|
Standard 6-digit hex format:
|
||||||
|
- `"#ff0000"` - Red
|
||||||
|
- `"#00ff00"` - Green
|
||||||
|
- `"#0000ff"` - Blue
|
||||||
|
- `"#808080"` - Gray
|
||||||
|
- `"#ffffff"` - White
|
||||||
|
- `"#000000"` - Black
|
||||||
|
|
||||||
|
RGB values: `#RRGGBB` where each component is `00-ff` (0-255)
|
||||||
|
|
||||||
|
### 256-Color Palette
|
||||||
|
|
||||||
|
Use numeric indices (0-255) to reference the xterm 256-color palette:
|
||||||
|
|
||||||
|
**Colors 0-15:** Basic ANSI colors (terminal-dependent, may be themed)
|
||||||
|
- `0` - Black
|
||||||
|
- `1` - Red
|
||||||
|
- `2` - Green
|
||||||
|
- `3` - Yellow
|
||||||
|
- `4` - Blue
|
||||||
|
- `5` - Magenta
|
||||||
|
- `6` - Cyan
|
||||||
|
- `7` - White
|
||||||
|
- `8-15` - Bright variants
|
||||||
|
|
||||||
|
**Colors 16-231:** 6×6×6 RGB cube (standardized)
|
||||||
|
- Formula: `16 + 36×R + 6×G + B` where R, G, B are 0-5
|
||||||
|
- Example: `39` = bright cyan, `196` = bright red
|
||||||
|
|
||||||
|
**Colors 232-255:** Grayscale ramp (standardized)
|
||||||
|
- `232` - Darkest gray
|
||||||
|
- `255` - Near white
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"gray": 242,
|
||||||
|
"brightCyan": 51,
|
||||||
|
"darkBlue": 18
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"muted": "gray",
|
||||||
|
"accent": "brightCyan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Works everywhere (`TERM=xterm-256color`)
|
||||||
|
- No truecolor detection needed
|
||||||
|
- Standardized RGB cube (16-231) looks the same on all terminals
|
||||||
|
|
||||||
|
### Terminal Compatibility
|
||||||
|
|
||||||
|
Pi uses 24-bit RGB colors (`\x1b[38;2;R;G;Bm`). Most modern terminals support this:
|
||||||
|
|
||||||
|
- ✅ iTerm2, Alacritty, Kitty, WezTerm
|
||||||
|
- ✅ Windows Terminal
|
||||||
|
- ✅ VS Code integrated terminal
|
||||||
|
- ✅ Modern GNOME Terminal, Konsole
|
||||||
|
|
||||||
|
For older terminals with only 256-color support, Pi automatically falls back to the nearest 256-color approximation.
|
||||||
|
|
||||||
|
To check if your terminal supports truecolor:
|
||||||
|
```bash
|
||||||
|
echo $COLORTERM # Should output "truecolor" or "24bit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Themes
|
||||||
|
|
||||||
|
See the built-in themes for complete examples:
|
||||||
|
- [Dark theme](../src/themes/dark.json)
|
||||||
|
- [Light theme](../src/themes/light.json)
|
||||||
|
|
||||||
|
## Schema Validation
|
||||||
|
|
||||||
|
Themes are validated on load using [TypeBox](https://github.com/sinclairzx81/typebox) + [Ajv](https://ajv.js.org/).
|
||||||
|
|
||||||
|
Invalid themes will show an error with details about what's wrong:
|
||||||
|
```
|
||||||
|
Error loading theme 'my-theme':
|
||||||
|
- colors.accent: must be string or number
|
||||||
|
- colors.mdHeading: required property missing
|
||||||
|
```
|
||||||
|
|
||||||
|
For editor support, the JSON schema is available at:
|
||||||
|
```
|
||||||
|
https://pi.mariozechner.at/theme-schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to your theme file for auto-completion and validation:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://pi.mariozechner.at/theme-schema.json",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Theme Class
|
||||||
|
|
||||||
|
Themes are loaded and converted to a `Theme` class that provides type-safe color methods:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Theme {
|
||||||
|
// Apply foreground color
|
||||||
|
fg(color: ThemeColor, text: string): string
|
||||||
|
|
||||||
|
// Apply background color
|
||||||
|
bg(color: ThemeBg, text: string): string
|
||||||
|
|
||||||
|
// Text attributes (preserve current colors)
|
||||||
|
bold(text: string): string
|
||||||
|
dim(text: string): string
|
||||||
|
italic(text: string): string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Theme Instance
|
||||||
|
|
||||||
|
The active theme is available as a global singleton in `coding-agent`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// theme.ts
|
||||||
|
export let theme: Theme;
|
||||||
|
|
||||||
|
export function setTheme(name: string) {
|
||||||
|
theme = loadTheme(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage throughout coding-agent
|
||||||
|
import { theme } from './theme.js';
|
||||||
|
|
||||||
|
theme.fg('accent', 'Selected')
|
||||||
|
theme.bg('userMessageBg', content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### TUI Component Theming
|
||||||
|
|
||||||
|
TUI components (like `Markdown`, `SelectList`, `Editor`) are in the `@mariozechner/pi-tui` package and don't have direct access to the theme. Instead, they define interfaces for the colors they need:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In @mariozechner/pi-tui
|
||||||
|
export interface MarkdownTheme {
|
||||||
|
heading: (text: string) => string;
|
||||||
|
link: (text: string) => string;
|
||||||
|
code: (text: string) => string;
|
||||||
|
codeBlock: (text: string) => string;
|
||||||
|
codeBlockBorder: (text: string) => string;
|
||||||
|
quote: (text: string) => string;
|
||||||
|
quoteBorder: (text: string) => string;
|
||||||
|
hr: (text: string) => string;
|
||||||
|
listBullet: (text: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Markdown {
|
||||||
|
constructor(
|
||||||
|
text: string,
|
||||||
|
paddingX: number,
|
||||||
|
paddingY: number,
|
||||||
|
defaultTextStyle?: DefaultTextStyle,
|
||||||
|
theme?: MarkdownTheme // Optional theme functions
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage in component
|
||||||
|
renderHeading(text: string) {
|
||||||
|
return this.theme.heading(text); // Applies color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `coding-agent` provides themed functions when creating components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In coding-agent
|
||||||
|
import { theme } from './theme.js';
|
||||||
|
import { Markdown } from '@mariozechner/pi-tui';
|
||||||
|
|
||||||
|
// Helper to create markdown theme functions
|
||||||
|
function getMarkdownTheme(): MarkdownTheme {
|
||||||
|
return {
|
||||||
|
heading: (text) => theme.fg('mdHeading', text),
|
||||||
|
link: (text) => theme.fg('mdLink', text),
|
||||||
|
code: (text) => theme.fg('mdCode', text),
|
||||||
|
codeBlock: (text) => theme.fg('mdCodeBlock', text),
|
||||||
|
codeBlockBorder: (text) => theme.fg('mdCodeBlockBorder', text),
|
||||||
|
quote: (text) => theme.fg('mdQuote', text),
|
||||||
|
quoteBorder: (text) => theme.fg('mdQuoteBorder', text),
|
||||||
|
hr: (text) => theme.fg('mdHr', text),
|
||||||
|
listBullet: (text) => theme.fg('mdListBullet', text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create markdown with theme
|
||||||
|
const md = new Markdown(
|
||||||
|
text,
|
||||||
|
1, 1,
|
||||||
|
{ bgColor: theme.bg('userMessageBg') },
|
||||||
|
getMarkdownTheme()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach:
|
||||||
|
- Keeps TUI components theme-agnostic (reusable in other projects)
|
||||||
|
- Maintains type safety via interfaces
|
||||||
|
- Allows components to have sensible defaults if no theme provided
|
||||||
|
- Centralizes theme access in `coding-agent`
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
```typescript
|
||||||
|
const theme = loadTheme('dark');
|
||||||
|
|
||||||
|
// Apply foreground colors
|
||||||
|
theme.fg('accent', 'Selected')
|
||||||
|
theme.fg('success', '✓ Done')
|
||||||
|
theme.fg('error', 'Failed')
|
||||||
|
|
||||||
|
// Apply background colors
|
||||||
|
theme.bg('userMessageBg', content)
|
||||||
|
theme.bg('toolSuccessBg', output)
|
||||||
|
|
||||||
|
// Combine styles
|
||||||
|
theme.bold(theme.fg('accent', 'Title'))
|
||||||
|
theme.dim(theme.fg('muted', 'metadata'))
|
||||||
|
|
||||||
|
// Nested foreground + background
|
||||||
|
const userMsg = theme.bg('userMessageBg',
|
||||||
|
theme.fg('userMessageText', 'Hello')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Color resolution:**
|
||||||
|
|
||||||
|
1. **Detect terminal capabilities:**
|
||||||
|
- Check `$COLORTERM` env var (`truecolor` or `24bit` → truecolor support)
|
||||||
|
- Check `$TERM` env var (`*-256color` → 256-color support)
|
||||||
|
- Fallback to 256-color mode if detection fails
|
||||||
|
|
||||||
|
2. **Load JSON theme file**
|
||||||
|
|
||||||
|
3. **Resolve `vars` references recursively:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"primary": "#0066cc",
|
||||||
|
"accent": "primary"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"accent": "accent" // → "primary" → "#0066cc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Convert colors to ANSI codes based on terminal capability:**
|
||||||
|
|
||||||
|
**Truecolor mode (24-bit):**
|
||||||
|
- Hex (`"#ff0000"`) → `\x1b[38;2;255;0;0m`
|
||||||
|
- 256-color (`42`) → `\x1b[38;5;42m` (keep as-is)
|
||||||
|
- Empty string (`""`) → `\x1b[39m`
|
||||||
|
|
||||||
|
**256-color mode:**
|
||||||
|
- Hex (`"#ff0000"`) → convert to nearest RGB cube color → `\x1b[38;5;196m`
|
||||||
|
- 256-color (`42`) → `\x1b[38;5;42m` (keep as-is)
|
||||||
|
- Empty string (`""`) → `\x1b[39m`
|
||||||
|
|
||||||
|
**Hex to 256-color conversion:**
|
||||||
|
```typescript
|
||||||
|
// Convert RGB to 6x6x6 cube (colors 16-231)
|
||||||
|
r_index = Math.round(r / 255 * 5)
|
||||||
|
g_index = Math.round(g / 255 * 5)
|
||||||
|
b_index = Math.round(b / 255 * 5)
|
||||||
|
color_index = 16 + 36 * r_index + 6 * g_index + b_index
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Cache as `Theme` instance**
|
||||||
|
|
||||||
|
This ensures themes work correctly regardless of terminal capabilities, with graceful degradation from truecolor to 256-color.
|
||||||
|
|
@ -1,310 +0,0 @@
|
||||||
# Theme System Analysis
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
Issue #7: In terminals with light backgrounds, some outputs use dark colors that are hard to read. We need a theme system that allows users to choose between light and dark themes.
|
|
||||||
|
|
||||||
## Current Color Usage Analysis
|
|
||||||
|
|
||||||
### Color Usage Statistics
|
|
||||||
|
|
||||||
Total chalk color calls: 132 across 14 files
|
|
||||||
|
|
||||||
Most frequent colors:
|
|
||||||
- `chalk.dim` (48 occurrences) - Used for secondary text
|
|
||||||
- `chalk.gray` (28 occurrences) - Used for borders, metadata, dimmed content
|
|
||||||
- `chalk.bold` (20 occurrences) - Used for emphasis
|
|
||||||
- `chalk.blue` (12 occurrences) - Used for selections, borders, links
|
|
||||||
- `chalk.cyan` (9 occurrences) - Used for primary UI elements (logo, list bullets, code)
|
|
||||||
- `chalk.red` (7 occurrences) - Used for errors, stderr output
|
|
||||||
- `chalk.green` (6 occurrences) - Used for success, stdout output
|
|
||||||
- `chalk.yellow` (3 occurrences) - Used for headings in markdown
|
|
||||||
- `chalk.bgRgb` (6 occurrences) - Used for custom backgrounds in Text/Markdown
|
|
||||||
|
|
||||||
### Files Using Colors
|
|
||||||
|
|
||||||
#### coding-agent Package
|
|
||||||
1. **main.ts** - CLI output messages
|
|
||||||
2. **tui/assistant-message.ts** - Thinking text (gray italic), errors (red), aborted (red)
|
|
||||||
3. **tui/dynamic-border.ts** - Configurable border color (default blue)
|
|
||||||
4. **tui/footer.ts** - Stats and pwd (gray)
|
|
||||||
5. **tui/model-selector.ts** - Borders (blue), selection arrow (blue), provider badge (gray), checkmark (green)
|
|
||||||
6. **tui/session-selector.ts** - Border (blue), selection cursor (blue), metadata (dim)
|
|
||||||
7. **tui/thinking-selector.ts** - Border (blue)
|
|
||||||
8. **tui/tool-execution.ts** - stdout (green), stderr (red), dim lines (dim), line numbers
|
|
||||||
9. **tui/tui-renderer.ts** - Logo (bold cyan), instructions (dim/gray)
|
|
||||||
|
|
||||||
#### tui Package
|
|
||||||
1. **components/editor.ts** - Horizontal border (gray)
|
|
||||||
2. **components/loader.ts** - Spinner (cyan), message (dim)
|
|
||||||
3. **components/markdown.ts** - Complex color system:
|
|
||||||
- H1 headings: bold.underline.yellow
|
|
||||||
- H2 headings: bold.yellow
|
|
||||||
- H3+ headings: bold
|
|
||||||
- Code blocks: gray (delimiters), dim (indent), green (code)
|
|
||||||
- List bullets: cyan
|
|
||||||
- Blockquotes: gray (pipe), italic (text)
|
|
||||||
- Horizontal rules: gray
|
|
||||||
- Inline code: gray (backticks), cyan (code)
|
|
||||||
- Links: underline.blue (text), gray (URL)
|
|
||||||
- Strikethrough: strikethrough
|
|
||||||
- Tables: bold (headers)
|
|
||||||
4. **components/select-list.ts** - No matches (gray), selection arrow (blue), selected item (blue), description (gray)
|
|
||||||
5. **components/text.ts** - Custom bgRgb support
|
|
||||||
|
|
||||||
### Color System Architecture
|
|
||||||
|
|
||||||
#### Current Implementation
|
|
||||||
- Colors are hardcoded using `chalk` directly
|
|
||||||
- No centralized theme management
|
|
||||||
- No way to switch themes at runtime
|
|
||||||
- Some components accept color parameters (e.g., DynamicBorder, Text, Markdown)
|
|
||||||
|
|
||||||
#### Markdown Component Color System
|
|
||||||
The Markdown component has a `Color` type enum:
|
|
||||||
```typescript
|
|
||||||
type Color = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" |
|
|
||||||
"bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray"
|
|
||||||
```
|
|
||||||
|
|
||||||
It accepts optional `bgColor` and `fgColor` parameters, plus `customBgRgb`.
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
### Theme Structure
|
|
||||||
|
|
||||||
Create a centralized theme system with semantic color names:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Theme {
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
// UI Chrome
|
|
||||||
border: ChalkFunction;
|
|
||||||
selection: ChalkFunction;
|
|
||||||
selectionText: ChalkFunction;
|
|
||||||
|
|
||||||
// Text hierarchy
|
|
||||||
primary: ChalkFunction;
|
|
||||||
secondary: ChalkFunction;
|
|
||||||
dim: ChalkFunction;
|
|
||||||
|
|
||||||
// Semantic colors
|
|
||||||
error: ChalkFunction;
|
|
||||||
success: ChalkFunction;
|
|
||||||
warning: ChalkFunction;
|
|
||||||
info: ChalkFunction;
|
|
||||||
|
|
||||||
// Code/output
|
|
||||||
code: ChalkFunction;
|
|
||||||
codeDelimiter: ChalkFunction;
|
|
||||||
stdout: ChalkFunction;
|
|
||||||
stderr: ChalkFunction;
|
|
||||||
|
|
||||||
// Markdown specific
|
|
||||||
heading1: ChalkFunction;
|
|
||||||
heading2: ChalkFunction;
|
|
||||||
heading3: ChalkFunction;
|
|
||||||
link: ChalkFunction;
|
|
||||||
linkUrl: ChalkFunction;
|
|
||||||
listBullet: ChalkFunction;
|
|
||||||
blockquote: ChalkFunction;
|
|
||||||
blockquotePipe: ChalkFunction;
|
|
||||||
inlineCode: ChalkFunction;
|
|
||||||
inlineCodeDelimiter: ChalkFunction;
|
|
||||||
|
|
||||||
// Backgrounds (optional, for components like Text/Markdown)
|
|
||||||
backgroundRgb?: { r: number; g: number; b: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChalkFunction = (str: string) => string;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Built-in Themes
|
|
||||||
|
|
||||||
#### Dark Theme (current default)
|
|
||||||
```typescript
|
|
||||||
const darkTheme: Theme = {
|
|
||||||
name: "dark",
|
|
||||||
border: chalk.blue,
|
|
||||||
selection: chalk.blue,
|
|
||||||
selectionText: chalk.blue,
|
|
||||||
primary: (s) => s, // no color
|
|
||||||
secondary: chalk.gray,
|
|
||||||
dim: chalk.dim,
|
|
||||||
error: chalk.red,
|
|
||||||
success: chalk.green,
|
|
||||||
warning: chalk.yellow,
|
|
||||||
info: chalk.cyan,
|
|
||||||
code: chalk.green,
|
|
||||||
codeDelimiter: chalk.gray,
|
|
||||||
stdout: chalk.green,
|
|
||||||
stderr: chalk.red,
|
|
||||||
heading1: chalk.bold.underline.yellow,
|
|
||||||
heading2: chalk.bold.yellow,
|
|
||||||
heading3: chalk.bold,
|
|
||||||
link: chalk.underline.blue,
|
|
||||||
linkUrl: chalk.gray,
|
|
||||||
listBullet: chalk.cyan,
|
|
||||||
blockquote: chalk.italic,
|
|
||||||
blockquotePipe: chalk.gray,
|
|
||||||
inlineCode: chalk.cyan,
|
|
||||||
inlineCodeDelimiter: chalk.gray,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Light Theme
|
|
||||||
```typescript
|
|
||||||
const lightTheme: Theme = {
|
|
||||||
name: "light",
|
|
||||||
border: chalk.blue,
|
|
||||||
selection: chalk.blue,
|
|
||||||
selectionText: chalk.blue.bold,
|
|
||||||
primary: (s) => s,
|
|
||||||
secondary: chalk.gray,
|
|
||||||
dim: chalk.gray, // Don't use chalk.dim on light backgrounds
|
|
||||||
error: chalk.red.bold,
|
|
||||||
success: chalk.green.bold,
|
|
||||||
warning: chalk.yellow.bold,
|
|
||||||
info: chalk.cyan.bold,
|
|
||||||
code: chalk.green.bold,
|
|
||||||
codeDelimiter: chalk.gray,
|
|
||||||
stdout: chalk.green.bold,
|
|
||||||
stderr: chalk.red.bold,
|
|
||||||
heading1: chalk.bold.underline.blue,
|
|
||||||
heading2: chalk.bold.blue,
|
|
||||||
heading3: chalk.bold,
|
|
||||||
link: chalk.underline.blue,
|
|
||||||
linkUrl: chalk.blue,
|
|
||||||
listBullet: chalk.blue.bold,
|
|
||||||
blockquote: chalk.italic,
|
|
||||||
blockquotePipe: chalk.gray,
|
|
||||||
inlineCode: chalk.blue.bold,
|
|
||||||
inlineCodeDelimiter: chalk.gray,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation Plan
|
|
||||||
|
|
||||||
#### 1. Create Theme Module
|
|
||||||
**Location:** `packages/tui/src/theme.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Theme { ... }
|
|
||||||
export const darkTheme: Theme = { ... };
|
|
||||||
export const lightTheme: Theme = { ... };
|
|
||||||
export const themes = { dark: darkTheme, light: lightTheme };
|
|
||||||
|
|
||||||
let currentTheme: Theme = darkTheme;
|
|
||||||
|
|
||||||
export function setTheme(theme: Theme): void {
|
|
||||||
currentTheme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTheme(): Theme {
|
|
||||||
return currentTheme;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Update Settings Manager
|
|
||||||
**Location:** `packages/coding-agent/src/settings-manager.ts`
|
|
||||||
|
|
||||||
Add `theme` field to Settings interface:
|
|
||||||
```typescript
|
|
||||||
export interface Settings {
|
|
||||||
lastChangelogVersion?: string;
|
|
||||||
theme?: "dark" | "light";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Create Theme Selector Component
|
|
||||||
**Location:** `packages/coding-agent/src/tui/theme-selector.ts`
|
|
||||||
|
|
||||||
Similar to ModelSelector and ThinkingSelector, create a TUI component for selecting themes.
|
|
||||||
|
|
||||||
#### 4. Refactor Color Usage
|
|
||||||
|
|
||||||
Replace all hardcoded `chalk.*` calls with `theme.*`:
|
|
||||||
|
|
||||||
**Example - Before:**
|
|
||||||
```typescript
|
|
||||||
lines.push(chalk.blue("─".repeat(width)));
|
|
||||||
const cursor = chalk.blue("› ");
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example - After:**
|
|
||||||
```typescript
|
|
||||||
const theme = getTheme();
|
|
||||||
lines.push(theme.border("─".repeat(width)));
|
|
||||||
const cursor = theme.selection("› ");
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Update Components
|
|
||||||
|
|
||||||
##### High Priority (User-facing content issues)
|
|
||||||
1. **markdown.ts** - Update all color calls to use theme
|
|
||||||
2. **tool-execution.ts** - stdout/stderr colors
|
|
||||||
3. **assistant-message.ts** - Error messages
|
|
||||||
4. **tui-renderer.ts** - Logo and instructions
|
|
||||||
5. **footer.ts** - Stats display
|
|
||||||
|
|
||||||
##### Medium Priority (UI chrome)
|
|
||||||
6. **dynamic-border.ts** - Accept theme parameter
|
|
||||||
7. **model-selector.ts** - Selection colors
|
|
||||||
8. **session-selector.ts** - Selection colors
|
|
||||||
9. **thinking-selector.ts** - Border colors
|
|
||||||
10. **select-list.ts** - Selection colors
|
|
||||||
11. **loader.ts** - Spinner color
|
|
||||||
12. **editor.ts** - Border color
|
|
||||||
|
|
||||||
##### Low Priority (CLI output)
|
|
||||||
13. **main.ts** - CLI messages
|
|
||||||
|
|
||||||
#### 6. Add Theme Slash Command
|
|
||||||
**Location:** `packages/coding-agent/src/tui/tui-renderer.ts`
|
|
||||||
|
|
||||||
Add `/theme` command similar to `/model` and `/thinking`.
|
|
||||||
|
|
||||||
#### 7. Initialize Theme on Startup
|
|
||||||
**Location:** `packages/coding-agent/src/main.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Load theme from settings
|
|
||||||
const settingsManager = new SettingsManager();
|
|
||||||
const themeName = settingsManager.getTheme() || "dark";
|
|
||||||
const theme = themes[themeName] || darkTheme;
|
|
||||||
setTheme(theme);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration Strategy
|
|
||||||
|
|
||||||
1. **Phase 1:** Create theme infrastructure (theme.ts, types, built-in themes)
|
|
||||||
2. **Phase 2:** Update TUI package components (markdown, text, loader, editor, select-list)
|
|
||||||
3. **Phase 3:** Update coding-agent TUI components (all tui/*.ts files)
|
|
||||||
4. **Phase 4:** Add theme selector and persistence
|
|
||||||
5. **Phase 5:** Update CLI output in main.ts (optional, low priority)
|
|
||||||
|
|
||||||
### Testing Plan
|
|
||||||
|
|
||||||
1. Test both themes in terminals with light backgrounds
|
|
||||||
2. Test both themes in terminals with dark backgrounds
|
|
||||||
3. Verify theme switching works at runtime via `/theme`
|
|
||||||
4. Verify theme persists across sessions via settings.json
|
|
||||||
5. Test all components for readability in both themes
|
|
||||||
|
|
||||||
### Open Questions
|
|
||||||
|
|
||||||
1. Should we support custom user themes loaded from a JSON file?
|
|
||||||
2. Should we auto-detect terminal background color and choose theme automatically?
|
|
||||||
3. Should theme apply to background colors used in Text/Markdown components?
|
|
||||||
4. Do we need more than two themes initially?
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
None - the default theme will remain "dark" matching current behavior.
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
|
|
||||||
- Theme getter is called frequently (on every render)
|
|
||||||
- Should be a simple variable access, not a function call chain
|
|
||||||
- Consider caching theme functions if performance becomes an issue
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-coding-agent",
|
"name": "@mariozechner/pi-coding-agent",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.21",
|
"@mariozechner/pi-agent": "^0.7.22",
|
||||||
"@mariozechner/pi-ai": "^0.7.21",
|
"@mariozechner/pi-ai": "^0.7.22",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^11.0.3"
|
"glob": "^11.0.3"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi",
|
"name": "@mariozechner/pi",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "CLI tool for managing vLLM deployments on GPU pods",
|
"description": "CLI tool for managing vLLM deployments on GPU pods",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.21",
|
"@mariozechner/pi-agent": "^0.7.22",
|
||||||
"chalk": "^5.5.0"
|
"chalk": "^5.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-proxy",
|
"name": "@mariozechner/pi-proxy",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "CORS and authentication proxy for pi-ai",
|
"description": "CORS and authentication proxy for pi-ai",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-tui",
|
"name": "@mariozechner/pi-tui",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,22 @@ function wrapSingleLine(line: string, width: number): string[] {
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
const wordVisibleLength = visibleWidth(word);
|
const wordVisibleLength = visibleWidth(word);
|
||||||
|
|
||||||
|
// Word itself is too long - break it character by character
|
||||||
|
if (wordVisibleLength > width) {
|
||||||
|
if (currentLine) {
|
||||||
|
wrapped.push(currentLine);
|
||||||
|
currentLine = "";
|
||||||
|
currentVisibleLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break long word
|
||||||
|
const broken = breakLongWord(word, width, tracker);
|
||||||
|
wrapped.push(...broken.slice(0, -1));
|
||||||
|
currentLine = broken[broken.length - 1];
|
||||||
|
currentVisibleLength = visibleWidth(currentLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if adding this word would exceed width
|
// Check if adding this word would exceed width
|
||||||
const spaceNeeded = currentVisibleLength > 0 ? 1 : 0;
|
const spaceNeeded = currentVisibleLength > 0 ? 1 : 0;
|
||||||
const totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;
|
const totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;
|
||||||
|
|
@ -190,6 +206,42 @@ function wrapSingleLine(line: string, width: number): string[] {
|
||||||
return wrapped.length > 0 ? wrapped : [""];
|
return wrapped.length > 0 ? wrapped : [""];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
let currentLine = tracker.getActiveCodes();
|
||||||
|
let currentWidth = 0;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < word.length) {
|
||||||
|
const ansiResult = extractAnsiCode(word, i);
|
||||||
|
if (ansiResult) {
|
||||||
|
currentLine += ansiResult.code;
|
||||||
|
tracker.process(ansiResult.code);
|
||||||
|
i += ansiResult.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char = word[i];
|
||||||
|
const charWidth = visibleWidth(char);
|
||||||
|
|
||||||
|
if (currentWidth + charWidth > width) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = tracker.getActiveCodes();
|
||||||
|
currentWidth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine += char;
|
||||||
|
currentWidth += charWidth;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.length > 0 ? lines : [""];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply background color to a line, padding to full width.
|
* Apply background color to a line, padding to full width.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-web-ui",
|
"name": "@mariozechner/pi-web-ui",
|
||||||
"version": "0.7.21",
|
"version": "0.7.22",
|
||||||
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
|
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
"@mariozechner/pi-ai": "^0.7.21",
|
"@mariozechner/pi-ai": "^0.7.22",
|
||||||
"@mariozechner/pi-tui": "^0.7.21",
|
"@mariozechner/pi-tui": "^0.7.22",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.544.0",
|
"lucide": "^0.544.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue