fix(themes): reconcile element/syntax tokens with main's derive+adapt pipeline

Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys
flow through buildPalette → adaptColorsToBackground instead of a hand-mapped
color block, so they inherit #20379's contrast/polarity machinery. thinking
and syntaxComment track the EFFECTIVE muted (banner_dim override included);
the skin's `background` feeds the surface (it also paints the terminal via
OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the
routing/independence contracts rather than pre-adaptation hexes.
This commit is contained in:
Brooklyn Nicholson 2026-07-21 20:52:40 -05:00
parent 428a0534ee
commit 50170fdd2d
2 changed files with 27 additions and 12 deletions

View file

@ -536,16 +536,21 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => {
it('gives tool + thinking their own keys, defaulting to accent + muted', async () => {
const { fromSkin } = await importThemeWithCleanEnv()
// Independent override: recolor tool markers without touching accent.
const themed = fromSkin({ ui_accent: '#111111', ui_tool: '#ff0000', ui_thinking: '#00ff00' }, {})
// Independent override: recoloring tool/thinking doesn't leak into accent.
// (Values flow through #20379's contrast adaptation, so assert the
// independence contract, not raw pre-adaptation hexes.)
const themed = fromSkin({ ui_accent: '#3aa0ff', ui_tool: '#ff0000', ui_thinking: '#00ff00' }, {})
const baseline = fromSkin({ ui_accent: '#3aa0ff' }, {})
expect(themed.color.tool).toBe('#ff0000')
expect(themed.color.thinking).toBe('#00ff00')
expect(themed.color.accent).toBe('#111111')
expect(themed.color.tool).not.toBe(themed.color.accent)
expect(themed.color.accent).toBe(baseline.color.accent) // override didn't touch accent
// Default: tool follows accent, thinking follows muted.
const fallback = fromSkin({ ui_accent: '#abcdef', banner_dim: '#123456' }, {})
expect(fallback.color.tool).toBe('#abcdef')
expect(fallback.color.thinking).toBe('#123456')
// Default: tool follows accent, thinking follows muted — same source →
// identical after adaptation.
const fallback = fromSkin({ ui_accent: '#3aa0ff', banner_dim: '#8a8a8a' }, {})
expect(fallback.color.tool).toBe(fallback.color.accent)
expect(fallback.color.thinking).toBe(fallback.color.muted)
})
it('gives code syntax its own keys, defaulting to accent/text/border/muted', async () => {
@ -602,8 +607,13 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => {
const { fromSkin } = await importThemeWithCleanEnv()
const { color } = fromSkin({ background: '#0a0a0a', banner_text: '#fafafa', ui_error: '#dd2222' }, {})
// background paints the surface → status/completion bg; banner_text → status
// fg; ui_error → critical. Semantic hues flow through contrast adaptation,
// so `statusCritical` is asserted to track `ui_error` identically rather
// than pinning an adapted hex.
expect(color.statusBg).toBe('#0a0a0a')
expect(color.completionBg).toBe('#0a0a0a')
expect(color.statusFg).toBe('#fafafa')
expect(color.statusCritical).toBe('#dd2222')
expect(color.statusCritical).toBe(fromSkin({ ui_error: '#dd2222' }, {}).color.error)
})
})

View file

@ -840,7 +840,10 @@ export function fromSkin(
// 3. Authored tone overrides: a skin may still hand-tune any tone; the
// derived ladder is the default, not a cage. Chip/selection re-derive
// from the FINAL surface so dependents stay coherent with overrides.
const surface = c('completion_menu_bg') ?? derived.completionBg
// `background` is theme-sdk's cross-surface base: the TUI paints the
// terminal with it (applySkin → setTerminalBackground), so panels/status
// must sit on it too — fall the surface back to it below completion_menu_bg.
const surface = c('completion_menu_bg') ?? c('background') ?? derived.completionBg
// Re-mix the chip only when the skin authored its own surface; otherwise
// the derived value already carries the identity seeds (e.g. Hermes navy).
@ -859,12 +862,14 @@ export function fromSkin(
sessionLabel: c('session_label') ?? c('banner_dim') ?? derived.sessionLabel,
sessionBorder: c('session_border') ?? c('banner_dim') ?? derived.sessionBorder,
statusBg: c('status_bar_bg') ?? surface,
statusFg: c('status_bar_text') ?? derived.statusFg,
statusFg: c('status_bar_text') ?? c('ui_text') ?? c('banner_text') ?? derived.statusFg,
selectionBg: c('selection_bg') ?? c('completion_menu_current_bg') ?? derived.selectionBg,
// Element tokens + skinnable diffs (theme-sdk): overridable, else the
// derived defaults (tool→accent, thinking→muted, diff_* → DIFF_* ladder).
// thinking tracks the EFFECTIVE muted (banner_dim override included), not
// just derived.muted, so recoloring muted carries the reasoning body with it.
tool: c('ui_tool') ?? derived.tool,
thinking: c('ui_thinking') ?? derived.thinking,
thinking: c('ui_thinking') ?? c('banner_dim') ?? derived.thinking,
diffAdded: c('diff_added') ?? derived.diffAdded,
diffRemoved: c('diff_removed') ?? derived.diffRemoved,
diffAddedWord: c('diff_added_word') ?? derived.diffAddedWord,
@ -873,7 +878,7 @@ export function fromSkin(
syntaxString: c('syntax_string') ?? derived.syntaxString,
syntaxNumber: c('syntax_number') ?? derived.syntaxNumber,
syntaxKeyword: c('syntax_keyword') ?? derived.syntaxKeyword,
syntaxComment: c('syntax_comment') ?? derived.syntaxComment
syntaxComment: c('syntax_comment') ?? c('banner_dim') ?? derived.syntaxComment
}
// 4. Guard: contrast floors against the real background + fill polarity.