Merge pull request #72523 from NousResearch/bb/tui-resize-color

fix(tui): paint the OSC-10 default foreground on quantizing terminals
This commit is contained in:
brooklyn! 2026-07-27 02:07:52 -05:00 committed by GitHub
commit d7311b586c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 2 deletions

View file

@ -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)
}
})
})

View file

@ -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'
@ -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) => {

View file

@ -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 = {