feat(themes): element tokens (ui_tool, ui_thinking) + skinnable diffs
Theming was semantic-only: the gold tool `●` was `accent`, shared with headings/links/chevrons, so "recolor tool calls" was impossible and the agent had no key to point at. Add `ui_tool` (● + tool spinner) and `ui_thinking` (reasoning body) tokens that fall back to accent/muted — defaults unchanged, but now independently settable. Make diffs skinnable too (`diff_*`), which fromSkin previously hardcoded. Document the full element→key map in the skill so Hermes knows which knob turns what.
This commit is contained in:
parent
727f6704a7
commit
30ee6f749d
|
|
@ -37,6 +37,13 @@ export const SKIN_COLOR_TOKENS = [
|
|||
'ui_warn',
|
||||
'ui_error',
|
||||
'ui_label',
|
||||
// Element-specific (fall back to accent/muted when unset).
|
||||
'ui_tool',
|
||||
'ui_thinking',
|
||||
'diff_added',
|
||||
'diff_removed',
|
||||
'diff_added_word',
|
||||
'diff_removed_word',
|
||||
// CLI / TUI chrome.
|
||||
'prompt',
|
||||
'input_rule',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ All fields are optional. Missing values inherit from the ``default`` skin.
|
|||
ui_ok: "#4caf50" # Success indicators
|
||||
ui_error: "#ef5350" # Error indicators
|
||||
ui_warn: "#ffa726" # Warning indicators
|
||||
ui_tool: "#FFBF00" # Tool-call markers (● / spinner); falls back to ui_accent
|
||||
ui_thinking: "#CC9B1F" # Reasoning/thinking text; falls back to banner_dim
|
||||
diff_added: "#dcffdc" # Diff added-line background (TUI)
|
||||
diff_removed: "#ffdcdc" # Diff removed-line background
|
||||
diff_added_word: "#248a3d" # Diff added word-level foreground
|
||||
diff_removed_word: "#cf222e" # Diff removed word-level foreground
|
||||
prompt: "#FFF8DC" # Prompt text color
|
||||
input_rule: "#CD7F32" # Input area horizontal rule
|
||||
response_border: "#FFD700" # Response box border (ANSI)
|
||||
|
|
|
|||
|
|
@ -41,26 +41,33 @@ editors or ship built-in presets.
|
|||
3. `write_file` it to `<hermes-home>/skins/<name>.yaml`.
|
||||
4. Activate it (see Procedure). Confirm the change landed.
|
||||
|
||||
## Quick Reference
|
||||
## Quick Reference — element → key
|
||||
|
||||
Load-bearing color keys (hex, `#rrggbb`). The desktop GUI derives its whole
|
||||
palette from these; the TUI and CLI read the terminal-oriented ones directly.
|
||||
Hex (`#rrggbb`). Theming is **semantic**: one key colors every element that plays
|
||||
that role, so match the element to its key. To recolor a specific element, set the
|
||||
key in its row (element-specific keys fall back to the shared one when unset).
|
||||
|
||||
| Key | Drives |
|
||||
|---|---|
|
||||
| `background` | Base surface. Paints the whole TUI (OSC 11) + seeds the GUI. Set it. |
|
||||
| `ui_accent` / `banner_accent` | Brand accent: buttons, rings, primary. |
|
||||
| `banner_title` | Headings / primary text. |
|
||||
| `banner_text` / `ui_text` | Body foreground. |
|
||||
| `banner_border` / `ui_border` | Borders. |
|
||||
| `banner_dim` | Muted / secondary text. |
|
||||
| `ui_ok` / `ui_warn` / `ui_error` | Semantic status colors. |
|
||||
| `status_bar_bg` / `status_bar_text` | TUI status bar. |
|
||||
| `response_border` | CLI response box. |
|
||||
| Visible element | Key to set | Falls back to |
|
||||
|---|---|---|
|
||||
| App background (whole TUI + GUI) | `background` | terminal default |
|
||||
| **Tool-call marker** (`●`, tool spinner) | `ui_tool` | `ui_accent` |
|
||||
| **Thinking / reasoning text** | `ui_thinking` | `banner_dim` |
|
||||
| Accent — headings, links, chevrons, `Σ` | `ui_accent` / `banner_accent` | — |
|
||||
| Heading / primary text | `banner_title` / `ui_primary` | — |
|
||||
| Body / label text, user messages | `ui_text` / `banner_text`, `ui_label` | — |
|
||||
| Muted / secondary, tree connectors | `banner_dim` | — |
|
||||
| Borders, rules, gutters | `ui_border` / `banner_border` | — |
|
||||
| Prompt symbol color | `prompt` | `banner_text` |
|
||||
| Success / warn / error | `ui_ok` / `ui_warn` / `ui_error` | — |
|
||||
| Status bar text + usage | `status_bar_text`, `status_bar_good/warn/bad/critical` | — |
|
||||
| Diff add/remove (line + word) | `diff_added` / `diff_removed` / `diff_added_word` / `diff_removed_word` | built-in |
|
||||
| Completion menu | `completion_menu_bg` / `completion_menu_current_bg` / `…_meta_bg` | — |
|
||||
|
||||
`branding` (`agent_name`, `welcome`, `goodbye`, `prompt_symbol`, `help_header`),
|
||||
`spinner` (faces/verbs/wings), and `tool_prefix` are optional flavor. See the
|
||||
full schema in `hermes_cli/skin_engine.py`.
|
||||
Note the sharing: `ui_accent` colors tool markers **and** headings/links/chevrons,
|
||||
so to recolor *only* tool calls (the classic "change the gold `●`") set `ui_tool`.
|
||||
`branding` (`agent_name`, `prompt_symbol`, `welcome`, `goodbye`, `help_header`),
|
||||
`spinner`, and `tool_prefix` are optional flavor; full schema in
|
||||
`hermes_cli/skin_engine.py`.
|
||||
|
||||
## Procedure
|
||||
|
||||
|
|
|
|||
|
|
@ -533,6 +533,35 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => {
|
|||
expect(defaultThemeForCurrentBackground({ HERMES_TUI_BACKGROUND: '#ffffff' }).color).toEqual(LIGHT_THEME.color)
|
||||
})
|
||||
|
||||
it('gives tool + thinking their own keys, defaulting to accent + muted', async () => {
|
||||
const { fromSkin } = await importThemeWithCleanEnv()
|
||||
|
||||
// Independent override: recolor tool markers without touching accent.
|
||||
const themed = fromSkin({ ui_accent: '#111111', ui_tool: '#ff0000', ui_thinking: '#00ff00' }, {})
|
||||
expect(themed.color.tool).toBe('#ff0000')
|
||||
expect(themed.color.thinking).toBe('#00ff00')
|
||||
expect(themed.color.accent).toBe('#111111')
|
||||
|
||||
// Default: tool follows accent, thinking follows muted.
|
||||
const fallback = fromSkin({ ui_accent: '#abcdef', banner_dim: '#123456' }, {})
|
||||
expect(fallback.color.tool).toBe('#abcdef')
|
||||
expect(fallback.color.thinking).toBe('#123456')
|
||||
})
|
||||
|
||||
it('lets skins override diff colors', async () => {
|
||||
const { fromSkin } = await importThemeWithCleanEnv()
|
||||
|
||||
const { color } = fromSkin(
|
||||
{ diff_added: '#0a0', diff_removed: '#a00', diff_added_word: '#0f0', diff_removed_word: '#f00' },
|
||||
{}
|
||||
)
|
||||
|
||||
expect(color.diffAdded).toBe('#0a0')
|
||||
expect(color.diffRemoved).toBe('#a00')
|
||||
expect(color.diffAddedWord).toBe('#0f0')
|
||||
expect(color.diffRemovedWord).toBe('#f00')
|
||||
})
|
||||
|
||||
it('maps the status bar from skin status_bar_* keys', async () => {
|
||||
const { fromSkin } = await importThemeWithCleanEnv()
|
||||
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ function SubagentAccordion({
|
|||
color={t.color.text}
|
||||
content={
|
||||
<>
|
||||
<Text color={t.color.accent}>● </Text>
|
||||
<Text color={t.color.tool}>● </Text>
|
||||
{line}
|
||||
</>
|
||||
}
|
||||
|
|
@ -640,22 +640,22 @@ export const Thinking = memo(function Thinking({
|
|||
{preview ? (
|
||||
mode === 'full' ? (
|
||||
lines.map((line, index) => (
|
||||
<Text color={t.color.muted} key={index} wrap="wrap-trim">
|
||||
<Text color={t.color.thinking} key={index} wrap="wrap-trim">
|
||||
{line || ' '}
|
||||
{index === lines.length - 1 ? (
|
||||
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
|
||||
<StreamCursor color={t.color.thinking} streaming={streaming} visible={active} />
|
||||
) : null}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
<Text color={t.color.thinking} wrap="truncate-end">
|
||||
{preview}
|
||||
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
|
||||
<StreamCursor color={t.color.thinking} streaming={streaming} visible={active} />
|
||||
</Text>
|
||||
)
|
||||
) : (
|
||||
<Text color={t.color.muted}>
|
||||
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
|
||||
<Text color={t.color.thinking}>
|
||||
<StreamCursor color={t.color.thinking} streaming={streaming} visible={active} />
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
|
@ -855,7 +855,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
: [],
|
||||
content: (
|
||||
<>
|
||||
<Spinner color={t.color.accent} variant="tool" /> {label}
|
||||
<Spinner color={t.color.tool} variant="tool" /> {label}
|
||||
{tool.startedAt ? ` (${fmtElapsed(now - tool.startedAt)})` : ''}
|
||||
</>
|
||||
)
|
||||
|
|
@ -1072,7 +1072,7 @@ export const ToolTrail = memo(function ToolTrail({
|
|||
color={group.color}
|
||||
content={
|
||||
<>
|
||||
<Text color={t.color.accent}>● </Text>
|
||||
<Text color={t.color.tool}>● </Text>
|
||||
{toolLabel(group)}
|
||||
{isDelegateGroup ? (
|
||||
<Text color={t.color.statusFg} dim>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ export interface ThemeColors {
|
|||
error: string
|
||||
warn: string
|
||||
|
||||
/** Tool-call markers (● bullet, tool spinner). Defaults to `accent`. */
|
||||
tool: string
|
||||
/** Reasoning/thinking body text. Defaults to `muted`. */
|
||||
thinking: string
|
||||
|
||||
prompt: string
|
||||
sessionLabel: string
|
||||
sessionBorder: string
|
||||
|
|
@ -82,10 +87,11 @@ const ANSI_NORMALIZED_FOREGROUNDS: readonly (keyof ThemeColors)[] = [
|
|||
'statusWarn',
|
||||
'statusBad',
|
||||
'statusCritical',
|
||||
'shellDollar'
|
||||
'shellDollar',
|
||||
'tool'
|
||||
]
|
||||
|
||||
const ANSI_MUTED_FOREGROUNDS: readonly (keyof ThemeColors)[] = ['muted', 'sessionLabel', 'sessionBorder']
|
||||
const ANSI_MUTED_FOREGROUNDS: readonly (keyof ThemeColors)[] = ['muted', 'sessionLabel', 'sessionBorder', 'thinking']
|
||||
|
||||
function xtermEightBitRgb(colorNumber: number): [number, number, number] {
|
||||
if (colorNumber >= 232) {
|
||||
|
|
@ -299,6 +305,11 @@ export function buildPalette(seeds: ThemeSeeds, isLight: boolean): ThemeColors {
|
|||
error: seeds.error,
|
||||
warn: seeds.warn,
|
||||
|
||||
// Element tokens: independently settable, but default to their semantic
|
||||
// parents (tool marker → accent, reasoning body → muted).
|
||||
tool: seeds.accent,
|
||||
thinking: muted,
|
||||
|
||||
prompt: seeds.prompt ?? seeds.text,
|
||||
// sessionLabel/sessionBorder track the muted tone — "same role, same
|
||||
// colour" by design (#11300).
|
||||
|
|
@ -836,7 +847,15 @@ export function fromSkin(
|
|||
sessionBorder: c('session_border') ?? c('banner_dim') ?? derived.sessionBorder,
|
||||
statusBg: c('status_bar_bg') ?? surface,
|
||||
statusFg: c('status_bar_text') ?? derived.statusFg,
|
||||
selectionBg: c('selection_bg') ?? c('completion_menu_current_bg') ?? derived.selectionBg
|
||||
selectionBg: c('selection_bg') ?? c('completion_menu_current_bg') ?? derived.selectionBg,
|
||||
// Element tokens + skinnable diffs (theme-sdk): overridable, else the
|
||||
// derived defaults (tool→accent, thinking→muted, diff_* → DIFF_* ladder).
|
||||
tool: c('ui_tool') ?? derived.tool,
|
||||
thinking: c('ui_thinking') ?? derived.thinking,
|
||||
diffAdded: c('diff_added') ?? derived.diffAdded,
|
||||
diffRemoved: c('diff_removed') ?? derived.diffRemoved,
|
||||
diffAddedWord: c('diff_added_word') ?? derived.diffAddedWord,
|
||||
diffRemovedWord: c('diff_removed_word') ?? derived.diffRemovedWord
|
||||
}
|
||||
|
||||
// 4. Guard: contrast floors against the real background + fill polarity.
|
||||
|
|
@ -851,11 +870,12 @@ export function fromSkin(
|
|||
return normalizeThemeForAnsiLightTerminal(
|
||||
{
|
||||
// The element tokens theme-sdk introduced (ui_primary, ui_text,
|
||||
// ui_border, ui_ok/warn/error, shell_dollar, status_bar_*) are read
|
||||
// above into `seeds` and flow through buildPalette → adaptColorsToBackground,
|
||||
// so `adapted` already honors them AND applies #20379's contrast/polarity
|
||||
// machinery. Emitting a hand-mapped color block here would bypass that
|
||||
// adaptation and regress theme quality.
|
||||
// ui_border, ui_ok/warn/error, ui_tool, ui_thinking, shell_dollar,
|
||||
// status_bar_*, diff_*) are read into `seeds`/`assembled` above and
|
||||
// flow through buildPalette → adaptColorsToBackground, so `adapted`
|
||||
// already honors them AND applies #20379's contrast/polarity machinery.
|
||||
// Emitting a hand-mapped color block here would bypass that adaptation
|
||||
// and regress theme quality.
|
||||
color: adapted,
|
||||
|
||||
brand: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue