fix: derive diff/code bg colors from active theme instead of hardcoding KITT defaults

- makeTheme() and fileConfigToTheme() now compute DiffInsertBg, DiffDeleteBg,
  DiffEqualBg, DiffMissingBg, CodeBg, GutterBg, and WriteBg by blending the
  theme's own Background with its Success/Error colors, so every theme gets
  properly tinted diff backgrounds.
- Added color derivation helpers: parseHexColor, blendHex, deriveDiffBg.
- File-based themes still allow explicit diff color overrides; derived colors
  are used only as fallbacks.
- formatToolParams() now skips body-content keys (content, old_text, new_text,
  etc.) from the header line regardless of value length, preventing raw
  unformatted code from appearing above the formatted body.
This commit is contained in:
Ed Zynda
2026-03-25 17:41:37 +03:00
parent d1cffb85ef
commit db4bb19bac
3 changed files with 208 additions and 17 deletions
+13 -2
View File
@@ -111,14 +111,25 @@ func formatToolParams(toolArgs string, maxWidth int) string {
result.WriteString(primaryVal)
}
// Collect remaining parameters (skip large values like file content)
// Collect remaining parameters, skipping body-content keys (already
// rendered in the tool body) and any values that are too large.
bodyKeys := map[string]bool{
"content": true,
"old_text": true,
"new_text": true,
"oldText": true,
"newText": true,
"todos": true,
}
var remaining []string
for key, val := range params {
if key == primaryKey {
continue
}
if bodyKeys[key] {
continue
}
valStr := fmt.Sprintf("%v", val)
// Skip very large values (e.g., oldString, newString, content, todos)
if len(valStr) > 100 {
continue
}
+110 -15
View File
@@ -7,11 +7,86 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// ---------------------------------------------------------------------------
// Color derivation helpers
// ---------------------------------------------------------------------------
// parseHexColor parses a "#RRGGBB" hex string into r, g, b components (0-255).
func parseHexColor(hex string) (r, g, b int) {
hex = strings.TrimPrefix(hex, "#")
if len(hex) == 6 {
if v, err := strconv.ParseUint(hex[0:2], 16, 8); err == nil {
r = int(v)
}
if v, err := strconv.ParseUint(hex[2:4], 16, 8); err == nil {
g = int(v)
}
if v, err := strconv.ParseUint(hex[4:6], 16, 8); err == nil {
b = int(v)
}
}
return
}
// blendHex linearly interpolates between two hex colors by amount (0.01.0).
func blendHex(base, tint string, amount float64) string {
br, bg, bb := parseHexColor(base)
tr, tg, tb := parseHexColor(tint)
clamp := func(v int) int {
if v < 0 {
return 0
}
if v > 255 {
return 255
}
return v
}
r := clamp(int(float64(br)*(1-amount) + float64(tr)*amount))
g := clamp(int(float64(bg)*(1-amount) + float64(tg)*amount))
b := clamp(int(float64(bb)*(1-amount) + float64(tb)*amount))
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// deriveDiffBg computes diff / code background colors from the theme's
// background, success, and error hex pairs. Returns an adaptive color for each
// diff element. The tint amounts are tuned for subtle differentiation.
func deriveDiffBg(bgPair, successPair, errorPair [2]string) (diffInsert, diffDelete, diffEqual, diffMissing, codeBg, gutterBg, writeBg color.Color) {
derive := func(idx int) (color.Color, color.Color, color.Color, color.Color) {
bg := bgPair[idx]
// Contrast target: darken for light mode (idx 0), lighten for dark (idx 1).
contrast := "#000000"
if idx == 1 {
contrast = "#ffffff"
}
ins := blendHex(bg, successPair[idx], 0.13)
del := blendHex(bg, errorPair[idx], 0.13)
eq := blendHex(bg, contrast, 0.05)
miss := blendHex(bg, contrast, 0.03)
return AdaptiveColor(ins, ins), AdaptiveColor(del, del), AdaptiveColor(eq, eq), AdaptiveColor(miss, miss)
}
// Pick the correct index based on detected background.
idx := 0
if isDarkBg {
idx = 1
}
insL, delL, eqL, missL := derive(idx)
diffInsert = insL
diffDelete = delL
diffEqual = eqL
diffMissing = missL
codeBg = eqL
gutterBg = missL
writeBg = insL
return
}
// ThemeEntry is a named, loadable theme — either built-in or discovered from disk.
type ThemeEntry struct {
Name string // Display name (filename stem or preset name)
@@ -80,14 +155,9 @@ func makeTheme(p presetColors) Theme {
Accent: acOr(p.accent, ac(p.primary)),
Highlight: acOr(p.highlight, def.Highlight),
}
// Derive diff/code backgrounds from the base background.
t.DiffInsertBg = def.DiffInsertBg
t.DiffDeleteBg = def.DiffDeleteBg
t.DiffEqualBg = def.DiffEqualBg
t.DiffMissingBg = def.DiffMissingBg
t.CodeBg = def.CodeBg
t.GutterBg = def.GutterBg
t.WriteBg = def.WriteBg
// Derive diff/code backgrounds from the theme's own palette.
t.DiffInsertBg, t.DiffDeleteBg, t.DiffEqualBg, t.DiffMissingBg,
t.CodeBg, t.GutterBg, t.WriteBg = deriveDiffBg(p.background, p.success, p.error_)
// Markdown colors.
t.Markdown = MarkdownThemeColors{
Text: t.Text,
@@ -609,6 +679,17 @@ func loadThemeFile(path string) (Theme, error) {
func fileConfigToTheme(cfg themeFileConfig) Theme {
def := DefaultTheme()
// Resolve the base background/success/error hex pairs for diff derivation.
// We need the raw hex strings to feed deriveDiffBg.
bgPair := resolveHexPair(cfg.Background, [2]string{"#F0F0F0", "#0D0D0D"})
successPair := resolveHexPair(cfg.Success, [2]string{"#998800", "#CCAA00"})
errorPair := resolveHexPair(cfg.Error, [2]string{"#CC0000", "#FF3333"})
// Derive diff backgrounds from the theme's own palette.
derivedInsert, derivedDelete, derivedEqual, derivedMissing,
derivedCodeBg, derivedGutterBg, derivedWriteBg := deriveDiffBg(bgPair, successPair, errorPair)
return Theme{
Primary: cfg.Primary.resolve(def.Primary),
Secondary: cfg.Secondary.resolve(def.Secondary),
@@ -627,13 +708,13 @@ func fileConfigToTheme(cfg themeFileConfig) Theme {
Accent: cfg.Accent.resolve(def.Accent),
Highlight: cfg.Highlight.resolve(def.Highlight),
DiffInsertBg: cfg.DiffInsertBg.resolve(def.DiffInsertBg),
DiffDeleteBg: cfg.DiffDeleteBg.resolve(def.DiffDeleteBg),
DiffEqualBg: cfg.DiffEqualBg.resolve(def.DiffEqualBg),
DiffMissingBg: cfg.DiffMissingBg.resolve(def.DiffMissingBg),
CodeBg: cfg.CodeBg.resolve(def.CodeBg),
GutterBg: cfg.GutterBg.resolve(def.GutterBg),
WriteBg: cfg.WriteBg.resolve(def.WriteBg),
DiffInsertBg: cfg.DiffInsertBg.resolve(derivedInsert),
DiffDeleteBg: cfg.DiffDeleteBg.resolve(derivedDelete),
DiffEqualBg: cfg.DiffEqualBg.resolve(derivedEqual),
DiffMissingBg: cfg.DiffMissingBg.resolve(derivedMissing),
CodeBg: cfg.CodeBg.resolve(derivedCodeBg),
GutterBg: cfg.GutterBg.resolve(derivedGutterBg),
WriteBg: cfg.WriteBg.resolve(derivedWriteBg),
Markdown: MarkdownThemeColors{
Text: cfg.Markdown.Text.resolve(def.Markdown.Text),
@@ -651,3 +732,17 @@ func fileConfigToTheme(cfg themeFileConfig) Theme {
},
}
}
// resolveHexPair returns the hex pair from an adaptiveColorPair, falling back
// to defaults when the pair is empty.
func resolveHexPair(a adaptiveColorPair, fallback [2]string) [2]string {
light := a.Light
if light == "" {
light = fallback[0]
}
dark := a.Dark
if dark == "" {
dark = fallback[1]
}
return [2]string{light, dark}
}
+85
View File
@@ -0,0 +1,85 @@
package ui
import (
"testing"
)
func TestParseHexColor(t *testing.T) {
tests := []struct {
hex string
r, g, b int
}{
{"#000000", 0, 0, 0},
{"#ffffff", 255, 255, 255},
{"#1e1e2e", 0x1e, 0x1e, 0x2e},
{"#a6e3a1", 0xa6, 0xe3, 0xa1},
{"#f38ba8", 0xf3, 0x8b, 0xa8},
}
for _, tt := range tests {
r, g, b := parseHexColor(tt.hex)
if r != tt.r || g != tt.g || b != tt.b {
t.Errorf("parseHexColor(%q) = (%d,%d,%d), want (%d,%d,%d)",
tt.hex, r, g, b, tt.r, tt.g, tt.b)
}
}
}
func TestBlendHex(t *testing.T) {
// Blending with 0 amount should return the base color.
got := blendHex("#1e1e2e", "#a6e3a1", 0.0)
if got != "#1e1e2e" {
t.Errorf("blendHex with 0.0 = %q, want #1e1e2e", got)
}
// Blending with 1.0 amount should return the tint color.
got = blendHex("#1e1e2e", "#a6e3a1", 1.0)
if got != "#a6e3a1" {
t.Errorf("blendHex with 1.0 = %q, want #a6e3a1", got)
}
// Blending black and white at 0.5 should give mid gray.
got = blendHex("#000000", "#ffffff", 0.5)
// 127 = int(0 + 255*0.5) — truncated, so #7f7f7f
if got != "#7f7f7f" {
t.Errorf("blendHex black/white at 0.5 = %q, want #7f7f7f", got)
}
}
func TestDeriveDiffBgProducesDifferentColorsPerTheme(t *testing.T) {
// Catppuccin palette
catBg := [2]string{"#eff1f5", "#1e1e2e"}
catSuccess := [2]string{"#40a02b", "#a6e3a1"}
catError := [2]string{"#d20f39", "#f38ba8"}
// KITT palette
kittBg := [2]string{"#F0F0F0", "#0D0D0D"}
kittSuccess := [2]string{"#998800", "#CCAA00"}
kittError := [2]string{"#CC0000", "#FF3333"}
catInsert, catDelete, _, _, _, _, _ := deriveDiffBg(catBg, catSuccess, catError)
kittInsert, kittDelete, _, _, _, _, _ := deriveDiffBg(kittBg, kittSuccess, kittError)
if catInsert == kittInsert {
t.Error("catppuccin DiffInsertBg should differ from kitt DiffInsertBg")
}
if catDelete == kittDelete {
t.Error("catppuccin DiffDeleteBg should differ from kitt DiffDeleteBg")
}
}
func TestMakeThemeDerivesUniqueDiffColors(t *testing.T) {
themes := builtinThemes()
kitt := themes["kitt"]
cat := themes["catppuccin"]
// The catppuccin diff backgrounds should NOT equal the kitt defaults.
if cat.DiffInsertBg == kitt.DiffInsertBg {
t.Error("catppuccin DiffInsertBg should differ from kitt default")
}
if cat.DiffDeleteBg == kitt.DiffDeleteBg {
t.Error("catppuccin DiffDeleteBg should differ from kitt default")
}
if cat.DiffEqualBg == kitt.DiffEqualBg {
t.Error("catppuccin DiffEqualBg should differ from kitt default")
}
}