diff --git a/README.md b/README.md index 71b5a74b..27d02c8a 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,63 @@ # Pi Monorepo -A collection of tools for managing LLM deployments and building AI agents. +Tools for building AI agents and managing LLM deployments. ## Packages -- **[@mariozechner/pi-ai](packages/ai)** - Unified multi-provider LLM API -- **[@mariozechner/pi-web-ui](packages/web-ui)** - Web components for building AI chat interfaces -- **[@mariozechner/pi-proxy](packages/proxy)** - CORS proxy for browser-based LLM API calls -- **[@mariozechner/pi-tui](packages/tui)** - Terminal UI library with differential rendering -- **[@mariozechner/pi-agent](packages/agent)** - General-purpose agent with tool calling and session persistence -- **[@mariozechner/pi](packages/pods)** - CLI for managing vLLM deployments on GPU pods +| Package | Description | +|---------|-------------| +| **[@mariozechner/pi-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) | +| **[@mariozechner/pi-agent](packages/agent)** | Agent runtime with tool calling and state management | +| **[@mariozechner/pi-coding-agent](packages/coding-agent)** | Interactive coding agent CLI | +| **[@mariozechner/pi-tui](packages/tui)** | Terminal UI library with differential rendering | +| **[@mariozechner/pi-web-ui](packages/web-ui)** | Web components for AI chat interfaces | +| **[@mariozechner/pi-proxy](packages/proxy)** | CORS proxy for browser-based LLM API calls | +| **[@mariozechner/pi](packages/pods)** | CLI for managing vLLM deployments on GPU pods | **Related:** -- **[sitegeist](https://github.com/badlogic/sitegeist)** - Browser extension for AI-powered web navigation (uses pi-ai and pi-web-ui) +- **[sitegeist](https://github.com/badlogic/sitegeist)** - Browser extension for AI-powered web navigation ## Development -This is a monorepo using npm workspaces for package management and a dual TypeScript configuration for development and building. - -### Common Commands +### Setup ```bash -# Install all dependencies -npm install +npm install # Install all dependencies +npm run build # Build all packages +npm run check # Lint, format, and type check +``` -# Build all packages (required for publishing to NPM) -npm run build +### Running Without Building -# Clean out dist/ folders in all packages -npm run clean +Use `tsx` to run TypeScript source directly during development: -# Run linting, formatting, and tsc typechecking (no build needed) -npm run check - -# Run directly with tsx during development (no build needed) +```bash +cd packages/coding-agent && npx tsx src/cli.ts cd packages/pods && npx tsx src/cli.ts -cd packages/agent && npx tsx src/cli.ts ``` -### Package Dependencies +### Versioning (Lockstep) -The packages have the following dependency structure: - -`pi-tui` -> `pi-agent` -> `pi` - -When new packages are added, the must be inserted in the correct order in the `build` script in `package.json`. - -### TypeScript Configuration - -The monorepo uses a dual TypeScript configuration approach: -- **Root `tsconfig.json`**: Contains path mappings for all packages, used for type checking and development with `tsx` -- **Package `tsconfig.build.json`**: Clean build configuration with `rootDir` and `outDir`, used for production builds - -This setup allows: -- Type checking without building (`npm run check` works immediately) -- Running source files directly with `tsx` during development -- Clean, organized build outputs for publishing - -### Versioning - -All packages use **lockstep versioning** - they share the same version number: +**All packages MUST always have the same version number.** Use these commands to bump versions: ```bash -# Bump patch version (0.5.0 -> 0.5.1) -npm run version:patch - -# Bump minor version (0.5.0 -> 0.6.0) -npm run version:minor - -# Bump major version (0.5.0 -> 1.0.0) -npm run version:major +npm run version:patch # 0.7.5 -> 0.7.6 +npm run version:minor # 0.7.5 -> 0.8.0 +npm run version:major # 0.7.5 -> 1.0.0 ``` -These commands automatically: -1. Update all package versions -2. Sync inter-package dependency versions -3. Update package-lock.json +These commands: +1. Update all package versions to the same number +2. Update inter-package dependency versions (e.g., `pi-agent` depends on `pi-ai@^0.7.7`) +3. Update `package-lock.json` + +**Never manually edit version numbers.** The lockstep system ensures consistency across the monorepo. ### Publishing ```bash -# Dry run to see what would be published -npm run publish:dry - -# Publish all packages to npm -npm run publish +npm run publish:dry # Preview what will be published +npm run publish # Publish all packages to npm ``` ## License diff --git a/package-lock.json b/package-lock.json index c7d5cc7a..3cbc9dee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3193,11 +3193,11 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent", - "version": "0.7.5", + "version": "0.7.7", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.7.4", - "@mariozechner/pi-tui": "^0.7.4" + "@mariozechner/pi-ai": "^0.7.7", + "@mariozechner/pi-tui": "^0.7.7" }, "devDependencies": { "@types/node": "^24.3.0", @@ -3223,7 +3223,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.7.5", + "version": "0.7.7", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.61.0", @@ -3270,11 +3270,11 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.6", + "version": "0.7.7", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.4", - "@mariozechner/pi-ai": "^0.7.4", + "@mariozechner/pi-agent": "^0.7.7", + "@mariozechner/pi-ai": "^0.7.7", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" @@ -3317,10 +3317,10 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "0.7.5", + "version": "0.7.7", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.4", + "@mariozechner/pi-agent": "^0.7.7", "chalk": "^5.5.0" }, "bin": { @@ -3343,7 +3343,7 @@ }, "packages/proxy": { "name": "@mariozechner/pi-proxy", - "version": "0.7.5", + "version": "0.7.7", "dependencies": { "@hono/node-server": "^1.14.0", "hono": "^4.6.16" @@ -3359,7 +3359,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.7.5", + "version": "0.7.7", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -3398,12 +3398,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.7.5", + "version": "0.7.7", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.6.0", - "@mariozechner/pi-tui": "^0.7.4", + "@mariozechner/pi-ai": "^0.7.7", + "@mariozechner/pi-tui": "^0.7.7", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", @@ -3422,26 +3422,6 @@ "lit": "^3.3.1" } }, - "packages/web-ui/node_modules/@mariozechner/pi-ai": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.6.2.tgz", - "integrity": "sha512-sVuNRo7j2AL+dk2RQrjVa6+j5Hf+5wFssJoRs0EpSbaVlLveiEAXOYx8ajryirTvzzpAPzbOX4S/UoJiqDh1vQ==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.61.0", - "@google/genai": "^1.17.0", - "@sinclair/typebox": "^0.34.41", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "openai": "5.21.0", - "partial-json": "^0.1.7", - "zod-to-json-schema": "^3.24.6" - }, - "engines": { - "node": ">=20.0.0" - } - }, "packages/web-ui/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md deleted file mode 100644 index 7b2bc077..00000000 --- a/packages/agent/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog - -## [0.7.6] - Unreleased - -### Fixed - -- Fixed error message loss when `turn_end` event contains an error. Previously, errors in `turn_end` events (e.g., "Provider returned error" from OpenRouter Auto Router) were not captured in `agent.state.error`, making it appear as if the agent completed successfully. ([#6](https://github.com/badlogic/pi-mono/issues/6)) - -## [0.7.5] - 2025-11-13 - -Previous releases did not maintain a changelog. diff --git a/packages/agent/package.json b/packages/agent/package.json index 013e28fc..d58319d4 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent", - "version": "0.7.5", + "version": "0.7.7", "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.5", - "@mariozechner/pi-tui": "^0.7.5" + "@mariozechner/pi-ai": "^0.7.7", + "@mariozechner/pi-tui": "^0.7.7" }, "keywords": [ "ai", diff --git a/packages/ai/package.json b/packages/ai/package.json index 27beefae..2ee49ade 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.7.5", + "version": "0.7.7", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9f65fef5..53c38b0f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,10 +1,13 @@ # Changelog -## [0.7.7] - Unreleased +## [Unreleased] + +## [0.7.7] - 2025-11-13 ### Added -- Automatic changelog viewer on startup in interactive mode. When starting a new session (not continuing/resuming), the agent will display all changelog entries since the last version you used in a scrollable markdown viewer. The last shown version is tracked in `~/.pi/agent/settings.json`. +- Automatic changelog display on startup in interactive mode. When starting a new session (not continuing/resuming), the agent will display all changelog entries since the last version you used. The last shown version is tracked in `~/.pi/agent/settings.json`. +- `/changelog` command to display the changelog in the TUI - OpenRouter Auto Router model support ([#5](https://github.com/badlogic/pi-mono/pull/5)) - Windows Git Bash support with automatic detection and process tree termination ([#1](https://github.com/badlogic/pi-mono/pull/1)) @@ -18,6 +21,7 @@ - Fixed markdown list rendering bug where bullets were not displayed when list items contained inline code with cyan color formatting - Fixed context percentage showing 0% in footer when last assistant message was aborted ([#12](https://github.com/badlogic/pi-mono/issues/12)) +- Fixed error message loss when `turn_end` event contains an error. Previously, errors in `turn_end` events (e.g., "Provider returned error" from OpenRouter Auto Router) were not captured in `agent.state.error`, making it appear as if the agent completed successfully. ([#6](https://github.com/badlogic/pi-mono/issues/6)) ## [0.7.6] - 2025-11-13 diff --git a/packages/coding-agent/docs/color-inventory.md b/packages/coding-agent/docs/color-inventory.md new file mode 100644 index 00000000..ccc3ac50 --- /dev/null +++ b/packages/coding-agent/docs/color-inventory.md @@ -0,0 +1,112 @@ +# Color Usage Inventory + +## Complete list of all semantic color uses in the codebase + +### UI Chrome & Structure +- **border** - cyan - Borders around sections (changelog, selectors) +- **borderSubtle** - blue - Borders in selectors (model, session, thinking) +- **borderHorizontal** - gray - Horizontal separator in editor + +### Text Hierarchy +- **textPrimary** - default/none - Main content text +- **textSecondary** - gray - Metadata, timestamps, descriptions +- **textDim** - dim - De-emphasized content, placeholder text, "..." indicators +- **textBold** - bold - Emphasis (note: this is styling, not color) + +### Interactive/Selection +- **selectionCursor** - blue - "›" cursor in selection lists +- **selectionText** - bold+blue - Selected item text in session selector +- **selectionInfo** - gray - Scroll info "(1/10)" in selectors +- **checkmark** - green - "✓" checkmark for current model +- **providerBadge** - gray - "[anthropic]" provider labels + +### Feedback/Status +- **error** - red - Error messages +- **errorAborted** - red - "Aborted" message +- **success** - green - Success messages (stdout) +- **warning** - yellow - Warning messages +- **info** - cyan - Info messages + +### Tool Execution +- **toolCommand** - bold - "$ command" in tool execution +- **toolPath** - cyan - File paths in read tool +- **stdout** - green - Standard output lines +- **stderr** - red - Standard error lines +- **stdoutDim** - dim - Truncated stdout lines +- **stderrDim** - dim - Truncated stderr lines + +### Footer/Stats +- **footerText** - gray - All footer content (pwd and stats) + +### Logo/Branding +- **logoBrand** - bold+cyan - "pi" logo text +- **logoVersion** - dim - Version number +- **instructionsKey** - dim - Keyboard shortcut keys (esc, ctrl+c, etc.) +- **instructionsText** - gray - Instruction text ("to interrupt", etc.) + +### Markdown - Headings +- **markdownH1** - bold+underline+yellow - Level 1 headings +- **markdownH2** - bold+yellow - Level 2 headings +- **markdownH3** - bold - Level 3+ headings (uses bold modifier only) + +### Markdown - Emphasis +- **markdownBold** - bold - **bold** text +- **markdownItalic** - italic - *italic* text (also used for thinking text) +- **markdownStrikethrough** - strikethrough - ~~strikethrough~~ text + +### Markdown - Code +- **markdownCodeBlock** - green - Code block content +- **markdownCodeBlockIndent** - dim - " " indent before code +- **markdownCodeDelimiter** - gray - "```" delimiters +- **markdownInlineCode** - cyan - `inline code` content +- **markdownInlineCodeDelimiter** - gray - "`" backticks + +### Markdown - Links +- **markdownLinkText** - underline+blue - Link text +- **markdownLinkUrl** - gray - " (url)" when text != url + +### Markdown - Lists +- **markdownListBullet** - cyan - "- " or "1. " bullets + +### Markdown - Quotes +- **markdownQuoteText** - italic - Quoted text +- **markdownQuoteBorder** - gray - "│ " quote border + +### Markdown - Other +- **markdownHr** - gray - "─────" horizontal rules +- **markdownTableHeader** - bold - Table header cells + +### Loader/Spinner +- **spinnerFrame** - cyan - Spinner animation frame +- **spinnerMessage** - dim - Loading message text + +## Summary Statistics + +**Total semantic color uses: ~45** + +### By Color +- gray: 15 uses (metadata, borders, delimiters, dim text) +- cyan: 9 uses (brand, borders, code, bullets) +- blue: 6 uses (selection, links, borders) +- red: 5 uses (errors, stderr) +- green: 4 uses (success, stdout, code blocks) +- yellow: 3 uses (headings, warnings) +- bold: 8 uses (emphasis, headings, commands) +- dim: 8 uses (de-emphasis, placeholders) +- italic: 3 uses (quotes, thinking, emphasis) +- underline: 2 uses (headings, links) + +### By Category +- Markdown: 18 colors +- UI Chrome/Structure: 3 colors +- Text Hierarchy: 4 colors +- Interactive: 5 colors +- Feedback: 4 colors +- Tool Execution: 7 colors +- Footer: 1 color +- Logo/Instructions: 4 colors +- Loader: 2 colors + +## Recommendation + +We need approximately **35-40 distinct color values** for a complete theme, organized by semantic purpose. Some will be the same color (e.g., multiple uses of "gray"), but they should have separate semantic names so they can be customized independently. diff --git a/packages/coding-agent/docs/design-tokens.md b/packages/coding-agent/docs/design-tokens.md new file mode 100644 index 00000000..3fec87b7 --- /dev/null +++ b/packages/coding-agent/docs/design-tokens.md @@ -0,0 +1,938 @@ +# Design Tokens System + +## Overview + +A minimal design tokens system for terminal UI theming. Uses a two-layer approach: +1. **Primitive tokens** - Raw color values +2. **Semantic tokens** - Purpose-based mappings that reference primitives + +## Architecture + +### Primitive Tokens (Colors) + +These are the raw chalk color functions - the "palette": + +```typescript +interface ColorPrimitives { + // Grays + gray50: ChalkFunction; // Lightest gray + gray100: ChalkFunction; + gray200: ChalkFunction; + gray300: ChalkFunction; + gray400: ChalkFunction; + gray500: ChalkFunction; // Mid gray + gray600: ChalkFunction; + gray700: ChalkFunction; + gray800: ChalkFunction; + gray900: ChalkFunction; // Darkest gray + + // Colors + blue: ChalkFunction; + cyan: ChalkFunction; + green: ChalkFunction; + yellow: ChalkFunction; + red: ChalkFunction; + magenta: ChalkFunction; + + // Modifiers + bold: ChalkFunction; + dim: ChalkFunction; + italic: ChalkFunction; + underline: ChalkFunction; + strikethrough: ChalkFunction; + + // Special + none: ChalkFunction; // Pass-through, no styling +} + +type ChalkFunction = (str: string) => string; +``` + +### Semantic Tokens (Design Decisions) + +These map primitives to purposes: + +```typescript +interface SemanticTokens { + // Text hierarchy + text: { + primary: ChalkFunction; // Main content text + secondary: ChalkFunction; // Supporting text + tertiary: ChalkFunction; // De-emphasized text + disabled: ChalkFunction; // Inactive/disabled text + }; + + // Interactive elements + interactive: { + default: ChalkFunction; // Default interactive elements + hover: ChalkFunction; // Hovered/selected state + active: ChalkFunction; // Active/current state + }; + + // Feedback + feedback: { + error: ChalkFunction; + warning: ChalkFunction; + success: ChalkFunction; + info: ChalkFunction; + }; + + // Borders & dividers + border: { + default: ChalkFunction; + subtle: ChalkFunction; + emphasis: ChalkFunction; + }; + + // Code + code: { + text: ChalkFunction; + keyword: ChalkFunction; + string: ChalkFunction; + comment: ChalkFunction; + delimiter: ChalkFunction; + }; + + // Markdown specific + markdown: { + heading: { + h1: ChalkFunction; + h2: ChalkFunction; + h3: ChalkFunction; + }; + emphasis: { + bold: ChalkFunction; + italic: ChalkFunction; + strikethrough: ChalkFunction; + }; + link: { + text: ChalkFunction; + url: ChalkFunction; + }; + quote: { + text: ChalkFunction; + border: ChalkFunction; + }; + list: { + bullet: ChalkFunction; + }; + code: { + inline: ChalkFunction; + inlineDelimiter: ChalkFunction; + block: ChalkFunction; + blockDelimiter: ChalkFunction; + }; + }; + + // Output streams + output: { + stdout: ChalkFunction; + stderr: ChalkFunction; + neutral: ChalkFunction; + }; +} +``` + +### Theme Structure + +A theme combines primitives with semantic mappings: + +```typescript +interface Theme { + name: string; + primitives: ColorPrimitives; + tokens: SemanticTokens; +} +``` + +## Built-in Themes + +### Dark Theme + +```typescript +const darkPrimitives: ColorPrimitives = { + // Grays - for dark backgrounds, lighter = more prominent + gray50: chalk.white, + gray100: (s) => s, // No color = terminal default + gray200: chalk.white, + gray300: (s) => s, + gray400: chalk.gray, + gray500: chalk.gray, + gray600: chalk.gray, + gray700: chalk.dim, + gray800: chalk.dim, + gray900: chalk.black, + + // Colors + blue: chalk.blue, + cyan: chalk.cyan, + green: chalk.green, + yellow: chalk.yellow, + red: chalk.red, + magenta: chalk.magenta, + + // Modifiers + bold: chalk.bold, + dim: chalk.dim, + italic: chalk.italic, + underline: chalk.underline, + strikethrough: chalk.strikethrough, + + // Special + none: (s) => s, +}; + +const darkTheme: Theme = { + name: "dark", + primitives: darkPrimitives, + tokens: { + text: { + primary: darkPrimitives.gray100, + secondary: darkPrimitives.gray400, + tertiary: darkPrimitives.gray700, + disabled: darkPrimitives.dim, + }, + + interactive: { + default: darkPrimitives.blue, + hover: darkPrimitives.blue, + active: (s) => darkPrimitives.bold(darkPrimitives.blue(s)), + }, + + feedback: { + error: darkPrimitives.red, + warning: darkPrimitives.yellow, + success: darkPrimitives.green, + info: darkPrimitives.cyan, + }, + + border: { + default: darkPrimitives.blue, + subtle: darkPrimitives.gray600, + emphasis: darkPrimitives.cyan, + }, + + code: { + text: darkPrimitives.green, + keyword: darkPrimitives.cyan, + string: darkPrimitives.green, + comment: darkPrimitives.gray600, + delimiter: darkPrimitives.gray600, + }, + + markdown: { + heading: { + h1: (s) => darkPrimitives.underline(darkPrimitives.bold(darkPrimitives.yellow(s))), + h2: (s) => darkPrimitives.bold(darkPrimitives.yellow(s)), + h3: darkPrimitives.bold, + }, + emphasis: { + bold: darkPrimitives.bold, + italic: darkPrimitives.italic, + strikethrough: darkPrimitives.strikethrough, + }, + link: { + text: (s) => darkPrimitives.underline(darkPrimitives.blue(s)), + url: darkPrimitives.gray600, + }, + quote: { + text: darkPrimitives.italic, + border: darkPrimitives.gray600, + }, + list: { + bullet: darkPrimitives.cyan, + }, + code: { + inline: darkPrimitives.cyan, + inlineDelimiter: darkPrimitives.gray600, + block: darkPrimitives.green, + blockDelimiter: darkPrimitives.gray600, + }, + }, + + output: { + stdout: darkPrimitives.green, + stderr: darkPrimitives.red, + neutral: darkPrimitives.gray600, + }, + }, +}; +``` + +### Light Theme + +```typescript +const lightPrimitives: ColorPrimitives = { + // Grays - for light backgrounds, darker = more prominent + gray50: chalk.black, + gray100: (s) => s, // No color = terminal default + gray200: chalk.black, + gray300: (s) => s, + gray400: chalk.gray, // Use actual gray, not dim + gray500: chalk.gray, + gray600: chalk.gray, + gray700: chalk.gray, + gray800: chalk.gray, + gray900: chalk.white, + + // Colors - use bold variants for better visibility on light bg + blue: (s) => chalk.bold(chalk.blue(s)), + cyan: (s) => chalk.bold(chalk.cyan(s)), + green: (s) => chalk.bold(chalk.green(s)), + yellow: (s) => chalk.bold(chalk.yellow(s)), + red: (s) => chalk.bold(chalk.red(s)), + magenta: (s) => chalk.bold(chalk.magenta(s)), + + // Modifiers + bold: chalk.bold, + dim: chalk.gray, // Don't use chalk.dim on light bg! + italic: chalk.italic, + underline: chalk.underline, + strikethrough: chalk.strikethrough, + + // Special + none: (s) => s, +}; + +const lightTheme: Theme = { + name: "light", + primitives: lightPrimitives, + tokens: { + text: { + primary: lightPrimitives.gray100, + secondary: lightPrimitives.gray400, + tertiary: lightPrimitives.gray600, + disabled: lightPrimitives.dim, + }, + + interactive: { + default: lightPrimitives.blue, + hover: lightPrimitives.blue, + active: (s) => lightPrimitives.bold(lightPrimitives.blue(s)), + }, + + feedback: { + error: lightPrimitives.red, + warning: (s) => chalk.bold(chalk.yellow(s)), // Yellow needs extra bold + success: lightPrimitives.green, + info: lightPrimitives.cyan, + }, + + border: { + default: lightPrimitives.blue, + subtle: lightPrimitives.gray400, + emphasis: lightPrimitives.cyan, + }, + + code: { + text: lightPrimitives.green, + keyword: lightPrimitives.cyan, + string: lightPrimitives.green, + comment: lightPrimitives.gray600, + delimiter: lightPrimitives.gray600, + }, + + markdown: { + heading: { + h1: (s) => lightPrimitives.underline(lightPrimitives.bold(lightPrimitives.blue(s))), + h2: (s) => lightPrimitives.bold(lightPrimitives.blue(s)), + h3: lightPrimitives.bold, + }, + emphasis: { + bold: lightPrimitives.bold, + italic: lightPrimitives.italic, + strikethrough: lightPrimitives.strikethrough, + }, + link: { + text: (s) => lightPrimitives.underline(lightPrimitives.blue(s)), + url: lightPrimitives.blue, + }, + quote: { + text: lightPrimitives.italic, + border: lightPrimitives.gray600, + }, + list: { + bullet: lightPrimitives.blue, + }, + code: { + inline: lightPrimitives.blue, + inlineDelimiter: lightPrimitives.gray600, + block: lightPrimitives.green, + blockDelimiter: lightPrimitives.gray600, + }, + }, + + output: { + stdout: lightPrimitives.green, + stderr: lightPrimitives.red, + neutral: lightPrimitives.gray600, + }, + }, +}; +``` + +## Usage Examples + +### Simple Text Styling + +```typescript +const theme = getTheme(); + +// Before +console.log(chalk.gray("Secondary text")); + +// After +console.log(theme.tokens.text.secondary("Secondary text")); +``` + +### Interactive Elements + +```typescript +const theme = getTheme(); + +// Before +const cursor = chalk.blue("› "); + +// After +const cursor = theme.tokens.interactive.default("› "); +``` + +### Error Messages + +```typescript +const theme = getTheme(); + +// Before +this.contentContainer.addChild(new Text(chalk.red("Error: " + errorMsg))); + +// After +this.contentContainer.addChild(new Text(theme.tokens.feedback.error("Error: " + errorMsg))); +``` + +### Markdown Headings + +```typescript +const theme = getTheme(); + +// Before +lines.push(chalk.bold.yellow(headingText)); + +// After +lines.push(theme.tokens.markdown.heading.h2(headingText)); +``` + +### Borders + +```typescript +const theme = getTheme(); + +// Before +this.addChild(new Text(chalk.blue("─".repeat(80)))); + +// After +this.addChild(new Text(theme.tokens.border.default("─".repeat(80)))); +``` + +## User Configuration + +### Theme File Format + +Themes can be defined in JSON files that users can customize. The system will load themes from: +1. Built-in themes (dark, light) - hardcoded in the app +2. User themes in `~/.pi/agent/themes/` directory + +**Example: `~/.pi/agent/themes/my-theme.json`** + +```json +{ + "name": "my-theme", + "extends": "dark", + "primitives": { + "blue": "blueBright", + "cyan": "cyanBright", + "green": "greenBright" + }, + "tokens": { + "text": { + "primary": "white" + }, + "interactive": { + "default": ["bold", "blue"] + }, + "markdown": { + "heading": { + "h1": ["bold", "underline", "magenta"], + "h2": ["bold", "magenta"] + } + } + } +} +``` + +### JSON Schema + +Themes in JSON can reference: +1. **Chalk color names**: `"red"`, `"blue"`, `"gray"`, `"white"`, `"black"`, etc. +2. **Chalk bright colors**: `"redBright"`, `"blueBright"`, etc. +3. **Chalk modifiers**: `"bold"`, `"dim"`, `"italic"`, `"underline"`, `"strikethrough"` +4. **Combinations**: `["bold", "blue"]` or `["underline", "bold", "cyan"]` +5. **Primitive references**: `"$gray400"` to reference another primitive +6. **None/passthrough**: `"none"` or `""` for no styling + +### Supported Chalk Values + +```typescript +type ChalkColorName = + // Basic colors + | "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" + // Bright variants + | "blackBright" | "redBright" | "greenBright" | "yellowBright" + | "blueBright" | "magentaBright" | "cyanBright" | "whiteBright" + // Modifiers + | "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse" + // Special + | "none"; + +type ChalkValue = ChalkColorName | ChalkColorName[] | string; // string allows "$primitive" refs +``` + +### Theme Extension + +Themes can extend other themes using `"extends": "dark"` or `"extends": "light"`. Only the overridden values need to be specified. + +**Example: Minimal override** + +```json +{ + "name": "solarized-dark", + "extends": "dark", + "tokens": { + "feedback": { + "error": "magenta", + "warning": "yellow" + }, + "markdown": { + "heading": { + "h1": ["bold", "cyan"], + "h2": ["bold", "blue"] + } + } + } +} +``` + +### Loading Order + +1. Load built-in themes (dark, light) +2. Scan `~/.pi/agent/themes/*.json` +3. Parse and validate each JSON theme +4. Build theme by: + - Start with base theme (if extends specified) + - Apply primitive overrides + - Apply token overrides + - Convert JSON values to chalk functions + +## Implementation + +### Theme Module Structure + +**Location:** `packages/tui/src/theme/` + +``` +theme/ + ├── index.ts # Public API + ├── types.ts # Type definitions + ├── primitives.ts # Color primitives for each theme + ├── tokens.ts # Semantic token mappings + ├── themes.ts # Built-in theme definitions + ├── registry.ts # Theme management (current, set, get) + ├── loader.ts # JSON theme loader + └── parser.ts # JSON to ChalkFunction converter +``` + +### Public API + +```typescript +// packages/tui/src/theme/index.ts +export { type Theme, type SemanticTokens, type ColorPrimitives } from './types.js'; +export { darkTheme, lightTheme } from './themes.js'; +export { getTheme, setTheme, getThemeNames } from './registry.js'; +``` + +### Theme Registry + +```typescript +// packages/tui/src/theme/registry.ts +import { darkTheme, lightTheme } from './themes.js'; +import type { Theme } from './types.js'; + +const themes = new Map([ + ['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 new file mode 100644 index 00000000..4ce53ff9 --- /dev/null +++ b/packages/coding-agent/docs/theme-colors.md @@ -0,0 +1,182 @@ +# Minimal Theme Color Set + +## Complete list of required theme colors + +Based on analysis of all color usage in the codebase. + +### Text Hierarchy (3 colors) +- **textPrimary** - Main content text (default terminal color) +- **textSecondary** - Metadata, supporting text +- **textTertiary** - De-emphasized text (dimmed/muted) + +### UI Chrome (4 colors) +- **border** - Primary borders (around changelog, selectors) +- **borderSubtle** - Subtle borders/separators +- **uiBackground** - General UI background elements +- **scrollInfo** - Scroll position indicators like "(1/10)" + +### Interactive Elements (4 colors) +- **interactionDefault** - Default interactive state (unselected) +- **interactionHover** - Hovered/focused state +- **interactionActive** - Currently active/selected item +- **interactionSuccess** - Success indicator (checkmarks) + +### Feedback/Status (4 colors) +- **feedbackError** - Errors, failures +- **feedbackSuccess** - Success, completed +- **feedbackWarning** - Warnings, cautions +- **feedbackInfo** - Informational messages + +### Branding (2 colors) +- **brandPrimary** - Logo, primary brand color +- **brandSecondary** - Secondary brand elements + +### Tool Execution (6 colors + 3 backgrounds) +- **toolCommand** - Command text in tool headers +- **toolPath** - File paths +- **toolStdout** - Standard output +- **toolStderr** - Standard error +- **toolDimmed** - Truncated/hidden lines +- **toolNeutral** - Neutral tool output +- **toolBgPending** - Background for pending tool execution +- **toolBgSuccess** - Background for successful tool execution +- **toolBgError** - Background for failed tool execution + +### Markdown - Structure (5 colors) +- **mdHeading1** - H1 headings +- **mdHeading2** - H2 headings +- **mdHeading3** - H3+ headings +- **mdHr** - Horizontal rules +- **mdTable** - Table borders and structure + +### Markdown - Code (4 colors) +- **mdCodeBlock** - Code block content +- **mdCodeBlockDelimiter** - Code block ``` delimiters +- **mdCodeInline** - Inline `code` content +- **mdCodeInlineDelimiter** - Inline code ` backticks + +### Markdown - Lists & Quotes (3 colors) +- **mdListBullet** - List bullets (- or 1.) +- **mdQuoteText** - Blockquote text +- **mdQuoteBorder** - Blockquote border (│) + +### Markdown - Links (2 colors) +- **mdLinkText** - Link text +- **mdLinkUrl** - Link URL in parentheses + +### Backgrounds (2 colors) +- **bgUserMessage** - Background for user messages +- **bgDefault** - Default/transparent background + +### Special/Optional (2 colors) +- **spinner** - Loading spinner animation +- **thinking** - Thinking/reasoning text + +## Total: 44 colors + +### Grouped by Common Values + +Many of these will share the same value. Typical groupings: + +**"Secondary" family** (gray-ish): +- textSecondary +- textTertiary +- borderSubtle +- scrollInfo +- toolDimmed +- mdHr +- mdCodeBlockDelimiter +- mdCodeInlineDelimiter +- mdQuoteBorder +- mdLinkUrl + +**"Primary accent" family** (blue-ish): +- border +- interactionDefault +- interactionHover +- interactionActive +- brandPrimary +- mdLinkText + +**"Success" family** (green-ish): +- feedbackSuccess +- interactionSuccess +- toolStdout +- mdCodeBlock + +**"Error" family** (red-ish): +- feedbackError +- toolStderr + +**"Code/Tech" family** (cyan-ish): +- brandPrimary +- mdCodeInline +- mdListBullet +- spinner + +**"Emphasis" family** (yellow-ish): +- mdHeading1 +- mdHeading2 +- feedbackWarning + +## Simplified Minimal Set (Alternative) + +If we want to reduce further, we could consolidate to ~25 colors by using more shared values: + +### Core Colors (8) +- **text** - Primary text +- **textMuted** - Secondary/dimmed text +- **accent** - Primary accent (blue) +- **accentSubtle** - Subtle accent +- **success** - Green +- **error** - Red +- **warning** - Yellow +- **info** - Cyan + +### Backgrounds (4) +- **bgDefault** - Transparent/default +- **bgUserMessage** - User message background +- **bgSuccess** - Success state background +- **bgError** - Error state background + +### Specialized (13) +- **border** - Primary borders +- **borderSubtle** - Subtle borders +- **selection** - Selected items +- **brand** - Brand/logo color +- **mdHeading** - All headings (or separate h1/h2) +- **mdCode** - All code (blocks + inline) +- **mdCodeDelimiter** - Code delimiters +- **mdList** - List bullets +- **mdLink** - Links +- **mdQuote** - Quotes +- **toolCommand** - Command text +- **toolPath** - File paths +- **spinner** - Loading indicator + +**Total: 25 colors** (vs 44 in the detailed version) + +## Recommendation + +Start with the **44-color detailed set** because: +1. Gives maximum flexibility for theming +2. Each has a clear semantic purpose +3. Themes can set many to the same value if desired +4. Easier to add granular control than to split apart later + +Users creating themes can start by setting common values and override specific ones: + +```json +{ + "name": "my-theme", + "_comment": "Set common values first", + "textSecondary": "gray", + "textTertiary": "gray", + "borderSubtle": "gray", + "mdCodeBlockDelimiter": "gray", + + "_comment": "Then override specific ones", + "mdHeading1": "yellow", + "error": "red" +} +``` diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md new file mode 100644 index 00000000..0529f62b --- /dev/null +++ b/packages/coding-agent/docs/themes.md @@ -0,0 +1,310 @@ +# Theme System Analysis + +## Problem Statement + +Issue #7: In terminals with light backgrounds, some outputs use dark colors that are hard to read. We need a theme system that allows users to choose between light and dark themes. + +## Current Color Usage Analysis + +### Color Usage Statistics + +Total chalk color calls: 132 across 14 files + +Most frequent colors: +- `chalk.dim` (48 occurrences) - Used for secondary text +- `chalk.gray` (28 occurrences) - Used for borders, metadata, dimmed content +- `chalk.bold` (20 occurrences) - Used for emphasis +- `chalk.blue` (12 occurrences) - Used for selections, borders, links +- `chalk.cyan` (9 occurrences) - Used for primary UI elements (logo, list bullets, code) +- `chalk.red` (7 occurrences) - Used for errors, stderr output +- `chalk.green` (6 occurrences) - Used for success, stdout output +- `chalk.yellow` (3 occurrences) - Used for headings in markdown +- `chalk.bgRgb` (6 occurrences) - Used for custom backgrounds in Text/Markdown + +### Files Using Colors + +#### coding-agent Package +1. **main.ts** - CLI output messages +2. **tui/assistant-message.ts** - Thinking text (gray italic), errors (red), aborted (red) +3. **tui/dynamic-border.ts** - Configurable border color (default blue) +4. **tui/footer.ts** - Stats and pwd (gray) +5. **tui/model-selector.ts** - Borders (blue), selection arrow (blue), provider badge (gray), checkmark (green) +6. **tui/session-selector.ts** - Border (blue), selection cursor (blue), metadata (dim) +7. **tui/thinking-selector.ts** - Border (blue) +8. **tui/tool-execution.ts** - stdout (green), stderr (red), dim lines (dim), line numbers +9. **tui/tui-renderer.ts** - Logo (bold cyan), instructions (dim/gray) + +#### tui Package +1. **components/editor.ts** - Horizontal border (gray) +2. **components/loader.ts** - Spinner (cyan), message (dim) +3. **components/markdown.ts** - Complex color system: + - H1 headings: bold.underline.yellow + - H2 headings: bold.yellow + - H3+ headings: bold + - Code blocks: gray (delimiters), dim (indent), green (code) + - List bullets: cyan + - Blockquotes: gray (pipe), italic (text) + - Horizontal rules: gray + - Inline code: gray (backticks), cyan (code) + - Links: underline.blue (text), gray (URL) + - Strikethrough: strikethrough + - Tables: bold (headers) +4. **components/select-list.ts** - No matches (gray), selection arrow (blue), selected item (blue), description (gray) +5. **components/text.ts** - Custom bgRgb support + +### Color System Architecture + +#### Current Implementation +- Colors are hardcoded using `chalk` directly +- No centralized theme management +- No way to switch themes at runtime +- Some components accept color parameters (e.g., DynamicBorder, Text, Markdown) + +#### Markdown Component Color System +The Markdown component has a `Color` type enum: +```typescript +type Color = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | + "bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray" +``` + +It accepts optional `bgColor` and `fgColor` parameters, plus `customBgRgb`. + +## Proposed Solution + +### Theme Structure + +Create a centralized theme system with semantic color names: + +```typescript +interface Theme { + name: string; + + // UI Chrome + border: ChalkFunction; + selection: ChalkFunction; + selectionText: ChalkFunction; + + // Text hierarchy + primary: ChalkFunction; + secondary: ChalkFunction; + dim: ChalkFunction; + + // Semantic colors + error: ChalkFunction; + success: ChalkFunction; + warning: ChalkFunction; + info: ChalkFunction; + + // Code/output + code: ChalkFunction; + codeDelimiter: ChalkFunction; + stdout: ChalkFunction; + stderr: ChalkFunction; + + // Markdown specific + heading1: ChalkFunction; + heading2: ChalkFunction; + heading3: ChalkFunction; + link: ChalkFunction; + linkUrl: ChalkFunction; + listBullet: ChalkFunction; + blockquote: ChalkFunction; + blockquotePipe: ChalkFunction; + inlineCode: ChalkFunction; + inlineCodeDelimiter: ChalkFunction; + + // Backgrounds (optional, for components like Text/Markdown) + backgroundRgb?: { r: number; g: number; b: number }; +} + +type ChalkFunction = (str: string) => string; +``` + +### Built-in Themes + +#### Dark Theme (current default) +```typescript +const darkTheme: Theme = { + name: "dark", + border: chalk.blue, + selection: chalk.blue, + selectionText: chalk.blue, + primary: (s) => s, // no color + secondary: chalk.gray, + dim: chalk.dim, + error: chalk.red, + success: chalk.green, + warning: chalk.yellow, + info: chalk.cyan, + code: chalk.green, + codeDelimiter: chalk.gray, + stdout: chalk.green, + stderr: chalk.red, + heading1: chalk.bold.underline.yellow, + heading2: chalk.bold.yellow, + heading3: chalk.bold, + link: chalk.underline.blue, + linkUrl: chalk.gray, + listBullet: chalk.cyan, + blockquote: chalk.italic, + blockquotePipe: chalk.gray, + inlineCode: chalk.cyan, + inlineCodeDelimiter: chalk.gray, +}; +``` + +#### Light Theme +```typescript +const lightTheme: Theme = { + name: "light", + border: chalk.blue, + selection: chalk.blue, + selectionText: chalk.blue.bold, + primary: (s) => s, + secondary: chalk.gray, + dim: chalk.gray, // Don't use chalk.dim on light backgrounds + error: chalk.red.bold, + success: chalk.green.bold, + warning: chalk.yellow.bold, + info: chalk.cyan.bold, + code: chalk.green.bold, + codeDelimiter: chalk.gray, + stdout: chalk.green.bold, + stderr: chalk.red.bold, + heading1: chalk.bold.underline.blue, + heading2: chalk.bold.blue, + heading3: chalk.bold, + link: chalk.underline.blue, + linkUrl: chalk.blue, + listBullet: chalk.blue.bold, + blockquote: chalk.italic, + blockquotePipe: chalk.gray, + inlineCode: chalk.blue.bold, + inlineCodeDelimiter: chalk.gray, +}; +``` + +### Implementation Plan + +#### 1. Create Theme Module +**Location:** `packages/tui/src/theme.ts` + +```typescript +export interface Theme { ... } +export const darkTheme: Theme = { ... }; +export const lightTheme: Theme = { ... }; +export const themes = { dark: darkTheme, light: lightTheme }; + +let currentTheme: Theme = darkTheme; + +export function setTheme(theme: Theme): void { + currentTheme = theme; +} + +export function getTheme(): Theme { + return currentTheme; +} +``` + +#### 2. Update Settings Manager +**Location:** `packages/coding-agent/src/settings-manager.ts` + +Add `theme` field to Settings interface: +```typescript +export interface Settings { + lastChangelogVersion?: string; + theme?: "dark" | "light"; +} +``` + +#### 3. Create Theme Selector Component +**Location:** `packages/coding-agent/src/tui/theme-selector.ts` + +Similar to ModelSelector and ThinkingSelector, create a TUI component for selecting themes. + +#### 4. Refactor Color Usage + +Replace all hardcoded `chalk.*` calls with `theme.*`: + +**Example - Before:** +```typescript +lines.push(chalk.blue("─".repeat(width))); +const cursor = chalk.blue("› "); +``` + +**Example - After:** +```typescript +const theme = getTheme(); +lines.push(theme.border("─".repeat(width))); +const cursor = theme.selection("› "); +``` + +#### 5. Update Components + +##### High Priority (User-facing content issues) +1. **markdown.ts** - Update all color calls to use theme +2. **tool-execution.ts** - stdout/stderr colors +3. **assistant-message.ts** - Error messages +4. **tui-renderer.ts** - Logo and instructions +5. **footer.ts** - Stats display + +##### Medium Priority (UI chrome) +6. **dynamic-border.ts** - Accept theme parameter +7. **model-selector.ts** - Selection colors +8. **session-selector.ts** - Selection colors +9. **thinking-selector.ts** - Border colors +10. **select-list.ts** - Selection colors +11. **loader.ts** - Spinner color +12. **editor.ts** - Border color + +##### Low Priority (CLI output) +13. **main.ts** - CLI messages + +#### 6. Add Theme Slash Command +**Location:** `packages/coding-agent/src/tui/tui-renderer.ts` + +Add `/theme` command similar to `/model` and `/thinking`. + +#### 7. Initialize Theme on Startup +**Location:** `packages/coding-agent/src/main.ts` + +```typescript +// Load theme from settings +const settingsManager = new SettingsManager(); +const themeName = settingsManager.getTheme() || "dark"; +const theme = themes[themeName] || darkTheme; +setTheme(theme); +``` + +### Migration Strategy + +1. **Phase 1:** Create theme infrastructure (theme.ts, types, built-in themes) +2. **Phase 2:** Update TUI package components (markdown, text, loader, editor, select-list) +3. **Phase 3:** Update coding-agent TUI components (all tui/*.ts files) +4. **Phase 4:** Add theme selector and persistence +5. **Phase 5:** Update CLI output in main.ts (optional, low priority) + +### Testing Plan + +1. Test both themes in terminals with light backgrounds +2. Test both themes in terminals with dark backgrounds +3. Verify theme switching works at runtime via `/theme` +4. Verify theme persists across sessions via settings.json +5. Test all components for readability in both themes + +### Open Questions + +1. Should we support custom user themes loaded from a JSON file? +2. Should we auto-detect terminal background color and choose theme automatically? +3. Should theme apply to background colors used in Text/Markdown components? +4. Do we need more than two themes initially? + +### Breaking Changes + +None - the default theme will remain "dark" matching current behavior. + +### Performance Considerations + +- Theme getter is called frequently (on every render) +- Should be a simple variable access, not a function call chain +- Consider caching theme functions if performance becomes an issue diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 25ae4e44..d72f743c 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.6", + "version": "0.7.7", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "bin": { @@ -21,8 +21,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.5", - "@mariozechner/pi-ai": "^0.7.5", + "@mariozechner/pi-agent": "^0.7.7", + "@mariozechner/pi-ai": "^0.7.7", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index d824de7e..2020e7ff 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -12,8 +12,10 @@ import { TUI, } from "@mariozechner/pi-tui"; import chalk from "chalk"; +import { getChangelogPath, getNewEntries, parseChangelog } from "../changelog.js"; import { exportSessionToHtml } from "../export-html.js"; import type { SessionManager } from "../session-manager.js"; +import { SettingsManager } from "../settings-manager.js"; import { AssistantMessageComponent } from "./assistant-message.js"; import { CustomEditor } from "./custom-editor.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -92,9 +94,14 @@ export class TuiRenderer { description: "Show session info and stats", }; + const changelogCommand: SlashCommand = { + name: "changelog", + description: "Show changelog entries", + }; + // Setup autocomplete for file paths and slash commands const autocompleteProvider = new CombinedAutocompleteProvider( - [thinkingCommand, modelCommand, exportCommand, sessionCommand], + [thinkingCommand, modelCommand, exportCommand, sessionCommand, changelogCommand], process.cwd(), ); this.editor.setAutocompleteProvider(autocompleteProvider); @@ -194,6 +201,13 @@ export class TuiRenderer { return; } + // Check for /changelog command + if (text === "/changelog") { + this.handleChangelogCommand(); + this.editor.setText(""); + return; + } + if (this.onInputCallback) { this.onInputCallback(text); } @@ -648,6 +662,25 @@ export class TuiRenderer { this.ui.requestRender(); } + private handleChangelogCommand(): void { + const changelogPath = getChangelogPath(); + const allEntries = parseChangelog(changelogPath); + + // Show all entries in reverse order (oldest first, newest last) + const changelogMarkdown = + allEntries.length > 0 + ? allEntries + .reverse() + .map((e) => e.content) + .join("\n\n") + : "No changelog entries found."; + + // Display in chat + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild(new Markdown(changelogMarkdown)); + this.ui.requestRender(); + } + stop(): void { if (this.loadingAnimation) { this.loadingAnimation.stop(); diff --git a/packages/pods/package.json b/packages/pods/package.json index 8287b844..ebd78d93 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi", - "version": "0.7.5", + "version": "0.7.7", "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.5", + "@mariozechner/pi-agent": "^0.7.7", "chalk": "^5.5.0" }, "devDependencies": {} diff --git a/packages/proxy/package.json b/packages/proxy/package.json index 9754e376..c25508ae 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-proxy", - "version": "0.7.5", + "version": "0.7.7", "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 0accdb71..971cf15d 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.7.5", + "version": "0.7.7", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 7f6bd7f7..764e06ef 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.5", + "version": "0.7.7", "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.6.0", - "@mariozechner/pi-tui": "^0.7.5", + "@mariozechner/pi-ai": "^0.7.7", + "@mariozechner/pi-tui": "^0.7.7", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", diff --git a/scripts/sync-versions.js b/scripts/sync-versions.js index 3b070db9..77d2d44e 100644 --- a/scripts/sync-versions.js +++ b/scripts/sync-versions.js @@ -1,82 +1,96 @@ #!/usr/bin/env node /** - * Syncs inter-package dependency versions in the monorepo - * Updates internal @mariozechner/* package versions in dependent packages - * to match their current versions + * Syncs ALL @mariozechner/* package dependency versions to match their current versions. + * This ensures lockstep versioning across the monorepo. */ -import { readFileSync, writeFileSync } from 'fs'; +import { readFileSync, writeFileSync, readdirSync } from 'fs'; import { join } from 'path'; const packagesDir = join(process.cwd(), 'packages'); +const packageDirs = readdirSync(packagesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); -// Read current versions -const tui = JSON.parse(readFileSync(join(packagesDir, 'tui/package.json'), 'utf8')); -const ai = JSON.parse(readFileSync(join(packagesDir, 'ai/package.json'), 'utf8')); -const agent = JSON.parse(readFileSync(join(packagesDir, 'agent/package.json'), 'utf8')); -const codingAgent = JSON.parse(readFileSync(join(packagesDir, 'coding-agent/package.json'), 'utf8')); -const pods = JSON.parse(readFileSync(join(packagesDir, 'pods/package.json'), 'utf8')); -const webUi = JSON.parse(readFileSync(join(packagesDir, 'web-ui/package.json'), 'utf8')); +// Read all package.json files and build version map +const packages = {}; +const versionMap = {}; + +for (const dir of packageDirs) { + const pkgPath = join(packagesDir, dir, 'package.json'); + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + packages[dir] = { path: pkgPath, data: pkg }; + versionMap[pkg.name] = pkg.version; + } catch (e) { + console.error(`Failed to read ${pkgPath}:`, e.message); + } +} console.log('Current versions:'); -console.log(` @mariozechner/pi-tui: ${tui.version}`); -console.log(` @mariozechner/pi-ai: ${ai.version}`); -console.log(` @mariozechner/pi-agent: ${agent.version}`); -console.log(` @mariozechner/coding-agent: ${codingAgent.version}`); -console.log(` @mariozechner/pi: ${pods.version}`); -console.log(` @mariozechner/pi-web-ui: ${webUi.version}`); - -// Update agent's dependencies -let agentUpdated = false; -if (agent.dependencies['@mariozechner/pi-tui']) { - const oldVersion = agent.dependencies['@mariozechner/pi-tui']; - agent.dependencies['@mariozechner/pi-tui'] = `^${tui.version}`; - console.log(`\nUpdated agent's dependency on pi-tui: ${oldVersion} → ^${tui.version}`); - agentUpdated = true; -} -if (agent.dependencies['@mariozechner/pi-ai']) { - const oldVersion = agent.dependencies['@mariozechner/pi-ai']; - agent.dependencies['@mariozechner/pi-ai'] = `^${ai.version}`; - console.log(`Updated agent's dependency on pi-ai: ${oldVersion} → ^${ai.version}`); - agentUpdated = true; -} -if (agentUpdated) { - writeFileSync(join(packagesDir, 'agent/package.json'), JSON.stringify(agent, null, '\t') + '\n'); +for (const [name, version] of Object.entries(versionMap).sort()) { + console.log(` ${name}: ${version}`); } -// Update coding-agent's dependencies -let codingAgentUpdated = false; -if (codingAgent.dependencies['@mariozechner/pi-ai']) { - const oldVersion = codingAgent.dependencies['@mariozechner/pi-ai']; - codingAgent.dependencies['@mariozechner/pi-ai'] = `^${ai.version}`; - console.log(`Updated coding-agent's dependency on pi-ai: ${oldVersion} → ^${ai.version}`); - codingAgentUpdated = true; -} -if (codingAgent.dependencies['@mariozechner/pi-agent']) { - const oldVersion = codingAgent.dependencies['@mariozechner/pi-agent']; - codingAgent.dependencies['@mariozechner/pi-agent'] = `^${agent.version}`; - console.log(`Updated coding-agent's dependency on pi-agent: ${oldVersion} → ^${agent.version}`); - codingAgentUpdated = true; -} -if (codingAgentUpdated) { - writeFileSync(join(packagesDir, 'coding-agent/package.json'), JSON.stringify(codingAgent, null, '\t') + '\n'); +// Verify all versions are the same (lockstep) +const versions = new Set(Object.values(versionMap)); +if (versions.size > 1) { + console.error('\n❌ ERROR: Not all packages have the same version!'); + console.error('Expected lockstep versioning. Run one of:'); + console.error(' npm run version:patch'); + console.error(' npm run version:minor'); + console.error(' npm run version:major'); + process.exit(1); } -// Update pods' dependency on agent -if (pods.dependencies['@mariozechner/pi-agent']) { - const oldVersion = pods.dependencies['@mariozechner/pi-agent']; - pods.dependencies['@mariozechner/pi-agent'] = `^${agent.version}`; - writeFileSync(join(packagesDir, 'pods/package.json'), JSON.stringify(pods, null, '\t') + '\n'); - console.log(`Updated pods' dependency on pi-agent: ${oldVersion} → ^${agent.version}`); +console.log('\n✅ All packages at same version (lockstep)'); + +// Update all inter-package dependencies +let totalUpdates = 0; +for (const [dir, pkg] of Object.entries(packages)) { + let updated = false; + + // Check dependencies + if (pkg.data.dependencies) { + for (const [depName, currentVersion] of Object.entries(pkg.data.dependencies)) { + if (versionMap[depName]) { + const newVersion = `^${versionMap[depName]}`; + if (currentVersion !== newVersion) { + console.log(`\n${pkg.data.name}:`); + console.log(` ${depName}: ${currentVersion} → ${newVersion}`); + pkg.data.dependencies[depName] = newVersion; + updated = true; + totalUpdates++; + } + } + } + } + + // Check devDependencies + if (pkg.data.devDependencies) { + for (const [depName, currentVersion] of Object.entries(pkg.data.devDependencies)) { + if (versionMap[depName]) { + const newVersion = `^${versionMap[depName]}`; + if (currentVersion !== newVersion) { + console.log(`\n${pkg.data.name}:`); + console.log(` ${depName}: ${currentVersion} → ${newVersion} (devDependencies)`); + pkg.data.devDependencies[depName] = newVersion; + updated = true; + totalUpdates++; + } + } + } + } + + // Write if updated + if (updated) { + writeFileSync(pkg.path, JSON.stringify(pkg.data, null, '\t') + '\n'); + } } -// Update web-ui's dependency on tui -if (webUi.dependencies['@mariozechner/pi-tui']) { - const oldVersion = webUi.dependencies['@mariozechner/pi-tui']; - webUi.dependencies['@mariozechner/pi-tui'] = `^${tui.version}`; - writeFileSync(join(packagesDir, 'web-ui/package.json'), JSON.stringify(webUi, null, '\t') + '\n'); - console.log(`Updated web-ui's dependency on pi-tui: ${oldVersion} → ^${tui.version}`); +if (totalUpdates === 0) { + console.log('\nAll inter-package dependencies already in sync.'); +} else { + console.log(`\n✅ Updated ${totalUpdates} dependency version(s)`); } - -console.log('\n✅ Version sync complete!'); \ No newline at end of file