feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live - #68857
Merged
Conversation
Contributor
૮ >ﻌ< ა ci reviewrunning on ef81d9d CI timingsCI timings · View jobWall time 9m36s vs 10m10s (-5.6%). 8 job(s) slower, 11 faster, 2 unchanged.
|
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces a cross-surface “skin” contract so a single resolved Hermes skin (from the Python skin engine + $HERMES_HOME/skins/<name>.yaml) can theme the CLI, TUI, and desktop consistently. This formalizes the wire shape in @hermes/shared and wires the desktop to ingest backend skins via gateway.ready / skin.changed and a config.get skin fallback.
Changes:
- Adds canonical
HermesSkin/ token types in@hermes/sharedand updates the TUI to consume the shared shape. - Extends the TUI skin→theme mapping (status bar + background-related fallbacks) with tests.
- Implements backend→desktop skin ingestion (registry merge + guarded apply) plus converter + tests; extends backend
config.get skinto include the resolved palette.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui-tui/src/theme.ts | Switches fromSkin to shared skin types and maps additional status bar/background keys. |
| ui-tui/src/gatewayTypes.ts | Replaces local GatewaySkin with canonical HermesSkin. |
| ui-tui/src/tests/theme.test.ts | Adds test coverage for the new status bar mappings/fallbacks. |
| tui_gateway/server.py | Makes config.get skin return { value, skin: resolve_skin() } (adds full resolved palette). |
| skills/hermes-themes/templates/skin.yaml | Adds a skin authoring template for prompt-driven/user-driven theme creation. |
| skills/hermes-themes/SKILL.md | Adds a skill doc teaching Hermes how to author + activate skins. |
| hermes_cli/skin_engine.py | Updates skin engine docs to reflect cross-surface SDK intent. |
| apps/shared/src/skin.ts | Introduces the canonical cross-surface skin contract + token lists/types. |
| apps/shared/src/index.ts | Re-exports the new skin contract from the shared package entrypoint. |
| apps/shared/package.json | Exposes ./skin export path for consumers. |
| apps/desktop/src/themes/user-themes.ts | Merges backend-provided skins into theme resolution + listing. |
| apps/desktop/src/themes/skin.ts | Adds HermesSkin → DesktopTheme converter. |
| apps/desktop/src/themes/skin.test.ts | Adds converter invariants tests. |
| apps/desktop/src/themes/index.ts | Updates theme exports (adds backend ingest + converter; removes dead hook export). |
| apps/desktop/src/themes/context.tsx | Consumes backend theme store reactivity and applies backend-driven pending skin switches. |
| apps/desktop/src/themes/backend-sync.ts | Implements backend skin ingestion, registry merge, and guarded apply logic. |
| apps/desktop/src/themes/backend-sync.test.ts | Adds tests for seeding/apply guards and registry behavior. |
| apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts | Ingests backend skins on gateway.ready / skin.changed events. |
| apps/desktop/src/app/session/hooks/use-hermes-config.ts | Adds a post-turn config.get skin fetch to catch config.yaml-driven skin changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
OutThisLife
force-pushed
the
bb/theme-sdk
branch
from
July 22, 2026 01:54
27854b8 to
b0dc95c
Compare
… desktop Make the Python skin engine the single source of truth for a canonical theme shape consumed by every surface, so a skin authored in $HERMES_HOME/skins/*.yaml (by a user or by Hermes from a prompt) themes the CLI, TUI, and desktop GUI at once — the theme analogue of the plugin SDK. - @hermes/shared: canonical `HermesSkin` token shape + `SKIN_COLOR_TOKENS` enum, consumed by both TS surfaces (TUI `GatewaySkin` and desktop dedup onto it). - Desktop: `skinToDesktopTheme` resolver (skin → CSS-var palette, VS Code-style derive-from-seed) + `backend-sync` that registers backend skins into the theme registry (Appearance/Cmd-K/`/skin`) and applies on a real change. Seeds on gateway.ready (never stomps a persisted pick), applies on skin.changed and the post-turn `config.get skin` poll (catch-all for agent-edited config.yaml). - TUI: `fromSkin` now maps the status bar + `background` keys it was dropping. - Gateway: `config.get skin` also returns the full resolved palette (additive). - Skill: `hermes-themes` teaches the agent to author + activate a skin. Each surface keeps its own normalizing resolver (ansi for the TUI, CSS vars for the desktop, prompt_toolkit/Rich for the CLI).
…aml hand-edit
The skill told the agent to `patch` display.skin into config.yaml; a stray indent
corrupts the file and breaks the live gateway (the reported "/ menu broke"), and
a raw file edit never live-applies in a running CLI/TUI ("nothing happened").
Route activation through the safe writer (`hermes config set display.skin`), and
state plainly that a tool call can't hot-switch a running CLI/TUI — the user runs
`/skin <name>` (desktop still auto-repaints on the next turn).
…cher A skin Hermes activates (`hermes config set display.skin X`) or recolors in place now goes live on every surface (CLI, TUI, desktop) within ~half a second, on its own — no `/skin`, no tool-hook timing, no user action. A gateway daemon polls the resolved skin signature `(name, active-file mtime)` every 0.5s and broadcasts `skin.changed` on any real move — a name switch OR a live color edit to the active skin. It routes through the SAME path `/skin` uses, so all surfaces repaint identically. The watcher seeds its baseline at gateway.ready (stdio + ws) so it only fires on a real change; the `/skin` RPC seeds the baseline too so it never double-broadcasts. Subsumes the desktop's post-turn `config.get skin` poll (its skin.changed handler already applies).
The TUI inherited the terminal's background; now a skin's `background` paints the whole surface via OSC 11 when a skin is applied, and clears back to the terminal default (OSC 111) on revert and on exit (ridden in through resetTerminalModes). Opt-in: a skin with no `background` leaves the terminal untouched, and the restore only fires if we actually painted. Desktop already themed its own bg; this closes the loop so Hermes owns its background on every surface.
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.
Changing one color ("make the tool ● cyan") forked `default` — which has no
`background` — so applying it reset the terminal to its own (black) default and
dropped the active skin's palette. Teach the skill to edit the active skin's file
in place for a tweak (watcher repaints on the mtime bump), and to fork a built-in
only by carrying its full palette. Hard pitfall: never fork `default` for a tweak.
…ntouched Changing a single color kept wrecking the rest because the agent hand-authored a new skin (often from `default`, which has no `background`, resetting the terminal to black). Add `hermes skin set <key> <hex>`: edits the ACTIVE skin's one key in place (a built-in is forked into an editable copy carrying its full palette), so everything else — background included — is preserved. Plus `skin use` / `skin list`. The skill now points tweaks at this command instead of hand-authoring.
Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't be themed independently. Add syntax_string/number/keyword/comment skin keys → syntax* theme tokens (defaulting to those brand tokens, so defaults are unchanged) and point the highlighter at them. Documented in the element→key map.
… pipeline Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys flow through buildPalette → adaptColorsToBackground instead of a hand-mapped color block, so they inherit #20379's contrast/polarity machinery. thinking and syntaxComment track the EFFECTIVE muted (banner_dim override included); the skin's `background` feeds the surface (it also paints the terminal via OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the routing/independence contracts rather than pre-adaptation hexes.
ingestBackendSkin returned early for name === 'default' even when apply=true, so a real runtime switch to the default skin (/skin default on CLI/TUI, or config.set display.skin=default) emitted skin.changed but never repainted the desktop. 'default' is no-opinion on the PALETTE (the desktop keeps its own nous default, so we still never register a converted theme under it), but it IS a valid apply TARGET: setTheme normalizes 'default' -> nous, so switching back repaints to the desktop default. Skip only the registry step for 'default' and let it flow through the apply guard. Addresses Copilot review.
OutThisLife
force-pushed
the
bb/theme-sdk
branch
from
July 22, 2026 02:00
4ac9db7 to
300a0f1
Compare
austinpickett
previously approved these changes
Jul 22, 2026
The new hermes skin subcommand must be declared so startup plugin discovery can skip when the user targets it.
OutThisLife
enabled auto-merge
July 22, 2026 02:21
austinpickett
approved these changes
Jul 22, 2026
This was referenced Jul 22, 2026
Merged
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Turns theming into an SDK with one canonical shape: the Python skin engine is the single source of truth, and a skin authored in
$HERMES_HOME/skins/<name>.yaml— by a user or by Hermes from a prompt — themes the CLI, TUI, and desktop GUI, and applies live across every surface. The theme analogue of the plugin SDK: drop a file, everything picks it up.The contract (
@hermes/shared)apps/shared/src/skin.ts— canonicalHermesSkinpayload +SkinColors/SkinBrandingand theSKIN_COLOR_TOKENSenum. Every TypeScript surface consumes this one shape (TUI'sGatewaySkinand the desktop dedup onto it). Each surface owns a normalizing resolver:hermes_cli/skin_engine.py→ prompt_toolkit / RichfromSkin→ ansi-safeTheme(Ink)skinToDesktopTheme→ CSS variables (Tailwind/shadcn), registered viabackend-syncinto Appearance / Cmd-K //skinLive application (Hermes applies it itself)
A gateway skin watcher polls the resolved skin signature
(name, active-file mtime)and broadcastsskin.changedon any real move — so when Hermes runshermes config set display.skin Xor edits the active skin's colors, every surface repaints within ~a second, through the same path/skinuses. No slash command, no tool-hook timing. Seeded atgateway.ready(stdio + ws) so it only fires on a change.Deterministic tweaks —
hermes skin sethermes skin set <key> <hex>edits the active skin's one key in place (a built-in is forked into an editable copy carrying its full palette), so changing one color never disturbs the rest — background included. Plushermes skin use/hermes skin list. This replaces error-prone hand-authoring for tweaks.Own background (TUI)
A skin's
backgroundnow paints the whole TUI via OSC 11 (opt-in; cleared on revert and on exit throughresetTerminalModes). Desktop already themed its own background — this closes the loop.Element-level tokens (semantic + granular)
Theming stays semantic, with independent knobs where they matter, each falling back to a shared token so defaults are unchanged:
ui_tool— tool●marker + tool spinner (wasaccent)ui_thinking— reasoning/thinking body (wasmuted)diff_added/diff_removed/diff_added_word/diff_removed_word— were hardcodedsyntax_string/syntax_number/syntax_keyword/syntax_comment— code highlighting (was brand tokens)The
hermes-themesskill documents the full element → key map so Hermes knows which knob turns what.Test plan
fromSkin(element tokens, diffs, syntax), OSC 11 background paint/clear/restore — 1222 passinghermes skin set(in-place preserves bg, built-in fork, hex validation), end-to-end config→skin.changed with real files@hermes/shared, ui-tui, desktop-touched files) + lint cleanKnown follow-ups (not this PR)
ui_tool/ui_thinking/diff_*/syntax_*) — the desktop renders those off its own@assistant-uiCSS vars; wiring them is a focused follow-up.status_bar_bgas a distinct TUI status-row fill (OSC 11 already paints behind it); status strong/dim nuance.