From 50170fdd2d4d0bec6ab2bf6efecdd1b93064a736 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 21 Jul 2026 20:52:40 -0500 Subject: [PATCH] fix(themes): reconcile element/syntax tokens with main's derive+adapt pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ui-tui/src/__tests__/theme.test.ts | 26 ++++++++++++++++++-------- ui-tui/src/theme.ts | 13 +++++++++---- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ui-tui/src/__tests__/theme.test.ts b/ui-tui/src/__tests__/theme.test.ts index 3ba6513e2ab8..ad54c285fa64 100644 --- a/ui-tui/src/__tests__/theme.test.ts +++ b/ui-tui/src/__tests__/theme.test.ts @@ -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) }) }) diff --git a/ui-tui/src/theme.ts b/ui-tui/src/theme.ts index de18534d6582..bfd777a2f0c3 100644 --- a/ui-tui/src/theme.ts +++ b/ui-tui/src/theme.ts @@ -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.