Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ui-tui/src/__tests__/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,3 +653,31 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => {
expect(color.statusCritical).toBe(fromSkin({ ui_error: '#dd2222' }, {}).color.error)
})
})

describe('themeToneHex', () => {
it('resolves a tone to the literal color it paints as', async () => {
const { themeToneHex } = await importThemeWithCleanEnv()

// 232+ is the grayscale ramp (8 + (n-232)*10); 16-231 is the 6x6x6 cube.
expect(themeToneHex('ansi256(238)')).toBe('#444444')
expect(themeToneHex('ansi256(161)')).toBe('#d7005f')
// An authored hex is already literal.
expect(themeToneHex('#e77fa3')).toBe('#e77fa3')
// No paintable color ⇒ '', which releases the terminal default.
expect(themeToneHex('')).toBe('')
expect(themeToneHex('ansi256(999)')).toBe('')
expect(themeToneHex('inherit')).toBe('')
})

it('makes every tone paintable on a quantizing terminal', async () => {
// The contract OSC-10 depends on: whatever the palette normalizer does to
// a tone, themeToneHex still yields a literal `#rrggbb`. Asserted over the
// whole palette so a new tone can't silently regress the default paint.
const { fromSkin, themeToneHex } = await importThemeWithEnv({ TERM_PROGRAM: 'Apple_Terminal' })
const { color } = fromSkin({ background: '#f6f9fd', ui_text: '#4a4550' }, {})

for (const tone of Object.values(color)) {
expect(themeToneHex(tone)).toMatch(/^#[0-9a-f]{6}$/i)
}
})
})
6 changes: 4 additions & 2 deletions ui-tui/src/app/createGatewayEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { topLevelSubagents } from '../lib/subagentTree.js'
import { isPaintableHex, setTerminalBackground, setTerminalForeground } from '../lib/terminalModes.js'
import { formatAbandonedClarify, formatToolCall, stripAnsi } from '../lib/text.js'
import { bootSeededPin, invalidateBootBackground, writeBootTheme } from '../lib/themeBoot.js'
import { defaultThemeForCurrentBackground, fromSkin, skinIsLight, type Theme } from '../theme.js'
import { defaultThemeForCurrentBackground, fromSkin, skinIsLight, type Theme, themeToneHex } from '../theme.js'
import type { Msg, SubagentProgress, SubagentStatus } from '../types.js'

import { applyDelegationStatus, getDelegationState } from './delegationStore.js'
Expand Down Expand Up @@ -126,11 +126,13 @@ const themesEqual = (a: Theme, b: Theme) => {
// the theme's text color. Without the pair, a dark skin on a light terminal
// leaves default-fg text at the HOST's near-black: invisible. Opt-in stays
// intact: no `background` ⇒ both defaults restore to the terminal's own.
// The text tone resolves through themeToneHex because a limited-palette
// terminal quantizes it to `ansi256(N)`, which OSC-10 cannot speak.
const paintTerminalDefaults = (theme: Theme) => {
const background = lastSkin?.colors?.background ?? ''

setTerminalBackground(background)
setTerminalForeground(isPaintableHex(background) ? theme.color.text : '')
setTerminalForeground(isPaintableHex(background) ? themeToneHex(theme.color.text) : '')
}

const applySkin = (s: GatewaySkin) => {
Expand Down
25 changes: 25 additions & 0 deletions ui-tui/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,31 @@ function normalizeAnsiForeground(color: string): string {
return `ansi256(${ansi})`
}

const ANSI256_RE = /^ansi256\((\d{1,3})\)$/

/**
* The literal `#rrggbb` a theme tone paints as, or '' when it has none.
*
* The inverse of `normalizeAnsiForeground`: tones are not uniformly hex, since
* a limited-palette terminal rewrites the foregrounds to `ansi256(N)`.
* Consumers needing a real color — OSC 10/11, which only speak `#rrggbb` —
* resolve through here rather than hex-testing the tone, which would silently
* skip exactly the terminals that did the quantizing.
*/
export function themeToneHex(tone: string): string {
const ansi = ANSI256_RE.exec(tone.trim())

if (ansi) {
const n = Number(ansi[1])

return n <= 255 ? toHex(xtermEightBitRgb(n)) : ''
}

const rgb = parseColor(tone)

return rgb ? toHex(rgb) : ''
}

// ── Defaults ─────────────────────────────────────────────────────────

const BRAND: ThemeBrand = {
Expand Down
Loading