Release v0.8.0

This commit is contained in:
Mario Zechner 2025-11-21 03:12:42 +01:00
parent cc88095140
commit 85adcf22bf
48 changed files with 1530 additions and 608 deletions

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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)"

View file

@ -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(),
};
}