spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/tui/theme.ts4 * Description: The signature "khaelis" theme (obsidian + magma) + light variant — capability ladder, NO_COLOR degradation, never color-alone (TUI v2 §1).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { Background, ColorDepth } from "./renderer/capabilities.js";11import { mixOklch } from "./theme/gradient.js";1213/**14 * Style roles (TUI_DESIGN §12). Color reinforces state; it never carries it15 * alone — every state pairs a symbol or word, so monochrome (`NO_COLOR`,16 * `TERM=dumb`, non-TTY) simply renders roles as identity.17 */18export type StyleRole =19 | "text"20 | "dim"21 | "accent"22 | "accentDim" // the 2 Hz status-dot alternate shade — the only animation23 | "success"24 | "warning"25 | "error"26 | "code"27 | "violet" // thinking state, model segment, brand gradient start28 | "cyan" // reading state, branch segment, brand gradient end29 | "magenta" // verifying state30 | "teal" // process tools, context segment31 | "orange" // cost segment, waiting state32 | "bold"33 | "italic"34 | "strike"35 | "underline"36 | "invert"37 | "synKeyword"38 | "synString"39 | "synComment"40 | "synNumber"41 | "synFunction"42 | "synType";4344/** Named per-character truecolor gradients (fallback: solid color → identity). */45export type GradientName = "brand" | "ember";4647/** Subtle full-line background tints (truecolor only; identity elsewhere). */48export type LineTint = "add" | "del";4950export interface Theme {51 name: string;52 colorDepth: ColorDepth;53 background: Background;54 /** Style `s` for `role`. Monochrome themes return `s` unchanged. */55 paint(role: StyleRole, s: string): string;56 /**57 * Per-character truecolor gradient across the visible characters of a plain58 * (unstyled) string. ANSI-256/16 degrade to a solid brand color; monochrome59 * is identity — hierarchy never depends on the gradient (TUI_DESIGN §12).60 */61 paintGradient(name: GradientName, s: string): string;62 /** Subtle diff-line background tint. Truecolor only; identity elsewhere. */63 tintLine(kind: LineTint, s: string): string;64}6566type Rgb = readonly [number, number, number];6768interface Palette {69 text: Rgb;70 dim: Rgb;71 accent: Rgb;72 accentDim: Rgb;73 success: Rgb;74 warning: Rgb;75 error: Rgb;76 code: Rgb;77 violet: Rgb;78 cyan: Rgb;79 magenta: Rgb;80 teal: Rgb;81 orange: Rgb;82 synKeyword: Rgb;83 synString: Rgb;84 synComment: Rgb;85 synNumber: Rgb;86 synFunction: Rgb;87 synType: Rgb;88}8990/**91 * khaelis — the signature dark palette: obsidian + magma (TUI v2 §1.1).92 * accent = ember #FF6B35, orange = ember-glow #FFB86B, teal = #2DD4BF.93 * Not another generic violet-cyan CLI theme — the warm accent "flows"94 * through phase ribbon, prompts, and gradients.95 */96const DARK: Palette = {97 text: [201, 209, 227], // #c9d1e398 dim: [91, 101, 122], // #5b657a99 accent: [255, 107, 53], // #ff6b35 — ember (magma)100 accentDim: [179, 73, 31], // ember, banked101 success: [126, 231, 135], // #7ee787102 warning: [240, 180, 41], // #f0b429103 error: [255, 92, 87], // #ff5c57104 code: [154, 165, 206],105 violet: [187, 154, 247], // #bb9af7106 cyan: [125, 207, 255], // #7dcfff107 magenta: [255, 121, 198], // #ff79c6108 teal: [45, 212, 191], // #2dd4bf — cool counterpoint to the ember109 orange: [255, 184, 107], // #ffb86b — ember-glow110 synKeyword: [187, 154, 247], // violet111 synString: [126, 231, 135], // green112 synComment: [91, 101, 122], // dim113 synNumber: [255, 184, 107], // ember-glow114 synFunction: [125, 207, 255], // cyan115 synType: [45, 212, 191], // teal116};117118/** khaelor-light — same hue system, adjusted for light backgrounds. */119const LIGHT: Palette = {120 text: [46, 52, 64],121 dim: [122, 130, 144],122 accent: [46, 89, 168],123 accentDim: [130, 152, 199],124 success: [77, 124, 15],125 warning: [161, 98, 7],126 error: [190, 24, 60],127 code: [88, 96, 128],128 violet: [124, 58, 237], // #7c3aed129 cyan: [14, 116, 144], // #0e7490130 magenta: [190, 24, 93], // #be185d131 teal: [15, 118, 110], // #0f766e132 orange: [194, 65, 12], // #c2410c133 synKeyword: [124, 58, 237],134 synString: [77, 124, 15],135 synComment: [122, 130, 144],136 synNumber: [194, 65, 12],137 synFunction: [14, 116, 144],138 synType: [4, 120, 87], // #047857139};140141/** ANSI-256 quantization of the two palettes (precomputed, no math at paint time). */142const DARK_256: Record<keyof Palette, number> = {143 text: 189,144 dim: 60,145 accent: 202, // ember146 accentDim: 130,147 success: 114,148 warning: 178,149 error: 203,150 code: 146,151 violet: 141,152 cyan: 117,153 magenta: 212,154 teal: 43,155 orange: 215, // ember-glow156 synKeyword: 141,157 synString: 114,158 synComment: 60,159 synNumber: 215,160 synFunction: 117,161 synType: 43,162};163164const LIGHT_256: Record<keyof Palette, number> = {165 text: 237,166 dim: 245,167 accent: 25,168 accentDim: 110,169 success: 64,170 warning: 130,171 error: 161,172 code: 60,173 violet: 91,174 cyan: 30,175 magenta: 162,176 teal: 29,177 orange: 166,178 synKeyword: 91,179 synString: 64,180 synComment: 245,181 synNumber: 166,182 synFunction: 30,183 synType: 29,184};185186/** ANSI-16 role mapping (foreground SGR codes). */187const ANSI16: Record<keyof Palette, number> = {188 text: 39, // default foreground189 dim: 90,190 accent: 94,191 accentDim: 34,192 success: 32,193 warning: 33,194 error: 31,195 code: 36,196 violet: 95,197 cyan: 96,198 magenta: 35,199 teal: 36,200 orange: 33,201 synKeyword: 95,202 synString: 32,203 synComment: 90,204 synNumber: 33,205 synFunction: 96,206 synType: 36,207};208209/** Gradient endpoints per named gradient (roles resolved against the palette). */210const GRADIENTS: Record<GradientName, [keyof Palette, keyof Palette]> = {211 brand: ["violet", "cyan"],212 // The signature: ember → ember-glow, flowing across wordmark, phase ribbon,213 // active panel borders, and the context gauge (TUI v2 §1.3).214 ember: ["accent", "orange"],215};216217/** Diff-line background tints — subtle, truecolor only. */218const TINTS_DARK: Record<LineTint, Rgb> = { add: [26, 42, 32], del: [46, 26, 32] };219const TINTS_LIGHT: Record<LineTint, Rgb> = { add: [230, 246, 232], del: [250, 228, 232] };220221const FORMAT_OPEN: Partial<Record<StyleRole, string>> = {222 bold: "\x1b[1m",223 italic: "\x1b[3m",224 strike: "\x1b[9m",225 underline: "\x1b[4m",226 invert: "\x1b[7m",227};228229const FORMAT_CLOSE: Partial<Record<StyleRole, string>> = {230 bold: "\x1b[22m",231 italic: "\x1b[23m",232 strike: "\x1b[29m",233 underline: "\x1b[24m",234 invert: "\x1b[27m",235};236237const CLOSE_FG = "\x1b[39m";238239function isColorRole(role: StyleRole): role is keyof Palette {240 return !(role in FORMAT_OPEN);241}242243export interface ResolveThemeOptions {244 colorDepth: ColorDepth;245 background?: Background;246}247248/**249 * Resolve the theme for the detected capabilities. One exceptional default250 * (dark), a light variant when the background is detectably light, and full251 * monochrome degradation — `paint` is then the identity function, emitting252 * zero SGR codes (TUI_DESIGN §12).253 */254export function resolveTheme(options: ResolveThemeOptions): Theme {255 const background: Background = options.background ?? "dark";256 const depth = options.colorDepth;257 const name = background === "dark" ? "khaelis" : "khaelor-light";258259 if (depth === "mono") {260 return {261 name: `${name} (mono)`,262 colorDepth: depth,263 background,264 paint: (_role, s) => s,265 paintGradient: (_name, s) => s,266 tintLine: (_kind, s) => s,267 };268 }269270 const palette = background === "dark" ? DARK : LIGHT;271 const palette256 = background === "dark" ? DARK_256 : LIGHT_256;272 const tints = background === "dark" ? TINTS_DARK : TINTS_LIGHT;273274 const open = (role: keyof Palette): string => {275 if (depth === "truecolor") {276 const [r, g, b] = palette[role];277 return `\x1b[38;2;${r};${g};${b}m`;278 }279 if (depth === "ansi256") return `\x1b[38;5;${palette256[role]}m`;280 const code = ANSI16[role];281 return code === 39 ? "" : `\x1b[${code}m`;282 };283284 return {285 name,286 colorDepth: depth,287 background,288 paint(role: StyleRole, s: string): string {289 if (s === "") return s;290 if (isColorRole(role)) {291 const o = open(role);292 return o === "" ? s : o + s + CLOSE_FG;293 }294 return (FORMAT_OPEN[role] as string) + s + (FORMAT_CLOSE[role] as string);295 },296 paintGradient(gradient: GradientName, s: string): string {297 if (s === "") return s;298 const [fromRole, toRole] = GRADIENTS[gradient];299 if (depth !== "truecolor") {300 // Solid brand color — the gradient is truecolor-only polish.301 const o = open(fromRole);302 return o === "" ? s : o + s + CLOSE_FG;303 }304 const from = palette[fromRole];305 const to = palette[toRole];306 const chars = [...s];307 const steps = Math.max(1, chars.length - 1);308 let out = "";309 for (let i = 0; i < chars.length; i++) {310 // OKLCH interpolation — perceptually smooth, no muddy midpoints (TUI v2 §1.3).311 const [r, g, b] = mixOklch(from, to, i / steps);312 out += `\x1b[38;2;${r};${g};${b}m${chars[i] as string}`;313 }314 return out + CLOSE_FG;315 },316 tintLine(kind: LineTint, s: string): string {317 if (s === "" || depth !== "truecolor") return s;318 const [r, g, b] = tints[kind];319 return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;320 },321 };322}323