diff --git a/ui-tui/src/__tests__/theme.test.ts b/ui-tui/src/__tests__/theme.test.ts index 86a9768b0fd..4fe165c8d56 100644 --- a/ui-tui/src/__tests__/theme.test.ts +++ b/ui-tui/src/__tests__/theme.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { DEFAULT_THEME, fromSkin } from '../theme.js' +import { DARK_THEME, DEFAULT_THEME, fromSkin, LIGHT_THEME } from '../theme.js' describe('DEFAULT_THEME', () => { it('has brand defaults', () => { @@ -15,6 +15,26 @@ describe('DEFAULT_THEME', () => { }) }) +describe('LIGHT_THEME', () => { + it('avoids bright-yellow accents unreadable on white backgrounds (#11300)', () => { + expect(LIGHT_THEME.color.gold).not.toBe('#FFD700') + expect(LIGHT_THEME.color.amber).not.toBe('#FFBF00') + expect(LIGHT_THEME.color.dim).not.toBe('#B8860B') + expect(LIGHT_THEME.color.statusWarn).not.toBe('#FFD700') + }) + + it('keeps the same shape as DARK_THEME', () => { + expect(Object.keys(LIGHT_THEME.color).sort()).toEqual(Object.keys(DARK_THEME.color).sort()) + expect(LIGHT_THEME.brand).toEqual(DARK_THEME.brand) + }) +}) + +describe('DEFAULT_THEME aliasing', () => { + it('defaults to DARK_THEME when HERMES_TUI_LIGHT is unset', () => { + expect(DEFAULT_THEME).toBe(DARK_THEME) + }) +}) + describe('fromSkin', () => { it('overrides banner colors', () => { expect(fromSkin({ banner_title: '#FF0000' }, {}).color.gold).toBe('#FF0000') diff --git a/ui-tui/src/theme.ts b/ui-tui/src/theme.ts index 88bc3c39081..386e436f523 100644 --- a/ui-tui/src/theme.ts +++ b/ui-tui/src/theme.ts @@ -78,7 +78,17 @@ function mix(a: string, b: string, t: number) { // ── Defaults ───────────────────────────────────────────────────────── -export const DEFAULT_THEME: Theme = { +const BRAND: ThemeBrand = { + name: 'Hermes Agent', + icon: '⚕', + prompt: '❯', + welcome: 'Type your message or /help for commands.', + goodbye: 'Goodbye! ⚕', + tool: '┊', + helpHeader: '(^_^)? Commands' +} + +export const DARK_THEME: Theme = { color: { gold: '#FFD700', amber: '#FFBF00', @@ -112,20 +122,59 @@ export const DEFAULT_THEME: Theme = { shellDollar: '#4dabf7' }, - brand: { - name: 'Hermes Agent', - icon: '⚕', - prompt: '❯', - welcome: 'Type your message or /help for commands.', - goodbye: 'Goodbye! ⚕', - tool: '┊', - helpHeader: '(^_^)? Commands' - }, + brand: BRAND, bannerLogo: '', bannerHero: '' } +// Light-terminal palette: darker golds/ambers that stay legible on white +// backgrounds. Same shape as DARK_THEME so `fromSkin` still layers on top +// cleanly (#11300). +export const LIGHT_THEME: Theme = { + color: { + gold: '#8B6914', + amber: '#A0651C', + bronze: '#7A4F1F', + cornsilk: '#3D2F13', + dim: '#7A5A0F', + completionBg: '#F5F5F5', + completionCurrentBg: mix('#F5F5F5', '#A0651C', 0.25), + + label: '#7A5A0F', + ok: '#2E7D32', + error: '#C62828', + warn: '#E65100', + + prompt: '#2B2014', + sessionLabel: '#7A5A0F', + sessionBorder: '#7A5A0F', + + statusBg: '#F5F5F5', + statusFg: '#333333', + statusGood: '#2E7D32', + statusWarn: '#8B6914', + statusBad: '#D84315', + statusCritical: '#B71C1C', + selectionBg: '#D4E4F7', + + diffAdded: 'rgb(200,240,200)', + diffRemoved: 'rgb(240,200,200)', + diffAddedWord: 'rgb(27,94,32)', + diffRemovedWord: 'rgb(183,28,28)', + shellDollar: '#1565C0' + }, + + brand: BRAND, + + bannerLogo: '', + bannerHero: '' +} + +const LIGHT_MODE = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_LIGHT ?? '').trim()) + +export const DEFAULT_THEME: Theme = LIGHT_MODE ? LIGHT_THEME : DARK_THEME + // ── Skin → Theme ───────────────────────────────────────────────────── export function fromSkin(