mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-18 00:02:45 +00:00
Fix lockstep versioning and improve documentation
- Sync all packages to version 0.7.7 - Rewrite sync-versions.js to handle ALL inter-package dependencies automatically - Fix web-ui dependency on pi-ai (was 0.6.0, now 0.7.7) - Move agent fix changelog entry to coding-agent CHANGELOG - Remove redundant agent CHANGELOG.md - Improve README.md with clearer lockstep versioning docs - Add /changelog command to display full changelog in TUI (newest last) - Fix changelog description (not a scrollable viewer, just displays in chat) - Update CHANGELOG for 0.7.7 release
This commit is contained in:
parent
7b347291ff
commit
bc670bc63c
17 changed files with 1721 additions and 186 deletions
93
README.md
93
README.md
|
|
@ -1,90 +1,63 @@
|
||||||
# Pi Monorepo
|
# Pi Monorepo
|
||||||
|
|
||||||
A collection of tools for managing LLM deployments and building AI agents.
|
Tools for building AI agents and managing LLM deployments.
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
- **[@mariozechner/pi-ai](packages/ai)** - Unified multi-provider LLM API
|
| Package | Description |
|
||||||
- **[@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-ai](packages/ai)** | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||||
- **[@mariozechner/pi-tui](packages/tui)** - Terminal UI library with differential rendering
|
| **[@mariozechner/pi-agent](packages/agent)** | Agent runtime with tool calling and state management |
|
||||||
- **[@mariozechner/pi-agent](packages/agent)** - General-purpose agent with tool calling and session persistence
|
| **[@mariozechner/pi-coding-agent](packages/coding-agent)** | Interactive coding agent CLI |
|
||||||
- **[@mariozechner/pi](packages/pods)** - CLI for managing vLLM deployments on GPU pods
|
| **[@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:**
|
**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
|
## Development
|
||||||
|
|
||||||
This is a monorepo using npm workspaces for package management and a dual TypeScript configuration for development and building.
|
### Setup
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install all dependencies
|
npm install # Install all dependencies
|
||||||
npm install
|
npm run build # Build all packages
|
||||||
|
npm run check # Lint, format, and type check
|
||||||
|
```
|
||||||
|
|
||||||
# Build all packages (required for publishing to NPM)
|
### Running Without Building
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Clean out dist/ folders in all packages
|
Use `tsx` to run TypeScript source directly during development:
|
||||||
npm run clean
|
|
||||||
|
|
||||||
# Run linting, formatting, and tsc typechecking (no build needed)
|
```bash
|
||||||
npm run check
|
cd packages/coding-agent && npx tsx src/cli.ts
|
||||||
|
|
||||||
# Run directly with tsx during development (no build needed)
|
|
||||||
cd packages/pods && 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:
|
**All packages MUST always have the same version number.** Use these commands to bump versions:
|
||||||
|
|
||||||
`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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Bump patch version (0.5.0 -> 0.5.1)
|
npm run version:patch # 0.7.5 -> 0.7.6
|
||||||
npm run version:patch
|
npm run version:minor # 0.7.5 -> 0.8.0
|
||||||
|
npm run version:major # 0.7.5 -> 1.0.0
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
These commands automatically:
|
These commands:
|
||||||
1. Update all package versions
|
1. Update all package versions to the same number
|
||||||
2. Sync inter-package dependency versions
|
2. Update inter-package dependency versions (e.g., `pi-agent` depends on `pi-ai@^0.7.7`)
|
||||||
3. Update package-lock.json
|
3. Update `package-lock.json`
|
||||||
|
|
||||||
|
**Never manually edit version numbers.** The lockstep system ensures consistency across the monorepo.
|
||||||
|
|
||||||
### Publishing
|
### Publishing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Dry run to see what would be published
|
npm run publish:dry # Preview what will be published
|
||||||
npm run publish:dry
|
npm run publish # Publish all packages to npm
|
||||||
|
|
||||||
# Publish all packages to npm
|
|
||||||
npm run publish
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
|
|
@ -3193,11 +3193,11 @@
|
||||||
},
|
},
|
||||||
"packages/agent": {
|
"packages/agent": {
|
||||||
"name": "@mariozechner/pi-agent",
|
"name": "@mariozechner/pi-agent",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.7.4",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"@mariozechner/pi-tui": "^0.7.4"
|
"@mariozechner/pi-tui": "^0.7.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
|
|
@ -3223,7 +3223,7 @@
|
||||||
},
|
},
|
||||||
"packages/ai": {
|
"packages/ai": {
|
||||||
"name": "@mariozechner/pi-ai",
|
"name": "@mariozechner/pi-ai",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.61.0",
|
"@anthropic-ai/sdk": "^0.61.0",
|
||||||
|
|
@ -3270,11 +3270,11 @@
|
||||||
},
|
},
|
||||||
"packages/coding-agent": {
|
"packages/coding-agent": {
|
||||||
"name": "@mariozechner/pi-coding-agent",
|
"name": "@mariozechner/pi-coding-agent",
|
||||||
"version": "0.7.6",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.4",
|
"@mariozechner/pi-agent": "^0.7.7",
|
||||||
"@mariozechner/pi-ai": "^0.7.4",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^11.0.3"
|
"glob": "^11.0.3"
|
||||||
|
|
@ -3317,10 +3317,10 @@
|
||||||
},
|
},
|
||||||
"packages/pods": {
|
"packages/pods": {
|
||||||
"name": "@mariozechner/pi",
|
"name": "@mariozechner/pi",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.4",
|
"@mariozechner/pi-agent": "^0.7.7",
|
||||||
"chalk": "^5.5.0"
|
"chalk": "^5.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -3343,7 +3343,7 @@
|
||||||
},
|
},
|
||||||
"packages/proxy": {
|
"packages/proxy": {
|
||||||
"name": "@mariozechner/pi-proxy",
|
"name": "@mariozechner/pi-proxy",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.14.0",
|
"@hono/node-server": "^1.14.0",
|
||||||
"hono": "^4.6.16"
|
"hono": "^4.6.16"
|
||||||
|
|
@ -3359,7 +3359,7 @@
|
||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@mariozechner/pi-tui",
|
"name": "@mariozechner/pi-tui",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
|
|
@ -3398,12 +3398,12 @@
|
||||||
},
|
},
|
||||||
"packages/web-ui": {
|
"packages/web-ui": {
|
||||||
"name": "@mariozechner/pi-web-ui",
|
"name": "@mariozechner/pi-web-ui",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
"@mariozechner/pi-ai": "^0.6.0",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"@mariozechner/pi-tui": "^0.7.4",
|
"@mariozechner/pi-tui": "^0.7.7",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.544.0",
|
"lucide": "^0.544.0",
|
||||||
|
|
@ -3422,26 +3422,6 @@
|
||||||
"lit": "^3.3.1"
|
"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": {
|
"packages/web-ui/node_modules/chalk": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-agent",
|
"name": "@mariozechner/pi-agent",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.7.5",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"@mariozechner/pi-tui": "^0.7.5"
|
"@mariozechner/pi-tui": "^0.7.7"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-ai",
|
"name": "@mariozechner/pi-ai",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.7.7] - Unreleased
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.7.7] - 2025-11-13
|
||||||
|
|
||||||
### Added
|
### 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))
|
- 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))
|
- 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 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 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
|
## [0.7.6] - 2025-11-13
|
||||||
|
|
||||||
|
|
|
||||||
112
packages/coding-agent/docs/color-inventory.md
Normal file
112
packages/coding-agent/docs/color-inventory.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Color Usage Inventory
|
||||||
|
|
||||||
|
## Complete list of all semantic color uses in the codebase
|
||||||
|
|
||||||
|
### UI Chrome & Structure
|
||||||
|
- **border** - cyan - Borders around sections (changelog, selectors)
|
||||||
|
- **borderSubtle** - blue - Borders in selectors (model, session, thinking)
|
||||||
|
- **borderHorizontal** - gray - Horizontal separator in editor
|
||||||
|
|
||||||
|
### Text Hierarchy
|
||||||
|
- **textPrimary** - default/none - Main content text
|
||||||
|
- **textSecondary** - gray - Metadata, timestamps, descriptions
|
||||||
|
- **textDim** - dim - De-emphasized content, placeholder text, "..." indicators
|
||||||
|
- **textBold** - bold - Emphasis (note: this is styling, not color)
|
||||||
|
|
||||||
|
### Interactive/Selection
|
||||||
|
- **selectionCursor** - blue - "›" cursor in selection lists
|
||||||
|
- **selectionText** - bold+blue - Selected item text in session selector
|
||||||
|
- **selectionInfo** - gray - Scroll info "(1/10)" in selectors
|
||||||
|
- **checkmark** - green - "✓" checkmark for current model
|
||||||
|
- **providerBadge** - gray - "[anthropic]" provider labels
|
||||||
|
|
||||||
|
### Feedback/Status
|
||||||
|
- **error** - red - Error messages
|
||||||
|
- **errorAborted** - red - "Aborted" message
|
||||||
|
- **success** - green - Success messages (stdout)
|
||||||
|
- **warning** - yellow - Warning messages
|
||||||
|
- **info** - cyan - Info messages
|
||||||
|
|
||||||
|
### Tool Execution
|
||||||
|
- **toolCommand** - bold - "$ command" in tool execution
|
||||||
|
- **toolPath** - cyan - File paths in read tool
|
||||||
|
- **stdout** - green - Standard output lines
|
||||||
|
- **stderr** - red - Standard error lines
|
||||||
|
- **stdoutDim** - dim - Truncated stdout lines
|
||||||
|
- **stderrDim** - dim - Truncated stderr lines
|
||||||
|
|
||||||
|
### Footer/Stats
|
||||||
|
- **footerText** - gray - All footer content (pwd and stats)
|
||||||
|
|
||||||
|
### Logo/Branding
|
||||||
|
- **logoBrand** - bold+cyan - "pi" logo text
|
||||||
|
- **logoVersion** - dim - Version number
|
||||||
|
- **instructionsKey** - dim - Keyboard shortcut keys (esc, ctrl+c, etc.)
|
||||||
|
- **instructionsText** - gray - Instruction text ("to interrupt", etc.)
|
||||||
|
|
||||||
|
### Markdown - Headings
|
||||||
|
- **markdownH1** - bold+underline+yellow - Level 1 headings
|
||||||
|
- **markdownH2** - bold+yellow - Level 2 headings
|
||||||
|
- **markdownH3** - bold - Level 3+ headings (uses bold modifier only)
|
||||||
|
|
||||||
|
### Markdown - Emphasis
|
||||||
|
- **markdownBold** - bold - **bold** text
|
||||||
|
- **markdownItalic** - italic - *italic* text (also used for thinking text)
|
||||||
|
- **markdownStrikethrough** - strikethrough - ~~strikethrough~~ text
|
||||||
|
|
||||||
|
### Markdown - Code
|
||||||
|
- **markdownCodeBlock** - green - Code block content
|
||||||
|
- **markdownCodeBlockIndent** - dim - " " indent before code
|
||||||
|
- **markdownCodeDelimiter** - gray - "```" delimiters
|
||||||
|
- **markdownInlineCode** - cyan - `inline code` content
|
||||||
|
- **markdownInlineCodeDelimiter** - gray - "`" backticks
|
||||||
|
|
||||||
|
### Markdown - Links
|
||||||
|
- **markdownLinkText** - underline+blue - Link text
|
||||||
|
- **markdownLinkUrl** - gray - " (url)" when text != url
|
||||||
|
|
||||||
|
### Markdown - Lists
|
||||||
|
- **markdownListBullet** - cyan - "- " or "1. " bullets
|
||||||
|
|
||||||
|
### Markdown - Quotes
|
||||||
|
- **markdownQuoteText** - italic - Quoted text
|
||||||
|
- **markdownQuoteBorder** - gray - "│ " quote border
|
||||||
|
|
||||||
|
### Markdown - Other
|
||||||
|
- **markdownHr** - gray - "─────" horizontal rules
|
||||||
|
- **markdownTableHeader** - bold - Table header cells
|
||||||
|
|
||||||
|
### Loader/Spinner
|
||||||
|
- **spinnerFrame** - cyan - Spinner animation frame
|
||||||
|
- **spinnerMessage** - dim - Loading message text
|
||||||
|
|
||||||
|
## Summary Statistics
|
||||||
|
|
||||||
|
**Total semantic color uses: ~45**
|
||||||
|
|
||||||
|
### By Color
|
||||||
|
- gray: 15 uses (metadata, borders, delimiters, dim text)
|
||||||
|
- cyan: 9 uses (brand, borders, code, bullets)
|
||||||
|
- blue: 6 uses (selection, links, borders)
|
||||||
|
- red: 5 uses (errors, stderr)
|
||||||
|
- green: 4 uses (success, stdout, code blocks)
|
||||||
|
- yellow: 3 uses (headings, warnings)
|
||||||
|
- bold: 8 uses (emphasis, headings, commands)
|
||||||
|
- dim: 8 uses (de-emphasis, placeholders)
|
||||||
|
- italic: 3 uses (quotes, thinking, emphasis)
|
||||||
|
- underline: 2 uses (headings, links)
|
||||||
|
|
||||||
|
### By Category
|
||||||
|
- Markdown: 18 colors
|
||||||
|
- UI Chrome/Structure: 3 colors
|
||||||
|
- Text Hierarchy: 4 colors
|
||||||
|
- Interactive: 5 colors
|
||||||
|
- Feedback: 4 colors
|
||||||
|
- Tool Execution: 7 colors
|
||||||
|
- Footer: 1 color
|
||||||
|
- Logo/Instructions: 4 colors
|
||||||
|
- Loader: 2 colors
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
We need approximately **35-40 distinct color values** for a complete theme, organized by semantic purpose. Some will be the same color (e.g., multiple uses of "gray"), but they should have separate semantic names so they can be customized independently.
|
||||||
938
packages/coding-agent/docs/design-tokens.md
Normal file
938
packages/coding-agent/docs/design-tokens.md
Normal file
|
|
@ -0,0 +1,938 @@
|
||||||
|
# Design Tokens System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A minimal design tokens system for terminal UI theming. Uses a two-layer approach:
|
||||||
|
1. **Primitive tokens** - Raw color values
|
||||||
|
2. **Semantic tokens** - Purpose-based mappings that reference primitives
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Primitive Tokens (Colors)
|
||||||
|
|
||||||
|
These are the raw chalk color functions - the "palette":
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ColorPrimitives {
|
||||||
|
// Grays
|
||||||
|
gray50: ChalkFunction; // Lightest gray
|
||||||
|
gray100: ChalkFunction;
|
||||||
|
gray200: ChalkFunction;
|
||||||
|
gray300: ChalkFunction;
|
||||||
|
gray400: ChalkFunction;
|
||||||
|
gray500: ChalkFunction; // Mid gray
|
||||||
|
gray600: ChalkFunction;
|
||||||
|
gray700: ChalkFunction;
|
||||||
|
gray800: ChalkFunction;
|
||||||
|
gray900: ChalkFunction; // Darkest gray
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
blue: ChalkFunction;
|
||||||
|
cyan: ChalkFunction;
|
||||||
|
green: ChalkFunction;
|
||||||
|
yellow: ChalkFunction;
|
||||||
|
red: ChalkFunction;
|
||||||
|
magenta: ChalkFunction;
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
bold: ChalkFunction;
|
||||||
|
dim: ChalkFunction;
|
||||||
|
italic: ChalkFunction;
|
||||||
|
underline: ChalkFunction;
|
||||||
|
strikethrough: ChalkFunction;
|
||||||
|
|
||||||
|
// Special
|
||||||
|
none: ChalkFunction; // Pass-through, no styling
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChalkFunction = (str: string) => string;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Semantic Tokens (Design Decisions)
|
||||||
|
|
||||||
|
These map primitives to purposes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SemanticTokens {
|
||||||
|
// Text hierarchy
|
||||||
|
text: {
|
||||||
|
primary: ChalkFunction; // Main content text
|
||||||
|
secondary: ChalkFunction; // Supporting text
|
||||||
|
tertiary: ChalkFunction; // De-emphasized text
|
||||||
|
disabled: ChalkFunction; // Inactive/disabled text
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive elements
|
||||||
|
interactive: {
|
||||||
|
default: ChalkFunction; // Default interactive elements
|
||||||
|
hover: ChalkFunction; // Hovered/selected state
|
||||||
|
active: ChalkFunction; // Active/current state
|
||||||
|
};
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
feedback: {
|
||||||
|
error: ChalkFunction;
|
||||||
|
warning: ChalkFunction;
|
||||||
|
success: ChalkFunction;
|
||||||
|
info: ChalkFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Borders & dividers
|
||||||
|
border: {
|
||||||
|
default: ChalkFunction;
|
||||||
|
subtle: ChalkFunction;
|
||||||
|
emphasis: ChalkFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Code
|
||||||
|
code: {
|
||||||
|
text: ChalkFunction;
|
||||||
|
keyword: ChalkFunction;
|
||||||
|
string: ChalkFunction;
|
||||||
|
comment: ChalkFunction;
|
||||||
|
delimiter: ChalkFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown specific
|
||||||
|
markdown: {
|
||||||
|
heading: {
|
||||||
|
h1: ChalkFunction;
|
||||||
|
h2: ChalkFunction;
|
||||||
|
h3: ChalkFunction;
|
||||||
|
};
|
||||||
|
emphasis: {
|
||||||
|
bold: ChalkFunction;
|
||||||
|
italic: ChalkFunction;
|
||||||
|
strikethrough: ChalkFunction;
|
||||||
|
};
|
||||||
|
link: {
|
||||||
|
text: ChalkFunction;
|
||||||
|
url: ChalkFunction;
|
||||||
|
};
|
||||||
|
quote: {
|
||||||
|
text: ChalkFunction;
|
||||||
|
border: ChalkFunction;
|
||||||
|
};
|
||||||
|
list: {
|
||||||
|
bullet: ChalkFunction;
|
||||||
|
};
|
||||||
|
code: {
|
||||||
|
inline: ChalkFunction;
|
||||||
|
inlineDelimiter: ChalkFunction;
|
||||||
|
block: ChalkFunction;
|
||||||
|
blockDelimiter: ChalkFunction;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output streams
|
||||||
|
output: {
|
||||||
|
stdout: ChalkFunction;
|
||||||
|
stderr: ChalkFunction;
|
||||||
|
neutral: ChalkFunction;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Structure
|
||||||
|
|
||||||
|
A theme combines primitives with semantic mappings:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Theme {
|
||||||
|
name: string;
|
||||||
|
primitives: ColorPrimitives;
|
||||||
|
tokens: SemanticTokens;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Themes
|
||||||
|
|
||||||
|
### Dark Theme
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const darkPrimitives: ColorPrimitives = {
|
||||||
|
// Grays - for dark backgrounds, lighter = more prominent
|
||||||
|
gray50: chalk.white,
|
||||||
|
gray100: (s) => s, // No color = terminal default
|
||||||
|
gray200: chalk.white,
|
||||||
|
gray300: (s) => s,
|
||||||
|
gray400: chalk.gray,
|
||||||
|
gray500: chalk.gray,
|
||||||
|
gray600: chalk.gray,
|
||||||
|
gray700: chalk.dim,
|
||||||
|
gray800: chalk.dim,
|
||||||
|
gray900: chalk.black,
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
blue: chalk.blue,
|
||||||
|
cyan: chalk.cyan,
|
||||||
|
green: chalk.green,
|
||||||
|
yellow: chalk.yellow,
|
||||||
|
red: chalk.red,
|
||||||
|
magenta: chalk.magenta,
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
bold: chalk.bold,
|
||||||
|
dim: chalk.dim,
|
||||||
|
italic: chalk.italic,
|
||||||
|
underline: chalk.underline,
|
||||||
|
strikethrough: chalk.strikethrough,
|
||||||
|
|
||||||
|
// Special
|
||||||
|
none: (s) => s,
|
||||||
|
};
|
||||||
|
|
||||||
|
const darkTheme: Theme = {
|
||||||
|
name: "dark",
|
||||||
|
primitives: darkPrimitives,
|
||||||
|
tokens: {
|
||||||
|
text: {
|
||||||
|
primary: darkPrimitives.gray100,
|
||||||
|
secondary: darkPrimitives.gray400,
|
||||||
|
tertiary: darkPrimitives.gray700,
|
||||||
|
disabled: darkPrimitives.dim,
|
||||||
|
},
|
||||||
|
|
||||||
|
interactive: {
|
||||||
|
default: darkPrimitives.blue,
|
||||||
|
hover: darkPrimitives.blue,
|
||||||
|
active: (s) => darkPrimitives.bold(darkPrimitives.blue(s)),
|
||||||
|
},
|
||||||
|
|
||||||
|
feedback: {
|
||||||
|
error: darkPrimitives.red,
|
||||||
|
warning: darkPrimitives.yellow,
|
||||||
|
success: darkPrimitives.green,
|
||||||
|
info: darkPrimitives.cyan,
|
||||||
|
},
|
||||||
|
|
||||||
|
border: {
|
||||||
|
default: darkPrimitives.blue,
|
||||||
|
subtle: darkPrimitives.gray600,
|
||||||
|
emphasis: darkPrimitives.cyan,
|
||||||
|
},
|
||||||
|
|
||||||
|
code: {
|
||||||
|
text: darkPrimitives.green,
|
||||||
|
keyword: darkPrimitives.cyan,
|
||||||
|
string: darkPrimitives.green,
|
||||||
|
comment: darkPrimitives.gray600,
|
||||||
|
delimiter: darkPrimitives.gray600,
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
heading: {
|
||||||
|
h1: (s) => darkPrimitives.underline(darkPrimitives.bold(darkPrimitives.yellow(s))),
|
||||||
|
h2: (s) => darkPrimitives.bold(darkPrimitives.yellow(s)),
|
||||||
|
h3: darkPrimitives.bold,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
bold: darkPrimitives.bold,
|
||||||
|
italic: darkPrimitives.italic,
|
||||||
|
strikethrough: darkPrimitives.strikethrough,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
text: (s) => darkPrimitives.underline(darkPrimitives.blue(s)),
|
||||||
|
url: darkPrimitives.gray600,
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
text: darkPrimitives.italic,
|
||||||
|
border: darkPrimitives.gray600,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
bullet: darkPrimitives.cyan,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
inline: darkPrimitives.cyan,
|
||||||
|
inlineDelimiter: darkPrimitives.gray600,
|
||||||
|
block: darkPrimitives.green,
|
||||||
|
blockDelimiter: darkPrimitives.gray600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
stdout: darkPrimitives.green,
|
||||||
|
stderr: darkPrimitives.red,
|
||||||
|
neutral: darkPrimitives.gray600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Light Theme
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lightPrimitives: ColorPrimitives = {
|
||||||
|
// Grays - for light backgrounds, darker = more prominent
|
||||||
|
gray50: chalk.black,
|
||||||
|
gray100: (s) => s, // No color = terminal default
|
||||||
|
gray200: chalk.black,
|
||||||
|
gray300: (s) => s,
|
||||||
|
gray400: chalk.gray, // Use actual gray, not dim
|
||||||
|
gray500: chalk.gray,
|
||||||
|
gray600: chalk.gray,
|
||||||
|
gray700: chalk.gray,
|
||||||
|
gray800: chalk.gray,
|
||||||
|
gray900: chalk.white,
|
||||||
|
|
||||||
|
// Colors - use bold variants for better visibility on light bg
|
||||||
|
blue: (s) => chalk.bold(chalk.blue(s)),
|
||||||
|
cyan: (s) => chalk.bold(chalk.cyan(s)),
|
||||||
|
green: (s) => chalk.bold(chalk.green(s)),
|
||||||
|
yellow: (s) => chalk.bold(chalk.yellow(s)),
|
||||||
|
red: (s) => chalk.bold(chalk.red(s)),
|
||||||
|
magenta: (s) => chalk.bold(chalk.magenta(s)),
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
bold: chalk.bold,
|
||||||
|
dim: chalk.gray, // Don't use chalk.dim on light bg!
|
||||||
|
italic: chalk.italic,
|
||||||
|
underline: chalk.underline,
|
||||||
|
strikethrough: chalk.strikethrough,
|
||||||
|
|
||||||
|
// Special
|
||||||
|
none: (s) => s,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightTheme: Theme = {
|
||||||
|
name: "light",
|
||||||
|
primitives: lightPrimitives,
|
||||||
|
tokens: {
|
||||||
|
text: {
|
||||||
|
primary: lightPrimitives.gray100,
|
||||||
|
secondary: lightPrimitives.gray400,
|
||||||
|
tertiary: lightPrimitives.gray600,
|
||||||
|
disabled: lightPrimitives.dim,
|
||||||
|
},
|
||||||
|
|
||||||
|
interactive: {
|
||||||
|
default: lightPrimitives.blue,
|
||||||
|
hover: lightPrimitives.blue,
|
||||||
|
active: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
|
||||||
|
},
|
||||||
|
|
||||||
|
feedback: {
|
||||||
|
error: lightPrimitives.red,
|
||||||
|
warning: (s) => chalk.bold(chalk.yellow(s)), // Yellow needs extra bold
|
||||||
|
success: lightPrimitives.green,
|
||||||
|
info: lightPrimitives.cyan,
|
||||||
|
},
|
||||||
|
|
||||||
|
border: {
|
||||||
|
default: lightPrimitives.blue,
|
||||||
|
subtle: lightPrimitives.gray400,
|
||||||
|
emphasis: lightPrimitives.cyan,
|
||||||
|
},
|
||||||
|
|
||||||
|
code: {
|
||||||
|
text: lightPrimitives.green,
|
||||||
|
keyword: lightPrimitives.cyan,
|
||||||
|
string: lightPrimitives.green,
|
||||||
|
comment: lightPrimitives.gray600,
|
||||||
|
delimiter: lightPrimitives.gray600,
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
heading: {
|
||||||
|
h1: (s) => lightPrimitives.underline(lightPrimitives.bold(lightPrimitives.blue(s))),
|
||||||
|
h2: (s) => lightPrimitives.bold(lightPrimitives.blue(s)),
|
||||||
|
h3: lightPrimitives.bold,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
bold: lightPrimitives.bold,
|
||||||
|
italic: lightPrimitives.italic,
|
||||||
|
strikethrough: lightPrimitives.strikethrough,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
text: (s) => lightPrimitives.underline(lightPrimitives.blue(s)),
|
||||||
|
url: lightPrimitives.blue,
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
text: lightPrimitives.italic,
|
||||||
|
border: lightPrimitives.gray600,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
bullet: lightPrimitives.blue,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
inline: lightPrimitives.blue,
|
||||||
|
inlineDelimiter: lightPrimitives.gray600,
|
||||||
|
block: lightPrimitives.green,
|
||||||
|
blockDelimiter: lightPrimitives.gray600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
stdout: lightPrimitives.green,
|
||||||
|
stderr: lightPrimitives.red,
|
||||||
|
neutral: lightPrimitives.gray600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Simple Text Styling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
|
// Before
|
||||||
|
console.log(chalk.gray("Secondary text"));
|
||||||
|
|
||||||
|
// After
|
||||||
|
console.log(theme.tokens.text.secondary("Secondary text"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Elements
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
|
// Before
|
||||||
|
const cursor = chalk.blue("› ");
|
||||||
|
|
||||||
|
// After
|
||||||
|
const cursor = theme.tokens.interactive.default("› ");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Messages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
|
// Before
|
||||||
|
this.contentContainer.addChild(new Text(chalk.red("Error: " + errorMsg)));
|
||||||
|
|
||||||
|
// After
|
||||||
|
this.contentContainer.addChild(new Text(theme.tokens.feedback.error("Error: " + errorMsg)));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdown Headings
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
|
// Before
|
||||||
|
lines.push(chalk.bold.yellow(headingText));
|
||||||
|
|
||||||
|
// After
|
||||||
|
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Borders
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
|
||||||
|
// Before
|
||||||
|
this.addChild(new Text(chalk.blue("─".repeat(80))));
|
||||||
|
|
||||||
|
// After
|
||||||
|
this.addChild(new Text(theme.tokens.border.default("─".repeat(80))));
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Configuration
|
||||||
|
|
||||||
|
### Theme File Format
|
||||||
|
|
||||||
|
Themes can be defined in JSON files that users can customize. The system will load themes from:
|
||||||
|
1. Built-in themes (dark, light) - hardcoded in the app
|
||||||
|
2. User themes in `~/.pi/agent/themes/` directory
|
||||||
|
|
||||||
|
**Example: `~/.pi/agent/themes/my-theme.json`**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-theme",
|
||||||
|
"extends": "dark",
|
||||||
|
"primitives": {
|
||||||
|
"blue": "blueBright",
|
||||||
|
"cyan": "cyanBright",
|
||||||
|
"green": "greenBright"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"text": {
|
||||||
|
"primary": "white"
|
||||||
|
},
|
||||||
|
"interactive": {
|
||||||
|
"default": ["bold", "blue"]
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"heading": {
|
||||||
|
"h1": ["bold", "underline", "magenta"],
|
||||||
|
"h2": ["bold", "magenta"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Schema
|
||||||
|
|
||||||
|
Themes in JSON can reference:
|
||||||
|
1. **Chalk color names**: `"red"`, `"blue"`, `"gray"`, `"white"`, `"black"`, etc.
|
||||||
|
2. **Chalk bright colors**: `"redBright"`, `"blueBright"`, etc.
|
||||||
|
3. **Chalk modifiers**: `"bold"`, `"dim"`, `"italic"`, `"underline"`, `"strikethrough"`
|
||||||
|
4. **Combinations**: `["bold", "blue"]` or `["underline", "bold", "cyan"]`
|
||||||
|
5. **Primitive references**: `"$gray400"` to reference another primitive
|
||||||
|
6. **None/passthrough**: `"none"` or `""` for no styling
|
||||||
|
|
||||||
|
### Supported Chalk Values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ChalkColorName =
|
||||||
|
// Basic colors
|
||||||
|
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
|
||||||
|
// Bright variants
|
||||||
|
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
|
||||||
|
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
|
||||||
|
// Modifiers
|
||||||
|
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
|
||||||
|
// Special
|
||||||
|
| "none";
|
||||||
|
|
||||||
|
type ChalkValue = ChalkColorName | ChalkColorName[] | string; // string allows "$primitive" refs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Extension
|
||||||
|
|
||||||
|
Themes can extend other themes using `"extends": "dark"` or `"extends": "light"`. Only the overridden values need to be specified.
|
||||||
|
|
||||||
|
**Example: Minimal override**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "solarized-dark",
|
||||||
|
"extends": "dark",
|
||||||
|
"tokens": {
|
||||||
|
"feedback": {
|
||||||
|
"error": "magenta",
|
||||||
|
"warning": "yellow"
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"heading": {
|
||||||
|
"h1": ["bold", "cyan"],
|
||||||
|
"h2": ["bold", "blue"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading Order
|
||||||
|
|
||||||
|
1. Load built-in themes (dark, light)
|
||||||
|
2. Scan `~/.pi/agent/themes/*.json`
|
||||||
|
3. Parse and validate each JSON theme
|
||||||
|
4. Build theme by:
|
||||||
|
- Start with base theme (if extends specified)
|
||||||
|
- Apply primitive overrides
|
||||||
|
- Apply token overrides
|
||||||
|
- Convert JSON values to chalk functions
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Theme Module Structure
|
||||||
|
|
||||||
|
**Location:** `packages/tui/src/theme/`
|
||||||
|
|
||||||
|
```
|
||||||
|
theme/
|
||||||
|
├── index.ts # Public API
|
||||||
|
├── types.ts # Type definitions
|
||||||
|
├── primitives.ts # Color primitives for each theme
|
||||||
|
├── tokens.ts # Semantic token mappings
|
||||||
|
├── themes.ts # Built-in theme definitions
|
||||||
|
├── registry.ts # Theme management (current, set, get)
|
||||||
|
├── loader.ts # JSON theme loader
|
||||||
|
└── parser.ts # JSON to ChalkFunction converter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/tui/src/theme/index.ts
|
||||||
|
export { type Theme, type SemanticTokens, type ColorPrimitives } from './types.js';
|
||||||
|
export { darkTheme, lightTheme } from './themes.js';
|
||||||
|
export { getTheme, setTheme, getThemeNames } from './registry.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Registry
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/tui/src/theme/registry.ts
|
||||||
|
import { darkTheme, lightTheme } from './themes.js';
|
||||||
|
import type { Theme } from './types.js';
|
||||||
|
|
||||||
|
const themes = new Map<string, Theme>([
|
||||||
|
['dark', darkTheme],
|
||||||
|
['light', lightTheme],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let currentTheme: Theme = darkTheme;
|
||||||
|
|
||||||
|
export function getTheme(): Theme {
|
||||||
|
return currentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(name: string): void {
|
||||||
|
const theme = themes.get(name);
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error(`Theme "${name}" not found`);
|
||||||
|
}
|
||||||
|
currentTheme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeNames(): string[] {
|
||||||
|
return Array.from(themes.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerTheme(theme: Theme): void {
|
||||||
|
themes.set(theme.name, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeByName(name: string): Theme | undefined {
|
||||||
|
return themes.get(name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Theme Parser
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/tui/src/theme/parser.ts
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import type { ChalkFunction } from './types.js';
|
||||||
|
|
||||||
|
type ChalkColorName =
|
||||||
|
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray"
|
||||||
|
| "blackBright" | "redBright" | "greenBright" | "yellowBright"
|
||||||
|
| "blueBright" | "magentaBright" | "cyanBright" | "whiteBright"
|
||||||
|
| "bold" | "dim" | "italic" | "underline" | "strikethrough" | "inverse"
|
||||||
|
| "none";
|
||||||
|
|
||||||
|
type JsonThemeValue = ChalkColorName | ChalkColorName[] | string;
|
||||||
|
|
||||||
|
interface JsonTheme {
|
||||||
|
name: string;
|
||||||
|
extends?: string;
|
||||||
|
primitives?: Record<string, JsonThemeValue>;
|
||||||
|
tokens?: any; // Partial<SemanticTokens> but with JsonThemeValue instead of ChalkFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map chalk color names to actual chalk functions
|
||||||
|
const chalkMap: Record<ChalkColorName, any> = {
|
||||||
|
black: chalk.black,
|
||||||
|
red: chalk.red,
|
||||||
|
green: chalk.green,
|
||||||
|
yellow: chalk.yellow,
|
||||||
|
blue: chalk.blue,
|
||||||
|
magenta: chalk.magenta,
|
||||||
|
cyan: chalk.cyan,
|
||||||
|
white: chalk.white,
|
||||||
|
gray: chalk.gray,
|
||||||
|
blackBright: chalk.blackBright,
|
||||||
|
redBright: chalk.redBright,
|
||||||
|
greenBright: chalk.greenBright,
|
||||||
|
yellowBright: chalk.yellowBright,
|
||||||
|
blueBright: chalk.blueBright,
|
||||||
|
magentaBright: chalk.magentaBright,
|
||||||
|
cyanBright: chalk.cyanBright,
|
||||||
|
whiteBright: chalk.whiteBright,
|
||||||
|
bold: chalk.bold,
|
||||||
|
dim: chalk.dim,
|
||||||
|
italic: chalk.italic,
|
||||||
|
underline: chalk.underline,
|
||||||
|
strikethrough: chalk.strikethrough,
|
||||||
|
inverse: chalk.inverse,
|
||||||
|
none: (s: string) => s,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseThemeValue(
|
||||||
|
value: JsonThemeValue,
|
||||||
|
primitives?: Record<string, ChalkFunction>
|
||||||
|
): ChalkFunction {
|
||||||
|
// Handle primitive reference: "$gray400"
|
||||||
|
if (typeof value === 'string' && value.startsWith('
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Infrastructure
|
||||||
|
1. Create theme module with types, primitives, and built-in themes
|
||||||
|
2. Export from `@mariozechner/pi-tui`
|
||||||
|
3. Add tests for theme functions
|
||||||
|
|
||||||
|
### Phase 2: Component Migration (Priority Order)
|
||||||
|
1. **Markdown** (biggest impact, 50+ color calls)
|
||||||
|
2. **ToolExecution** (stdout/stderr readability)
|
||||||
|
3. **SelectList** (used everywhere)
|
||||||
|
4. **Footer** (always visible)
|
||||||
|
5. **TuiRenderer** (logo, instructions)
|
||||||
|
6. Other components
|
||||||
|
|
||||||
|
### Phase 3: Persistence & UI
|
||||||
|
1. Add theme to SettingsManager
|
||||||
|
2. Create ThemeSelector component
|
||||||
|
3. Add `/theme` slash command
|
||||||
|
4. Initialize theme on startup
|
||||||
|
|
||||||
|
### Example Migration
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
// markdown.ts
|
||||||
|
if (headingLevel === 1) {
|
||||||
|
lines.push(chalk.bold.underline.yellow(headingText));
|
||||||
|
} else if (headingLevel === 2) {
|
||||||
|
lines.push(chalk.bold.yellow(headingText));
|
||||||
|
} else {
|
||||||
|
lines.push(chalk.bold(headingPrefix + headingText));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
// markdown.ts
|
||||||
|
import { getTheme } from '@mariozechner/pi-tui/theme';
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
if (headingLevel === 1) {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h1(headingText));
|
||||||
|
} else if (headingLevel === 2) {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
||||||
|
} else {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of This Approach
|
||||||
|
|
||||||
|
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
|
||||||
|
2. **Maintainable**: Change all headings by editing one token mapping
|
||||||
|
3. **Extensible**: Easy to add new themes without touching components
|
||||||
|
4. **Type-safe**: Full TypeScript support
|
||||||
|
5. **Testable**: Can test themes independently
|
||||||
|
6. **Minimal**: Only what we need, no over-engineering
|
||||||
|
7. **Composable**: Can chain primitives (bold + underline + color)
|
||||||
|
|
||||||
|
## Key Differences from Themes.md
|
||||||
|
|
||||||
|
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
|
||||||
|
- **Composability**: Can combine primitive modifiers
|
||||||
|
- **Better light theme**: Properly handles chalk.dim and color visibility issues
|
||||||
|
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
|
||||||
|
- **Easier to extend**: Add new token without changing primitives
|
||||||
|
- **Better for sharing**: Could export just primitives for custom themes
|
||||||
|
)) {
|
||||||
|
const primitiveName = value.slice(1);
|
||||||
|
if (primitives && primitives[primitiveName]) {
|
||||||
|
return primitives[primitiveName];
|
||||||
|
}
|
||||||
|
throw new Error(`Primitive reference "${value}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array of chalk names (composition): ["bold", "blue"]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (str: string) => {
|
||||||
|
let result = str;
|
||||||
|
for (const name of value) {
|
||||||
|
const chalkFn = chalkMap[name as ChalkColorName];
|
||||||
|
if (!chalkFn) {
|
||||||
|
throw new Error(`Unknown chalk function: ${name}`);
|
||||||
|
}
|
||||||
|
result = chalkFn(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single chalk name: "blue"
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const chalkFn = chalkMap[value as ChalkColorName];
|
||||||
|
if (!chalkFn) {
|
||||||
|
throw new Error(`Unknown chalk function: ${value}`);
|
||||||
|
}
|
||||||
|
return chalkFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid theme value: ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep merge objects, used for extending themes
|
||||||
|
function deepMerge(target: any, source: any): any {
|
||||||
|
const result = { ...target };
|
||||||
|
|
||||||
|
for (const key in source) {
|
||||||
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||||||
|
result[key] = deepMerge(target[key] || {}, source[key]);
|
||||||
|
} else {
|
||||||
|
result[key] = source[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJsonTheme(json: JsonTheme, baseTheme?: Theme): Theme {
|
||||||
|
// Start with base theme if extending
|
||||||
|
let primitives: Record<string, ChalkFunction> = {};
|
||||||
|
let tokens: any = {};
|
||||||
|
|
||||||
|
if (json.extends && baseTheme) {
|
||||||
|
// Copy base theme primitives and tokens
|
||||||
|
primitives = { ...baseTheme.primitives };
|
||||||
|
tokens = deepMerge({}, baseTheme.tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and override primitives
|
||||||
|
if (json.primitives) {
|
||||||
|
for (const [key, value] of Object.entries(json.primitives)) {
|
||||||
|
primitives[key] = parseThemeValue(value, primitives);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and override tokens (recursive)
|
||||||
|
if (json.tokens) {
|
||||||
|
const parsedTokens = parseTokens(json.tokens, primitives);
|
||||||
|
tokens = deepMerge(tokens, parsedTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: json.name,
|
||||||
|
primitives,
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTokens(obj: any, primitives: Record<string, ChalkFunction>): any {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
// Nested object, recurse
|
||||||
|
result[key] = parseTokens(value, primitives);
|
||||||
|
} else {
|
||||||
|
// Leaf value, parse it
|
||||||
|
result[key] = parseThemeValue(value as JsonThemeValue, primitives);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Theme Loader
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/tui/src/theme/loader.ts
|
||||||
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { parseJsonTheme } from './parser.js';
|
||||||
|
import { getThemeByName, registerTheme } from './registry.js';
|
||||||
|
import type { Theme } from './types.js';
|
||||||
|
|
||||||
|
export function loadUserThemes(themesDir: string): Theme[] {
|
||||||
|
const themes: Theme[] = [];
|
||||||
|
|
||||||
|
if (!existsSync(themesDir)) {
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(themesDir).filter(f => f.endsWith('.json'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(join(themesDir, file), 'utf-8');
|
||||||
|
const json = JSON.parse(content);
|
||||||
|
|
||||||
|
// Get base theme if extending
|
||||||
|
let baseTheme: Theme | undefined;
|
||||||
|
if (json.extends) {
|
||||||
|
baseTheme = getThemeByName(json.extends);
|
||||||
|
if (!baseTheme) {
|
||||||
|
console.warn(`Theme ${json.name} extends unknown theme "${json.extends}", skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = parseJsonTheme(json, baseTheme);
|
||||||
|
registerTheme(theme);
|
||||||
|
themes.push(theme);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load theme from ${file}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return themes;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Infrastructure
|
||||||
|
1. Create theme module with types, primitives, and built-in themes
|
||||||
|
2. Export from `@mariozechner/pi-tui`
|
||||||
|
3. Add tests for theme functions
|
||||||
|
|
||||||
|
### Phase 2: Component Migration (Priority Order)
|
||||||
|
1. **Markdown** (biggest impact, 50+ color calls)
|
||||||
|
2. **ToolExecution** (stdout/stderr readability)
|
||||||
|
3. **SelectList** (used everywhere)
|
||||||
|
4. **Footer** (always visible)
|
||||||
|
5. **TuiRenderer** (logo, instructions)
|
||||||
|
6. Other components
|
||||||
|
|
||||||
|
### Phase 3: Persistence & UI
|
||||||
|
1. Add theme to SettingsManager
|
||||||
|
2. Create ThemeSelector component
|
||||||
|
3. Add `/theme` slash command
|
||||||
|
4. Initialize theme on startup
|
||||||
|
|
||||||
|
### Example Migration
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
// markdown.ts
|
||||||
|
if (headingLevel === 1) {
|
||||||
|
lines.push(chalk.bold.underline.yellow(headingText));
|
||||||
|
} else if (headingLevel === 2) {
|
||||||
|
lines.push(chalk.bold.yellow(headingText));
|
||||||
|
} else {
|
||||||
|
lines.push(chalk.bold(headingPrefix + headingText));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
// markdown.ts
|
||||||
|
import { getTheme } from '@mariozechner/pi-tui/theme';
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
if (headingLevel === 1) {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h1(headingText));
|
||||||
|
} else if (headingLevel === 2) {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h2(headingText));
|
||||||
|
} else {
|
||||||
|
lines.push(theme.tokens.markdown.heading.h3(headingPrefix + headingText));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of This Approach
|
||||||
|
|
||||||
|
1. **Separation of Concerns**: Color values (primitives) separate from usage (tokens)
|
||||||
|
2. **Maintainable**: Change all headings by editing one token mapping
|
||||||
|
3. **Extensible**: Easy to add new themes without touching components
|
||||||
|
4. **Type-safe**: Full TypeScript support
|
||||||
|
5. **Testable**: Can test themes independently
|
||||||
|
6. **Minimal**: Only what we need, no over-engineering
|
||||||
|
7. **Composable**: Can chain primitives (bold + underline + color)
|
||||||
|
|
||||||
|
## Key Differences from Themes.md
|
||||||
|
|
||||||
|
- **Two-layer system**: Primitives + Semantic tokens (vs. flat theme object)
|
||||||
|
- **Composability**: Can combine primitive modifiers
|
||||||
|
- **Better light theme**: Properly handles chalk.dim and color visibility issues
|
||||||
|
- **More organized**: Tokens grouped by purpose (text, interactive, markdown, etc.)
|
||||||
|
- **Easier to extend**: Add new token without changing primitives
|
||||||
|
- **Better for sharing**: Could export just primitives for custom themes
|
||||||
182
packages/coding-agent/docs/theme-colors.md
Normal file
182
packages/coding-agent/docs/theme-colors.md
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
# Minimal Theme Color Set
|
||||||
|
|
||||||
|
## Complete list of required theme colors
|
||||||
|
|
||||||
|
Based on analysis of all color usage in the codebase.
|
||||||
|
|
||||||
|
### Text Hierarchy (3 colors)
|
||||||
|
- **textPrimary** - Main content text (default terminal color)
|
||||||
|
- **textSecondary** - Metadata, supporting text
|
||||||
|
- **textTertiary** - De-emphasized text (dimmed/muted)
|
||||||
|
|
||||||
|
### UI Chrome (4 colors)
|
||||||
|
- **border** - Primary borders (around changelog, selectors)
|
||||||
|
- **borderSubtle** - Subtle borders/separators
|
||||||
|
- **uiBackground** - General UI background elements
|
||||||
|
- **scrollInfo** - Scroll position indicators like "(1/10)"
|
||||||
|
|
||||||
|
### Interactive Elements (4 colors)
|
||||||
|
- **interactionDefault** - Default interactive state (unselected)
|
||||||
|
- **interactionHover** - Hovered/focused state
|
||||||
|
- **interactionActive** - Currently active/selected item
|
||||||
|
- **interactionSuccess** - Success indicator (checkmarks)
|
||||||
|
|
||||||
|
### Feedback/Status (4 colors)
|
||||||
|
- **feedbackError** - Errors, failures
|
||||||
|
- **feedbackSuccess** - Success, completed
|
||||||
|
- **feedbackWarning** - Warnings, cautions
|
||||||
|
- **feedbackInfo** - Informational messages
|
||||||
|
|
||||||
|
### Branding (2 colors)
|
||||||
|
- **brandPrimary** - Logo, primary brand color
|
||||||
|
- **brandSecondary** - Secondary brand elements
|
||||||
|
|
||||||
|
### Tool Execution (6 colors + 3 backgrounds)
|
||||||
|
- **toolCommand** - Command text in tool headers
|
||||||
|
- **toolPath** - File paths
|
||||||
|
- **toolStdout** - Standard output
|
||||||
|
- **toolStderr** - Standard error
|
||||||
|
- **toolDimmed** - Truncated/hidden lines
|
||||||
|
- **toolNeutral** - Neutral tool output
|
||||||
|
- **toolBgPending** - Background for pending tool execution
|
||||||
|
- **toolBgSuccess** - Background for successful tool execution
|
||||||
|
- **toolBgError** - Background for failed tool execution
|
||||||
|
|
||||||
|
### Markdown - Structure (5 colors)
|
||||||
|
- **mdHeading1** - H1 headings
|
||||||
|
- **mdHeading2** - H2 headings
|
||||||
|
- **mdHeading3** - H3+ headings
|
||||||
|
- **mdHr** - Horizontal rules
|
||||||
|
- **mdTable** - Table borders and structure
|
||||||
|
|
||||||
|
### Markdown - Code (4 colors)
|
||||||
|
- **mdCodeBlock** - Code block content
|
||||||
|
- **mdCodeBlockDelimiter** - Code block ``` delimiters
|
||||||
|
- **mdCodeInline** - Inline `code` content
|
||||||
|
- **mdCodeInlineDelimiter** - Inline code ` backticks
|
||||||
|
|
||||||
|
### Markdown - Lists & Quotes (3 colors)
|
||||||
|
- **mdListBullet** - List bullets (- or 1.)
|
||||||
|
- **mdQuoteText** - Blockquote text
|
||||||
|
- **mdQuoteBorder** - Blockquote border (│)
|
||||||
|
|
||||||
|
### Markdown - Links (2 colors)
|
||||||
|
- **mdLinkText** - Link text
|
||||||
|
- **mdLinkUrl** - Link URL in parentheses
|
||||||
|
|
||||||
|
### Backgrounds (2 colors)
|
||||||
|
- **bgUserMessage** - Background for user messages
|
||||||
|
- **bgDefault** - Default/transparent background
|
||||||
|
|
||||||
|
### Special/Optional (2 colors)
|
||||||
|
- **spinner** - Loading spinner animation
|
||||||
|
- **thinking** - Thinking/reasoning text
|
||||||
|
|
||||||
|
## Total: 44 colors
|
||||||
|
|
||||||
|
### Grouped by Common Values
|
||||||
|
|
||||||
|
Many of these will share the same value. Typical groupings:
|
||||||
|
|
||||||
|
**"Secondary" family** (gray-ish):
|
||||||
|
- textSecondary
|
||||||
|
- textTertiary
|
||||||
|
- borderSubtle
|
||||||
|
- scrollInfo
|
||||||
|
- toolDimmed
|
||||||
|
- mdHr
|
||||||
|
- mdCodeBlockDelimiter
|
||||||
|
- mdCodeInlineDelimiter
|
||||||
|
- mdQuoteBorder
|
||||||
|
- mdLinkUrl
|
||||||
|
|
||||||
|
**"Primary accent" family** (blue-ish):
|
||||||
|
- border
|
||||||
|
- interactionDefault
|
||||||
|
- interactionHover
|
||||||
|
- interactionActive
|
||||||
|
- brandPrimary
|
||||||
|
- mdLinkText
|
||||||
|
|
||||||
|
**"Success" family** (green-ish):
|
||||||
|
- feedbackSuccess
|
||||||
|
- interactionSuccess
|
||||||
|
- toolStdout
|
||||||
|
- mdCodeBlock
|
||||||
|
|
||||||
|
**"Error" family** (red-ish):
|
||||||
|
- feedbackError
|
||||||
|
- toolStderr
|
||||||
|
|
||||||
|
**"Code/Tech" family** (cyan-ish):
|
||||||
|
- brandPrimary
|
||||||
|
- mdCodeInline
|
||||||
|
- mdListBullet
|
||||||
|
- spinner
|
||||||
|
|
||||||
|
**"Emphasis" family** (yellow-ish):
|
||||||
|
- mdHeading1
|
||||||
|
- mdHeading2
|
||||||
|
- feedbackWarning
|
||||||
|
|
||||||
|
## Simplified Minimal Set (Alternative)
|
||||||
|
|
||||||
|
If we want to reduce further, we could consolidate to ~25 colors by using more shared values:
|
||||||
|
|
||||||
|
### Core Colors (8)
|
||||||
|
- **text** - Primary text
|
||||||
|
- **textMuted** - Secondary/dimmed text
|
||||||
|
- **accent** - Primary accent (blue)
|
||||||
|
- **accentSubtle** - Subtle accent
|
||||||
|
- **success** - Green
|
||||||
|
- **error** - Red
|
||||||
|
- **warning** - Yellow
|
||||||
|
- **info** - Cyan
|
||||||
|
|
||||||
|
### Backgrounds (4)
|
||||||
|
- **bgDefault** - Transparent/default
|
||||||
|
- **bgUserMessage** - User message background
|
||||||
|
- **bgSuccess** - Success state background
|
||||||
|
- **bgError** - Error state background
|
||||||
|
|
||||||
|
### Specialized (13)
|
||||||
|
- **border** - Primary borders
|
||||||
|
- **borderSubtle** - Subtle borders
|
||||||
|
- **selection** - Selected items
|
||||||
|
- **brand** - Brand/logo color
|
||||||
|
- **mdHeading** - All headings (or separate h1/h2)
|
||||||
|
- **mdCode** - All code (blocks + inline)
|
||||||
|
- **mdCodeDelimiter** - Code delimiters
|
||||||
|
- **mdList** - List bullets
|
||||||
|
- **mdLink** - Links
|
||||||
|
- **mdQuote** - Quotes
|
||||||
|
- **toolCommand** - Command text
|
||||||
|
- **toolPath** - File paths
|
||||||
|
- **spinner** - Loading indicator
|
||||||
|
|
||||||
|
**Total: 25 colors** (vs 44 in the detailed version)
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Start with the **44-color detailed set** because:
|
||||||
|
1. Gives maximum flexibility for theming
|
||||||
|
2. Each has a clear semantic purpose
|
||||||
|
3. Themes can set many to the same value if desired
|
||||||
|
4. Easier to add granular control than to split apart later
|
||||||
|
|
||||||
|
Users creating themes can start by setting common values and override specific ones:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-theme",
|
||||||
|
"_comment": "Set common values first",
|
||||||
|
"textSecondary": "gray",
|
||||||
|
"textTertiary": "gray",
|
||||||
|
"borderSubtle": "gray",
|
||||||
|
"mdCodeBlockDelimiter": "gray",
|
||||||
|
|
||||||
|
"_comment": "Then override specific ones",
|
||||||
|
"mdHeading1": "yellow",
|
||||||
|
"error": "red"
|
||||||
|
}
|
||||||
|
```
|
||||||
310
packages/coding-agent/docs/themes.md
Normal file
310
packages/coding-agent/docs/themes.md
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
# Theme System Analysis
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Issue #7: In terminals with light backgrounds, some outputs use dark colors that are hard to read. We need a theme system that allows users to choose between light and dark themes.
|
||||||
|
|
||||||
|
## Current Color Usage Analysis
|
||||||
|
|
||||||
|
### Color Usage Statistics
|
||||||
|
|
||||||
|
Total chalk color calls: 132 across 14 files
|
||||||
|
|
||||||
|
Most frequent colors:
|
||||||
|
- `chalk.dim` (48 occurrences) - Used for secondary text
|
||||||
|
- `chalk.gray` (28 occurrences) - Used for borders, metadata, dimmed content
|
||||||
|
- `chalk.bold` (20 occurrences) - Used for emphasis
|
||||||
|
- `chalk.blue` (12 occurrences) - Used for selections, borders, links
|
||||||
|
- `chalk.cyan` (9 occurrences) - Used for primary UI elements (logo, list bullets, code)
|
||||||
|
- `chalk.red` (7 occurrences) - Used for errors, stderr output
|
||||||
|
- `chalk.green` (6 occurrences) - Used for success, stdout output
|
||||||
|
- `chalk.yellow` (3 occurrences) - Used for headings in markdown
|
||||||
|
- `chalk.bgRgb` (6 occurrences) - Used for custom backgrounds in Text/Markdown
|
||||||
|
|
||||||
|
### Files Using Colors
|
||||||
|
|
||||||
|
#### coding-agent Package
|
||||||
|
1. **main.ts** - CLI output messages
|
||||||
|
2. **tui/assistant-message.ts** - Thinking text (gray italic), errors (red), aborted (red)
|
||||||
|
3. **tui/dynamic-border.ts** - Configurable border color (default blue)
|
||||||
|
4. **tui/footer.ts** - Stats and pwd (gray)
|
||||||
|
5. **tui/model-selector.ts** - Borders (blue), selection arrow (blue), provider badge (gray), checkmark (green)
|
||||||
|
6. **tui/session-selector.ts** - Border (blue), selection cursor (blue), metadata (dim)
|
||||||
|
7. **tui/thinking-selector.ts** - Border (blue)
|
||||||
|
8. **tui/tool-execution.ts** - stdout (green), stderr (red), dim lines (dim), line numbers
|
||||||
|
9. **tui/tui-renderer.ts** - Logo (bold cyan), instructions (dim/gray)
|
||||||
|
|
||||||
|
#### tui Package
|
||||||
|
1. **components/editor.ts** - Horizontal border (gray)
|
||||||
|
2. **components/loader.ts** - Spinner (cyan), message (dim)
|
||||||
|
3. **components/markdown.ts** - Complex color system:
|
||||||
|
- H1 headings: bold.underline.yellow
|
||||||
|
- H2 headings: bold.yellow
|
||||||
|
- H3+ headings: bold
|
||||||
|
- Code blocks: gray (delimiters), dim (indent), green (code)
|
||||||
|
- List bullets: cyan
|
||||||
|
- Blockquotes: gray (pipe), italic (text)
|
||||||
|
- Horizontal rules: gray
|
||||||
|
- Inline code: gray (backticks), cyan (code)
|
||||||
|
- Links: underline.blue (text), gray (URL)
|
||||||
|
- Strikethrough: strikethrough
|
||||||
|
- Tables: bold (headers)
|
||||||
|
4. **components/select-list.ts** - No matches (gray), selection arrow (blue), selected item (blue), description (gray)
|
||||||
|
5. **components/text.ts** - Custom bgRgb support
|
||||||
|
|
||||||
|
### Color System Architecture
|
||||||
|
|
||||||
|
#### Current Implementation
|
||||||
|
- Colors are hardcoded using `chalk` directly
|
||||||
|
- No centralized theme management
|
||||||
|
- No way to switch themes at runtime
|
||||||
|
- Some components accept color parameters (e.g., DynamicBorder, Text, Markdown)
|
||||||
|
|
||||||
|
#### Markdown Component Color System
|
||||||
|
The Markdown component has a `Color` type enum:
|
||||||
|
```typescript
|
||||||
|
type Color = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" |
|
||||||
|
"bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray"
|
||||||
|
```
|
||||||
|
|
||||||
|
It accepts optional `bgColor` and `fgColor` parameters, plus `customBgRgb`.
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
### Theme Structure
|
||||||
|
|
||||||
|
Create a centralized theme system with semantic color names:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Theme {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// UI Chrome
|
||||||
|
border: ChalkFunction;
|
||||||
|
selection: ChalkFunction;
|
||||||
|
selectionText: ChalkFunction;
|
||||||
|
|
||||||
|
// Text hierarchy
|
||||||
|
primary: ChalkFunction;
|
||||||
|
secondary: ChalkFunction;
|
||||||
|
dim: ChalkFunction;
|
||||||
|
|
||||||
|
// Semantic colors
|
||||||
|
error: ChalkFunction;
|
||||||
|
success: ChalkFunction;
|
||||||
|
warning: ChalkFunction;
|
||||||
|
info: ChalkFunction;
|
||||||
|
|
||||||
|
// Code/output
|
||||||
|
code: ChalkFunction;
|
||||||
|
codeDelimiter: ChalkFunction;
|
||||||
|
stdout: ChalkFunction;
|
||||||
|
stderr: ChalkFunction;
|
||||||
|
|
||||||
|
// Markdown specific
|
||||||
|
heading1: ChalkFunction;
|
||||||
|
heading2: ChalkFunction;
|
||||||
|
heading3: ChalkFunction;
|
||||||
|
link: ChalkFunction;
|
||||||
|
linkUrl: ChalkFunction;
|
||||||
|
listBullet: ChalkFunction;
|
||||||
|
blockquote: ChalkFunction;
|
||||||
|
blockquotePipe: ChalkFunction;
|
||||||
|
inlineCode: ChalkFunction;
|
||||||
|
inlineCodeDelimiter: ChalkFunction;
|
||||||
|
|
||||||
|
// Backgrounds (optional, for components like Text/Markdown)
|
||||||
|
backgroundRgb?: { r: number; g: number; b: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChalkFunction = (str: string) => string;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in Themes
|
||||||
|
|
||||||
|
#### Dark Theme (current default)
|
||||||
|
```typescript
|
||||||
|
const darkTheme: Theme = {
|
||||||
|
name: "dark",
|
||||||
|
border: chalk.blue,
|
||||||
|
selection: chalk.blue,
|
||||||
|
selectionText: chalk.blue,
|
||||||
|
primary: (s) => s, // no color
|
||||||
|
secondary: chalk.gray,
|
||||||
|
dim: chalk.dim,
|
||||||
|
error: chalk.red,
|
||||||
|
success: chalk.green,
|
||||||
|
warning: chalk.yellow,
|
||||||
|
info: chalk.cyan,
|
||||||
|
code: chalk.green,
|
||||||
|
codeDelimiter: chalk.gray,
|
||||||
|
stdout: chalk.green,
|
||||||
|
stderr: chalk.red,
|
||||||
|
heading1: chalk.bold.underline.yellow,
|
||||||
|
heading2: chalk.bold.yellow,
|
||||||
|
heading3: chalk.bold,
|
||||||
|
link: chalk.underline.blue,
|
||||||
|
linkUrl: chalk.gray,
|
||||||
|
listBullet: chalk.cyan,
|
||||||
|
blockquote: chalk.italic,
|
||||||
|
blockquotePipe: chalk.gray,
|
||||||
|
inlineCode: chalk.cyan,
|
||||||
|
inlineCodeDelimiter: chalk.gray,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Light Theme
|
||||||
|
```typescript
|
||||||
|
const lightTheme: Theme = {
|
||||||
|
name: "light",
|
||||||
|
border: chalk.blue,
|
||||||
|
selection: chalk.blue,
|
||||||
|
selectionText: chalk.blue.bold,
|
||||||
|
primary: (s) => s,
|
||||||
|
secondary: chalk.gray,
|
||||||
|
dim: chalk.gray, // Don't use chalk.dim on light backgrounds
|
||||||
|
error: chalk.red.bold,
|
||||||
|
success: chalk.green.bold,
|
||||||
|
warning: chalk.yellow.bold,
|
||||||
|
info: chalk.cyan.bold,
|
||||||
|
code: chalk.green.bold,
|
||||||
|
codeDelimiter: chalk.gray,
|
||||||
|
stdout: chalk.green.bold,
|
||||||
|
stderr: chalk.red.bold,
|
||||||
|
heading1: chalk.bold.underline.blue,
|
||||||
|
heading2: chalk.bold.blue,
|
||||||
|
heading3: chalk.bold,
|
||||||
|
link: chalk.underline.blue,
|
||||||
|
linkUrl: chalk.blue,
|
||||||
|
listBullet: chalk.blue.bold,
|
||||||
|
blockquote: chalk.italic,
|
||||||
|
blockquotePipe: chalk.gray,
|
||||||
|
inlineCode: chalk.blue.bold,
|
||||||
|
inlineCodeDelimiter: chalk.gray,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Plan
|
||||||
|
|
||||||
|
#### 1. Create Theme Module
|
||||||
|
**Location:** `packages/tui/src/theme.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Theme { ... }
|
||||||
|
export const darkTheme: Theme = { ... };
|
||||||
|
export const lightTheme: Theme = { ... };
|
||||||
|
export const themes = { dark: darkTheme, light: lightTheme };
|
||||||
|
|
||||||
|
let currentTheme: Theme = darkTheme;
|
||||||
|
|
||||||
|
export function setTheme(theme: Theme): void {
|
||||||
|
currentTheme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTheme(): Theme {
|
||||||
|
return currentTheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Update Settings Manager
|
||||||
|
**Location:** `packages/coding-agent/src/settings-manager.ts`
|
||||||
|
|
||||||
|
Add `theme` field to Settings interface:
|
||||||
|
```typescript
|
||||||
|
export interface Settings {
|
||||||
|
lastChangelogVersion?: string;
|
||||||
|
theme?: "dark" | "light";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Create Theme Selector Component
|
||||||
|
**Location:** `packages/coding-agent/src/tui/theme-selector.ts`
|
||||||
|
|
||||||
|
Similar to ModelSelector and ThinkingSelector, create a TUI component for selecting themes.
|
||||||
|
|
||||||
|
#### 4. Refactor Color Usage
|
||||||
|
|
||||||
|
Replace all hardcoded `chalk.*` calls with `theme.*`:
|
||||||
|
|
||||||
|
**Example - Before:**
|
||||||
|
```typescript
|
||||||
|
lines.push(chalk.blue("─".repeat(width)));
|
||||||
|
const cursor = chalk.blue("› ");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example - After:**
|
||||||
|
```typescript
|
||||||
|
const theme = getTheme();
|
||||||
|
lines.push(theme.border("─".repeat(width)));
|
||||||
|
const cursor = theme.selection("› ");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Update Components
|
||||||
|
|
||||||
|
##### High Priority (User-facing content issues)
|
||||||
|
1. **markdown.ts** - Update all color calls to use theme
|
||||||
|
2. **tool-execution.ts** - stdout/stderr colors
|
||||||
|
3. **assistant-message.ts** - Error messages
|
||||||
|
4. **tui-renderer.ts** - Logo and instructions
|
||||||
|
5. **footer.ts** - Stats display
|
||||||
|
|
||||||
|
##### Medium Priority (UI chrome)
|
||||||
|
6. **dynamic-border.ts** - Accept theme parameter
|
||||||
|
7. **model-selector.ts** - Selection colors
|
||||||
|
8. **session-selector.ts** - Selection colors
|
||||||
|
9. **thinking-selector.ts** - Border colors
|
||||||
|
10. **select-list.ts** - Selection colors
|
||||||
|
11. **loader.ts** - Spinner color
|
||||||
|
12. **editor.ts** - Border color
|
||||||
|
|
||||||
|
##### Low Priority (CLI output)
|
||||||
|
13. **main.ts** - CLI messages
|
||||||
|
|
||||||
|
#### 6. Add Theme Slash Command
|
||||||
|
**Location:** `packages/coding-agent/src/tui/tui-renderer.ts`
|
||||||
|
|
||||||
|
Add `/theme` command similar to `/model` and `/thinking`.
|
||||||
|
|
||||||
|
#### 7. Initialize Theme on Startup
|
||||||
|
**Location:** `packages/coding-agent/src/main.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Load theme from settings
|
||||||
|
const settingsManager = new SettingsManager();
|
||||||
|
const themeName = settingsManager.getTheme() || "dark";
|
||||||
|
const theme = themes[themeName] || darkTheme;
|
||||||
|
setTheme(theme);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Strategy
|
||||||
|
|
||||||
|
1. **Phase 1:** Create theme infrastructure (theme.ts, types, built-in themes)
|
||||||
|
2. **Phase 2:** Update TUI package components (markdown, text, loader, editor, select-list)
|
||||||
|
3. **Phase 3:** Update coding-agent TUI components (all tui/*.ts files)
|
||||||
|
4. **Phase 4:** Add theme selector and persistence
|
||||||
|
5. **Phase 5:** Update CLI output in main.ts (optional, low priority)
|
||||||
|
|
||||||
|
### Testing Plan
|
||||||
|
|
||||||
|
1. Test both themes in terminals with light backgrounds
|
||||||
|
2. Test both themes in terminals with dark backgrounds
|
||||||
|
3. Verify theme switching works at runtime via `/theme`
|
||||||
|
4. Verify theme persists across sessions via settings.json
|
||||||
|
5. Test all components for readability in both themes
|
||||||
|
|
||||||
|
### Open Questions
|
||||||
|
|
||||||
|
1. Should we support custom user themes loaded from a JSON file?
|
||||||
|
2. Should we auto-detect terminal background color and choose theme automatically?
|
||||||
|
3. Should theme apply to background colors used in Text/Markdown components?
|
||||||
|
4. Do we need more than two themes initially?
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
None - the default theme will remain "dark" matching current behavior.
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
- Theme getter is called frequently (on every render)
|
||||||
|
- Should be a simple variable access, not a function call chain
|
||||||
|
- Consider caching theme functions if performance becomes an issue
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-coding-agent",
|
"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",
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.5",
|
"@mariozechner/pi-agent": "^0.7.7",
|
||||||
"@mariozechner/pi-ai": "^0.7.5",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^11.0.3"
|
"glob": "^11.0.3"
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ import {
|
||||||
TUI,
|
TUI,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { getChangelogPath, getNewEntries, parseChangelog } from "../changelog.js";
|
||||||
import { exportSessionToHtml } from "../export-html.js";
|
import { exportSessionToHtml } from "../export-html.js";
|
||||||
import type { SessionManager } from "../session-manager.js";
|
import type { SessionManager } from "../session-manager.js";
|
||||||
|
import { SettingsManager } from "../settings-manager.js";
|
||||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||||
import { CustomEditor } from "./custom-editor.js";
|
import { CustomEditor } from "./custom-editor.js";
|
||||||
import { DynamicBorder } from "./dynamic-border.js";
|
import { DynamicBorder } from "./dynamic-border.js";
|
||||||
|
|
@ -92,9 +94,14 @@ export class TuiRenderer {
|
||||||
description: "Show session info and stats",
|
description: "Show session info and stats",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changelogCommand: SlashCommand = {
|
||||||
|
name: "changelog",
|
||||||
|
description: "Show changelog entries",
|
||||||
|
};
|
||||||
|
|
||||||
// Setup autocomplete for file paths and slash commands
|
// Setup autocomplete for file paths and slash commands
|
||||||
const autocompleteProvider = new CombinedAutocompleteProvider(
|
const autocompleteProvider = new CombinedAutocompleteProvider(
|
||||||
[thinkingCommand, modelCommand, exportCommand, sessionCommand],
|
[thinkingCommand, modelCommand, exportCommand, sessionCommand, changelogCommand],
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
);
|
);
|
||||||
this.editor.setAutocompleteProvider(autocompleteProvider);
|
this.editor.setAutocompleteProvider(autocompleteProvider);
|
||||||
|
|
@ -194,6 +201,13 @@ export class TuiRenderer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for /changelog command
|
||||||
|
if (text === "/changelog") {
|
||||||
|
this.handleChangelogCommand();
|
||||||
|
this.editor.setText("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onInputCallback) {
|
if (this.onInputCallback) {
|
||||||
this.onInputCallback(text);
|
this.onInputCallback(text);
|
||||||
}
|
}
|
||||||
|
|
@ -648,6 +662,25 @@ export class TuiRenderer {
|
||||||
this.ui.requestRender();
|
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 {
|
stop(): void {
|
||||||
if (this.loadingAnimation) {
|
if (this.loadingAnimation) {
|
||||||
this.loadingAnimation.stop();
|
this.loadingAnimation.stop();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi",
|
"name": "@mariozechner/pi",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"description": "CLI tool for managing vLLM deployments on GPU pods",
|
"description": "CLI tool for managing vLLM deployments on GPU pods",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-agent": "^0.7.5",
|
"@mariozechner/pi-agent": "^0.7.7",
|
||||||
"chalk": "^5.5.0"
|
"chalk": "^5.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-proxy",
|
"name": "@mariozechner/pi-proxy",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "CORS and authentication proxy for pi-ai",
|
"description": "CORS and authentication proxy for pi-ai",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-tui",
|
"name": "@mariozechner/pi-tui",
|
||||||
"version": "0.7.5",
|
"version": "0.7.7",
|
||||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mariozechner/pi-web-ui",
|
"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",
|
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
"@mariozechner/pi-ai": "^0.6.0",
|
"@mariozechner/pi-ai": "^0.7.7",
|
||||||
"@mariozechner/pi-tui": "^0.7.5",
|
"@mariozechner/pi-tui": "^0.7.7",
|
||||||
"docx-preview": "^0.3.7",
|
"docx-preview": "^0.3.7",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.544.0",
|
"lucide": "^0.544.0",
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,96 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs inter-package dependency versions in the monorepo
|
* Syncs ALL @mariozechner/* package dependency versions to match their current versions.
|
||||||
* Updates internal @mariozechner/* package versions in dependent packages
|
* This ensures lockstep versioning across the monorepo.
|
||||||
* to match their current versions
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
const packagesDir = join(process.cwd(), 'packages');
|
const packagesDir = join(process.cwd(), 'packages');
|
||||||
|
const packageDirs = readdirSync(packagesDir, { withFileTypes: true })
|
||||||
|
.filter(dirent => dirent.isDirectory())
|
||||||
|
.map(dirent => dirent.name);
|
||||||
|
|
||||||
// Read current versions
|
// Read all package.json files and build version map
|
||||||
const tui = JSON.parse(readFileSync(join(packagesDir, 'tui/package.json'), 'utf8'));
|
const packages = {};
|
||||||
const ai = JSON.parse(readFileSync(join(packagesDir, 'ai/package.json'), 'utf8'));
|
const versionMap = {};
|
||||||
const agent = JSON.parse(readFileSync(join(packagesDir, 'agent/package.json'), 'utf8'));
|
|
||||||
const codingAgent = JSON.parse(readFileSync(join(packagesDir, 'coding-agent/package.json'), 'utf8'));
|
for (const dir of packageDirs) {
|
||||||
const pods = JSON.parse(readFileSync(join(packagesDir, 'pods/package.json'), 'utf8'));
|
const pkgPath = join(packagesDir, dir, 'package.json');
|
||||||
const webUi = JSON.parse(readFileSync(join(packagesDir, 'web-ui/package.json'), 'utf8'));
|
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('Current versions:');
|
||||||
console.log(` @mariozechner/pi-tui: ${tui.version}`);
|
for (const [name, version] of Object.entries(versionMap).sort()) {
|
||||||
console.log(` @mariozechner/pi-ai: ${ai.version}`);
|
console.log(` ${name}: ${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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update coding-agent's dependencies
|
// Verify all versions are the same (lockstep)
|
||||||
let codingAgentUpdated = false;
|
const versions = new Set(Object.values(versionMap));
|
||||||
if (codingAgent.dependencies['@mariozechner/pi-ai']) {
|
if (versions.size > 1) {
|
||||||
const oldVersion = codingAgent.dependencies['@mariozechner/pi-ai'];
|
console.error('\n❌ ERROR: Not all packages have the same version!');
|
||||||
codingAgent.dependencies['@mariozechner/pi-ai'] = `^${ai.version}`;
|
console.error('Expected lockstep versioning. Run one of:');
|
||||||
console.log(`Updated coding-agent's dependency on pi-ai: ${oldVersion} → ^${ai.version}`);
|
console.error(' npm run version:patch');
|
||||||
codingAgentUpdated = true;
|
console.error(' npm run version:minor');
|
||||||
}
|
console.error(' npm run version:major');
|
||||||
if (codingAgent.dependencies['@mariozechner/pi-agent']) {
|
process.exit(1);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update pods' dependency on agent
|
console.log('\n✅ All packages at same version (lockstep)');
|
||||||
if (pods.dependencies['@mariozechner/pi-agent']) {
|
|
||||||
const oldVersion = pods.dependencies['@mariozechner/pi-agent'];
|
// Update all inter-package dependencies
|
||||||
pods.dependencies['@mariozechner/pi-agent'] = `^${agent.version}`;
|
let totalUpdates = 0;
|
||||||
writeFileSync(join(packagesDir, 'pods/package.json'), JSON.stringify(pods, null, '\t') + '\n');
|
for (const [dir, pkg] of Object.entries(packages)) {
|
||||||
console.log(`Updated pods' dependency on pi-agent: ${oldVersion} → ^${agent.version}`);
|
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 (totalUpdates === 0) {
|
||||||
if (webUi.dependencies['@mariozechner/pi-tui']) {
|
console.log('\nAll inter-package dependencies already in sync.');
|
||||||
const oldVersion = webUi.dependencies['@mariozechner/pi-tui'];
|
} else {
|
||||||
webUi.dependencies['@mariozechner/pi-tui'] = `^${tui.version}`;
|
console.log(`\n✅ Updated ${totalUpdates} dependency version(s)`);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✅ Version sync complete!');
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue