mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 22:03:45 +00:00
Release v0.8.0
This commit is contained in:
parent
cc88095140
commit
85adcf22bf
48 changed files with 1530 additions and 608 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.8.0] - 2025-11-21
|
||||
|
||||
### Added
|
||||
|
||||
- **Theme System**: Full theming support with 44 customizable color tokens. Two built-in themes (`dark`, `light`) with auto-detection based on terminal background. Use `/theme` command to select themes interactively. Custom themes in `~/.pi/agent/themes/*.json` support live editing - changes apply immediately when the file is saved. Themes use RGB hex values for consistent rendering across terminals. VS Code users: set `terminal.integrated.minimumContrastRatio` to `1` for proper color rendering. See [Theme Documentation](docs/theme.md) for details.
|
||||
|
||||
## [0.7.29] - 2025-11-20
|
||||
|
||||
### Improved
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Works on Linux, macOS, and Windows (barely tested, needs Git Bash running in the
|
|||
- [API Keys](#api-keys)
|
||||
- [OAuth Authentication (Optional)](#oauth-authentication-optional)
|
||||
- [Custom Models and Providers](#custom-models-and-providers)
|
||||
- [Themes](#themes)
|
||||
- [Slash Commands](#slash-commands)
|
||||
- [Editor Features](#editor-features)
|
||||
- [Project Context Files](#project-context-files)
|
||||
|
|
@ -284,6 +285,79 @@ If the file contains errors (JSON syntax, schema violations, missing fields), th
|
|||
|
||||
See the configuration structure above. Create `~/.pi/agent/models.json` with your Ollama setup, then use `/model` to select your local models. The agent can also help you write this file if you point it to this README.
|
||||
|
||||
## Themes
|
||||
|
||||
Pi supports customizable color themes for the TUI. Two built-in themes are available: `dark` (default) and `light`.
|
||||
|
||||
### Selecting a Theme
|
||||
|
||||
Use the `/theme` command to interactively select a theme, or edit your settings file:
|
||||
|
||||
```bash
|
||||
# Interactive selector
|
||||
pi
|
||||
/theme
|
||||
|
||||
# Or edit ~/.pi/agent/settings.json
|
||||
{
|
||||
"theme": "dark" # or "light"
|
||||
}
|
||||
```
|
||||
|
||||
On first run, Pi auto-detects your terminal background (dark/light) and selects an appropriate theme.
|
||||
|
||||
### Custom Themes
|
||||
|
||||
Create custom themes in `~/.pi/agent/themes/*.json`. Custom themes support **live editing** - when you select a custom theme, Pi watches the file and automatically reloads when you save changes.
|
||||
|
||||
**Workflow for creating themes:**
|
||||
1. Copy a built-in theme as a starting point:
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/themes
|
||||
# Copy dark theme
|
||||
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/dark.json ~/.pi/agent/themes/my-theme.json
|
||||
# Or copy light theme
|
||||
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/light.json ~/.pi/agent/themes/my-theme.json
|
||||
```
|
||||
2. Use `/theme` to select "my-theme"
|
||||
3. Edit `~/.pi/agent/themes/my-theme.json` - changes apply immediately on save
|
||||
4. Iterate until satisfied (no need to re-select the theme)
|
||||
|
||||
See [Theme Documentation](docs/theme.md) for:
|
||||
- Complete list of 44 color tokens
|
||||
- Theme format and examples
|
||||
- Color value formats (hex, RGB, terminal default)
|
||||
|
||||
Example custom theme:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"accent": "#00aaff",
|
||||
"muted": "#6c6c6c"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "accent",
|
||||
"muted": "muted",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code Terminal Color Issue
|
||||
|
||||
**Important:** VS Code's integrated terminal has a known issue with rendering truecolor (24-bit RGB) values. By default, it applies a "minimum contrast ratio" adjustment that can make colors look washed out or identical.
|
||||
|
||||
To fix this, set the contrast ratio to 1 in VS Code settings:
|
||||
|
||||
1. Open Settings (Cmd/Ctrl + ,)
|
||||
2. Search for: `terminal.integrated.minimumContrastRatio`
|
||||
3. Set to: `1`
|
||||
|
||||
This ensures VS Code renders the exact RGB colors defined in your theme.
|
||||
|
||||
## Slash Commands
|
||||
|
||||
The CLI supports several commands to control its behavior:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Themes allow you to customize the colors used throughout the coding agent TUI.
|
|||
|
||||
Every theme must define all color tokens. There are no optional colors.
|
||||
|
||||
### Core UI (9 colors)
|
||||
### Core UI (10 colors)
|
||||
|
||||
| Token | Purpose | Examples |
|
||||
|-------|---------|----------|
|
||||
|
|
@ -18,9 +18,10 @@ Every theme must define all color tokens. There are no optional colors.
|
|||
| `error` | Error states | Error messages, diff deletions |
|
||||
| `warning` | Warning states | Warning messages |
|
||||
| `muted` | Secondary/dimmed text | Metadata, descriptions, output |
|
||||
| `dim` | Very dimmed text | Less important info, placeholders |
|
||||
| `text` | Default text color | Main content (usually `""`) |
|
||||
|
||||
### Backgrounds & Content Text (6 colors)
|
||||
### Backgrounds & Content Text (7 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
|
|
@ -29,14 +30,16 @@ Every theme must define all color tokens. There are no optional colors.
|
|||
| `toolPendingBg` | Tool execution box (pending state) |
|
||||
| `toolSuccessBg` | Tool execution box (success state) |
|
||||
| `toolErrorBg` | Tool execution box (error state) |
|
||||
| `toolText` | Tool execution box text color (all states) |
|
||||
| `toolTitle` | Tool execution title/heading (e.g., `$ command`, `read file.txt`) |
|
||||
| `toolOutput` | Tool execution output text |
|
||||
|
||||
### Markdown (9 colors)
|
||||
### Markdown (10 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `mdHeading` | Heading text (`#`, `##`, etc) |
|
||||
| `mdLink` | Link text and URLs |
|
||||
| `mdLink` | Link text |
|
||||
| `mdLinkUrl` | Link URL (in parentheses) |
|
||||
| `mdCode` | Inline code (backticks) |
|
||||
| `mdCodeBlock` | Code block content |
|
||||
| `mdCodeBlockBorder` | Code block fences (```) |
|
||||
|
|
@ -71,7 +74,21 @@ Future-proofing for syntax highlighting support:
|
|||
| `syntaxOperator` | Operators (`+`, `-`, etc) |
|
||||
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
|
||||
|
||||
**Total: 36 color tokens** (all required)
|
||||
### Thinking Level Borders (5 colors)
|
||||
|
||||
Editor border colors that indicate the current thinking/reasoning level:
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `thinkingOff` | Border when thinking is off (most subtle) |
|
||||
| `thinkingMinimal` | Border for minimal thinking |
|
||||
| `thinkingLow` | Border for low thinking |
|
||||
| `thinkingMedium` | Border for medium thinking |
|
||||
| `thinkingHigh` | Border for high thinking (most prominent) |
|
||||
|
||||
These create a visual hierarchy: off → minimal → low → medium → high
|
||||
|
||||
**Total: 44 color tokens** (all required)
|
||||
|
||||
## Theme Format
|
||||
|
||||
|
|
@ -241,7 +258,13 @@ Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
|||
"syntaxNumber": "#ff00ff",
|
||||
"syntaxType": "#00aaff",
|
||||
"syntaxOperator": "primary",
|
||||
"syntaxPunctuation": "secondary"
|
||||
"syntaxPunctuation": "secondary",
|
||||
|
||||
"thinkingOff": "secondary",
|
||||
"thinkingMinimal": "primary",
|
||||
"thinkingLow": "#00aaff",
|
||||
"thinkingMedium": "#00ffff",
|
||||
"thinkingHigh": "#ff00ff"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
@ -22,8 +22,8 @@
|
|||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent": "^0.7.29",
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-agent": "^0.8.0",
|
||||
"@mariozechner/pi-ai": "^0.8.0",
|
||||
"chalk": "^5.5.0",
|
||||
"diff": "^8.0.2",
|
||||
"glob": "^11.0.3"
|
||||
|
|
|
|||
|
|
@ -236,8 +236,8 @@ Guidelines:
|
|||
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||
|
||||
Documentation:
|
||||
- Your own documentation (including custom model setup) is at: ${readmePath}
|
||||
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider.`;
|
||||
- Your own documentation (including custom model setup and theme creation) is at: ${readmePath}
|
||||
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.`;
|
||||
|
||||
// Append project context files
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@
|
|||
"name": "dark",
|
||||
"vars": {
|
||||
"cyan": "#00d7ff",
|
||||
"blue": "#0087ff",
|
||||
"green": "#00ff00",
|
||||
"red": "#ff0000",
|
||||
"blue": "#5f87ff",
|
||||
"green": "#b5bd68",
|
||||
"red": "#cc6666",
|
||||
"yellow": "#ffff00",
|
||||
"gray": 242,
|
||||
"darkGray": 238,
|
||||
"gray": "#808080",
|
||||
"dimGray": "#666666",
|
||||
"darkGray": "#303030",
|
||||
"accent": "#8abeb7",
|
||||
"userMsgBg": "#343541",
|
||||
"toolPendingBg": "#282832",
|
||||
"toolSuccessBg": "#283228",
|
||||
"toolErrorBg": "#3c2828"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "cyan",
|
||||
"accent": "accent",
|
||||
"border": "blue",
|
||||
"borderAccent": "cyan",
|
||||
"borderMuted": "darkGray",
|
||||
|
|
@ -23,6 +25,7 @@
|
|||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "gray",
|
||||
"dim": "dimGray",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "userMsgBg",
|
||||
|
|
@ -30,17 +33,19 @@
|
|||
"toolPendingBg": "toolPendingBg",
|
||||
"toolSuccessBg": "toolSuccessBg",
|
||||
"toolErrorBg": "toolErrorBg",
|
||||
"toolText": "",
|
||||
"toolTitle": "",
|
||||
"toolOutput": "gray",
|
||||
|
||||
"mdHeading": "cyan",
|
||||
"mdLink": "blue",
|
||||
"mdCode": "cyan",
|
||||
"mdCodeBlock": "",
|
||||
"mdHeading": "#f0c674",
|
||||
"mdLink": "#81a2be",
|
||||
"mdLinkUrl": "dimGray",
|
||||
"mdCode": "accent",
|
||||
"mdCodeBlock": "green",
|
||||
"mdCodeBlockBorder": "gray",
|
||||
"mdQuote": "gray",
|
||||
"mdQuoteBorder": "gray",
|
||||
"mdHr": "gray",
|
||||
"mdListBullet": "cyan",
|
||||
"mdListBullet": "accent",
|
||||
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
|
|
@ -54,6 +59,12 @@
|
|||
"syntaxNumber": "yellow",
|
||||
"syntaxType": "cyan",
|
||||
"syntaxOperator": "",
|
||||
"syntaxPunctuation": "gray"
|
||||
"syntaxPunctuation": "gray",
|
||||
|
||||
"thinkingOff": "darkGray",
|
||||
"thinkingMinimal": "#4e4e4e",
|
||||
"thinkingLow": "#5f87af",
|
||||
"thinkingMedium": "#81a2be",
|
||||
"thinkingHigh": "#b294bb"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,29 @@
|
|||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "light",
|
||||
"vars": {
|
||||
"darkCyan": "#008899",
|
||||
"darkBlue": "#0066cc",
|
||||
"darkGreen": "#008800",
|
||||
"darkRed": "#cc0000",
|
||||
"darkYellow": "#aa8800",
|
||||
"mediumGray": 242,
|
||||
"lightGray": 250,
|
||||
"teal": "#5f8787",
|
||||
"blue": "#5f87af",
|
||||
"green": "#87af87",
|
||||
"red": "#af5f5f",
|
||||
"yellow": "#d7af5f",
|
||||
"mediumGray": "#6c6c6c",
|
||||
"dimGray": "#8a8a8a",
|
||||
"lightGray": "#b0b0b0",
|
||||
"userMsgBg": "#e8e8e8",
|
||||
"toolPendingBg": "#e8e8f0",
|
||||
"toolSuccessBg": "#e8f0e8",
|
||||
"toolErrorBg": "#f0e8e8"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "darkCyan",
|
||||
"border": "darkBlue",
|
||||
"borderAccent": "darkCyan",
|
||||
"accent": "teal",
|
||||
"border": "blue",
|
||||
"borderAccent": "teal",
|
||||
"borderMuted": "lightGray",
|
||||
"success": "darkGreen",
|
||||
"error": "darkRed",
|
||||
"warning": "darkYellow",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "mediumGray",
|
||||
"dim": "dimGray",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "userMsgBg",
|
||||
|
|
@ -30,30 +32,38 @@
|
|||
"toolPendingBg": "toolPendingBg",
|
||||
"toolSuccessBg": "toolSuccessBg",
|
||||
"toolErrorBg": "toolErrorBg",
|
||||
"toolText": "",
|
||||
"toolTitle": "",
|
||||
"toolOutput": "mediumGray",
|
||||
|
||||
"mdHeading": "darkCyan",
|
||||
"mdLink": "darkBlue",
|
||||
"mdCode": "darkCyan",
|
||||
"mdCodeBlock": "",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "blue",
|
||||
"mdLinkUrl": "dimGray",
|
||||
"mdCode": "teal",
|
||||
"mdCodeBlock": "green",
|
||||
"mdCodeBlockBorder": "mediumGray",
|
||||
"mdQuote": "mediumGray",
|
||||
"mdQuoteBorder": "mediumGray",
|
||||
"mdHr": "mediumGray",
|
||||
"mdListBullet": "darkCyan",
|
||||
"mdListBullet": "green",
|
||||
|
||||
"toolDiffAdded": "darkGreen",
|
||||
"toolDiffRemoved": "darkRed",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "mediumGray",
|
||||
|
||||
"syntaxComment": "mediumGray",
|
||||
"syntaxKeyword": "darkCyan",
|
||||
"syntaxFunction": "darkBlue",
|
||||
"syntaxKeyword": "teal",
|
||||
"syntaxFunction": "blue",
|
||||
"syntaxVariable": "",
|
||||
"syntaxString": "darkGreen",
|
||||
"syntaxNumber": "darkYellow",
|
||||
"syntaxType": "darkCyan",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "yellow",
|
||||
"syntaxType": "teal",
|
||||
"syntaxOperator": "",
|
||||
"syntaxPunctuation": "mediumGray"
|
||||
"syntaxPunctuation": "mediumGray",
|
||||
|
||||
"thinkingOff": "lightGray",
|
||||
"thinkingMinimal": "#9e9e9e",
|
||||
"thinkingLow": "#5f87af",
|
||||
"thinkingMedium": "#5f8787",
|
||||
"thinkingHigh": "#875f87"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"error",
|
||||
"warning",
|
||||
"muted",
|
||||
"dim",
|
||||
"text",
|
||||
"userMessageBg",
|
||||
"userMessageText",
|
||||
|
|
@ -105,6 +106,10 @@
|
|||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Secondary/dimmed text"
|
||||
},
|
||||
"dim": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Very dimmed text (more subtle than muted)"
|
||||
},
|
||||
"text": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Default text color (usually empty string)"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { MarkdownTheme } from "@mariozechner/pi-tui";
|
||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||
import chalk from "chalk";
|
||||
|
|
@ -25,7 +25,7 @@ const ThemeJsonSchema = Type.Object({
|
|||
name: Type.String(),
|
||||
vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),
|
||||
colors: Type.Object({
|
||||
// Core UI (9 colors)
|
||||
// Core UI (10 colors)
|
||||
accent: ColorValueSchema,
|
||||
border: ColorValueSchema,
|
||||
borderAccent: ColorValueSchema,
|
||||
|
|
@ -34,17 +34,20 @@ const ThemeJsonSchema = Type.Object({
|
|||
error: ColorValueSchema,
|
||||
warning: ColorValueSchema,
|
||||
muted: ColorValueSchema,
|
||||
dim: ColorValueSchema,
|
||||
text: ColorValueSchema,
|
||||
// Backgrounds & Content Text (6 colors)
|
||||
// Backgrounds & Content Text (7 colors)
|
||||
userMessageBg: ColorValueSchema,
|
||||
userMessageText: ColorValueSchema,
|
||||
toolPendingBg: ColorValueSchema,
|
||||
toolSuccessBg: ColorValueSchema,
|
||||
toolErrorBg: ColorValueSchema,
|
||||
toolText: ColorValueSchema,
|
||||
// Markdown (9 colors)
|
||||
toolTitle: ColorValueSchema,
|
||||
toolOutput: ColorValueSchema,
|
||||
// Markdown (10 colors)
|
||||
mdHeading: ColorValueSchema,
|
||||
mdLink: ColorValueSchema,
|
||||
mdLinkUrl: ColorValueSchema,
|
||||
mdCode: ColorValueSchema,
|
||||
mdCodeBlock: ColorValueSchema,
|
||||
mdCodeBlockBorder: ColorValueSchema,
|
||||
|
|
@ -66,6 +69,12 @@ const ThemeJsonSchema = Type.Object({
|
|||
syntaxType: ColorValueSchema,
|
||||
syntaxOperator: ColorValueSchema,
|
||||
syntaxPunctuation: ColorValueSchema,
|
||||
// Thinking Level Borders (5 colors)
|
||||
thinkingOff: ColorValueSchema,
|
||||
thinkingMinimal: ColorValueSchema,
|
||||
thinkingLow: ColorValueSchema,
|
||||
thinkingMedium: ColorValueSchema,
|
||||
thinkingHigh: ColorValueSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -82,11 +91,14 @@ export type ThemeColor =
|
|||
| "error"
|
||||
| "warning"
|
||||
| "muted"
|
||||
| "dim"
|
||||
| "text"
|
||||
| "userMessageText"
|
||||
| "toolText"
|
||||
| "toolTitle"
|
||||
| "toolOutput"
|
||||
| "mdHeading"
|
||||
| "mdLink"
|
||||
| "mdLinkUrl"
|
||||
| "mdCode"
|
||||
| "mdCodeBlock"
|
||||
| "mdCodeBlockBorder"
|
||||
|
|
@ -105,7 +117,12 @@ export type ThemeColor =
|
|||
| "syntaxNumber"
|
||||
| "syntaxType"
|
||||
| "syntaxOperator"
|
||||
| "syntaxPunctuation";
|
||||
| "syntaxPunctuation"
|
||||
| "thinkingOff"
|
||||
| "thinkingMinimal"
|
||||
| "thinkingLow"
|
||||
| "thinkingMedium"
|
||||
| "thinkingHigh";
|
||||
|
||||
export type ThemeBg = "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
||||
|
||||
|
|
@ -216,8 +233,6 @@ function resolveThemeColors<T extends Record<string, ColorValue>>(
|
|||
// Theme Class
|
||||
// ============================================================================
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
export class Theme {
|
||||
private fgColors: Map<ThemeColor, string>;
|
||||
private bgColors: Map<ThemeBg, string>;
|
||||
|
|
@ -242,13 +257,13 @@ export class Theme {
|
|||
fg(color: ThemeColor, text: string): string {
|
||||
const ansi = this.fgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
||||
return `${ansi}${text}${RESET}`;
|
||||
return `${ansi}${text}\x1b[39m`; // Reset only foreground color
|
||||
}
|
||||
|
||||
bg(color: ThemeBg, text: string): string {
|
||||
const ansi = this.bgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
||||
return `${ansi}${text}${RESET}`;
|
||||
return `${ansi}${text}\x1b[49m`; // Reset only background color
|
||||
}
|
||||
|
||||
bold(text: string): string {
|
||||
|
|
@ -278,6 +293,24 @@ export class Theme {
|
|||
getColorMode(): ColorMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high"): (str: string) => string {
|
||||
// Map thinking levels to dedicated theme colors
|
||||
switch (level) {
|
||||
case "off":
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
case "minimal":
|
||||
return (str: string) => this.fg("thinkingMinimal", str);
|
||||
case "low":
|
||||
return (str: string) => this.fg("thinkingLow", str);
|
||||
case "medium":
|
||||
return (str: string) => this.fg("thinkingMedium", str);
|
||||
case "high":
|
||||
return (str: string) => this.fg("thinkingHigh", str);
|
||||
default:
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -369,7 +402,8 @@ function detectTerminalBackground(): "dark" | "light" {
|
|||
if (parts.length >= 2) {
|
||||
const bg = parseInt(parts[1], 10);
|
||||
if (!Number.isNaN(bg)) {
|
||||
return bg < 8 ? "dark" : "light";
|
||||
const result = bg < 8 ? "dark" : "light";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -385,14 +419,109 @@ function getDefaultTheme(): string {
|
|||
// ============================================================================
|
||||
|
||||
export let theme: Theme;
|
||||
let currentThemeName: string | undefined;
|
||||
let themeWatcher: fs.FSWatcher | undefined;
|
||||
let onThemeChangeCallback: (() => void) | undefined;
|
||||
|
||||
export function initTheme(themeName?: string): void {
|
||||
const name = themeName ?? getDefaultTheme();
|
||||
theme = loadTheme(name);
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme silently
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
}
|
||||
}
|
||||
|
||||
export function setTheme(name: string): void {
|
||||
theme = loadTheme(name);
|
||||
export function setTheme(name: string): { success: boolean; error?: string } {
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function onThemeChange(callback: () => void): void {
|
||||
onThemeChangeCallback = callback;
|
||||
}
|
||||
|
||||
function startThemeWatcher(): void {
|
||||
// Stop existing watcher if any
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
|
||||
// Only watch if it's a custom theme (not built-in)
|
||||
if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
|
||||
return;
|
||||
}
|
||||
|
||||
const themesDir = getThemesDir();
|
||||
const themeFile = path.join(themesDir, `${currentThemeName}.json`);
|
||||
|
||||
// Only watch if the file exists
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
themeWatcher = fs.watch(themeFile, (eventType) => {
|
||||
if (eventType === "change") {
|
||||
// Debounce rapid changes
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Reload the theme
|
||||
theme = loadTheme(currentThemeName!);
|
||||
// Notify callback (to invalidate UI)
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors (file might be in invalid state while being edited)
|
||||
}
|
||||
}, 100);
|
||||
} else if (eventType === "rename") {
|
||||
// File was deleted or renamed - fall back to default theme
|
||||
setTimeout(() => {
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors starting watcher
|
||||
}
|
||||
}
|
||||
|
||||
export function stopThemeWatcher(): void {
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -403,6 +532,7 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|||
return {
|
||||
heading: (text: string) => theme.fg("mdHeading", text),
|
||||
link: (text: string) => theme.fg("mdLink", text),
|
||||
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
||||
code: (text: string) => theme.fg("mdCode", text),
|
||||
codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
|
||||
codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
|
||||
|
|
@ -410,5 +540,26 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|||
quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
|
||||
hr: (text: string) => theme.fg("mdHr", text),
|
||||
listBullet: (text: string) => theme.fg("mdListBullet", text),
|
||||
bold: (text: string) => theme.bold(text),
|
||||
italic: (text: string) => theme.italic(text),
|
||||
underline: (text: string) => theme.underline(text),
|
||||
strikethrough: (text: string) => chalk.strikethrough(text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSelectListTheme(): SelectListTheme {
|
||||
return {
|
||||
selectedPrefix: (text: string) => theme.fg("accent", text),
|
||||
selectedText: (text: string) => theme.fg("accent", text),
|
||||
description: (text: string) => theme.fg("muted", text),
|
||||
scrollInfo: (text: string) => theme.fg("muted", text),
|
||||
noMatch: (text: string) => theme.fg("muted", text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getEditorTheme(): EditorTheme {
|
||||
return {
|
||||
borderColor: (text: string) => theme.fg("borderMuted", text),
|
||||
selectList: getSelectListTheme(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a complete assistant message
|
||||
|
|
@ -38,13 +38,13 @@ export class AssistantMessageComponent extends Container {
|
|||
if (content.type === "text" && content.text.trim()) {
|
||||
// Assistant text messages with no background - trim the text
|
||||
// Set paddingY=0 to avoid extra spacing before tool executions
|
||||
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0));
|
||||
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
|
||||
} else if (content.type === "thinking" && content.thinking.trim()) {
|
||||
// Thinking traces in dark gray italic
|
||||
// Thinking traces in muted color, italic
|
||||
// Use Markdown component with default text style for consistent styling
|
||||
this.contentContainer.addChild(
|
||||
new Markdown(content.thinking.trim(), 1, 0, {
|
||||
color: "gray",
|
||||
new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
|
||||
color: (text: string) => theme.fg("muted", text),
|
||||
italic: true,
|
||||
}),
|
||||
);
|
||||
|
|
@ -57,11 +57,11 @@ export class AssistantMessageComponent extends Container {
|
|||
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
|
||||
if (!hasToolCalls) {
|
||||
if (message.stopReason === "aborted") {
|
||||
this.contentContainer.addChild(new Text(chalk.red("\nAborted"), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", "\nAborted"), 1, 0));
|
||||
} else if (message.stopReason === "error") {
|
||||
const errorMsg = message.errorMessage || "Unknown error";
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(chalk.red(`Error: ${errorMsg}`), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
|
|
@ -7,10 +7,14 @@ import chalk from "chalk";
|
|||
export class DynamicBorder implements Component {
|
||||
private color: (str: string) => string;
|
||||
|
||||
constructor(color: (str: string) => string = chalk.blue) {
|
||||
constructor(color: (str: string) => string = (str) => theme.fg("border", str)) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
return [this.color("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { AgentState } from "@mariozechner/pi-agent";
|
||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { visibleWidth } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { type Component, visibleWidth } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Footer component that shows pwd, token stats, and context usage
|
||||
*/
|
||||
export class FooterComponent {
|
||||
export class FooterComponent implements Component {
|
||||
private state: AgentState;
|
||||
|
||||
constructor(state: AgentState) {
|
||||
|
|
@ -17,6 +17,10 @@ export class FooterComponent {
|
|||
this.state = state;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
// Calculate cumulative usage from all assistant messages
|
||||
let totalInput = 0;
|
||||
|
|
@ -50,7 +54,8 @@ export class FooterComponent {
|
|||
lastAssistantMessage.usage.cacheWrite
|
||||
: 0;
|
||||
const contextWindow = this.state.model?.contextWindow || 0;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : "0.0";
|
||||
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
||||
const contextPercent = contextPercentValue.toFixed(1);
|
||||
|
||||
// Format token counts (similar to web-ui)
|
||||
const formatTokens = (count: number): string => {
|
||||
|
|
@ -80,8 +85,18 @@ export class FooterComponent {
|
|||
if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);
|
||||
if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
|
||||
if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
|
||||
if (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);
|
||||
statsParts.push(`${contextPercent}%`);
|
||||
if (totalCost) statsParts.push(`${totalCost.toFixed(3)}`);
|
||||
|
||||
// Colorize context percentage based on usage
|
||||
let contextPercentStr: string;
|
||||
if (contextPercentValue > 90) {
|
||||
contextPercentStr = theme.fg("error", `${contextPercent}%`);
|
||||
} else if (contextPercentValue > 70) {
|
||||
contextPercentStr = theme.fg("warning", `${contextPercent}%`);
|
||||
} else {
|
||||
contextPercentStr = `${contextPercent}%`;
|
||||
}
|
||||
statsParts.push(contextPercentStr);
|
||||
|
||||
const statsLeft = statsParts.join(" ");
|
||||
|
||||
|
|
@ -126,6 +141,6 @@ export class FooterComponent {
|
|||
}
|
||||
|
||||
// Return two lines: pwd and stats
|
||||
return [chalk.gray(pwd), chalk.gray(statsLine)];
|
||||
return [theme.fg("dim", pwd), theme.fg("dim", statsLine)];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { Container, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getAvailableModels } from "../model-config.js";
|
||||
import type { SettingsManager } from "../settings-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface ModelItem {
|
||||
provider: string;
|
||||
|
|
@ -42,12 +43,12 @@ export class ModelSelectorComponent extends Container {
|
|||
this.onCancelCallback = onCancel;
|
||||
|
||||
// Add top border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add hint about API key filtering
|
||||
this.addChild(
|
||||
new Text(chalk.yellow("Only showing models with configured API keys (see README for details)"), 0, 0),
|
||||
new Text(theme.fg("warning", "Only showing models with configured API keys (see README for details)"), 0, 0),
|
||||
);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ export class ModelSelectorComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Load models and do initial render
|
||||
this.loadModels().then(() => {
|
||||
|
|
@ -150,15 +151,15 @@ export class ModelSelectorComponent extends Container {
|
|||
|
||||
let line = "";
|
||||
if (isSelected) {
|
||||
const prefix = chalk.blue("→ ");
|
||||
const prefix = theme.fg("accent", "→ ");
|
||||
const modelText = `${item.id}`;
|
||||
const providerBadge = chalk.gray(`[${item.provider}]`);
|
||||
const checkmark = isCurrent ? chalk.green(" ✓") : "";
|
||||
line = prefix + chalk.blue(modelText) + " " + providerBadge + checkmark;
|
||||
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
||||
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
||||
line = prefix + theme.fg("accent", modelText) + " " + providerBadge + checkmark;
|
||||
} else {
|
||||
const modelText = ` ${item.id}`;
|
||||
const providerBadge = chalk.gray(`[${item.provider}]`);
|
||||
const checkmark = isCurrent ? chalk.green(" ✓") : "";
|
||||
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
||||
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
||||
line = modelText + " " + providerBadge + checkmark;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +168,7 @@ export class ModelSelectorComponent extends Container {
|
|||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
||||
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
||||
}
|
||||
|
||||
|
|
@ -176,10 +177,10 @@ export class ModelSelectorComponent extends Container {
|
|||
// Show error in red
|
||||
const errorLines = this.errorMessage.split("\n");
|
||||
for (const line of errorLines) {
|
||||
this.listContainer.addChild(new Text(chalk.red(line), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
||||
}
|
||||
} else if (this.filteredModels.length === 0) {
|
||||
this.listContainer.addChild(new Text(chalk.gray(" No matching models"), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getOAuthProviders, type OAuthProviderInfo } from "../oauth/index.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders an OAuth provider selector
|
||||
|
|
@ -24,12 +25,12 @@ export class OAuthSelectorComponent extends Container {
|
|||
this.loadProviders();
|
||||
|
||||
// Add top border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add title
|
||||
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
||||
this.addChild(new Text(chalk.bold(title), 0, 0));
|
||||
this.addChild(new Text(theme.bold(title), 0, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Create list container
|
||||
|
|
@ -39,7 +40,7 @@ export class OAuthSelectorComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Initial render
|
||||
this.updateList();
|
||||
|
|
@ -62,11 +63,11 @@ export class OAuthSelectorComponent extends Container {
|
|||
|
||||
let line = "";
|
||||
if (isSelected) {
|
||||
const prefix = chalk.blue("→ ");
|
||||
const text = isAvailable ? chalk.blue(provider.name) : chalk.dim(provider.name);
|
||||
const prefix = theme.fg("accent", "→ ");
|
||||
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
||||
line = prefix + text;
|
||||
} else {
|
||||
const text = isAvailable ? ` ${provider.name}` : chalk.dim(` ${provider.name}`);
|
||||
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
||||
line = text;
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export class OAuthSelectorComponent extends Container {
|
|||
if (this.allProviders.length === 0) {
|
||||
const message =
|
||||
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
||||
this.listContainer.addChild(new Text(chalk.gray(` ${message}`), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("muted", ` ${message}`), 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import { type Component, Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders a queue mode selector with borders
|
||||
|
|
@ -36,7 +28,7 @@ export class QueueModeSelectorComponent extends Container {
|
|||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(queueModes, 2);
|
||||
this.selectList = new SelectList(queueModes, 2, getSelectListTheme());
|
||||
|
||||
// Preselect current mode
|
||||
const currentIndex = queueModes.findIndex((item) => item.value === currentMode);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import { type Component, Container, Input, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface SessionItem {
|
||||
path: string;
|
||||
|
|
@ -67,6 +59,10 @@ class SessionList implements Component {
|
|||
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
|
|
@ -75,7 +71,7 @@ class SessionList implements Component {
|
|||
lines.push(""); // Blank line after search
|
||||
|
||||
if (this.filteredSessions.length === 0) {
|
||||
lines.push(chalk.gray(" No sessions found"));
|
||||
lines.push(theme.fg("muted", " No sessions found"));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -112,16 +108,16 @@ class SessionList implements Component {
|
|||
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
|
||||
|
||||
// First line: cursor + message
|
||||
const cursor = isSelected ? chalk.blue("› ") : " ";
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const maxMsgWidth = width - 2; // Account for cursor
|
||||
const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);
|
||||
const messageLine = cursor + (isSelected ? chalk.bold(truncatedMsg) : truncatedMsg);
|
||||
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
||||
|
||||
// Second line: metadata (dimmed)
|
||||
const modified = formatDate(session.modified);
|
||||
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
|
||||
const metadata = ` ${modified} · ${msgCount}`;
|
||||
const metadataLine = chalk.dim(metadata);
|
||||
const metadataLine = theme.fg("dim", metadata);
|
||||
|
||||
lines.push(messageLine);
|
||||
lines.push(metadataLine);
|
||||
|
|
@ -130,7 +126,7 @@ class SessionList implements Component {
|
|||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.filteredSessions.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.filteredSessions.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`);
|
||||
lines.push(scrollInfo);
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +181,7 @@ export class SessionSelectorComponent extends Container {
|
|||
|
||||
// Add header
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(chalk.bold("Resume Session"), 1, 0));
|
||||
this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getAvailableThemes, theme } from "../theme/theme.js";
|
||||
import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
|
|
@ -7,9 +7,16 @@ import { DynamicBorder } from "./dynamic-border.js";
|
|||
*/
|
||||
export class ThemeSelectorComponent extends Container {
|
||||
private selectList: SelectList;
|
||||
private onPreview: (themeName: string) => void;
|
||||
|
||||
constructor(currentTheme: string, onSelect: (themeName: string) => void, onCancel: () => void) {
|
||||
constructor(
|
||||
currentTheme: string,
|
||||
onSelect: (themeName: string) => void,
|
||||
onCancel: () => void,
|
||||
onPreview: (themeName: string) => void,
|
||||
) {
|
||||
super();
|
||||
this.onPreview = onPreview;
|
||||
|
||||
// Get available themes and create select items
|
||||
const themes = getAvailableThemes();
|
||||
|
|
@ -20,10 +27,10 @@ export class ThemeSelectorComponent extends Container {
|
|||
}));
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(themeItems, 10);
|
||||
this.selectList = new SelectList(themeItems, 10, getSelectListTheme());
|
||||
|
||||
// Preselect current theme
|
||||
const currentIndex = themes.indexOf(currentTheme);
|
||||
|
|
@ -39,10 +46,14 @@ export class ThemeSelectorComponent extends Container {
|
|||
onCancel();
|
||||
};
|
||||
|
||||
this.selectList.onSelectionChange = (item) => {
|
||||
this.onPreview(item.value);
|
||||
};
|
||||
|
||||
this.addChild(this.selectList);
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
||||
this.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
getSelectList(): SelectList {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import type { ThinkingLevel } from "@mariozechner/pi-agent";
|
||||
import { type Component, Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders a thinking level selector with borders
|
||||
|
|
@ -32,7 +24,7 @@ export class ThinkingSelectorComponent extends Container {
|
|||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(thinkingLevels, 5);
|
||||
this.selectList = new SelectList(thinkingLevels, 5, getSelectListTheme());
|
||||
|
||||
// Preselect current level
|
||||
const currentIndex = thinkingLevels.findIndex((item) => item.value === currentLevel);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as os from "node:os";
|
||||
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import * as Diff from "diff";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Convert absolute path to tilde notation if it's in home directory
|
||||
|
|
@ -22,104 +21,6 @@ function replaceTabs(text: string): string {
|
|||
return text.replace(/\t/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unified diff with line numbers and context
|
||||
*/
|
||||
function generateDiff(oldStr: string, newStr: string): string {
|
||||
const parts = Diff.diffLines(oldStr, newStr);
|
||||
const output: string[] = [];
|
||||
|
||||
// Calculate max line number for padding
|
||||
const oldLines = oldStr.split("\n");
|
||||
const newLines = newStr.split("\n");
|
||||
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
||||
const lineNumWidth = String(maxLineNum).length;
|
||||
|
||||
const CONTEXT_LINES = 2; // Show 2 lines of context around changes
|
||||
|
||||
let oldLineNum = 1;
|
||||
let newLineNum = 1;
|
||||
let lastWasChange = false;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const raw = part.value.split("\n");
|
||||
if (raw[raw.length - 1] === "") {
|
||||
raw.pop();
|
||||
}
|
||||
|
||||
if (part.added || part.removed) {
|
||||
// Show the change
|
||||
for (const line of raw) {
|
||||
if (part.added) {
|
||||
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.green(`${lineNum} ${line}`));
|
||||
newLineNum++;
|
||||
} else {
|
||||
// removed
|
||||
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.red(`${lineNum} ${line}`));
|
||||
oldLineNum++;
|
||||
}
|
||||
}
|
||||
lastWasChange = true;
|
||||
} else {
|
||||
// Context lines - only show a few before/after changes
|
||||
const isFirstPart = i === 0;
|
||||
const isLastPart = i === parts.length - 1;
|
||||
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
||||
|
||||
if (lastWasChange || nextPartIsChange || isFirstPart || isLastPart) {
|
||||
// Show context
|
||||
let linesToShow = raw;
|
||||
let skipStart = 0;
|
||||
let skipEnd = 0;
|
||||
|
||||
if (!isFirstPart && !lastWasChange) {
|
||||
// Show only last N lines as leading context
|
||||
skipStart = Math.max(0, raw.length - CONTEXT_LINES);
|
||||
linesToShow = raw.slice(skipStart);
|
||||
}
|
||||
|
||||
if (!isLastPart && !nextPartIsChange && linesToShow.length > CONTEXT_LINES) {
|
||||
// Show only first N lines as trailing context
|
||||
skipEnd = linesToShow.length - CONTEXT_LINES;
|
||||
linesToShow = linesToShow.slice(0, CONTEXT_LINES);
|
||||
}
|
||||
|
||||
// Add ellipsis if we skipped lines at start
|
||||
if (skipStart > 0) {
|
||||
output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`));
|
||||
}
|
||||
|
||||
for (const line of linesToShow) {
|
||||
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.dim(`${lineNum} ${line}`));
|
||||
oldLineNum++;
|
||||
newLineNum++;
|
||||
}
|
||||
|
||||
// Add ellipsis if we skipped lines at end
|
||||
if (skipEnd > 0) {
|
||||
output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`));
|
||||
}
|
||||
|
||||
// Update line numbers for skipped lines
|
||||
oldLineNum += skipStart + skipEnd;
|
||||
newLineNum += skipStart + skipEnd;
|
||||
} else {
|
||||
// Skip these context lines entirely
|
||||
oldLineNum += raw.length;
|
||||
newLineNum += raw.length;
|
||||
}
|
||||
|
||||
lastWasChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a tool call with its result (updateable)
|
||||
*/
|
||||
|
|
@ -140,7 +41,7 @@ export class ToolExecutionComponent extends Container {
|
|||
this.args = args;
|
||||
this.addChild(new Spacer(1));
|
||||
// Content with colored background and padding
|
||||
this.contentText = new Text("", 1, 1, { r: 40, g: 40, b: 50 });
|
||||
this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.addChild(this.contentText);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
|
@ -165,13 +66,13 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
const bgColor = this.result
|
||||
const bgFn = this.result
|
||||
? this.result.isError
|
||||
? { r: 60, g: 40, b: 40 }
|
||||
: { r: 40, g: 50, b: 40 }
|
||||
: { r: 40, g: 40, b: 50 };
|
||||
? (text: string) => theme.bg("toolErrorBg", text)
|
||||
: (text: string) => theme.bg("toolSuccessBg", text)
|
||||
: (text: string) => theme.bg("toolPendingBg", text);
|
||||
|
||||
this.contentText.setCustomBgRgb(bgColor);
|
||||
this.contentText.setCustomBgFn(bgFn);
|
||||
this.contentText.setText(this.formatToolExecution());
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +101,7 @@ export class ToolExecutionComponent extends Container {
|
|||
// Format based on tool type
|
||||
if (this.toolName === "bash") {
|
||||
const command = this.args?.command || "";
|
||||
text = chalk.bold(`$ ${command || chalk.dim("...")}`);
|
||||
text = theme.fg("toolTitle", theme.bold(`$ ${command || theme.fg("toolOutput", "...")}`));
|
||||
|
||||
if (this.result) {
|
||||
// Show output without code fences - more minimal
|
||||
|
|
@ -211,9 +112,9 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(line)).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -223,13 +124,13 @@ export class ToolExecutionComponent extends Container {
|
|||
const limit = this.args?.limit;
|
||||
|
||||
// Build path display with offset/limit suffix
|
||||
let pathDisplay = path ? chalk.cyan(path) : chalk.dim("...");
|
||||
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
||||
if (offset !== undefined) {
|
||||
const endLine = limit !== undefined ? offset + limit : "";
|
||||
pathDisplay += chalk.dim(`:${offset}${endLine ? `-${endLine}` : ""}`);
|
||||
pathDisplay += theme.fg("toolOutput", `:${offset}${endLine ? `-${endLine}` : ""}`);
|
||||
}
|
||||
|
||||
text = chalk.bold("read") + " " + pathDisplay;
|
||||
text = theme.fg("toolTitle", theme.bold("read")) + " " + pathDisplay;
|
||||
|
||||
if (this.result) {
|
||||
const output = this.getTextOutput();
|
||||
|
|
@ -238,9 +139,9 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(replaceTabs(line))).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
} else if (this.toolName === "write") {
|
||||
|
|
@ -249,7 +150,10 @@ export class ToolExecutionComponent extends Container {
|
|||
const lines = fileContent ? fileContent.split("\n") : [];
|
||||
const totalLines = lines.length;
|
||||
|
||||
text = chalk.bold("write") + " " + (path ? chalk.cyan(path) : chalk.dim("..."));
|
||||
text =
|
||||
theme.fg("toolTitle", theme.bold("write")) +
|
||||
" " +
|
||||
(path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
|
||||
if (totalLines > 10) {
|
||||
text += ` (${totalLines} lines)`;
|
||||
}
|
||||
|
|
@ -260,32 +164,35 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(replaceTabs(line))).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
} else if (this.toolName === "edit") {
|
||||
const path = shortenPath(this.args?.file_path || this.args?.path || "");
|
||||
text = chalk.bold("edit") + " " + (path ? chalk.cyan(path) : chalk.dim("..."));
|
||||
text =
|
||||
theme.fg("toolTitle", theme.bold("edit")) +
|
||||
" " +
|
||||
(path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
|
||||
|
||||
if (this.result) {
|
||||
// Show error message if it's an error
|
||||
if (this.result.isError) {
|
||||
const errorText = this.getTextOutput();
|
||||
if (errorText) {
|
||||
text += "\n\n" + chalk.red(errorText);
|
||||
text += "\n\n" + theme.fg("error", errorText);
|
||||
}
|
||||
} else if (this.result.details?.diff) {
|
||||
// Show diff if available
|
||||
const diffLines = this.result.details.diff.split("\n");
|
||||
const coloredLines = diffLines.map((line: string) => {
|
||||
if (line.startsWith("+")) {
|
||||
return chalk.green(line);
|
||||
return theme.fg("toolDiffAdded", line);
|
||||
} else if (line.startsWith("-")) {
|
||||
return chalk.red(line);
|
||||
return theme.fg("toolDiffRemoved", line);
|
||||
} else {
|
||||
return chalk.dim(line);
|
||||
return theme.fg("toolDiffContext", line);
|
||||
}
|
||||
});
|
||||
text += "\n\n" + coloredLines.join("\n");
|
||||
|
|
@ -293,7 +200,7 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
} else {
|
||||
// Generic tool
|
||||
text = chalk.bold(this.toolName);
|
||||
text = theme.fg("toolTitle", theme.bold(this.toolName));
|
||||
|
||||
const content = JSON.stringify(this.args, null, 2);
|
||||
text += "\n\n" + content;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
TruncatedText,
|
||||
TUI,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { getChangelogPath, parseChangelog } from "../changelog.js";
|
||||
import { exportSessionToHtml } from "../export-html.js";
|
||||
|
|
@ -21,7 +21,7 @@ import { getApiKeyForModel, getAvailableModels } from "../model-config.js";
|
|||
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
import type { SettingsManager } from "../settings-manager.js";
|
||||
import { setTheme } from "../theme/theme.js";
|
||||
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||
import { CustomEditor } from "./custom-editor.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
|
@ -114,7 +114,7 @@ export class TuiRenderer {
|
|||
this.chatContainer = new Container();
|
||||
this.pendingMessagesContainer = new Container();
|
||||
this.statusContainer = new Container();
|
||||
this.editor = new CustomEditor();
|
||||
this.editor = new CustomEditor(getEditorTheme());
|
||||
this.editorContainer = new Container(); // Container to hold editor or selector
|
||||
this.editorContainer.addChild(this.editor); // Start with editor
|
||||
this.footer = new FooterComponent(agent.state);
|
||||
|
|
@ -193,34 +193,34 @@ export class TuiRenderer {
|
|||
if (this.isInitialized) return;
|
||||
|
||||
// Add header with logo and instructions
|
||||
const logo = chalk.bold.cyan("pi") + chalk.dim(` v${this.version}`);
|
||||
const logo = theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${this.version}`);
|
||||
const instructions =
|
||||
chalk.dim("esc") +
|
||||
chalk.gray(" to interrupt") +
|
||||
theme.fg("dim", "esc") +
|
||||
theme.fg("muted", " to interrupt") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+c") +
|
||||
chalk.gray(" to clear") +
|
||||
theme.fg("dim", "ctrl+c") +
|
||||
theme.fg("muted", " to clear") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+c twice") +
|
||||
chalk.gray(" to exit") +
|
||||
theme.fg("dim", "ctrl+c twice") +
|
||||
theme.fg("muted", " to exit") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+k") +
|
||||
chalk.gray(" to delete line") +
|
||||
theme.fg("dim", "ctrl+k") +
|
||||
theme.fg("muted", " to delete line") +
|
||||
"\n" +
|
||||
chalk.dim("shift+tab") +
|
||||
chalk.gray(" to cycle thinking") +
|
||||
theme.fg("dim", "shift+tab") +
|
||||
theme.fg("muted", " to cycle thinking") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+p") +
|
||||
chalk.gray(" to cycle models") +
|
||||
theme.fg("dim", "ctrl+p") +
|
||||
theme.fg("muted", " to cycle models") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+o") +
|
||||
chalk.gray(" to expand tools") +
|
||||
theme.fg("dim", "ctrl+o") +
|
||||
theme.fg("muted", " to expand tools") +
|
||||
"\n" +
|
||||
chalk.dim("/") +
|
||||
chalk.gray(" for commands") +
|
||||
theme.fg("dim", "/") +
|
||||
theme.fg("muted", " for commands") +
|
||||
"\n" +
|
||||
chalk.dim("drop files") +
|
||||
chalk.gray(" to attach");
|
||||
theme.fg("dim", "drop files") +
|
||||
theme.fg("muted", " to attach");
|
||||
const header = new Text(logo + "\n" + instructions, 1, 0);
|
||||
|
||||
// Setup UI layout
|
||||
|
|
@ -230,28 +230,28 @@ export class TuiRenderer {
|
|||
|
||||
// Add new version notification if available
|
||||
if (this.newVersion) {
|
||||
this.ui.addChild(new DynamicBorder(chalk.yellow));
|
||||
this.ui.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.addChild(
|
||||
new Text(
|
||||
chalk.bold.yellow("Update Available") +
|
||||
theme.bold(theme.fg("warning", "Update Available")) +
|
||||
"\n" +
|
||||
chalk.gray(`New version ${this.newVersion} is available. Run: `) +
|
||||
chalk.cyan("npm install -g @mariozechner/pi-coding-agent"),
|
||||
theme.fg("muted", `New version ${this.newVersion} is available. Run: `) +
|
||||
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
this.ui.addChild(new DynamicBorder(chalk.yellow));
|
||||
this.ui.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
}
|
||||
|
||||
// Add changelog if provided
|
||||
if (this.changelogMarkdown) {
|
||||
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0));
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0));
|
||||
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
this.ui.addChild(this.chatContainer);
|
||||
|
|
@ -435,6 +435,13 @@ export class TuiRenderer {
|
|||
// Start the UI
|
||||
this.ui.start();
|
||||
this.isInitialized = true;
|
||||
|
||||
// Set up theme file watcher for live reload
|
||||
onThemeChange(() => {
|
||||
this.ui.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
this.ui.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
||||
|
|
@ -454,7 +461,12 @@ export class TuiRenderer {
|
|||
this.loadingAnimation.stop();
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
this.loadingAnimation = new Loader(this.ui, "Working... (esc to interrupt)");
|
||||
this.loadingAnimation = new Loader(
|
||||
this.ui,
|
||||
(spinner) => theme.fg("accent", spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
"Working... (esc to interrupt)",
|
||||
);
|
||||
this.statusContainer.addChild(this.loadingAnimation);
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
|
|
@ -718,28 +730,9 @@ export class TuiRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
|
||||
// More thinking = more color (gray → dim colors → bright colors)
|
||||
switch (level) {
|
||||
case "off":
|
||||
return chalk.gray;
|
||||
case "minimal":
|
||||
return chalk.dim.blue;
|
||||
case "low":
|
||||
return chalk.blue;
|
||||
case "medium":
|
||||
return chalk.cyan;
|
||||
case "high":
|
||||
return chalk.magenta;
|
||||
default:
|
||||
return chalk.gray;
|
||||
}
|
||||
}
|
||||
|
||||
private updateEditorBorderColor(): void {
|
||||
const level = this.agent.state.thinkingLevel || "off";
|
||||
const color = this.getThinkingBorderColor(level);
|
||||
this.editor.borderColor = color;
|
||||
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -747,7 +740,7 @@ export class TuiRenderer {
|
|||
// Only cycle if model supports thinking
|
||||
if (!this.agent.state.model?.reasoning) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("Current model does not support thinking"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "Current model does not support thinking"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -769,7 +762,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show brief notification
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Thinking level: ${nextLevel}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${nextLevel}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -794,7 +787,7 @@ export class TuiRenderer {
|
|||
|
||||
if (modelsToUse.length === 1) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("Only one model in scope"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -824,7 +817,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show notification
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Switched to ${nextModel.name || nextModel.id}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -849,14 +842,14 @@ export class TuiRenderer {
|
|||
showError(errorMessage: string): void {
|
||||
// Show error message in the chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.red(`Error: ${errorMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showWarning(warningMessage: string): void {
|
||||
// Show warning message in the chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.yellow(`Warning: ${warningMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -876,7 +869,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -918,7 +911,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Queue mode: ${mode}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -956,15 +949,27 @@ export class TuiRenderer {
|
|||
currentTheme,
|
||||
(themeName) => {
|
||||
// Apply the selected theme
|
||||
setTheme(themeName);
|
||||
const result = setTheme(themeName);
|
||||
|
||||
// Save theme to settings
|
||||
this.settingsManager.setTheme(themeName);
|
||||
|
||||
// Show confirmation message with proper spacing
|
||||
// Invalidate all components to clear cached rendering
|
||||
this.ui.invalidate();
|
||||
|
||||
// Show confirmation or error message
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Theme: ${themeName}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
if (result.success) {
|
||||
const confirmText = new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
} else {
|
||||
const errorText = new Text(
|
||||
theme.fg("error", `Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`),
|
||||
1,
|
||||
0,
|
||||
);
|
||||
this.chatContainer.addChild(errorText);
|
||||
}
|
||||
|
||||
// Hide selector and show editor again
|
||||
this.hideThemeSelector();
|
||||
|
|
@ -975,6 +980,15 @@ export class TuiRenderer {
|
|||
this.hideThemeSelector();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
(themeName) => {
|
||||
// Preview theme on selection change
|
||||
const result = setTheme(themeName);
|
||||
if (result.success) {
|
||||
this.ui.invalidate();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
// If failed, theme already fell back to dark, just don't re-render
|
||||
},
|
||||
);
|
||||
|
||||
// Replace editor with selector
|
||||
|
|
@ -1007,7 +1021,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Model: ${model.id}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -1055,7 +1069,7 @@ export class TuiRenderer {
|
|||
// Don't show selector if there are no messages or only one message
|
||||
if (userMessages.length <= 1) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("No messages to branch from"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "No messages to branch from"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1088,7 +1102,7 @@ export class TuiRenderer {
|
|||
// Show confirmation message
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.dim(`Branched to new session from message ${messageIndex}`), 1, 0),
|
||||
new Text(theme.fg("dim", `Branched to new session from message ${messageIndex}`), 1, 0),
|
||||
);
|
||||
|
||||
// Put the selected message in the editor
|
||||
|
|
@ -1127,7 +1141,9 @@ export class TuiRenderer {
|
|||
const loggedInProviders = listOAuthProviders();
|
||||
if (loggedInProviders.length === 0) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("No OAuth providers logged in. Use /login first."), 1, 0));
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("dim", "No OAuth providers logged in. Use /login first."), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1144,7 +1160,7 @@ export class TuiRenderer {
|
|||
if (mode === "login") {
|
||||
// Handle login
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Logging in to ${providerId}...`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
|
||||
try {
|
||||
|
|
@ -1153,11 +1169,11 @@ export class TuiRenderer {
|
|||
(url: string) => {
|
||||
// Show auth URL to user
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.cyan("Opening browser to:"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(chalk.cyan(url), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.yellow("Paste the authorization code below:"), 1, 0),
|
||||
new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
|
||||
|
|
@ -1189,8 +1205,12 @@ export class TuiRenderer {
|
|||
|
||||
// Success
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.green(`✓ Successfully logged in to ${providerId}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Tokens saved to ~/.pi/agent/oauth.json`), 1, 0));
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0),
|
||||
);
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("dim", `Tokens saved to ~/.pi/agent/oauth.json`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
this.showError(`Login failed: ${error.message}`);
|
||||
|
|
@ -1202,10 +1222,10 @@ export class TuiRenderer {
|
|||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.green(`✓ Successfully logged out of ${providerId}`), 1, 0),
|
||||
new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),
|
||||
);
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.dim(`Credentials removed from ~/.pi/agent/oauth.json`), 1, 0),
|
||||
new Text(theme.fg("dim", `Credentials removed from ~/.pi/agent/oauth.json`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
|
|
@ -1246,13 +1266,13 @@ export class TuiRenderer {
|
|||
|
||||
// Show success message in chat - matching thinking level style
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Session exported to: ${filePath}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Session exported to: ${filePath}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
// Show error message in chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.red(`Failed to export session: ${error.message || "Unknown error"}`), 1, 0),
|
||||
new Text(theme.fg("error", `Failed to export session: ${error.message || "Unknown error"}`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -1299,29 +1319,29 @@ export class TuiRenderer {
|
|||
const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
|
||||
|
||||
// Build info text
|
||||
let info = `${chalk.bold("Session Info")}\n\n`;
|
||||
info += `${chalk.dim("File:")} ${sessionFile}\n`;
|
||||
info += `${chalk.dim("ID:")} ${this.sessionManager.getSessionId()}\n\n`;
|
||||
info += `${chalk.bold("Messages")}\n`;
|
||||
info += `${chalk.dim("User:")} ${userMessages}\n`;
|
||||
info += `${chalk.dim("Assistant:")} ${assistantMessages}\n`;
|
||||
info += `${chalk.dim("Tool Calls:")} ${toolCalls}\n`;
|
||||
info += `${chalk.dim("Tool Results:")} ${toolResults}\n`;
|
||||
info += `${chalk.dim("Total:")} ${totalMessages}\n\n`;
|
||||
info += `${chalk.bold("Tokens")}\n`;
|
||||
info += `${chalk.dim("Input:")} ${totalInput.toLocaleString()}\n`;
|
||||
info += `${chalk.dim("Output:")} ${totalOutput.toLocaleString()}\n`;
|
||||
let info = `${theme.bold("Session Info")}\n\n`;
|
||||
info += `${theme.fg("dim", "File:")} ${sessionFile}\n`;
|
||||
info += `${theme.fg("dim", "ID:")} ${this.sessionManager.getSessionId()}\n\n`;
|
||||
info += `${theme.bold("Messages")}\n`;
|
||||
info += `${theme.fg("dim", "User:")} ${userMessages}\n`;
|
||||
info += `${theme.fg("dim", "Assistant:")} ${assistantMessages}\n`;
|
||||
info += `${theme.fg("dim", "Tool Calls:")} ${toolCalls}\n`;
|
||||
info += `${theme.fg("dim", "Tool Results:")} ${toolResults}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalMessages}\n\n`;
|
||||
info += `${theme.bold("Tokens")}\n`;
|
||||
info += `${theme.fg("dim", "Input:")} ${totalInput.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Output:")} ${totalOutput.toLocaleString()}\n`;
|
||||
if (totalCacheRead > 0) {
|
||||
info += `${chalk.dim("Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
|
||||
}
|
||||
if (totalCacheWrite > 0) {
|
||||
info += `${chalk.dim("Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
|
||||
}
|
||||
info += `${chalk.dim("Total:")} ${totalTokens.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalTokens.toLocaleString()}\n`;
|
||||
|
||||
if (totalCost > 0) {
|
||||
info += `\n${chalk.bold("Cost")}\n`;
|
||||
info += `${chalk.dim("Total:")} ${totalCost.toFixed(4)}`;
|
||||
info += `\n${theme.bold("Cost")}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalCost.toFixed(4)}`;
|
||||
}
|
||||
|
||||
// Show info in chat
|
||||
|
|
@ -1345,11 +1365,11 @@ export class TuiRenderer {
|
|||
|
||||
// Display in chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1));
|
||||
this.chatContainer.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -1360,7 +1380,7 @@ export class TuiRenderer {
|
|||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||
|
||||
for (const message of this.queuedMessages) {
|
||||
const queuedText = chalk.dim("Queued: " + message);
|
||||
const queuedText = theme.fg("dim", "Queued: " + message);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
import { type Component, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
private colorFn: (text: string) => string;
|
||||
|
||||
constructor(colorFn: (text: string) => string = chalk.blue) {
|
||||
this.colorFn = colorFn;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
return [this.colorFn("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface UserMessageItem {
|
||||
index: number; // Index in the full messages array
|
||||
|
|
@ -39,11 +25,15 @@ class UserMessageList implements Component {
|
|||
this.selectedIndex = Math.max(0, messages.length - 1);
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
lines.push(chalk.gray(" No user messages found"));
|
||||
lines.push(theme.fg("muted", " No user messages found"));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -63,24 +53,24 @@ class UserMessageList implements Component {
|
|||
const normalizedMessage = message.text.replace(/\n/g, " ").trim();
|
||||
|
||||
// First line: cursor + message
|
||||
const cursor = isSelected ? chalk.blue("› ") : " ";
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const maxMsgWidth = width - 2; // Account for cursor
|
||||
const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);
|
||||
const messageLine = cursor + (isSelected ? chalk.bold(truncatedMsg) : truncatedMsg);
|
||||
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
||||
|
||||
lines.push(messageLine);
|
||||
|
||||
// Second line: metadata (position in history)
|
||||
const position = i + 1;
|
||||
const metadata = ` Message ${position} of ${this.messages.length}`;
|
||||
const metadataLine = chalk.dim(metadata);
|
||||
const metadataLine = theme.fg("muted", metadata);
|
||||
lines.push(metadataLine);
|
||||
lines.push(""); // Blank line between messages
|
||||
}
|
||||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.messages.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.messages.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.messages.length})`);
|
||||
lines.push(scrollInfo);
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +119,8 @@ export class UserMessageSelectorComponent extends Container {
|
|||
|
||||
// Add header
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(chalk.bold("Branch from Message"), 1, 0));
|
||||
this.addChild(new Text(chalk.dim("Select a message to create a new branch from that point"), 1, 0));
|
||||
this.addChild(new Text(theme.bold("Branch from Message"), 1, 0));
|
||||
this.addChild(new Text(theme.fg("muted", "Select a message to create a new branch from that point"), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Container, Markdown, Spacer } from "@mariozechner/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a user message
|
||||
|
|
@ -11,6 +12,11 @@ export class UserMessageComponent extends Container {
|
|||
if (!isFirst) {
|
||||
this.addChild(new Spacer(1));
|
||||
}
|
||||
this.addChild(new Markdown(text, 1, 1, { bgColor: "#343541" }));
|
||||
this.addChild(
|
||||
new Markdown(text, 1, 1, getMarkdownTheme(), {
|
||||
bgColor: (text: string) => theme.bg("userMessageBg", text),
|
||||
color: (text: string) => theme.fg("userMessageText", text),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
packages/coding-agent/test/test-theme-colors.ts
Normal file
75
packages/coding-agent/test/test-theme-colors.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { initTheme, theme } from "../src/theme/theme.js";
|
||||
|
||||
// Initialize with dark theme explicitly
|
||||
process.env.COLORTERM = "truecolor";
|
||||
initTheme("dark");
|
||||
|
||||
console.log("\n=== Foreground Colors ===\n");
|
||||
|
||||
// Core UI colors
|
||||
console.log("accent:", theme.fg("accent", "Sample text"));
|
||||
console.log("border:", theme.fg("border", "Sample text"));
|
||||
console.log("borderAccent:", theme.fg("borderAccent", "Sample text"));
|
||||
console.log("borderMuted:", theme.fg("borderMuted", "Sample text"));
|
||||
console.log("success:", theme.fg("success", "Sample text"));
|
||||
console.log("error:", theme.fg("error", "Sample text"));
|
||||
console.log("warning:", theme.fg("warning", "Sample text"));
|
||||
console.log("muted:", theme.fg("muted", "Sample text"));
|
||||
console.log("dim:", theme.fg("dim", "Sample text"));
|
||||
console.log("text:", theme.fg("text", "Sample text"));
|
||||
|
||||
console.log("\n=== Message Text Colors ===\n");
|
||||
console.log("userMessageText:", theme.fg("userMessageText", "Sample text"));
|
||||
console.log("toolTitle:", theme.fg("toolTitle", "Sample text"));
|
||||
console.log("toolOutput:", theme.fg("toolOutput", "Sample text"));
|
||||
|
||||
console.log("\n=== Markdown Colors ===\n");
|
||||
console.log("mdHeading:", theme.fg("mdHeading", "Sample text"));
|
||||
console.log("mdLink:", theme.fg("mdLink", "Sample text"));
|
||||
console.log("mdCode:", theme.fg("mdCode", "Sample text"));
|
||||
console.log("mdCodeBlock:", theme.fg("mdCodeBlock", "Sample text"));
|
||||
console.log("mdCodeBlockBorder:", theme.fg("mdCodeBlockBorder", "Sample text"));
|
||||
console.log("mdQuote:", theme.fg("mdQuote", "Sample text"));
|
||||
console.log("mdQuoteBorder:", theme.fg("mdQuoteBorder", "Sample text"));
|
||||
console.log("mdHr:", theme.fg("mdHr", "Sample text"));
|
||||
console.log("mdListBullet:", theme.fg("mdListBullet", "Sample text"));
|
||||
|
||||
console.log("\n=== Tool Diff Colors ===\n");
|
||||
console.log("toolDiffAdded:", theme.fg("toolDiffAdded", "Sample text"));
|
||||
console.log("toolDiffRemoved:", theme.fg("toolDiffRemoved", "Sample text"));
|
||||
console.log("toolDiffContext:", theme.fg("toolDiffContext", "Sample text"));
|
||||
|
||||
console.log("\n=== Thinking Border Colors ===\n");
|
||||
console.log("thinkingOff:", theme.fg("thinkingOff", "Sample text"));
|
||||
console.log("thinkingMinimal:", theme.fg("thinkingMinimal", "Sample text"));
|
||||
console.log("thinkingLow:", theme.fg("thinkingLow", "Sample text"));
|
||||
console.log("thinkingMedium:", theme.fg("thinkingMedium", "Sample text"));
|
||||
console.log("thinkingHigh:", theme.fg("thinkingHigh", "Sample text"));
|
||||
|
||||
console.log("\n=== Background Colors ===\n");
|
||||
console.log("userMessageBg:", theme.bg("userMessageBg", " Sample background text "));
|
||||
console.log("toolPendingBg:", theme.bg("toolPendingBg", " Sample background text "));
|
||||
console.log("toolSuccessBg:", theme.bg("toolSuccessBg", " Sample background text "));
|
||||
console.log("toolErrorBg:", theme.bg("toolErrorBg", " Sample background text "));
|
||||
|
||||
console.log("\n=== Raw ANSI Codes ===\n");
|
||||
console.log("thinkingMedium ANSI:", JSON.stringify(theme.getFgAnsi("thinkingMedium")));
|
||||
console.log("accent ANSI:", JSON.stringify(theme.getFgAnsi("accent")));
|
||||
console.log("muted ANSI:", JSON.stringify(theme.getFgAnsi("muted")));
|
||||
console.log("dim ANSI:", JSON.stringify(theme.getFgAnsi("dim")));
|
||||
|
||||
console.log("\n=== Direct RGB Test ===\n");
|
||||
console.log("Gray #6c6c6c: \x1b[38;2;108;108;108mSample text\x1b[0m");
|
||||
console.log("Gray #444444: \x1b[38;2;68;68;68mSample text\x1b[0m");
|
||||
console.log("Gray #303030: \x1b[38;2;48;48;48mSample text\x1b[0m");
|
||||
|
||||
console.log("\n=== Hex Color Test ===\n");
|
||||
console.log("Direct #00d7ff test: \x1b[38;2;0;215;255mBRIGHT CYAN\x1b[0m");
|
||||
console.log("Theme cyan (should match above):", theme.fg("accent", "BRIGHT CYAN"));
|
||||
|
||||
console.log("\n=== Environment ===\n");
|
||||
console.log("TERM:", process.env.TERM);
|
||||
console.log("COLORTERM:", process.env.COLORTERM);
|
||||
console.log("Color mode:", theme.getColorMode());
|
||||
|
||||
console.log("\n");
|
||||
Loading…
Add table
Add a link
Reference in a new issue