hermes-agent/apps/desktop/src/themes/vscode.test.ts
Brooklyn Nicholson 833410e02b feat(desktop): theme the terminal ANSI palette + restyle the Cmd-K / Ctrl-Tab HUDs
Imported VS Code themes now carry their integrated-terminal ANSI palette
(`terminal.ansi*`), keyed to the painted variant (terminal / darkTerminal).
The terminal adopts it when the full base-8 set is present and keeps its VS
Code defaults otherwise; withSurface still owns the background, so the pane
stays translucent.

Pull the command palette and session switcher into a shared top-center HUD
(`floating-hud.ts`): no dim/blur backdrop, one compact text + item-padding
size, sidebar-label-style section headers (brand-tinted, uppercase), and the
themed portal scrollbar.
2026-06-09 23:37:50 -05:00

171 lines
6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { contrastRatio } from './color'
import { convertVscodeColorTheme, parseVscodeTheme, vscodeThemeSlug } from './vscode'
describe('vscodeThemeSlug', () => {
it('namespaces, lowercases, and dashes', () => {
expect(vscodeThemeSlug('Dracula Soft')).toBe('vsc-dracula-soft')
expect(vscodeThemeSlug(' One Dark Pro!! ')).toBe('vsc-one-dark-pro')
})
it('falls back when the name has no usable characters', () => {
expect(vscodeThemeSlug('—')).toBe('vsc-theme')
})
})
describe('parseVscodeTheme (JSONC tolerance)', () => {
it('strips comments and trailing commas', () => {
const text = `{
// a line comment
"name": "Demo",
/* block comment */
"type": "dark",
"colors": {
"editor.background": "#1e1e2e", // inline
},
}`
const parsed = parseVscodeTheme(text)
expect(parsed.name).toBe('Demo')
expect(parsed.colors?.['editor.background']).toBe('#1e1e2e')
})
it('throws on a non-object', () => {
expect(() => parseVscodeTheme('42')).toThrow()
})
})
describe('convertVscodeColorTheme', () => {
const dracula = {
name: 'Dracula',
type: 'dark',
colors: {
'editor.background': '#282a36',
'editor.foreground': '#f8f8f2',
focusBorder: '#6272a4',
'editorWidget.background': '#21222c',
'sideBar.background': '#21222c',
errorForeground: '#ff5555',
// 8-digit hex (alpha) — must flatten over the background.
'panel.border': '#bd93f900'
}
}
it('maps the load-bearing tokens onto the palette', () => {
const { theme } = convertVscodeColorTheme(dracula, { source: 'dracula-theme.theme-dracula' })
expect(theme.name).toBe('vsc-dracula')
expect(theme.label).toBe('Dracula')
expect(theme.description).toContain('dracula-theme.theme-dracula')
expect(theme.colors.background).toBe('#282a36')
expect(theme.colors.foreground).toBe('#f8f8f2')
// One accent drives primary + ring + midground together...
expect(theme.colors.ring).toBe(theme.colors.primary)
expect(theme.colors.midground).toBe(theme.colors.primary)
// ...and it's nudged until it reads on the sidebar it labels (the dim
// focusBorder #6272a4 sits below AA, so it's lifted).
expect(contrastRatio(theme.colors.primary, theme.colors.sidebarBackground!)).toBeGreaterThanOrEqual(4.5)
expect(theme.colors.popover).toBe('#21222c')
expect(theme.colors.sidebarBackground).toBe('#21222c')
expect(theme.colors.destructive).toBe('#ff5555')
})
it('flattens alpha hex over the background (no #rrggbbaa leaks)', () => {
const { theme } = convertVscodeColorTheme(dracula)
expect(theme.colors.border).toMatch(/^#[0-9a-f]{6}$/)
// 00 alpha over the bg means the border collapses to the background.
expect(theme.colors.border).toBe('#282a36')
})
it('renders identically in both modes (single palette in both slots)', () => {
const { theme } = convertVscodeColorTheme(dracula)
expect(theme.darkColors).toBe(theme.colors)
})
it('records derived fallbacks for omitted tokens', () => {
const { derived } = convertVscodeColorTheme({
name: 'Sparse',
type: 'dark',
colors: { 'editor.background': '#101010', 'editor.foreground': '#fafafa' }
})
// No accent/elevated/sidebar/error tokens → all derived. The accent records
// its first candidate (button.background) when none of the family is present.
expect(derived).toContain('button.background')
expect(derived).toContain('editorWidget.background')
expect(derived).toContain('editorError.foreground')
})
it('buckets light vs dark from background luminance when type is absent', () => {
const light = convertVscodeColorTheme({
name: 'Bright',
colors: { 'editor.background': '#ffffff', 'editor.foreground': '#1a1a1a' }
}).theme
// A light background should keep a near-white background, not synth dark.
expect(light.colors.background).toBe('#ffffff')
})
it('throws when there is no colors map', () => {
expect(() => convertVscodeColorTheme({ name: 'Empty' })).toThrow(/colors/)
})
const fullAnsi = {
'terminal.ansiBlack': '#073642',
'terminal.ansiRed': '#dc322f',
'terminal.ansiGreen': '#859900',
'terminal.ansiYellow': '#b58900',
'terminal.ansiBlue': '#268bd2',
'terminal.ansiMagenta': '#d33682',
'terminal.ansiCyan': '#2aa198',
'terminal.ansiWhite': '#eee8d5',
'terminal.ansiBrightBlack': '#002b36',
'terminal.ansiBrightRed': '#cb4b16',
'terminal.ansiBrightGreen': '#586e75',
'terminal.ansiBrightYellow': '#657b83',
'terminal.ansiBrightBlue': '#839496',
'terminal.ansiBrightMagenta': '#6c71c4',
'terminal.ansiBrightCyan': '#93a1a1',
'terminal.ansiBrightWhite': '#fdf6e3'
}
it('lifts the ANSI palette when the full base-8 set is present', () => {
const { theme } = convertVscodeColorTheme({
name: 'Solarized Dark',
type: 'dark',
colors: {
'editor.background': '#002b36',
'editor.foreground': '#93a1a1',
'terminal.foreground': '#839496',
'terminalCursor.foreground': '#93a1a1',
// Alpha selection must survive un-flattened — xterm blends it.
'terminal.selectionBackground': '#073642aa',
...fullAnsi
}
})
expect(theme.terminal?.red).toBe('#dc322f')
expect(theme.terminal?.brightWhite).toBe('#fdf6e3')
expect(theme.terminal?.foreground).toBe('#839496')
expect(theme.terminal?.cursor).toBe('#93a1a1')
expect(theme.terminal?.selectionBackground).toBe('#073642aa')
// No background slot — the pane keeps the live surface (transparency).
expect('background' in (theme.terminal ?? {})).toBe(false)
})
it('keeps the default palette (no terminal slot) when the ANSI set is partial', () => {
const { theme } = convertVscodeColorTheme({
name: 'Half',
type: 'dark',
colors: {
'editor.background': '#101010',
'editor.foreground': '#fafafa',
'terminal.ansiRed': '#ff0000',
'terminal.ansiGreen': '#00ff00'
}
})
expect(theme.terminal).toBeUndefined()
})
})