From 82dceb22e1aab282808641252e12b3696c98409b Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 15 Dec 2025 22:32:23 +0100 Subject: [PATCH] Fix RGB to 256-color mapping for non-truecolor terminals --- AGENTS.md | 3 +- packages/coding-agent/CHANGELOG.md | 4 + .../src/modes/interactive/theme/theme.ts | 74 ++++++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5d51be7e..3e35c109 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,8 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t ## Commands - After code changes: `npm run check` (get full output, no tail) -- NEVER run: `npm run dev`, `npm run build` +- NEVER run: `npm run dev`, `npm run build`, `npm test` +- Only run specific tests if user instructs: `npm test -- test/specific.test.ts` - NEVER commit unless user asks ## GitHub Issues diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a0c34dec..0057dbb8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Improved RGB to 256-color mapping for terminals without truecolor support. Now correctly uses grayscale ramp for neutral colors and preserves semantic tints (green for success, red for error, blue for pending) instead of mapping everything to wrong cube colors. + ## [0.22.2] - 2025-12-15 ### Changed diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 4ce6304d..ec928beb 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -164,11 +164,77 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } { return { r, g, b }; } +// The 6x6x6 color cube channel values (indices 0-5) +const CUBE_VALUES = [0, 95, 135, 175, 215, 255]; + +// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238) +const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10); + +function findClosestCubeIndex(value: number): number { + let minDist = Infinity; + let minIdx = 0; + for (let i = 0; i < CUBE_VALUES.length; i++) { + const dist = Math.abs(value - CUBE_VALUES[i]); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + return minIdx; +} + +function findClosestGrayIndex(gray: number): number { + let minDist = Infinity; + let minIdx = 0; + for (let i = 0; i < GRAY_VALUES.length; i++) { + const dist = Math.abs(gray - GRAY_VALUES[i]); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + return minIdx; +} + +function colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { + // Weighted Euclidean distance (human eye is more sensitive to green) + const dr = r1 - r2; + const dg = g1 - g2; + const db = b1 - b2; + return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114; +} + function rgbTo256(r: number, g: number, b: number): number { - const rIndex = Math.round((r / 255) * 5); - const gIndex = Math.round((g / 255) * 5); - const bIndex = Math.round((b / 255) * 5); - return 16 + 36 * rIndex + 6 * gIndex + bIndex; + // Find closest color in the 6x6x6 cube + const rIdx = findClosestCubeIndex(r); + const gIdx = findClosestCubeIndex(g); + const bIdx = findClosestCubeIndex(b); + const cubeR = CUBE_VALUES[rIdx]; + const cubeG = CUBE_VALUES[gIdx]; + const cubeB = CUBE_VALUES[bIdx]; + const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx; + const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB); + + // Find closest grayscale + const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); + const grayIdx = findClosestGrayIndex(gray); + const grayValue = GRAY_VALUES[grayIdx]; + const grayIndex = 232 + grayIdx; + const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue); + + // Check if color has noticeable saturation (hue matters) + // If max-min spread is significant, prefer cube to preserve tint + const maxC = Math.max(r, g, b); + const minC = Math.min(r, g, b); + const spread = maxC - minC; + + // Only consider grayscale if color is nearly neutral (spread < 10) + // AND grayscale is actually closer + if (spread < 10 && grayDist < cubeDist) { + return grayIndex; + } + + return cubeIndex; } function hexTo256(hex: string): number {