Skip to content

feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live - #68857

Merged
OutThisLife merged 12 commits into
mainfrom
bb/theme-sdk
Jul 22, 2026
Merged

feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live#68857
OutThisLife merged 12 commits into
mainfrom
bb/theme-sdk

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

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 — canonical HermesSkin payload + SkinColors/SkinBranding and the SKIN_COLOR_TOKENS enum. Every TypeScript surface consumes this one shape (TUI's GatewaySkin and the desktop dedup onto it). Each surface owns a normalizing resolver:

  • CLIhermes_cli/skin_engine.py → prompt_toolkit / Rich
  • TUIfromSkin → ansi-safe Theme (Ink)
  • DesktopskinToDesktopTheme → CSS variables (Tailwind/shadcn), registered via backend-sync into Appearance / Cmd-K / /skin

Live application (Hermes applies it itself)

A gateway skin watcher polls the resolved skin signature (name, active-file mtime) and broadcasts skin.changed on any real move — so when Hermes runs hermes config set display.skin X or edits the active skin's colors, every surface repaints within ~a second, through the same path /skin uses. No slash command, no tool-hook timing. Seeded at gateway.ready (stdio + ws) so it only fires on a change.

Deterministic tweaks — hermes skin set

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 changing one color never disturbs the rest — background included. Plus hermes skin use / hermes skin list. This replaces error-prone hand-authoring for tweaks.

Own background (TUI)

A skin's background now paints the whole TUI via OSC 11 (opt-in; cleared on revert and on exit through resetTerminalModes). 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 (was accent)
  • ui_thinking — reasoning/thinking body (was muted)
  • diff_added / diff_removed / diff_added_word / diff_removed_word — were hardcoded
  • syntax_string / syntax_number / syntax_keyword / syntax_comment — code highlighting (was brand tokens)

The hermes-themes skill documents the full element → key map so Hermes knows which knob turns what.

Test plan

  • Desktop vitest: skin→DesktopTheme converter + backend-sync apply guard
  • TUI vitest: fromSkin (element tokens, diffs, syntax), OSC 11 background paint/clear/restore — 1222 passing
  • Python: gateway skin watcher/reconcile behavior, hermes skin set (in-place preserves bg, built-in fork, hex validation), end-to-end config→skin.changed with real files
  • typecheck (@hermes/shared, ui-tui, desktop-touched files) + lint clean
  • CI slice 7/8 shows two pre-existing timing flakes (MCP circuit-breaker; real-threading notification requeue) unrelated to this PR — both pass locally; re-running.

Known follow-ups (not this PR)

  • Desktop parity for the element tokens (ui_tool/ui_thinking/diff_*/syntax_*) — the desktop renders those off its own @assistant-ui CSS vars; wiring them is a focused follow-up.
  • status_bar_bg as a distinct TUI status-row fill (OSC 11 already paints behind it); status strong/dim nuance.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on ef81d9d

CI timings

CI timings · View job

Wall time 9m36s vs 10m10s (-5.6%). 8 job(s) slower, 11 faster, 2 unchanged.

  • Python tests / Run tests slice 8/8: +51.0s
  • Build&Test Docker image / build (arm64, ubuntu-24.04-arm, linux/arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope...: -43.0s
  • Python tests / Run tests slice 5/8: -41.0s
  • Python tests / Run tests slice 2/8: -39.0s
  • Python tests / Run tests slice 6/8: +24.0s

@OutThisLife
OutThisLife requested a review from a team July 21, 2026 19:00
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/desktop Electron desktop app (apps/desktop/*) tool/skills Skills system (list, view, manage) labels Jul 21, 2026
@OutThisLife
OutThisLife requested a review from Copilot July 21, 2026 19:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/shared and 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 skin to 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 HermesSkinDesktopTheme 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.

Comment thread apps/desktop/src/themes/backend-sync.ts Outdated
Comment thread apps/desktop/src/themes/backend-sync.test.ts Outdated
@OutThisLife OutThisLife changed the title feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop, live Jul 22, 2026
… 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.
austinpickett
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
OutThisLife merged commit 367810a into main Jul 22, 2026
43 checks passed
@OutThisLife
OutThisLife deleted the bb/theme-sdk branch July 22, 2026 02:23
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants