{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/theme/theme.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n\tapplyBackgroundToLine,\n\ttype Box,\n\ttype EditorTheme,\n\ttype Input,\n\ttype MarkdownTheme,\n\ttype SelectListTheme,\n\ttype SettingsListTheme,\n\tvisibleWidth,\n} from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { highlight, supportsLanguage } from \"cli-highlight\";\nimport { type Static, Type } from \"typebox\";\nimport { Compile } from \"typebox/compile\";\nimport { getCustomThemesDir, getThemesDir } from \"../../../config.js\";\nimport type { SourceInfo } from \"../../../core/source-info.js\";\nimport { closeWatcher, watchWithErrorHandler } from \"../../../utils/fs-watch.js\";\n\n// ============================================================================\n// Types & Schema\n// ============================================================================\n\nconst ColorValueSchema = Type.Union([\n\tType.String(), // hex \"#ff0000\", var ref \"primary\", or empty \"\"\n\tType.Integer({ minimum: 0, maximum: 255 }), // 256-color index\n]);\n\ntype ColorValue = Static<typeof ColorValueSchema>;\n\nconst ThemeJsonSchema = Type.Object({\n\t$schema: Type.Optional(Type.String()),\n\tname: Type.String(),\n\tdescription: Type.Optional(Type.String()),\n\tvars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),\n\tcolors: Type.Object({\n\t\t// Core UI (10 colors)\n\t\taccent: ColorValueSchema,\n\t\tborder: ColorValueSchema,\n\t\tborderAccent: ColorValueSchema,\n\t\tborderMuted: ColorValueSchema,\n\t\tsuccess: ColorValueSchema,\n\t\terror: ColorValueSchema,\n\t\twarning: ColorValueSchema,\n\t\tmuted: ColorValueSchema,\n\t\tdim: ColorValueSchema,\n\t\ttext: ColorValueSchema,\n\t\tthinkingText: ColorValueSchema,\n\t\t// Backgrounds & Content Text (11 colors)\n\t\tselectedBg: ColorValueSchema,\n\t\tuserMessageBg: ColorValueSchema,\n\t\tuserMessageText: ColorValueSchema,\n\t\tcustomMessageBg: ColorValueSchema,\n\t\tcustomMessageText: ColorValueSchema,\n\t\tcustomMessageLabel: ColorValueSchema,\n\t\ttoolPendingBg: ColorValueSchema,\n\t\ttoolSuccessBg: ColorValueSchema,\n\t\ttoolErrorBg: ColorValueSchema,\n\t\t// OPTIONAL so existing custom themes keep validating; missing entries fall\n\t\t// back to customMessageBg (see createTheme).\n\t\twarningBg: Type.Optional(ColorValueSchema),\n\t\ttoolTitle: ColorValueSchema,\n\t\ttoolOutput: ColorValueSchema,\n\t\t// Markdown (10 colors)\n\t\tmdHeading: ColorValueSchema,\n\t\tmdLink: ColorValueSchema,\n\t\tmdLinkUrl: ColorValueSchema,\n\t\tmdCode: ColorValueSchema,\n\t\tmdCodeBlock: ColorValueSchema,\n\t\tmdCodeBlockBorder: ColorValueSchema,\n\t\tmdQuote: ColorValueSchema,\n\t\tmdQuoteBorder: ColorValueSchema,\n\t\tmdHr: ColorValueSchema,\n\t\tmdListBullet: ColorValueSchema,\n\t\t// Tool Diffs (3 colors)\n\t\ttoolDiffAdded: ColorValueSchema,\n\t\ttoolDiffRemoved: ColorValueSchema,\n\t\ttoolDiffContext: ColorValueSchema,\n\t\t// Syntax Highlighting (9 colors)\n\t\tsyntaxComment: ColorValueSchema,\n\t\tsyntaxKeyword: ColorValueSchema,\n\t\tsyntaxFunction: ColorValueSchema,\n\t\tsyntaxVariable: ColorValueSchema,\n\t\tsyntaxString: ColorValueSchema,\n\t\tsyntaxNumber: ColorValueSchema,\n\t\tsyntaxType: ColorValueSchema,\n\t\tsyntaxOperator: ColorValueSchema,\n\t\tsyntaxPunctuation: ColorValueSchema,\n\t\t// Thinking Level Borders (6 colors)\n\t\tthinkingOff: ColorValueSchema,\n\t\tthinkingMinimal: ColorValueSchema,\n\t\tthinkingLow: ColorValueSchema,\n\t\tthinkingMedium: ColorValueSchema,\n\t\tthinkingHigh: ColorValueSchema,\n\t\tthinkingXhigh: ColorValueSchema,\n\t\t// Bash Mode (1 color)\n\t\tbashMode: ColorValueSchema,\n\t\t// Agent identity palette (6 colors). OPTIONAL so existing custom themes\n\t\t// keep validating: any missing entry falls back to `accent` (see\n\t\t// createTheme). Subagent types hash into this palette so the same agent\n\t\t// reads as the same color across the chat, task panel, and TaskOutput.\n\t\tagent1: Type.Optional(ColorValueSchema),\n\t\tagent2: Type.Optional(ColorValueSchema),\n\t\tagent3: Type.Optional(ColorValueSchema),\n\t\tagent4: Type.Optional(ColorValueSchema),\n\t\tagent5: Type.Optional(ColorValueSchema),\n\t\tagent6: Type.Optional(ColorValueSchema),\n\t\t// Brand chip (2 colors). OPTIONAL, and only honoured as a pair: a theme\n\t\t// that sets both paints the footer's brand mark as a filled chip instead\n\t\t// of accent-coloured text. Light themes need this — a brand hue bright\n\t\t// enough to read as a chip is rarely legible as text on a light canvas.\n\t\tbrandBg: Type.Optional(ColorValueSchema),\n\t\tbrandText: Type.Optional(ColorValueSchema),\n\t\t// Cut-out chrome (6 colors). All OPTIONAL, all with a defined fallback,\n\t\t// so a theme that sets none of them renders exactly as it does today.\n\t\t//\n\t\t// paperShadow: the offset band drawn under a filled message block, so the\n\t\t// block reads as a sheet laid on the page rather than printed into it. A\n\t\t// terminal has no sub-pixel offsets, so this is one extra row of `▔` — an\n\t\t// upper one-eighth block, a hairline along the top of its cells. Unset: no\n\t\t// shadow row is drawn.\n\t\tpaperShadow: Type.Optional(ColorValueSchema),\n\t\t// halftone: the *unfilled* remainder of a gauge or progress track. It has\n\t\t// to sit apart from `dim`, which is body-weight text — a track that reads\n\t\t// as text competes with the fill it is supposed to recede behind. Unset:\n\t\t// falls back to `dim`, which is what the tracks used before.\n\t\thalftone: Type.Optional(ColorValueSchema),\n\t\t// Headline chip (2 colors), honoured only as a pair. Markdown headings\n\t\t// render as a filled bar instead of coloured text. This is how a light\n\t\t// theme keeps a vivid brand hue: inside a chip a color can be far\n\t\t// brighter than it could ever be as text on the page. Unset: headings\n\t\t// stay `mdHeading`-coloured text.\n\t\theadlineBg: Type.Optional(ColorValueSchema),\n\t\theadlineText: Type.Optional(ColorValueSchema),\n\t\t// Tape chip (2 colors), honoured only as a pair. A label laid *across* a\n\t\t// block rather than set inside it — the extension tag, a tool's name in\n\t\t// radar. Separate from brandBg/brandText so taping a label does not\n\t\t// restyle the footer's brand mark. Unset: the label keeps its own color.\n\t\ttapeBg: Type.Optional(ColorValueSchema),\n\t\ttapeText: Type.Optional(ColorValueSchema),\n\t\t// activeToolBg: the marker stroke over the most recent tool row in radar.\n\t\t// Radar is a map of a run, and a map is most useful when it says where you\n\t\t// are on it — the stroke walks down the screen as the agent works, then\n\t\t// stays on the last thing it did. Unset: no stroke, which is how radar\n\t\t// rendered before. Kept separate from `selectedBg` so a theme can make the\n\t\t// transcript's mark quieter than its list selection.\n\t\tactiveToolBg: Type.Optional(ColorValueSchema),\n\t}),\n\texport: Type.Optional(\n\t\tType.Object({\n\t\t\tpageBg: Type.Optional(ColorValueSchema),\n\t\t\tcardBg: Type.Optional(ColorValueSchema),\n\t\t\tinfoBg: Type.Optional(ColorValueSchema),\n\t\t}),\n\t),\n});\n\ntype ThemeJson = Static<typeof ThemeJsonSchema>;\n\nconst validateThemeJson = Compile(ThemeJsonSchema);\n\nexport type ThemeColor =\n\t| \"accent\"\n\t| \"border\"\n\t| \"borderAccent\"\n\t| \"borderMuted\"\n\t| \"success\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"muted\"\n\t| \"dim\"\n\t| \"text\"\n\t| \"thinkingText\"\n\t| \"userMessageText\"\n\t| \"customMessageText\"\n\t| \"customMessageLabel\"\n\t| \"toolTitle\"\n\t| \"toolOutput\"\n\t| \"mdHeading\"\n\t| \"mdLink\"\n\t| \"mdLinkUrl\"\n\t| \"mdCode\"\n\t| \"mdCodeBlock\"\n\t| \"mdCodeBlockBorder\"\n\t| \"mdQuote\"\n\t| \"mdQuoteBorder\"\n\t| \"mdHr\"\n\t| \"mdListBullet\"\n\t| \"toolDiffAdded\"\n\t| \"toolDiffRemoved\"\n\t| \"toolDiffContext\"\n\t| \"syntaxComment\"\n\t| \"syntaxKeyword\"\n\t| \"syntaxFunction\"\n\t| \"syntaxVariable\"\n\t| \"syntaxString\"\n\t| \"syntaxNumber\"\n\t| \"syntaxType\"\n\t| \"syntaxOperator\"\n\t| \"syntaxPunctuation\"\n\t| \"thinkingOff\"\n\t| \"thinkingMinimal\"\n\t| \"thinkingLow\"\n\t| \"thinkingMedium\"\n\t| \"thinkingHigh\"\n\t| \"thinkingXhigh\"\n\t| \"bashMode\"\n\t| \"agent1\"\n\t| \"agent2\"\n\t| \"agent3\"\n\t| \"agent4\"\n\t| \"agent5\"\n\t| \"agent6\"\n\t| \"mcp\"\n\t| \"brandText\"\n\t| \"paperShadow\"\n\t| \"halftone\"\n\t| \"headlineText\"\n\t| \"tapeText\";\n\n/** The agent identity palette, in hash order. */\nexport const AGENT_COLOR_TOKENS = [\n\t\"agent1\",\n\t\"agent2\",\n\t\"agent3\",\n\t\"agent4\",\n\t\"agent5\",\n\t\"agent6\",\n] as const satisfies readonly ThemeColor[];\n\n/**\n * Stable color token for an agent type (\"explore\", \"plan\", a custom agent…).\n * The name hashes into the agent palette, so the same type renders in the same\n * color everywhere it appears — chat tool line, task panel rows/roster, and\n * TaskOutput — for the whole session and across sessions. Distinct types spread\n * over 6 hues; collisions are possible but the `[name]`/`#N` labels stay the\n * primary identifier, color is the at-a-glance cue.\n */\nexport function agentColorFor(agentType: string): ThemeColor {\n\t// djb2: tiny, stable, good spread for short identifier-like strings.\n\tlet hash = 5381;\n\tfor (let i = 0; i < agentType.length; i++) {\n\t\thash = ((hash << 5) + hash + agentType.charCodeAt(i)) >>> 0;\n\t}\n\treturn AGENT_COLOR_TOKENS[hash % AGENT_COLOR_TOKENS.length] as ThemeColor;\n}\n\n/**\n * The colour token for a session's slot (1-6). Sessions share the agent identity\n * palette: a session chip and an agent tag never appear on the same surface, so a\n * collision costs nothing but a coincidence, and reusing the tokens keeps every\n * theme file untouched. A dedicated `session1…6` palette can be layered on later\n * without breaking anything, since this is the only place the mapping lives.\n */\nexport function sessionColorToken(slot: number): ThemeColor {\n\tconst index = Number.isInteger(slot) ? slot - 1 : 0;\n\tconst wrapped = ((index % AGENT_COLOR_TOKENS.length) + AGENT_COLOR_TOKENS.length) % AGENT_COLOR_TOKENS.length;\n\treturn AGENT_COLOR_TOKENS[wrapped] as ThemeColor;\n}\n\nexport type ThemeBg =\n\t| \"selectedBg\"\n\t| \"userMessageBg\"\n\t| \"customMessageBg\"\n\t| \"toolPendingBg\"\n\t| \"toolSuccessBg\"\n\t| \"toolErrorBg\"\n\t| \"warningBg\"\n\t| \"brandBg\"\n\t| \"headlineBg\"\n\t| \"tapeBg\"\n\t| \"activeToolBg\";\n\ntype ColorMode = \"truecolor\" | \"256color\";\n\n// ============================================================================\n// Color Utilities\n// ============================================================================\n\nfunction detectColorMode(): ColorMode {\n\tconst colorterm = process.env.COLORTERM;\n\tif (colorterm === \"truecolor\" || colorterm === \"24bit\") {\n\t\treturn \"truecolor\";\n\t}\n\t// Windows Terminal supports truecolor\n\tif (process.env.WT_SESSION) {\n\t\treturn \"truecolor\";\n\t}\n\tconst term = process.env.TERM || \"\";\n\t// Fall back to 256color for truly limited terminals\n\tif (term === \"dumb\" || term === \"\" || term === \"linux\") {\n\t\treturn \"256color\";\n\t}\n\t// Terminal.app also doesn't support truecolor\n\tif (process.env.TERM_PROGRAM === \"Apple_Terminal\") {\n\t\treturn \"256color\";\n\t}\n\t// GNU screen doesn't support truecolor unless explicitly opted in via COLORTERM=truecolor.\n\t// TERM under screen is typically \"screen\", \"screen-256color\", or \"screen.xterm-256color\".\n\tif (term === \"screen\" || term.startsWith(\"screen-\") || term.startsWith(\"screen.\")) {\n\t\treturn \"256color\";\n\t}\n\t// Assume truecolor for everything else - virtually all modern terminals support it\n\treturn \"truecolor\";\n}\n\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } {\n\tconst cleaned = hex.replace(\"#\", \"\");\n\tif (cleaned.length !== 6) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\tconst r = parseInt(cleaned.substring(0, 2), 16);\n\tconst g = parseInt(cleaned.substring(2, 4), 16);\n\tconst b = parseInt(cleaned.substring(4, 6), 16);\n\tif (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {\n\t\tthrow new Error(`Invalid hex color: ${hex}`);\n\t}\n\treturn { r, g, b };\n}\n\n// The 6x6x6 color cube channel values (indices 0-5)\nconst CUBE_VALUES = [0, 95, 135, 175, 215, 255];\n\n// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238)\nconst GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10);\n\nfunction findClosestCubeIndex(value: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < CUBE_VALUES.length; i++) {\n\t\tconst dist = Math.abs(value - CUBE_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction findClosestGrayIndex(gray: number): number {\n\tlet minDist = Infinity;\n\tlet minIdx = 0;\n\tfor (let i = 0; i < GRAY_VALUES.length; i++) {\n\t\tconst dist = Math.abs(gray - GRAY_VALUES[i]);\n\t\tif (dist < minDist) {\n\t\t\tminDist = dist;\n\t\t\tminIdx = i;\n\t\t}\n\t}\n\treturn minIdx;\n}\n\nfunction colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {\n\t// Weighted Euclidean distance (human eye is more sensitive to green)\n\tconst dr = r1 - r2;\n\tconst dg = g1 - g2;\n\tconst db = b1 - b2;\n\treturn dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;\n}\n\nfunction rgbTo256(r: number, g: number, b: number): number {\n\t// Find closest color in the 6x6x6 cube\n\tconst rIdx = findClosestCubeIndex(r);\n\tconst gIdx = findClosestCubeIndex(g);\n\tconst bIdx = findClosestCubeIndex(b);\n\tconst cubeR = CUBE_VALUES[rIdx];\n\tconst cubeG = CUBE_VALUES[gIdx];\n\tconst cubeB = CUBE_VALUES[bIdx];\n\tconst cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;\n\tconst cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB);\n\n\t// Find closest grayscale\n\tconst gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);\n\tconst grayIdx = findClosestGrayIndex(gray);\n\tconst grayValue = GRAY_VALUES[grayIdx];\n\tconst grayIndex = 232 + grayIdx;\n\tconst grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue);\n\n\t// Check if color has noticeable saturation (hue matters)\n\t// If max-min spread is significant, prefer cube to preserve tint\n\tconst maxC = Math.max(r, g, b);\n\tconst minC = Math.min(r, g, b);\n\tconst spread = maxC - minC;\n\n\t// Only consider grayscale if color is nearly neutral (spread < 10)\n\t// AND grayscale is actually closer\n\tif (spread < 10 && grayDist < cubeDist) {\n\t\treturn grayIndex;\n\t}\n\n\treturn cubeIndex;\n}\n\nfunction hexTo256(hex: string): number {\n\tconst { r, g, b } = hexToRgb(hex);\n\treturn rgbTo256(r, g, b);\n}\n\nfunction fgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[39m\";\n\tif (typeof color === \"number\") return `\\x1b[38;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[38;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[38;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\nfunction bgAnsi(color: string | number, mode: ColorMode): string {\n\tif (color === \"\") return \"\\x1b[49m\";\n\tif (typeof color === \"number\") return `\\x1b[48;5;${color}m`;\n\tif (color.startsWith(\"#\")) {\n\t\tif (mode === \"truecolor\") {\n\t\t\tconst { r, g, b } = hexToRgb(color);\n\t\t\treturn `\\x1b[48;2;${r};${g};${b}m`;\n\t\t} else {\n\t\t\tconst index = hexTo256(color);\n\t\t\treturn `\\x1b[48;5;${index}m`;\n\t\t}\n\t}\n\tthrow new Error(`Invalid color value: ${color}`);\n}\n\n/** The two inks a chip can be written in: the darker one, and the lighter one. */\nconst CHIP_INK_DARK = \"#0b0b0f\";\nconst CHIP_INK_LIGHT = \"#ffffff\";\n\n/**\n * WCAG contrast between two resolved colours, or undefined if either is\n * unparseable.\n */\nfunction contrastRatio(a: string | number, b: string | number): number | undefined {\n\tconst [first, second] = [relativeLuminance(a), relativeLuminance(b)];\n\tif (first === undefined || second === undefined) return undefined;\n\treturn (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);\n}\n\n/**\n * The ink to lay on a chip filled with `color`, and how well it reads there —\n * or undefined when the fill's luminance cannot be worked out and we would be\n * guessing at legibility.\n *\n * Chips are filled with palette hues that differ wildly between themes — a dark\n * theme's agent1 is a bright cyan, a light theme's is a deep teal meant as ink on\n * paper — so a fixed text colour is unreadable in one theme or the other. The\n * ink is therefore whichever of the two actually measures better against this\n * fill, rather than whichever a fixed luminance cutoff nominates: near the\n * crossover the cutoff and the measurement disagree by a hair, and a hair is the\n * difference between a name that reads and one that does not.\n */\nfunction fillInk(color: string | number): { ink: string; ratio: number } | undefined {\n\tconst dark = contrastRatio(color, CHIP_INK_DARK);\n\tconst light = contrastRatio(color, CHIP_INK_LIGHT);\n\tif (dark === undefined || light === undefined) return undefined;\n\treturn dark >= light ? { ink: CHIP_INK_DARK, ratio: dark } : { ink: CHIP_INK_LIGHT, ratio: light };\n}\n\n/**\n * How light a chip fill has to be before its hue reads as that hue.\n *\n * A colour has two jobs in a light theme and they pull in opposite directions.\n * As *text* on paper it has to clear 4.5:1, which forces it dark — and a dark\n * yellow is brown, a dark cyan is navy, a dark green is bottle. As a *fill* it\n * only has to out-contrast the ink laid on it, which leaves it free to be the\n * hue it is named after. Lifting the fill to a floor lightness settles the\n * argument: the token stays ink where it is drawn as text, and the chip built\n * from it is recognisably yellow, cyan, green.\n *\n * 0.5 is where a fully saturated hue stops reading as a shade of brown and starts\n * reading as itself.\n */\nconst MIN_CHIP_FILL_LIGHTNESS = 0.5;\n\n/**\n * How well the name written on a chip has to read against the fill under it.\n *\n * Lightness alone is not a legibility test, because hues do not carry the same\n * brightness at the same lightness: a blue or a violet at 0.5 lands near\n * luminance 0.2, which is the crossover where dark ink and light ink are\n * *equally poor* — around 4.3:1 either way, under the 4.5:1 floor for body text\n * and well under what a two-word name on a 22-cell chip wants. Solarized Dark's\n * violet and magenta slots sat exactly there, which is what made a session's own\n * name the hardest thing on its input box to read.\n *\n * So the fill is measured rather than guessed at: it is moved until the better\n * of the two inks clears this bar. 5.5 leaves real headroom over AA without\n * forcing every chip to the top of the lightness range, where six hues start\n * looking like six pastels.\n */\nconst MIN_CHIP_INK_CONTRAST = 5.5;\n\n/**\n * The same bar, as a 256-colour terminal is able to keep it.\n *\n * Apple Terminal, GNU screen and TERM=linux round every fill to the nearest\n * entry of a 6×6×6 cube, and that rounding moves luminance. Holding the rounded\n * fill to the full 5.5 would mean lifting until it lands in a *different* cube\n * cell — and the cells these palettes round into are shared, so the lift that\n * bought contrast spent the thing the chip is for: Solarized's violet and blue\n * slots came out as the same swatch. The rounded fill answers to AA for body\n * text instead, which is a floor it can clear without leaving its own cell.\n */\nconst MIN_CHIP_INK_CONTRAST_QUANTIZED = 4.5;\n\n/**\n * Lightness past which lifting stops, so a hue is never bleached to paper.\n *\n * Nothing in the shipped palettes gets anywhere near it — the deepest ink in\n * them settles by 0.55. It is the backstop for a custom theme whose palette\n * entry is so dark that the walk would otherwise run to white.\n */\nconst MAX_CHIP_FILL_LIGHTNESS = 0.88;\n\n/**\n * Saturation a lifted fill is held under.\n *\n * Lifting at the token's own saturation is what turns a light theme's deep\n * inks into highlighter pen: `#00382d` (a bottle teal) at full saturation and\n * half lightness is `#00ffcd`, and `#035500` is `#09ff00`. Those are nobody's\n * idea of the theme's green. Pulling a lifted fill back to this ceiling is the\n * same move as mixing the ink with white — the hue survives, the fluorescence\n * does not. Only *lifted* fills are capped: a palette entry bright enough to be\n * its own chip is left exactly as the theme wrote it.\n */\nconst MAX_CHIP_FILL_SATURATION = 0.72;\n\n/** Step the lift walks in. Small enough that a fill never overshoots visibly. */\nconst CHIP_FILL_LIGHTNESS_STEP = 0.02;\n\n/** HSL of a hex colour, in 0-360 / 0-1 / 0-1. */\nfunction hexToHsl(hex: string): { h: number; s: number; l: number } {\n\tconst { r, g, b } = hexToRgb(hex);\n\tconst [red, green, blue] = [r / 255, g / 255, b / 255];\n\tconst max = Math.max(red, green, blue);\n\tconst min = Math.min(red, green, blue);\n\tconst delta = max - min;\n\tconst l = (max + min) / 2;\n\tif (delta === 0) return { h: 0, s: 0, l };\n\tconst raw =\n\t\tmax === red\n\t\t\t? (green - blue) / delta + (green < blue ? 6 : 0)\n\t\t\t: max === green\n\t\t\t\t? (blue - red) / delta + 2\n\t\t\t\t: (red - green) / delta + 4;\n\treturn { h: raw * 60, s: delta / (1 - Math.abs(2 * l - 1)), l };\n}\n\nfunction hslToHex(h: number, s: number, l: number): string {\n\tconst chroma = (1 - Math.abs(2 * l - 1)) * s;\n\tconst sector = ((((h % 360) + 360) % 360) / 60) % 6;\n\tconst second = chroma * (1 - Math.abs((sector % 2) - 1));\n\tconst [r, g, b] = (\n\t\tsector < 1\n\t\t\t? [chroma, second, 0]\n\t\t\t: sector < 2\n\t\t\t\t? [second, chroma, 0]\n\t\t\t\t: sector < 3\n\t\t\t\t\t? [0, chroma, second]\n\t\t\t\t\t: sector < 4\n\t\t\t\t\t\t? [0, second, chroma]\n\t\t\t\t\t\t: sector < 5\n\t\t\t\t\t\t\t? [second, 0, chroma]\n\t\t\t\t\t\t\t: [chroma, 0, second]\n\t) as [number, number, number];\n\tconst offset = l - chroma / 2;\n\treturn `#${[r, g, b]\n\t\t.map((channel) =>\n\t\t\tMath.round((channel + offset) * 255)\n\t\t\t\t.toString(16)\n\t\t\t\t.padStart(2, \"0\"),\n\t\t)\n\t\t.join(\"\")}`;\n}\n\n/**\n * What a colour looks like once the terminal has actually painted it.\n *\n * On a 256-colour terminal every fill is rounded to the nearest cube entry\n * first, and that rounding moves luminance — so an ink chosen against the\n * unrounded hex is an ink chosen against a colour nobody sees. Measuring the\n * rounded value instead keeps the chip's contrast honest on Apple Terminal,\n * GNU screen and TERM=linux, which is where it was least honest before.\n */\nfunction renderedColor(color: string | number, mode: ColorMode): string | number {\n\tif (mode !== \"256color\") return color;\n\tif (typeof color === \"number\") return color;\n\tif (!color.startsWith(\"#\")) return color;\n\ttry {\n\t\treturn ansi256ToHex(hexTo256(color));\n\t} catch {\n\t\treturn color;\n\t}\n}\n\n/**\n * The hue family a chip keeps white ink for.\n *\n * Slot 4 is magenta in every shipped theme (`/color magenta`, and the slot\n * order is a contract — see `docs/themes.md`), and magenta with a dark name on\n * it reads as pink with small print: the lift that makes dark ink legible is\n * exactly what bleaches the hue out of it. A deep magenta with a white name is\n * both more legible and more itself, so a magenta fill is deepened — never\n * lifted — until white clears the same bars every other fill answers to. When\n * no deep variant clears, the lift below still runs: legibility outranks ink.\n *\n * The window covers every shipped slot-4 token (325°–340°) with room on both\n * sides, and stops short of purple (nearest 293°) and red. The saturation gate\n * keeps out grays, whose hue is noise.\n */\nexport const MAGENTA_HUE_MIN = 305;\nexport const MAGENTA_HUE_MAX = 355;\nexport const MIN_MAGENTA_SATURATION = 0.25;\n\n/**\n * Darkest a magenta fill is deepened to in search of white-clearing ink.\n *\n * A chip is a mark on the page in both polarities, so the walk stops well\n * above the terminal's own ground — a fill here is still unmistakably lit on\n * the darkest shipped background.\n */\nconst MIN_MAGENTA_FILL_LIGHTNESS = 0.3;\n\n/** HSL of a palette entry known to be a saturated magenta, or undefined. */\nfunction magentaHsl(color: string | number): { h: number; s: number; l: number } | undefined {\n\tconst hex = typeof color === \"number\" ? ansi256ToHex(color) : color;\n\tif (!hex.startsWith(\"#\")) return undefined;\n\tlet hsl: { h: number; s: number; l: number };\n\ttry {\n\t\thsl = hexToHsl(hex);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (hsl.s <= MIN_MAGENTA_SATURATION) return undefined;\n\tconst hue = ((hsl.h % 360) + 360) % 360;\n\tif (hue < MAGENTA_HUE_MIN || hue > MAGENTA_HUE_MAX) return undefined;\n\treturn { h: hue, s: hsl.s, l: hsl.l };\n}\n\n/** Whether white ink on `candidate` clears both the exact and the rounded bar. */\nfunction clearsWithWhite(candidate: string | number, mode: ColorMode): boolean {\n\tconst exact = contrastRatio(candidate, CHIP_INK_LIGHT);\n\tconst rounded = contrastRatio(renderedColor(candidate, mode), CHIP_INK_LIGHT);\n\treturn (exact ?? 0) >= MIN_CHIP_INK_CONTRAST && (rounded ?? 0) >= MIN_CHIP_INK_CONTRAST_QUANTIZED;\n}\n\n/**\n * The colour a chip is actually filled with for a palette entry.\n *\n * Two things can be wrong with a palette entry used as a fill, and a theme's own\n * light/dark polarity does not decide which:\n *\n *  - On a light theme the entry is *ink* — dark enough to read on paper — and\n *    ink laid down as a swatch is a smudge whose hue nobody can name. That is\n *    what `MIN_CHIP_FILL_LIGHTNESS` answers.\n *  - On either polarity the entry can land in the luminance band where neither\n *    ink reads on it. That is what `MIN_CHIP_INK_CONTRAST` answers, and it is\n *    the case the old code did not check at all on a dark theme — which is\n *    precisely where Solarized Dark's violet and magenta chips lived.\n *\n * So the lift runs for every theme, and it runs against the *rendered* fill, but\n * it only moves a fill that has one of those problems: an entry already bright\n * enough to carry its ink comes back exactly as the theme wrote it. Only\n * lightness and (once lifted) saturation move; the hue is always the theme's, so\n * a lifted fill still reads as that theme's colour rather than a generic one.\n */\nfunction chipFill(color: string | number, lightBackdrop: boolean, mode: ColorMode): string | number {\n\t// Magenta keeps white ink (see MAGENTA_HUE_MIN): an entry that already\n\t// carries white is used exactly as the theme wrote it, on either polarity —\n\t// no lightness floor, which is what bleached deep magentas into pink. One\n\t// that does not is deepened at its own hue until white clears, so the chip\n\t// stays magenta instead of becoming the pink the lift would make it.\n\tconst magenta = magentaHsl(color);\n\tif (magenta) {\n\t\tif (clearsWithWhite(color, mode)) return color;\n\t\tfor (\n\t\t\tlet lightness = magenta.l - CHIP_FILL_LIGHTNESS_STEP;\n\t\t\tlightness >= MIN_MAGENTA_FILL_LIGHTNESS;\n\t\t\tlightness -= CHIP_FILL_LIGHTNESS_STEP\n\t\t) {\n\t\t\t// Capped like a lifted fill: a moved fill is the theme's no longer,\n\t\t\t// and full saturation at these lightnesses is neon either way.\n\t\t\tconst candidate = hslToHex(magenta.h, Math.min(magenta.s, MAX_CHIP_FILL_SATURATION), lightness);\n\t\t\tif (clearsWithWhite(candidate, mode)) return candidate;\n\t\t}\n\t\t// No deep variant clears — fall through to the lift, which buys\n\t\t// legibility with dark ink over an illegible magenta with white.\n\t}\n\n\t// Both the colour asked for and the colour the terminal will round it to have\n\t// to carry the ink — the first to the full bar, the second to the one it can\n\t// hold without being pushed out of its own cube cell.\n\tconst clears = (candidate: string | number) =>\n\t\t(fillInk(candidate)?.ratio ?? 0) >= MIN_CHIP_INK_CONTRAST &&\n\t\t(fillInk(renderedColor(candidate, mode))?.ratio ?? 0) >= MIN_CHIP_INK_CONTRAST_QUANTIZED;\n\t// The hue floor only applies where the palette is ink. A dark theme's entry\n\t// that already carries its ink is left alone however dark it is.\n\tif (!lightBackdrop && clears(color)) return color;\n\n\tconst hex = typeof color === \"number\" ? ansi256ToHex(color) : color;\n\tif (!hex.startsWith(\"#\")) return color;\n\tlet hsl: { h: number; s: number; l: number };\n\ttry {\n\t\thsl = hexToHsl(hex);\n\t} catch {\n\t\treturn color;\n\t}\n\n\t// A gray has no hue to rescue, so on a light theme there is nothing to lift it\n\t// for — but a mid-gray still sits in the band where neither ink reads, and\n\t// that has to be fixed wherever it happens.\n\tconst floor = lightBackdrop && hsl.s > 0 ? Math.max(hsl.l, MIN_CHIP_FILL_LIGHTNESS) : hsl.l;\n\tconst saturation = floor > hsl.l ? Math.min(hsl.s, MAX_CHIP_FILL_SATURATION) : hsl.s;\n\n\t// Up from the floor a step at a time, stopping as soon as the ink has room.\n\t// Lifting rather than darkening because a chip is a mark on a page in both\n\t// polarities: a fill darker than the terminal's own ground stops being one.\n\tfor (let lightness = floor; lightness <= MAX_CHIP_FILL_LIGHTNESS; lightness += CHIP_FILL_LIGHTNESS_STEP) {\n\t\tconst candidate = lightness <= hsl.l && saturation === hsl.s ? color : hslToHex(hsl.h, saturation, lightness);\n\t\tif (clears(candidate)) return candidate;\n\t}\n\t// Nothing in range carried the ink — take the top of the range, which is the\n\t// brightest the hue is allowed to get, over leaving the chip illegible.\n\treturn hslToHex(hsl.h, saturation, MAX_CHIP_FILL_LIGHTNESS);\n}\n\n/**\n * Whether the theme paints on paper rather than on a dark terminal, judged from\n * the surfaces it defines rather than from its name. `userMessageBg` is the one\n * surface every theme sets, so it is the reliable witness.\n */\nfunction hasLightSurfaces(bgColors: Record<ThemeBg, string | number>): boolean {\n\tfor (const candidate of [bgColors.userMessageBg, bgColors.selectedBg]) {\n\t\tif (candidate === undefined || candidate === \"\") continue;\n\t\tconst luminance = relativeLuminance(candidate);\n\t\tif (luminance !== undefined) return luminance > 0.5;\n\t}\n\treturn false;\n}\n\nfunction resolveVarRefs(\n\tvalue: ColorValue,\n\tvars: Record<string, ColorValue>,\n\tvisited = new Set<string>(),\n): string | number {\n\tif (typeof value === \"number\" || value === \"\" || value.startsWith(\"#\")) {\n\t\treturn value;\n\t}\n\tif (visited.has(value)) {\n\t\tthrow new Error(`Circular variable reference detected: ${value}`);\n\t}\n\tif (!(value in vars)) {\n\t\tthrow new Error(`Variable reference not found: ${value}`);\n\t}\n\tvisited.add(value);\n\treturn resolveVarRefs(vars[value], vars, visited);\n}\n\nfunction resolveThemeColors<T extends Record<string, ColorValue>>(\n\tcolors: T,\n\tvars: Record<string, ColorValue> = {},\n): Record<keyof T, string | number> {\n\tconst resolved: Record<string, string | number> = {};\n\tfor (const [key, value] of Object.entries(colors)) {\n\t\tresolved[key] = resolveVarRefs(value, vars);\n\t}\n\treturn resolved as Record<keyof T, string | number>;\n}\n\n// ============================================================================\n// Theme Class\n// ============================================================================\n\nexport class Theme {\n\treadonly name?: string;\n\treadonly sourcePath?: string;\n\tsourceInfo?: SourceInfo;\n\tprivate fgColors: Map<ThemeColor, string>;\n\tprivate bgColors: Map<ThemeBg, string>;\n\t/** Foreground tokens usable as a chip fill: background ANSI plus a legible ink. */\n\tprivate fillColors: Map<ThemeColor, string>;\n\tprivate mode: ColorMode;\n\n\tconstructor(\n\t\tfgColors: Record<ThemeColor, string | number>,\n\t\tbgColors: Record<ThemeBg, string | number>,\n\t\tmode: ColorMode,\n\t\toptions: { name?: string; sourcePath?: string; sourceInfo?: SourceInfo } = {},\n\t) {\n\t\tthis.name = options.name;\n\t\tthis.sourcePath = options.sourcePath;\n\t\tthis.sourceInfo = options.sourceInfo;\n\t\tthis.mode = mode;\n\t\t// Fallbacks are applied to the *raw* values rather than to the encoded\n\t\t// ANSI, because a fill has to be derived from the colour itself: it needs\n\t\t// the same value as both a background and a luminance to pick ink from.\n\t\tconst raw = new Map<ThemeColor, string | number>(Object.entries(fgColors) as [ThemeColor, string | number][]);\n\t\t// Agent palette fallback for themes built before it existed (including\n\t\t// Theme instances constructed directly by extensions): default to accent.\n\t\tconst accentRaw = raw.get(\"accent\");\n\t\tif (accentRaw !== undefined) {\n\t\t\tfor (const token of AGENT_COLOR_TOKENS) {\n\t\t\t\tif (!raw.has(token)) {\n\t\t\t\t\traw.set(token, accentRaw);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// A gauge always has an unfilled remainder to draw, so unlike the chip\n\t\t// pairs there is no \"skip it\" rendering to fall back to. `dim` is what\n\t\t// the tracks used before the token existed.\n\t\tconst dimRaw = raw.get(\"dim\");\n\t\tif (dimRaw !== undefined && !raw.has(\"halftone\")) {\n\t\t\traw.set(\"halftone\", dimRaw);\n\t\t}\n\t\tthis.fgColors = new Map();\n\t\tthis.fillColors = new Map();\n\t\t// A light theme's palette is ink, and ink does not read as its own hue when\n\t\t// it is laid down as a swatch, so a chip fill is lifted off it.\n\t\tconst lightBackdrop = hasLightSurfaces(bgColors);\n\t\tfor (const [key, value] of raw) {\n\t\t\tthis.fgColors.set(key, fgAnsi(value, mode));\n\t\t\t// \"\" means the terminal's own foreground: there is no colour to fill\n\t\t\t// with, so the token simply is not fillable.\n\t\t\tconst fill = value === \"\" ? value : chipFill(value, lightBackdrop, mode);\n\t\t\t// The ink answers to what the terminal will actually paint, which on a\n\t\t\t// 256-colour terminal is the rounded fill rather than the exact one.\n\t\t\tconst ink = fill === \"\" ? undefined : fillInk(renderedColor(fill, mode));\n\t\t\tif (ink !== undefined) {\n\t\t\t\tthis.fillColors.set(key, bgAnsi(fill, mode) + fgAnsi(ink.ink, mode));\n\t\t\t}\n\t\t}\n\t\tthis.bgColors = new Map();\n\t\tfor (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {\n\t\t\tthis.bgColors.set(key, bgAnsi(value, mode));\n\t\t}\n\t}\n\n\tfg(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[39m`; // Reset only foreground color\n\t}\n\n\tbg(color: ThemeBg, text: string): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn `${ansi}${text}\\x1b[49m`; // Reset only background color\n\t}\n\n\t/**\n\t * Paint `text` as a filled chip in a *foreground* token — the palette lives on\n\t * the foreground side, so `bg()`, which only knows the handful of surface\n\t * tokens, cannot do this. The ink comes from the fill's own luminance, which\n\t * is what lets one call render correctly on both a dark theme's bright hues\n\t * and a light theme's deep ones.\n\t *\n\t * Falls back to coloured text when the token is not fillable (see `canFill`):\n\t * a chip that loses its fill still reads, and callers that want the quieter\n\t * outline form should ask `canFill` first.\n\t */\n\tfill(color: ThemeColor, text: string): string {\n\t\tconst ansi = this.fillColors.get(color);\n\t\tif (!ansi) return this.fg(color, text);\n\t\treturn `${ansi}${text}\\x1b[39m\\x1b[49m`;\n\t}\n\n\t/** Whether `color` can be laid down as a fill with legible ink over it. */\n\tcanFill(color: ThemeColor): boolean {\n\t\treturn this.fillColors.has(color);\n\t}\n\n\t/** Whether the theme defines an optional token, so callers can pick a\n\t * fallback rendering instead of letting fg()/bg() throw. */\n\thas(color: ThemeColor): boolean {\n\t\treturn this.fgColors.has(color);\n\t}\n\n\thasBg(color: ThemeBg): boolean {\n\t\treturn this.bgColors.has(color);\n\t}\n\n\tbold(text: string): string {\n\t\treturn chalk.bold(text);\n\t}\n\n\tblink(text: string): string {\n\t\t// Terminal-native blink (SGR 5). Zero overhead; terminals that support\n\t\t// blink will animate, others render it as a steady glyph.\n\t\treturn `\\x1b[5m${text}\\x1b[25m`;\n\t}\n\n\titalic(text: string): string {\n\t\treturn chalk.italic(text);\n\t}\n\n\tunderline(text: string): string {\n\t\treturn chalk.underline(text);\n\t}\n\n\tinverse(text: string): string {\n\t\treturn chalk.inverse(text);\n\t}\n\n\tstrikethrough(text: string): string {\n\t\treturn chalk.strikethrough(text);\n\t}\n\n\tgetFgAnsi(color: ThemeColor): string {\n\t\tconst ansi = this.fgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetBgAnsi(color: ThemeBg): string {\n\t\tconst ansi = this.bgColors.get(color);\n\t\tif (!ansi) throw new Error(`Unknown theme background color: ${color}`);\n\t\treturn ansi;\n\t}\n\n\tgetColorMode(): ColorMode {\n\t\treturn this.mode;\n\t}\n\n\tgetThinkingBorderColor(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): (str: string) => string {\n\t\t// Map thinking levels to dedicated theme colors\n\t\tswitch (level) {\n\t\t\tcase \"off\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t\tcase \"minimal\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMinimal\", str);\n\t\t\tcase \"low\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingLow\", str);\n\t\t\tcase \"medium\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingMedium\", str);\n\t\t\tcase \"high\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingHigh\", str);\n\t\t\tcase \"xhigh\":\n\t\t\t\treturn (str: string) => this.fg(\"thinkingXhigh\", str);\n\t\t\tdefault:\n\t\t\t\treturn (str: string) => this.fg(\"thinkingOff\", str);\n\t\t}\n\t}\n\n\tgetBashModeBorderColor(): (str: string) => string {\n\t\treturn (str: string) => this.fg(\"bashMode\", str);\n\t}\n}\n\n// ============================================================================\n// Theme Loading\n// ============================================================================\n\nlet BUILTIN_THEMES: Record<string, ThemeJson> | undefined;\n\n/** Files in the built-in theme directory that are not themes. */\nconst NON_THEME_FILES = new Set([\"theme-schema.json\"]);\n\nfunction getBuiltinThemes(): Record<string, ThemeJson> {\n\tif (!BUILTIN_THEMES) {\n\t\t// Every *.json in the shipped theme directory is a built-in, so adding a\n\t\t// theme is a matter of dropping the file in (both build steps copy the\n\t\t// whole directory). `dark` and `light` are still the detection defaults.\n\t\tconst themesDir = getThemesDir();\n\t\tconst themes: Record<string, ThemeJson> = {};\n\t\tfor (const file of fs.readdirSync(themesDir).sort()) {\n\t\t\tif (!file.endsWith(\".json\") || NON_THEME_FILES.has(file)) continue;\n\t\t\tconst name = file.slice(0, -5);\n\t\t\tthemes[name] = parseThemeJsonContent(file, fs.readFileSync(path.join(themesDir, file), \"utf-8\"));\n\t\t}\n\t\tBUILTIN_THEMES = themes;\n\t}\n\treturn BUILTIN_THEMES;\n}\n\n/** True for themes shipped with the package, which live outside the custom themes dir. */\nfunction isBuiltinTheme(name: string): boolean {\n\treturn name in getBuiltinThemes();\n}\n\n/**\n * Built-ins that were retired, and what each one resolves to now.\n *\n * The shipped set is eight — four light, four dark — because a picker of\n * fourteen is a picker nobody reads to the end of, and half of those fourteen\n * were a second opinion on a theme already in the list. Retiring a name is not\n * the same as taking it away, though: somebody has `\"theme\": \"warm-light\"` in\n * their settings, and the loader's own failure path falls back to `dark`, which\n * would flip a light terminal to a dark theme on next launch.\n *\n * So a retired name still loads — as its nearest surviving relative, matched on\n * the thing that actually mattered about it: the accessibility pair keeps its\n * AAA floor (and gains colour-blind-safe hues), the `vox-*` pair keeps its\n * explanatory-journalism voice at the cut-out pair's stronger contrast, and\n * every one of them stays on the side of the light/dark split it was on.\n *\n * This is the last resort, consulted only after built-ins, extension-registered\n * themes and the user's own `themes/` directory have all been asked — so\n * somebody who keeps a copy of `warm-light.json` of their own still gets theirs.\n */\nconst RETIRED_THEMES: Readonly<Record<string, string>> = {\n\t\"high-contrast-dark\": \"colorsafe-dark\",\n\t\"high-contrast-light\": \"colorsafe-light\",\n\t\"warm-dark\": \"colorsafe-dark\",\n\t\"warm-light\": \"colorsafe-light\",\n\t\"vox-dark\": \"vox-cutout-dark\",\n\t\"vox-light\": \"vox-cutout-light\",\n};\n\n/** The theme a retired name now stands for, or undefined if the name is not retired. */\nexport function successorThemeFor(name: string): string | undefined {\n\tconst successor = RETIRED_THEMES[name];\n\treturn successor !== undefined && successor in getBuiltinThemes() ? successor : undefined;\n}\n\n/**\n * The theme `name` actually names today: itself if anything still defines it,\n * its successor if we retired it, and itself again if it is simply unknown —\n * the caller's own not-found handling is the right answer for that.\n */\nexport function resolveThemeName(name: string): string {\n\tif (getAvailableThemes().includes(name)) return name;\n\treturn successorThemeFor(name) ?? name;\n}\n\nexport function getAvailableThemes(): string[] {\n\tconst themes = new Set<string>(Object.keys(getBuiltinThemes()));\n\tconst customThemesDir = getCustomThemesDir();\n\tif (fs.existsSync(customThemesDir)) {\n\t\tconst files = fs.readdirSync(customThemesDir);\n\t\tfor (const file of files) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tthemes.add(file.slice(0, -5));\n\t\t\t}\n\t\t}\n\t}\n\tfor (const name of registeredThemes.keys()) {\n\t\tthemes.add(name);\n\t}\n\treturn Array.from(themes).sort();\n}\n\n/**\n * One-line description from a theme file, shown next to the name in the theme\n * picker. Undefined for themes that don't declare one, and for themes\n * registered as instances by extensions.\n */\nexport function getThemeDescription(name: string): string | undefined {\n\ttry {\n\t\treturn loadThemeJson(name).description;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport interface ThemeInfo {\n\tname: string;\n\tpath: string | undefined;\n}\n\nexport function getAvailableThemesWithPaths(): ThemeInfo[] {\n\tconst themesDir = getThemesDir();\n\tconst customThemesDir = getCustomThemesDir();\n\tconst result: ThemeInfo[] = [];\n\n\t// Built-in themes\n\tfor (const name of Object.keys(getBuiltinThemes())) {\n\t\tresult.push({ name, path: path.join(themesDir, `${name}.json`) });\n\t}\n\n\t// Custom themes\n\tif (fs.existsSync(customThemesDir)) {\n\t\tfor (const file of fs.readdirSync(customThemesDir)) {\n\t\t\tif (file.endsWith(\".json\")) {\n\t\t\t\tconst name = file.slice(0, -5);\n\t\t\t\tif (!result.some((t) => t.name === name)) {\n\t\t\t\t\tresult.push({ name, path: path.join(customThemesDir, file) });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const [name, theme] of registeredThemes.entries()) {\n\t\tif (!result.some((t) => t.name === name)) {\n\t\t\tresult.push({ name, path: theme.sourcePath });\n\t\t}\n\t}\n\n\treturn result.sort((a, b) => a.name.localeCompare(b.name));\n}\n\nfunction parseThemeJson(label: string, json: unknown): ThemeJson {\n\tif (!validateThemeJson.Check(json)) {\n\t\tconst errors = Array.from(validateThemeJson.Errors(json));\n\t\tconst missingColors = new Set<string>();\n\t\tconst otherErrors: string[] = [];\n\n\t\tfor (const error of errors) {\n\t\t\tif (error.keyword === \"required\" && error.instancePath === \"/colors\") {\n\t\t\t\tconst requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;\n\t\t\t\tfor (const requiredProperty of requiredProperties ?? []) {\n\t\t\t\t\tmissingColors.add(requiredProperty);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst path = error.instancePath || \"/\";\n\t\t\totherErrors.push(`  - ${path}: ${error.message}`);\n\t\t}\n\n\t\tlet errorMessage = `Invalid theme \"${label}\":\\n`;\n\t\tif (missingColors.size > 0) {\n\t\t\terrorMessage += \"\\nMissing required color tokens:\\n\";\n\t\t\terrorMessage += Array.from(missingColors)\n\t\t\t\t.sort()\n\t\t\t\t.map((color) => `  - ${color}`)\n\t\t\t\t.join(\"\\n\");\n\t\t\terrorMessage += '\\n\\nPlease add these colors to your theme\\'s \"colors\" object.';\n\t\t\terrorMessage += \"\\nSee the built-in themes (dark.json, light.json) for reference values.\";\n\t\t}\n\t\tif (otherErrors.length > 0) {\n\t\t\terrorMessage += `\\n\\nOther errors:\\n${otherErrors.join(\"\\n\")}`;\n\t\t}\n\n\t\tthrow new Error(errorMessage);\n\t}\n\n\treturn json as ThemeJson;\n}\n\nfunction parseThemeJsonContent(label: string, content: string): ThemeJson {\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(content);\n\t} catch (error) {\n\t\tthrow new Error(`Failed to parse theme ${label}: ${error}`);\n\t}\n\treturn parseThemeJson(label, json);\n}\n\nfunction loadThemeJson(name: string): ThemeJson {\n\tconst builtinThemes = getBuiltinThemes();\n\tif (name in builtinThemes) {\n\t\treturn builtinThemes[name];\n\t}\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme?.sourcePath) {\n\t\tconst content = fs.readFileSync(registeredTheme.sourcePath, \"utf-8\");\n\t\treturn parseThemeJsonContent(registeredTheme.sourcePath, content);\n\t}\n\tif (registeredTheme) {\n\t\tthrow new Error(`Theme \"${name}\" does not have a source path for export`);\n\t}\n\tconst customThemesDir = getCustomThemesDir();\n\tconst themePath = path.join(customThemesDir, `${name}.json`);\n\tif (!fs.existsSync(themePath)) {\n\t\t// Nobody defines this name any more. If we retired it ourselves, hand back\n\t\t// what it was retired in favour of rather than letting the caller fall all\n\t\t// the way back to `dark` — see RETIRED_THEMES.\n\t\tconst successor = successorThemeFor(name);\n\t\tif (successor) return builtinThemes[successor];\n\t\tthrow new Error(`Theme not found: ${name}`);\n\t}\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\treturn parseThemeJsonContent(name, content);\n}\n\nfunction createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme {\n\tconst colorMode = mode ?? detectColorMode();\n\tconst resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;\n\tconst bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;\n\tconst bgColorKeys: Set<string> = new Set([\n\t\t\"selectedBg\",\n\t\t\"userMessageBg\",\n\t\t\"customMessageBg\",\n\t\t\"toolPendingBg\",\n\t\t\"toolSuccessBg\",\n\t\t\"toolErrorBg\",\n\t\t\"warningBg\",\n\t\t\"brandBg\",\n\t\t\"headlineBg\",\n\t\t\"tapeBg\",\n\t\t\"activeToolBg\",\n\t]);\n\tfor (const [key, value] of Object.entries(resolvedColors)) {\n\t\tif (bgColorKeys.has(key)) {\n\t\t\tbgColors[key as ThemeBg] = value;\n\t\t} else {\n\t\t\tfgColors[key as ThemeColor] = value;\n\t\t}\n\t}\n\t// The agent palette is optional in theme JSON (older custom themes predate\n\t// it); missing entries fall back to accent so theme.fg never throws.\n\tfor (const token of AGENT_COLOR_TOKENS) {\n\t\tif (fgColors[token] === undefined) {\n\t\t\tfgColors[token] = fgColors.accent;\n\t\t}\n\t}\n\t// Same story for the warning surface: custom themes predate it, and the\n\t// billing notice needs *some* fill rather than a second render path.\n\tif (bgColors.warningBg === undefined) {\n\t\tbgColors.warningBg = bgColors.customMessageBg;\n\t}\n\treturn new Theme(fgColors, bgColors, colorMode, {\n\t\tname: themeJson.name,\n\t\tsourcePath,\n\t});\n}\n\nexport function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme {\n\tconst content = fs.readFileSync(themePath, \"utf-8\");\n\tconst themeJson = parseThemeJsonContent(themePath, content);\n\treturn createTheme(themeJson, mode, themePath);\n}\n\nfunction loadTheme(name: string, mode?: ColorMode): Theme {\n\tconst registeredTheme = registeredThemes.get(name);\n\tif (registeredTheme) {\n\t\treturn registeredTheme;\n\t}\n\tconst themeJson = loadThemeJson(name);\n\treturn createTheme(themeJson, mode);\n}\n\nexport function getThemeByName(name: string): Theme | undefined {\n\ttry {\n\t\treturn loadTheme(name);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction detectTerminalBackground(): \"dark\" | \"light\" {\n\tconst colorfgbg = process.env.COLORFGBG || \"\";\n\tif (colorfgbg) {\n\t\tconst parts = colorfgbg.split(\";\");\n\t\tif (parts.length >= 2) {\n\t\t\tconst bg = parseInt(parts[1], 10);\n\t\t\tif (!Number.isNaN(bg)) {\n\t\t\t\tconst result = bg < 8 ? \"dark\" : \"light\";\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\t}\n\treturn \"dark\";\n}\n\nfunction getDefaultTheme(): string {\n\treturn detectTerminalBackground();\n}\n\n// ============================================================================\n// Global Theme Instance\n// ============================================================================\n\n// Use globalThis to share theme across module loaders (tsx + jiti in dev mode)\nconst THEME_KEY = Symbol.for(\"@kolisachint/hoocode-agent:theme\");\n\n// Export theme as a getter that reads from globalThis\n// This ensures all module instances (tsx, jiti) see the same theme\nexport const theme: Theme = new Proxy({} as Theme, {\n\tget(_target, prop) {\n\t\tconst t = (globalThis as Record<symbol, Theme>)[THEME_KEY];\n\t\tif (!t) throw new Error(\"Theme not initialized. Call initTheme() first.\");\n\t\treturn (t as unknown as Record<string | symbol, unknown>)[prop];\n\t},\n});\n\nfunction setGlobalTheme(t: Theme): void {\n\t(globalThis as Record<symbol, Theme>)[THEME_KEY] = t;\n}\n\nlet currentThemeName: string | undefined;\nlet themeWatcher: fs.FSWatcher | undefined;\nlet themeReloadTimer: NodeJS.Timeout | undefined;\nlet onThemeChangeCallback: (() => void) | undefined;\nconst registeredThemes = new Map<string, Theme>();\n\nexport function setRegisteredThemes(themes: Theme[]): void {\n\tregisteredThemes.clear();\n\tfor (const theme of themes) {\n\t\tif (theme.name) {\n\t\t\tregisteredThemes.set(theme.name, theme);\n\t\t}\n\t}\n}\n\nexport function initTheme(themeName?: string, enableWatcher: boolean = false): void {\n\tconst requested = themeName ?? getDefaultTheme();\n\t// A retired name loads as its successor (see RETIRED_THEMES), so record the\n\t// successor as the current theme too — otherwise the picker would have no row\n\t// to mark as current and `/theme` would report a name that is no longer real.\n\tconst name = resolveThemeName(requested);\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t} catch (_error) {\n\t\t// Theme is invalid - fall back to dark theme silently\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t}\n}\n\nexport function setTheme(requested: string, enableWatcher: boolean = false): { success: boolean; error?: string } {\n\tconst name = resolveThemeName(requested);\n\tcurrentThemeName = name;\n\ttry {\n\t\tsetGlobalTheme(loadTheme(name));\n\t\tif (enableWatcher) {\n\t\t\tstartThemeWatcher();\n\t\t}\n\t\tif (onThemeChangeCallback) {\n\t\t\tonThemeChangeCallback();\n\t\t}\n\t\treturn { success: true };\n\t} catch (error) {\n\t\t// Theme is invalid - fall back to dark theme\n\t\tcurrentThemeName = \"dark\";\n\t\tsetGlobalTheme(loadTheme(\"dark\"));\n\t\t// Don't start watcher for fallback theme\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t};\n\t}\n}\n\nexport function setThemeInstance(themeInstance: Theme): void {\n\tsetGlobalTheme(themeInstance);\n\tcurrentThemeName = \"<in-memory>\";\n\tstopThemeWatcher(); // Can't watch a direct instance\n\tif (onThemeChangeCallback) {\n\t\tonThemeChangeCallback();\n\t}\n}\n\nexport function onThemeChange(callback: () => void): void {\n\tonThemeChangeCallback = callback;\n}\n\nfunction startThemeWatcher(): void {\n\tstopThemeWatcher();\n\n\t// Only watch if it's a custom theme (not built-in)\n\tif (!currentThemeName || isBuiltinTheme(currentThemeName)) {\n\t\treturn;\n\t}\n\n\tconst customThemesDir = getCustomThemesDir();\n\tconst watchedThemeName = currentThemeName;\n\tconst watchedFileName = `${watchedThemeName}.json`;\n\tconst themeFile = path.join(customThemesDir, watchedFileName);\n\n\t// Only watch if the file exists\n\tif (!fs.existsSync(themeFile)) {\n\t\treturn;\n\t}\n\n\tconst scheduleReload = () => {\n\t\tif (themeReloadTimer) {\n\t\t\tclearTimeout(themeReloadTimer);\n\t\t}\n\t\tthemeReloadTimer = setTimeout(() => {\n\t\t\tthemeReloadTimer = undefined;\n\n\t\t\t// Ignore stale timers after switching themes or stopping the watcher\n\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Keep the last successfully loaded theme active if the file is temporarily missing\n\t\t\tif (!fs.existsSync(themeFile)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Reload the theme from disk and refresh the registry cache\n\t\t\t\tconst reloadedTheme = loadThemeFromPath(themeFile);\n\t\t\t\tregisteredThemes.set(watchedThemeName, reloadedTheme);\n\t\t\t\tsetGlobalTheme(reloadedTheme);\n\t\t\t\t// Notify callback (to invalidate UI)\n\t\t\t\tif (onThemeChangeCallback) {\n\t\t\t\t\tonThemeChangeCallback();\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\t// Ignore errors (file might be in invalid state while being edited)\n\t\t\t}\n\t\t}, 100);\n\t};\n\n\tthemeWatcher =\n\t\twatchWithErrorHandler(\n\t\t\tcustomThemesDir,\n\t\t\t(_eventType, filename) => {\n\t\t\t\tif (currentThemeName !== watchedThemeName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!filename) {\n\t\t\t\t\tscheduleReload();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (filename !== watchedFileName) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tscheduleReload();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tcloseWatcher(themeWatcher);\n\t\t\t\tthemeWatcher = undefined;\n\t\t\t},\n\t\t) ?? undefined;\n}\n\nexport function stopThemeWatcher(): void {\n\tif (themeReloadTimer) {\n\t\tclearTimeout(themeReloadTimer);\n\t\tthemeReloadTimer = undefined;\n\t}\n\tcloseWatcher(themeWatcher);\n\tthemeWatcher = undefined;\n}\n\n// ============================================================================\n// HTML Export Helpers\n// ============================================================================\n\n/**\n * Convert a 256-color index to hex string.\n * Indices 0-15: basic colors (approximate)\n * Indices 16-231: 6x6x6 color cube\n * Indices 232-255: grayscale ramp\n */\nfunction ansi256ToHex(index: number): string {\n\t// Basic colors (0-15) - approximate common terminal values\n\tconst basicColors = [\n\t\t\"#000000\",\n\t\t\"#800000\",\n\t\t\"#008000\",\n\t\t\"#808000\",\n\t\t\"#000080\",\n\t\t\"#800080\",\n\t\t\"#008080\",\n\t\t\"#c0c0c0\",\n\t\t\"#808080\",\n\t\t\"#ff0000\",\n\t\t\"#00ff00\",\n\t\t\"#ffff00\",\n\t\t\"#0000ff\",\n\t\t\"#ff00ff\",\n\t\t\"#00ffff\",\n\t\t\"#ffffff\",\n\t];\n\tif (index < 16) {\n\t\treturn basicColors[index];\n\t}\n\n\t// Color cube (16-231): 6x6x6 = 216 colors\n\tif (index < 232) {\n\t\tconst cubeIndex = index - 16;\n\t\tconst r = Math.floor(cubeIndex / 36);\n\t\tconst g = Math.floor((cubeIndex % 36) / 6);\n\t\tconst b = cubeIndex % 6;\n\t\tconst toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, \"0\");\n\t\treturn `#${toHex(r)}${toHex(g)}${toHex(b)}`;\n\t}\n\n\t// Grayscale (232-255): 24 shades\n\tconst gray = 8 + (index - 232) * 10;\n\tconst grayHex = gray.toString(16).padStart(2, \"0\");\n\treturn `#${grayHex}${grayHex}${grayHex}`;\n}\n\n/** WCAG relative luminance (0-1) of a resolved color, or undefined if unparseable. */\nfunction relativeLuminance(color: string | number): number | undefined {\n\tconst hex = typeof color === \"number\" ? ansi256ToHex(color) : color;\n\tif (!hex.startsWith(\"#\")) return undefined;\n\tlet rgb: { r: number; g: number; b: number };\n\ttry {\n\t\trgb = hexToRgb(hex);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tconst toLinear = (c: number) => {\n\t\tconst s = c / 255;\n\t\treturn s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n\t};\n\treturn 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b);\n}\n\n/**\n * Whether a theme sits on a light backdrop, judged by its own colors rather\n * than its name — `light` is no longer the only light-background theme.\n */\nfunction hasLightBackdrop(themeJson: ThemeJson, resolved: Record<string, string | number>): boolean {\n\tconst pageBg = themeJson.export?.pageBg;\n\tconst candidates = [\n\t\tpageBg === undefined ? undefined : resolveVarRefs(pageBg, themeJson.vars ?? {}),\n\t\tresolved.userMessageBg,\n\t];\n\tfor (const candidate of candidates) {\n\t\tif (candidate === undefined || candidate === \"\") continue;\n\t\tconst luminance = relativeLuminance(candidate);\n\t\tif (luminance !== undefined) return luminance > 0.5;\n\t}\n\treturn false;\n}\n\n/**\n * Get resolved theme colors as CSS-compatible hex strings.\n * Used by HTML export to generate CSS custom properties.\n */\nexport function getResolvedThemeColors(themeName?: string): Record<string, string> {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\tconst themeJson = loadThemeJson(name);\n\tconst resolved = resolveThemeColors(themeJson.colors, themeJson.vars);\n\tconst isLight = hasLightBackdrop(themeJson, resolved);\n\n\t// Default text color for empty values (terminal uses default fg color)\n\tconst defaultText = isLight ? \"#000000\" : \"#e5e5e7\";\n\n\tconst cssColors: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(resolved)) {\n\t\tif (typeof value === \"number\") {\n\t\t\tcssColors[key] = ansi256ToHex(value);\n\t\t} else if (value === \"\") {\n\t\t\t// Empty means default terminal color - use sensible fallback for HTML\n\t\t\tcssColors[key] = defaultText;\n\t\t} else {\n\t\t\tcssColors[key] = value;\n\t\t}\n\t}\n\treturn cssColors;\n}\n\n/**\n * Get explicit export colors from theme JSON, if specified.\n * Returns undefined for each color that isn't explicitly set.\n */\nexport function getThemeExportColors(themeName?: string): {\n\tpageBg?: string;\n\tcardBg?: string;\n\tinfoBg?: string;\n} {\n\tconst name = themeName ?? currentThemeName ?? getDefaultTheme();\n\ttry {\n\t\tconst themeJson = loadThemeJson(name);\n\t\tconst exportSection = themeJson.export;\n\t\tif (!exportSection) return {};\n\n\t\tconst vars = themeJson.vars ?? {};\n\t\tconst resolve = (value: ColorValue | undefined): string | undefined => {\n\t\t\tif (value === undefined) return undefined;\n\t\t\tconst resolved = resolveVarRefs(value, vars);\n\t\t\tif (typeof resolved === \"number\") return ansi256ToHex(resolved);\n\t\t\tif (resolved === \"\") return undefined;\n\t\t\treturn resolved;\n\t\t};\n\n\t\treturn {\n\t\t\tpageBg: resolve(exportSection.pageBg),\n\t\t\tcardBg: resolve(exportSection.cardBg),\n\t\t\tinfoBg: resolve(exportSection.infoBg),\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n// ============================================================================\n// TUI Helpers\n// ============================================================================\n\ntype CliHighlightTheme = Record<string, (s: string) => string>;\n\nlet cachedHighlightThemeFor: Theme | undefined;\nlet cachedCliHighlightTheme: CliHighlightTheme | undefined;\n\nfunction buildCliHighlightTheme(t: Theme): CliHighlightTheme {\n\treturn {\n\t\tkeyword: (s: string) => t.fg(\"syntaxKeyword\", s),\n\t\tbuilt_in: (s: string) => t.fg(\"syntaxType\", s),\n\t\tliteral: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tnumber: (s: string) => t.fg(\"syntaxNumber\", s),\n\t\tstring: (s: string) => t.fg(\"syntaxString\", s),\n\t\tcomment: (s: string) => t.fg(\"syntaxComment\", s),\n\t\tfunction: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\ttitle: (s: string) => t.fg(\"syntaxFunction\", s),\n\t\tclass: (s: string) => t.fg(\"syntaxType\", s),\n\t\ttype: (s: string) => t.fg(\"syntaxType\", s),\n\t\tattr: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tvariable: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\tparams: (s: string) => t.fg(\"syntaxVariable\", s),\n\t\toperator: (s: string) => t.fg(\"syntaxOperator\", s),\n\t\tpunctuation: (s: string) => t.fg(\"syntaxPunctuation\", s),\n\t};\n}\n\nfunction getCliHighlightTheme(t: Theme): CliHighlightTheme {\n\tif (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {\n\t\tcachedHighlightThemeFor = t;\n\t\tcachedCliHighlightTheme = buildCliHighlightTheme(t);\n\t}\n\treturn cachedCliHighlightTheme;\n}\n\n/**\n * Highlight code with syntax coloring based on file extension or language.\n * Returns array of highlighted lines.\n */\nexport function highlightCode(code: string, lang?: string): string[] {\n\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t// LiveCodeServer, etc., coloring random English words as keywords.\n\tif (!validLang) {\n\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t}\n\tconst opts = {\n\t\tlanguage: validLang,\n\t\tignoreIllegals: true,\n\t\ttheme: getCliHighlightTheme(theme),\n\t};\n\ttry {\n\t\treturn highlight(code, opts).split(\"\\n\");\n\t} catch {\n\t\treturn code.split(\"\\n\");\n\t}\n}\n\n/**\n * Get language identifier from file path extension.\n */\nexport function getLanguageFromPath(filePath: string): string | undefined {\n\tconst ext = filePath.split(\".\").pop()?.toLowerCase();\n\tif (!ext) return undefined;\n\n\tconst extToLang: Record<string, string> = {\n\t\tts: \"typescript\",\n\t\ttsx: \"typescript\",\n\t\tjs: \"javascript\",\n\t\tjsx: \"javascript\",\n\t\tmjs: \"javascript\",\n\t\tcjs: \"javascript\",\n\t\tpy: \"python\",\n\t\trb: \"ruby\",\n\t\trs: \"rust\",\n\t\tgo: \"go\",\n\t\tjava: \"java\",\n\t\tkt: \"kotlin\",\n\t\tswift: \"swift\",\n\t\tc: \"c\",\n\t\th: \"c\",\n\t\tcpp: \"cpp\",\n\t\tcc: \"cpp\",\n\t\tcxx: \"cpp\",\n\t\thpp: \"cpp\",\n\t\tcs: \"csharp\",\n\t\tphp: \"php\",\n\t\tsh: \"bash\",\n\t\tbash: \"bash\",\n\t\tzsh: \"bash\",\n\t\tfish: \"fish\",\n\t\tps1: \"powershell\",\n\t\tsql: \"sql\",\n\t\thtml: \"html\",\n\t\thtm: \"html\",\n\t\tcss: \"css\",\n\t\tscss: \"scss\",\n\t\tsass: \"sass\",\n\t\tless: \"less\",\n\t\tjson: \"json\",\n\t\tyaml: \"yaml\",\n\t\tyml: \"yaml\",\n\t\ttoml: \"toml\",\n\t\txml: \"xml\",\n\t\tmd: \"markdown\",\n\t\tmarkdown: \"markdown\",\n\t\tdockerfile: \"dockerfile\",\n\t\tmakefile: \"makefile\",\n\t\tcmake: \"cmake\",\n\t\tlua: \"lua\",\n\t\tperl: \"perl\",\n\t\tr: \"r\",\n\t\tscala: \"scala\",\n\t\tclj: \"clojure\",\n\t\tex: \"elixir\",\n\t\texs: \"elixir\",\n\t\terl: \"erlang\",\n\t\ths: \"haskell\",\n\t\tml: \"ocaml\",\n\t\tvim: \"vim\",\n\t\tgraphql: \"graphql\",\n\t\tproto: \"protobuf\",\n\t\ttf: \"hcl\",\n\t\thcl: \"hcl\",\n\t};\n\n\treturn extToLang[ext];\n}\n\n/**\n * The `[branch]` / `[skill]` / `[extension]` tag that heads a message block.\n *\n * Five call sites had written the same bold-brackets-in-customMessageLabel\n * string by hand, which is why they are here now: a theme that sets the tape\n * pair renders the tag as a strip laid across the block instead, and that only\n * reads as one device if every tag agrees. Themes without the pair get exactly\n * the string the call sites used to build.\n */\nexport function messageLabel(name: string): string {\n\tif (theme.hasBg(\"tapeBg\") && theme.has(\"tapeText\")) {\n\t\treturn theme.bg(\"tapeBg\", theme.fg(\"tapeText\", `\\x1b[1m ${name} \\x1b[22m`));\n\t}\n\treturn theme.fg(\"customMessageLabel\", `\\x1b[1m[${name}]\\x1b[22m`);\n}\n\n/**\n * The shadow pass for a filled block, or nothing when the theme has no\n * `paperShadow`. Every block that reads as a pasted sheet — a user message, an\n * extension message, an error or warning notice — takes its shadow from here so\n * they cannot drift apart.\n */\nexport function getPaperShadowFn(): ((text: string) => string) | undefined {\n\treturn theme.has(\"paperShadow\") ? (text: string) => theme.fg(\"paperShadow\", text) : undefined;\n}\n\n/**\n * How far a sheet holds back from the right margin, leaving a gutter of page.\n *\n * One column, which is the whole of what the treatment needs and no more. The\n * gutter exists so the sheet has a right edge to show and somewhere to put the\n * shadow's column; the column is `▏`, a *left one-eighth* block, so it paints\n * the sheet's edge as a hairline at the left of that one cell and leaves the\n * rest as page. Three columns bought nothing the first one does not already\n * give and cost two columns of every message, on every line, forever.\n */\nexport const PAPER_INSET = 1;\n\n/**\n * Give a filled block the paper treatment: a gutter of page at its right, and a\n * shadow down that edge and along the bottom.\n *\n * The two arrive together because they are one decision. A block that runs edge\n * to edge has no right edge to show and nowhere to cast a shadow, so a theme\n * asking for paper is asking for the gutter that makes paper possible.\n * A theme without `paperShadow` gets the full-width band it has always drawn.\n *\n * The decision is handed to the box as a function, not as an answer, because\n * blocks outlive themes: the messages already on screen when the user switches\n * theme keep the boxes they were built with. Answering once meant a message\n * pasted under a cut-out theme went on asking a plain theme for `paperShadow`,\n * a token that theme never defined — which threw rather than rendering.\n */\nexport function applyPaperSheet(box: Box): void {\n\tbox.setPaper(() => {\n\t\tconst shadow = getPaperShadowFn();\n\t\treturn shadow ? { shadow, inset: PAPER_INSET } : undefined;\n\t});\n}\n\n/**\n * The fills that make a *block* — a pasted sheet of message — as opposed to a\n * chip or a row stroke.\n *\n * `selectedBg` and `activeToolBg` mark part of a row and `brandBg`, `headlineBg`\n * and `tapeBg` are chips laid on top of one; none of them is a sheet and none\n * takes a shadow. These four are, and every one of them has to look like the\n * others or the page stops reading as paper.\n */\nexport type BlockFill = \"userMessageBg\" | \"customMessageBg\" | \"warningBg\" | \"toolErrorBg\";\n\n/**\n * Fill a message block, and give it the edge that goes with the fill.\n *\n * These are one decision, so they are made in one call. Kept apart they drifted\n * exactly as you would expect: `[skill]`, `[compaction]` and `[branch]` blocks\n * were each written to use \"the same background colour as custom messages for\n * visual consistency\" and each stopped there, so under a cut-out theme they\n * rendered as flat full-width bands beside the sheets they were copying —\n * same paint, no gutter, no edge, no shadow, in the same transcript.\n *\n * A theme with no `paperShadow` still gets the full-width band it always had:\n * `applyPaperSheet` resolves that per frame, so this is safe on every theme and\n * survives a theme switch under blocks already on screen.\n */\nexport function applyBlockFill(box: Box, fill: BlockFill): void {\n\tbox.setBgFn((text: string) => theme.bg(fill, text));\n\tapplyPaperSheet(box);\n}\n\nexport function getMarkdownTheme(): MarkdownTheme {\n\t// A theme that sets the headline pair renders headings as a filled chip: the\n\t// text takes the chip's ink, and the fill is applied to the finished line so\n\t// the chip's padding never leaks into the inline-style prefix.\n\t//\n\t// Only the top two levels get a bar. Below that the renderer keeps the `###`\n\t// marker, and a marker inside a filled chip is saying the same thing twice —\n\t// the chip *is* the marker. A page of `###` bars is also just loud: two\n\t// levels of chip and the rest as coloured text is the hierarchy a printed\n\t// page would use.\n\t//\n\t// The pair is looked up per call rather than once here: a markdown theme is\n\t// built with the message that holds it and is still in use after the user\n\t// switches theme, so a decision frozen at build time would have that message\n\t// asking a plain theme for `headlineText`, which it never defined.\n\tconst chipped = (level: number) => level <= 2 && theme.hasBg(\"headlineBg\") && theme.has(\"headlineText\");\n\treturn {\n\t\theading: (text: string, level: number) => theme.fg(chipped(level) ? \"headlineText\" : \"mdHeading\", text),\n\t\theadingBlock: (line: string, level: number) => (chipped(level) ? theme.bg(\"headlineBg\", ` ${line} `) : line),\n\t\tlink: (text: string) => theme.fg(\"mdLink\", text),\n\t\tlinkUrl: (text: string) => theme.fg(\"mdLinkUrl\", text),\n\t\tcode: (text: string) => theme.fg(\"mdCode\", text),\n\t\tcodeBlock: (text: string) => theme.fg(\"mdCodeBlock\", text),\n\t\tcodeBlockBorder: (text: string) => theme.fg(\"mdCodeBlockBorder\", text),\n\t\tquote: (text: string) => theme.fg(\"mdQuote\", text),\n\t\tquoteBorder: (text: string) => theme.fg(\"mdQuoteBorder\", text),\n\t\thr: (text: string) => theme.fg(\"mdHr\", text),\n\t\tlistBullet: (text: string) => theme.fg(\"mdListBullet\", text),\n\t\tbold: (text: string) => theme.bold(text),\n\t\titalic: (text: string) => theme.italic(text),\n\t\tunderline: (text: string) => theme.underline(text),\n\t\tstrikethrough: (text: string) => chalk.strikethrough(text),\n\t\thighlightCode: (code: string, lang?: string): string[] => {\n\t\t\t// Validate language before highlighting to avoid stderr spam from cli-highlight\n\t\t\tconst validLang = lang && supportsLanguage(lang) ? lang : undefined;\n\t\t\t// Skip highlighting when no valid language is specified. cli-highlight's\n\t\t\t// auto-detection is unreliable and can misidentify prose as AppleScript,\n\t\t\t// LiveCodeServer, etc., coloring random English words as keywords.\n\t\t\tif (!validLang) {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t\tconst opts = {\n\t\t\t\tlanguage: validLang,\n\t\t\t\tignoreIllegals: true,\n\t\t\t\ttheme: getCliHighlightTheme(theme),\n\t\t\t};\n\t\t\ttry {\n\t\t\t\treturn highlight(code, opts).split(\"\\n\");\n\t\t\t} catch {\n\t\t\t\treturn code.split(\"\\n\").map((line) => theme.fg(\"mdCodeBlock\", line));\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * The cursor every picker marks its selected row with, trailing space included.\n * Pickers used to pick their own — `→`, `›` and `>` were all in use, and the\n * shared `SelectList` hardcoded a fourth — so which glyph you saw depended on\n * which list you had opened.\n */\nexport const SELECT_CURSOR = \"› \";\n\n/**\n * Dress an input line's caret in the session theme. Every place you can type —\n * a picker's query, the login dialog, an extension's prompt, a rename — is the\n * same signal as the main prompt, so it wears the prompt's `❯` (the `Input`\n * default) rather than a glyph of its own. Muted, because an input line usually\n * sits a row or two under an accent `›` marking a selected row and must not\n * out-shout it.\n *\n * Mutates rather than returning a theme object because `Input` takes no theme;\n * the colour is resolved at render, so a theme swap follows. Returns the input\n * so it can wrap a `new Input()` in a field initializer.\n */\nexport function styleInput(input: Input): Input {\n\tinput.promptColor = (text: string) => theme.fg(\"muted\", text);\n\treturn input;\n}\n\n/**\n * The blank gutter an unselected row is indented by. Derived from the cursor\n * rather than hardcoded, so a cursor of a different width keeps the column it\n * marks in the same place instead of shifting every unselected row by one.\n */\nexport const SELECT_GUTTER = \" \".repeat(visibleWidth(SELECT_CURSOR));\n\n/**\n * Paint a selected row as a filled band, padding it to the full width first so\n * the highlight reaches the right edge instead of stopping wherever the text\n * happens to stop. For components that render their own rows; `SelectList` and\n * `SettingsList` get the same treatment through their `selectedRow` theme hook.\n *\n * Pass a row that already fits the width. A row long enough to have been\n * truncated carries a reset at the cut, which ends the band on the ellipsis.\n */\nexport function paintSelectedRow(line: string, width: number): string {\n\treturn applyBackgroundToLine(line, width, (text: string) => theme.bg(\"selectedBg\", text));\n}\n\nexport function getSelectListTheme(): SelectListTheme {\n\treturn {\n\t\tselectedPrefix: (text: string) => theme.fg(\"accent\", text),\n\t\tselectedText: (text: string) => theme.fg(\"accent\", text),\n\t\tdescription: (text: string) => theme.fg(\"muted\", text),\n\t\tscrollInfo: (text: string) => theme.fg(\"muted\", text),\n\t\tnoMatch: (text: string) => theme.fg(\"muted\", text),\n\t\t// Left unstyled: SelectList passes the cursor through `selectedText`\n\t\t// with the rest of the row.\n\t\tcursor: SELECT_CURSOR,\n\t\tselectedRow: (text: string) => theme.bg(\"selectedBg\", text),\n\t};\n}\n\nexport function getEditorTheme(): EditorTheme {\n\treturn {\n\t\tborderColor: (text: string) => theme.fg(\"borderMuted\", text),\n\t\tselectList: getSelectListTheme(),\n\t};\n}\n\nexport function getSettingsListTheme(): SettingsListTheme {\n\treturn {\n\t\tlabel: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : text),\n\t\tvalue: (text: string, selected: boolean) => (selected ? theme.fg(\"accent\", text) : theme.fg(\"muted\", text)),\n\t\tdescription: (text: string) => theme.fg(\"dim\", text),\n\t\tcursor: theme.fg(\"accent\", SELECT_CURSOR),\n\t\thint: (text: string) => theme.fg(\"dim\", text),\n\t\tselectedRow: (text: string) => theme.bg(\"selectedBg\", text),\n\t};\n}\n"]}