fix(gateway): carry customCSS in the change-watcher's resolve_skin

The salvage landed customCSS in server.py's resolve_skin, but that
function's runtime home is change_watcher.py: method_ctx.bind_module
rebinds the split module's bodies onto server.py's globals at import
time, silently overwriting the server.py copy — so skin.changed went
out without customCSS and the desktop lost user CSS on every switch.

Carry the field in change_watcher.py's resolve_skin (the live source of
truth) and drop the duplicated watcher block from server.py, restoring
main's single-owner layout.
This commit is contained in:
Hermes Agent
2026-09-25 14:10:35 -05:00
committed by brooklyn!
parent ecaa1f001a
commit 179124cdff
2 changed files with 3 additions and 100 deletions

View File

@@ -20,7 +20,9 @@ def resolve_skin() -> dict:
"light_colors": skin.light_colors, "dark_colors": skin.dark_colors,
"branding": skin.branding, "banner_logo": skin.banner_logo,
"banner_hero": skin.banner_hero, "tool_prefix": skin.tool_prefix,
"help_header": (skin.branding or {}).get("help_header", "")}
"help_header": (skin.branding or {}).get("help_header", ""),
# Raw user CSS for the desktop GUI's <style> tag (32 KiB cap in the skin engine).
"customCSS": skin.custom_css}
except Exception:
return {}

View File

@@ -1428,105 +1428,6 @@ def _env_model_seed() -> str:
return (os.environ.get("HERMES_MODEL", "") or os.environ.get("HERMES_INFERENCE_MODEL", "")).strip()
def resolve_skin() -> dict:
try:
from hermes_cli.skin_engine import init_skin_from_config, get_active_skin
init_skin_from_config(_load_cfg())
skin = get_active_skin()
return {
"name": skin.name,
"colors": skin.colors,
# Paired palettes: the TUI detects the terminal's polarity and
# prefers the matching hand-tuned block over adapting `colors`.
"light_colors": skin.light_colors,
"dark_colors": skin.dark_colors,
"branding": skin.branding,
"banner_logo": skin.banner_logo,
"banner_hero": skin.banner_hero,
"tool_prefix": skin.tool_prefix,
"help_header": (skin.branding or {}).get("help_header", ""),
"customCSS": skin.custom_css,
}
except Exception:
return {}
# Signature of the last skin broadcast: (name, active user-file mtime). Lets the
# per-tool reconcile fire ``skin.changed`` on any real move — a name switch OR a
# live color edit to the active skin — and nothing else.
_last_skin_sig: tuple[str, float | None] | None = None
def _skin_sig() -> tuple[str, float | None]:
"""(active skin name, its user-file mtime). Built-ins have no file, so only
their name moves; a user skin's mtime lets an in-place color edit repaint too."""
name = str((_load_cfg().get("display") or {}).get("skin") or "default")
override = get_hermes_home_override()
home = override if isinstance(override, str) and override else _hermes_home
try:
mtime: float | None = (Path(home) / "skins" / f"{name}.yaml").stat().st_mtime
except OSError:
mtime = None
return name, mtime
def _note_skin_broadcast() -> None:
"""Sync the reconcile baseline after the /skin RPC emits, so the per-tool
check doesn't re-broadcast the skin /skin just applied."""
global _last_skin_sig
try:
_last_skin_sig = _skin_sig()
except Exception:
pass
def _broadcast_skin_if_changed() -> None:
"""Emit ``skin.changed`` when the active skin moved — the agent switched it
(``hermes config set display.skin``) OR edited the active skin's colors in
place ("I don't like that coral" → tweak the YAML).
Routes through the SAME live path as ``/skin`` so every surface (TUI + desktop)
repaints, no slash command. The signature check is a dict lookup + one stat,
so polling it is ~free.
"""
global _last_skin_sig
try:
sig = _skin_sig()
except Exception:
return
if sig == _last_skin_sig:
return
_last_skin_sig = sig
try:
_broadcast_global_event("skin.changed", resolve_skin())
except Exception:
pass
_skin_watcher_started = False
def _ensure_skin_watcher() -> None:
"""Poll the config for skin changes and broadcast ``skin.changed`` — so a skin
Hermes activates (``hermes config set display.skin``) or recolors goes live on
every surface within ~half a second, on its own, with no tool-hook or slash
command in the loop. Idempotent; started at gateway.ready."""
global _skin_watcher_started
if _skin_watcher_started:
return
_skin_watcher_started = True
_note_skin_broadcast() # seed the baseline so only a real change repaints
def _loop() -> None:
while True:
time.sleep(0.5)
_broadcast_skin_if_changed()
threading.Thread(target=_loop, name="hermes-skin-watcher", daemon=True).start()
def _resolve_model() -> str:
if env := _env_model_seed():
return env