mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-04 02:21:47 +00:00
fix(tui): normalize legacy Terminal.app colors (#17695)
Keep light Terminal.app TUI colors readable by normalizing non-banner theme tokens into ANSI256-safe buckets while preserving truecolor terminals.
This commit is contained in:
parent
31f70d1f2a
commit
4cc6da84a1
9 changed files with 487 additions and 50 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR'] as const
|
||||
const ENV_KEYS = ['COLORTERM', 'FORCE_COLOR', 'HERMES_TUI_TRUECOLOR', 'NO_COLOR', 'TERM', 'TERM_PROGRAM'] as const
|
||||
let importId = 0
|
||||
|
||||
async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
|
||||
const saved: Record<string, string | undefined> = {}
|
||||
|
|
@ -25,11 +26,39 @@ async function withCleanEnv(setup: () => void, body: () => Promise<void>) {
|
|||
}
|
||||
|
||||
describe('forceTruecolor', () => {
|
||||
it('sets COLORTERM=truecolor and FORCE_COLOR=3 when unset', async () => {
|
||||
it('does not force truecolor by default', async () => {
|
||||
await withCleanEnv(
|
||||
() => {},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=default-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does not infer truecolor from Apple Terminal on pre-Tahoe macOS', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.TERM_PROGRAM = 'Apple_Terminal'
|
||||
process.env.TERM = 'xterm-256color'
|
||||
},
|
||||
async () => {
|
||||
const mod = await import('../lib/forceTruecolor.js?t=apple-' + importId++)
|
||||
expect(mod.shouldForceTruecolor({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('sets COLORTERM=truecolor and FORCE_COLOR=3 when explicitly enabled', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=enabled-' + importId++)
|
||||
expect(process.env.COLORTERM).toBe('truecolor')
|
||||
expect(process.env.FORCE_COLOR).toBe('3')
|
||||
}
|
||||
|
|
@ -40,9 +69,10 @@ describe('forceTruecolor', () => {
|
|||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.HERMES_TUI_TRUECOLOR = '0'
|
||||
process.env.TERM_PROGRAM = 'Apple_Terminal'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=optout-' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=optout-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
|
|
@ -53,12 +83,41 @@ describe('forceTruecolor', () => {
|
|||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.NO_COLOR = '1'
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=no-color-' + Date.now())
|
||||
await import('../lib/forceTruecolor.js?t=no-color-' + importId++)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('respects existing FORCE_COLOR unless Hermes truecolor is explicit', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.FORCE_COLOR = ''
|
||||
},
|
||||
async () => {
|
||||
const mod = await import('../lib/forceTruecolor.js?t=force-color-' + importId++)
|
||||
expect(mod.shouldForceTruecolor(process.env)).toBe(false)
|
||||
expect(process.env.COLORTERM).toBeUndefined()
|
||||
expect(process.env.FORCE_COLOR).toBe('')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('lets explicit Hermes truecolor override existing FORCE_COLOR', async () => {
|
||||
await withCleanEnv(
|
||||
() => {
|
||||
process.env.FORCE_COLOR = '0'
|
||||
process.env.HERMES_TUI_TRUECOLOR = '1'
|
||||
},
|
||||
async () => {
|
||||
await import('../lib/forceTruecolor.js?t=explicit-force-' + importId++)
|
||||
expect(process.env.COLORTERM).toBe('truecolor')
|
||||
expect(process.env.FORCE_COLOR).toBe('3')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ const RELEVANT_ENV = [
|
|||
'HERMES_TUI_THEME',
|
||||
'HERMES_TUI_BACKGROUND',
|
||||
'COLORFGBG',
|
||||
'COLORTERM',
|
||||
'TERM_PROGRAM'
|
||||
] as const
|
||||
|
||||
async function importThemeWithCleanEnv() {
|
||||
async function importThemeWithEnv(env: Partial<Record<(typeof RELEVANT_ENV)[number], string>> = {}) {
|
||||
for (const key of RELEVANT_ENV) {
|
||||
vi.stubEnv(key, '')
|
||||
vi.stubEnv(key, env[key] ?? '')
|
||||
}
|
||||
|
||||
vi.resetModules()
|
||||
|
|
@ -29,6 +30,10 @@ async function importThemeWithCleanEnv() {
|
|||
return import('../theme.js')
|
||||
}
|
||||
|
||||
async function importThemeWithCleanEnv() {
|
||||
return importThemeWithEnv()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.resetModules()
|
||||
|
|
@ -84,6 +89,12 @@ describe('detectLightMode', () => {
|
|||
expect(detectLightMode({})).toBe(false)
|
||||
})
|
||||
|
||||
it('defaults Apple Terminal to light when no stronger signal is present', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
|
||||
expect(detectLightMode({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(true)
|
||||
})
|
||||
|
||||
it('honors HERMES_TUI_LIGHT on/off', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
|
||||
|
|
@ -159,8 +170,8 @@ describe('detectLightMode', () => {
|
|||
|
||||
it('treats COLORFGBG as authoritative when present so it dominates the TERM_PROGRAM allow-list', async () => {
|
||||
const { detectLightMode } = await importThemeWithCleanEnv()
|
||||
// Inject a light-default allow-list so the precedence test is
|
||||
// meaningful even though the production allow-list is empty.
|
||||
// Injecting the allow-list keeps this precedence rule explicit even if
|
||||
// production defaults change.
|
||||
const allowList = new Set(['Apple_Terminal'])
|
||||
|
||||
// Sanity: the allow-list alone WOULD turn this terminal light.
|
||||
|
|
@ -221,6 +232,40 @@ describe('fromSkin', () => {
|
|||
expect(fromSkin({}, {}).brand.icon).toBe(DEFAULT_THEME.brand.icon)
|
||||
})
|
||||
|
||||
it('normalizes non-banner foregrounds on light Apple Terminal', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: 'Apple_Terminal' })
|
||||
|
||||
const theme = fromSkin({
|
||||
banner_accent: '#FFBF00',
|
||||
banner_border: '#CD7F32',
|
||||
banner_dim: '#B8860B',
|
||||
banner_text: '#FFF8DC',
|
||||
banner_title: '#FFD700',
|
||||
prompt: '#FFF8DC'
|
||||
}, {})
|
||||
|
||||
expect(theme.color.primary).toBe('#FFD700')
|
||||
expect(theme.color.accent).toBe('#FFBF00')
|
||||
expect(theme.color.border).toBe('#CD7F32')
|
||||
expect(theme.color.muted).toBe('ansi256(245)')
|
||||
expect(theme.color.text).toBe('ansi256(136)')
|
||||
expect(theme.color.prompt).toBe('ansi256(136)')
|
||||
})
|
||||
|
||||
it('does not normalize light Apple Terminal when truecolor is advertised', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ COLORTERM: 'truecolor', TERM_PROGRAM: 'Apple_Terminal' })
|
||||
const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
|
||||
|
||||
expect(theme.color.text).toBe('#FFF8DC')
|
||||
})
|
||||
|
||||
it('normalizes Apple Terminal names before matching', async () => {
|
||||
const { fromSkin } = await importThemeWithEnv({ TERM_PROGRAM: ' Apple_Terminal ' })
|
||||
const theme = fromSkin({ banner_text: '#FFF8DC' }, {})
|
||||
|
||||
expect(theme.color.text).toBe('ansi256(136)')
|
||||
})
|
||||
|
||||
it('passes banner logo/hero', async () => {
|
||||
const { fromSkin } = await importThemeWithCleanEnv()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue