From 1f68d6eb40dbf3955b38060a5b4926f7f02b30db Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 19 Nov 2025 01:50:45 +0100 Subject: [PATCH] Release v0.7.22 --- package-lock.json | 28 +- packages/agent/package.json | 6 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 17 - packages/coding-agent/CHANGELOG.md | 6 + packages/coding-agent/docs/color-inventory.md | 112 --- packages/coding-agent/docs/design-tokens.md | 938 ------------------ packages/coding-agent/docs/theme-colors.md | 182 ---- packages/coding-agent/docs/theme.md | 563 +++++++++++ packages/coding-agent/docs/themes.md | 310 ------ packages/coding-agent/package.json | 6 +- packages/pods/package.json | 4 +- packages/proxy/package.json | 2 +- packages/tui/package.json | 2 +- packages/tui/src/utils.ts | 52 + packages/web-ui/package.json | 6 +- 16 files changed, 649 insertions(+), 1587 deletions(-) delete mode 100644 packages/coding-agent/docs/color-inventory.md delete mode 100644 packages/coding-agent/docs/design-tokens.md delete mode 100644 packages/coding-agent/docs/theme-colors.md create mode 100644 packages/coding-agent/docs/theme.md delete mode 100644 packages/coding-agent/docs/themes.md diff --git a/package-lock.json b/package-lock.json index dccfb315..459b8b8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3195,11 +3195,11 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.7.20", - "@mariozechner/pi-tui": "^0.7.20" + "@mariozechner/pi-ai": "^0.7.21", + "@mariozechner/pi-tui": "^0.7.21" }, "devDependencies": { "@types/node": "^24.3.0", @@ -3225,7 +3225,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.61.0", @@ -3272,11 +3272,11 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.20", - "@mariozechner/pi-ai": "^0.7.20", + "@mariozechner/pi-agent": "^0.7.21", + "@mariozechner/pi-ai": "^0.7.21", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" @@ -3319,10 +3319,10 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.20", + "@mariozechner/pi-agent": "^0.7.21", "chalk": "^5.5.0" }, "bin": { @@ -3345,7 +3345,7 @@ }, "packages/proxy": { "name": "@mariozechner/pi-proxy", - "version": "0.7.21", + "version": "0.7.22", "dependencies": { "@hono/node-server": "^1.14.0", "hono": "^4.6.16" @@ -3361,7 +3361,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -3400,12 +3400,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.7.21", + "version": "0.7.22", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.20", - "@mariozechner/pi-tui": "^0.7.20", + "@mariozechner/pi-ai": "^0.7.21", + "@mariozechner/pi-tui": "^0.7.21", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index 3221444b..3f01bb69 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent", - "version": "0.7.21", + "version": "0.7.22", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -18,8 +18,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.7.21", - "@mariozechner/pi-tui": "^0.7.21" + "@mariozechner/pi-ai": "^0.7.22", + "@mariozechner/pi-tui": "^0.7.22" }, "keywords": [ "ai", diff --git a/packages/ai/package.json b/packages/ai/package.json index 8e677d0b..fb858d7e 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.7.21", + "version": "0.7.22", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 7a8bdc99..5afb3b27 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3572,23 +3572,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 40000, } 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": { id: "google/gemini-2.5-flash", name: "Google: Gemini 2.5 Flash", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d6d680c7..cc0e51d9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,12 @@ ## [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 ### Fixed diff --git a/packages/coding-agent/docs/color-inventory.md b/packages/coding-agent/docs/color-inventory.md deleted file mode 100644 index ccc3ac50..00000000 --- a/packages/coding-agent/docs/color-inventory.md +++ /dev/null @@ -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. diff --git a/packages/coding-agent/docs/design-tokens.md b/packages/coding-agent/docs/design-tokens.md deleted file mode 100644 index 3fec87b7..00000000 --- a/packages/coding-agent/docs/design-tokens.md +++ /dev/null @@ -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([ - ['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; - tokens?: any; // Partial but with JsonThemeValue instead of ChalkFunction -} - -// Map chalk color names to actual chalk functions -const chalkMap: Record = { - 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 -): 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 = {}; - 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): 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 diff --git a/packages/coding-agent/docs/theme-colors.md b/packages/coding-agent/docs/theme-colors.md deleted file mode 100644 index 4ce53ff9..00000000 --- a/packages/coding-agent/docs/theme-colors.md +++ /dev/null @@ -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" -} -``` diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md new file mode 100644 index 00000000..b0db97a3 --- /dev/null +++ b/packages/coding-agent/docs/theme.md @@ -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. diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md deleted file mode 100644 index 0529f62b..00000000 --- a/packages/coding-agent/docs/themes.md +++ /dev/null @@ -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 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 01e6e8af..56921b33 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "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", "type": "module", "bin": { @@ -21,8 +21,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.21", - "@mariozechner/pi-ai": "^0.7.21", + "@mariozechner/pi-agent": "^0.7.22", + "@mariozechner/pi-ai": "^0.7.22", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" diff --git a/packages/pods/package.json b/packages/pods/package.json index ddd2630f..77841f13 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi", - "version": "0.7.21", + "version": "0.7.22", "description": "CLI tool for managing vLLM deployments on GPU pods", "type": "module", "bin": { @@ -34,7 +34,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.21", + "@mariozechner/pi-agent": "^0.7.22", "chalk": "^5.5.0" }, "devDependencies": {} diff --git a/packages/proxy/package.json b/packages/proxy/package.json index db0fbc50..1e88b31c 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-proxy", - "version": "0.7.21", + "version": "0.7.22", "type": "module", "description": "CORS and authentication proxy for pi-ai", "main": "dist/index.js", diff --git a/packages/tui/package.json b/packages/tui/package.json index 903aca56..6d2c6565 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "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", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index 8769d8f8..0ab0e5c6 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -160,6 +160,22 @@ function wrapSingleLine(line: string, width: number): string[] { for (const word of words) { 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 const spaceNeeded = currentVisibleLength > 0 ? 1 : 0; const totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength; @@ -190,6 +206,42 @@ function wrapSingleLine(line: string, width: number): string[] { 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. * diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 04871a09..f8e75372 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "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", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.21", - "@mariozechner/pi-tui": "^0.7.21", + "@mariozechner/pi-ai": "^0.7.22", + "@mariozechner/pi-tui": "^0.7.22", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0",