feat(skins): add Zeus skin — OLED-near-black dark surfaces with default gold accent#3328
feat(skins): add Zeus skin — OLED-near-black dark surfaces with default gold accent#3328heagandev wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new Zeus appearance skin with OLED-near-black dark surfaces while preserving the default gold accent, and wires it into registration, docs, and localization help text.
Changes:
- Add Zeus CSS variables and component overrides for dark mode.
- Register the Zeus skin in boot/runtime validation and settings config, plus early-load skin allowlist.
- Update docs/changelog and add tests that assert Zeus registration + key CSS affordances.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_zeus_skin.py | Adds tests to verify Zeus is registered across key files and its dark-surface CSS tokens/overrides exist. |
| static/style.css | Introduces the Zeus dark-only skin CSS variables and targeted component overrides (dialogs, modals, sessions, etc.). |
| static/index.html | Adds zeus to the early boot allowlist of skins read from localStorage. |
| static/i18n.js | Updates /theme help text in multiple locales to include zeus in the skin list. |
| static/boot.js | Adds Zeus to the _SKINS list used for validation/UI. |
| api/config.py | Adds zeus to the server-side supported skins set. |
| THEMES.md | Documents Zeus in the skins table. |
| CHANGELOG.md | Notes the addition of the Zeus skin in Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <meta name="apple-mobile-web-app-title" content="Hermes"> | ||
| <link rel="apple-touch-icon" sizes="512x512" href="static/apple-touch-icon.png"> | ||
| <script>(function(){try{var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna:1,catppuccin:1,hepburn:1,nous:1,'geist-contrast':1,neon:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;}catch(e){document.documentElement.classList.add('dark');}})()</script> | ||
| <script>(function(){try{var themes={light:1,dark:1,system:1},skins={default:1,ares:1,mono:1,slate:1,poseidon:1,sisyphus:1,charizard:1,sienna:1,catppuccin:1,hepburn:1,nous:1,'geist-contrast':1,neon:1,zeus:1},legacy={slate:['dark','slate'],solarized:['dark','poseidon'],monokai:['dark','sisyphus'],nord:['dark','slate'],oled:['dark','default']},t=(localStorage.getItem('hermes-theme')||'dark').toLowerCase(),s=(localStorage.getItem('hermes-skin')||'').toLowerCase(),m=legacy[t],theme=m?m[0]:(themes[t]?t:'dark'),skin=skins[s]?s:(m?m[1]:'default');localStorage.setItem('hermes-theme',theme);localStorage.setItem('hermes-skin',skin);if(theme==='system')theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(theme==='dark')document.documentElement.classList.add('dark');if(skin!=='default')document.documentElement.dataset.skin=skin;}catch(e){document.documentElement.classList.add('dark');}})()</script> |
There was a problem hiding this comment.
Clarifying the framing: Zeus defines dark-mode overrides only, but light mode is fully functional — it inherits the complete default skin palette rather than landing in a partially styled state. The index.html allowlist entry is intentional and consistent with how other skins handle this. Happy to add a clamping guard in a follow-up if the project wants to standardise across all skins that don't define a light variant.
| cmd_new: 'Start a new chat session', | ||
| cmd_usage: 'Toggle token usage display on/off', | ||
| cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast)', | ||
| cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard/sienna/catppuccin/nous/geist-contrast/zeus)', |
There was a problem hiding this comment.
Pre-existing gap not introduced by this PR — hepburn and neon are already absent from cmd_theme in current master. Added zeus consistently with the existing pattern. Happy to fix the broader gap in a separate PR if useful.
| Each skin defines paired light + dark variants so it reads cleanly on either | ||
| theme. The skin is applied as `data-skin="<name>"` on `<html>` (the default |
There was a problem hiding this comment.
Fixed in the latest commit — updated to "most skins define paired light + dark variants" with a note that skins without a light variant fall back to the default skin in light mode.
| REPO = Path(__file__).parent.parent | ||
| CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8") | ||
| BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8") | ||
| CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8") | ||
| INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8") | ||
| I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8") |
There was a problem hiding this comment.
Matches the pattern used in every existing skin test in the repo (e.g. test_geist_contrast_skin.py, test_nous_skin.py) — keeping consistent for now. Happy to convert to a session-scoped fixture if the project adopts that pattern more broadly.
| # Zeus is the last skin in each locale's cmd_theme string, so it appears | ||
| # as `…/zeus)` rather than `/zeus/`. There are 10 locales. | ||
| assert I18N_JS.count("zeus)") >= 9 |
There was a problem hiding this comment.
Fixed in the latest commit — tightened to == 10 to match the exact locale count.
…18n exact locale count, updated PR screenshots
## Release v0.51.250 — Release HR (stage-q22) UX-approved (dark + light-fallback screenshots). ### Added | PR | Author | Feature | |----|--------|---------| | #3328 | @heagandev | **Zeus appearance skin** — OLED-near-black dark surfaces that keep the default **gold accent** (a high-contrast "gold on black" look no existing skin offered). Selectable from Settings → Appearance or `/theme skin zeus`. Dark-focused; falls back to the default light palette in light mode. | ### Notes - The PR was 2 days / ~19 releases stale and CONFLICTING; re-applied surgically onto current master (CSS palette + `zeus` registered at all 5 sites: config allowlist, boot.js swatch, index.html boot-map, i18n `cmd_theme` ×12 locales, picker) + THEMES.md doc row. The PR's own `test_zeus_skin.py` (6 tests) passes against the re-applied version. - Fully scoped + additive: Codex verified every new CSS rule is under `:root.dark[data-skin="zeus"]` — no bleed into the default appearance or other skins. ### Gate - Full pytest suite: **7563 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · vision-verified dark (OLED+gold) + light (clean fallback) - Codex (regression): **SAFE TO SHIP** Co-authored-by: heagandev <heagandev@users.noreply.github.com>
|
Shipped in v0.51.250 (Release HR) — thank you @heagandev! 🙏 The Zeus skin (OLED-near-black surfaces with the default gold accent preserved) is on |
…➔ 0.51.252) (#813) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.230` → `0.51.252` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.252`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051252--2026-06-03--Release-HT-stage-q24--selection-bleed-fix--compatibility-docs) [Compare Source](nesquena/hermes-webui@v0.51.251...v0.51.252) ##### Fixed - The floating "selected-text reply" button no longer lets its own label get caught in a text selection (`user-select:none`), so dragging a selection near the button doesn't bleed into it. ([#​2481](nesquena/hermes-webui#2481), [@​rodboev](https://github.com/rodboev)) ##### Docs - README now has a **Compatibility** section documenting that the WebUI is tested against the matching hermes-agent release and that both should be upgraded together (until the stable agent API [#​2491](nesquena/hermes-webui#2491) lands). ([@​rodboev](https://github.com/rodboev)) ### [`v0.51.251`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051251--2026-06-03--Release-HS-stage-q23--composer--path-autocomplete) [Compare Source](nesquena/hermes-webui@v0.51.250...v0.51.251) ##### Fixed - Typing a `~/` path token in the composer (e.g. `check this file ~/`) now opens a home-directory path-suggestion dropdown, matching the TUI's path completion. It reuses the existing slash-command dropdown (positioning + keyboard nav) and the server's trusted `/api/workspaces/suggest` endpoint, and only replaces the matched path token on selection (surrounding message text is preserved). Slash-command autocomplete still takes precedence for `/`-prefixed input. ([#​3433](nesquena/hermes-webui#3433), [@​puneetdixit200](https://github.com/puneetdixit200)) ### [`v0.51.250`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051250--2026-06-03--Release-HR-stage-q22--Zeus-appearance-skin) [Compare Source](nesquena/hermes-webui@v0.51.249...v0.51.250) ##### Added - New **Zeus** appearance skin (Settings → Appearance, or `/theme skin zeus`) — OLED-near-black dark surfaces that keep the default gold accent, for a high-contrast "gold on black" look that no existing skin offered. All visual changes are scoped to `data-skin="zeus"`; it's dark-focused and falls back to the default light palette in light mode. ([#​3328](nesquena/hermes-webui#3328), [@​heagandev](https://github.com/heagandev)) ### [`v0.51.249`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051249--2026-06-03--Release-HQ-stage-q21--auto-expand-terminal-on-output-toggle) [Compare Source](nesquena/hermes-webui@v0.51.248...v0.51.249) ##### Added - New **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel surfaces itself automatically the first time a running command emits output, so long-running command output isn't silently collected behind a collapsed panel. The auto-expand does not steal focus from the composer, and fires once per stream (not per output chunk). Mirrors the existing `simplified_tool_calling` setting pattern; default-off means no behavior change on upgrade. ([#​2974](nesquena/hermes-webui#2974), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.248`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051248--2026-06-03--Release-HP-stage-q20--self-heal-deleted-WebUI-sessions-instead-of-bricking-the-chat) [Compare Source](nesquena/hermes-webui@v0.51.247...v0.51.248) ##### Fixed - A WebUI session whose sidecar was deleted server-side (e.g. after `docker compose --force-recreate`) but whose messages still live in `state.db` no longer **bricks the chat** — it looked alive (`GET /api/session` returned 200 from a synthesized CLI stub) while every action failed (`POST /api/session/draft` and `/api/chat/start` returned 404). Now the GET handler consults `_index.json` (the canonical WebUI session registry): if the id was a WebUI-origin session (empty/`webui`/`fork` source) whose sidecar is gone, it returns 404 so the client can self-heal — clearing the saved session id and stripping the stale `/session/<id>` URL — and falls through to the welcome screen. Genuine CLI-origin sessions keep their existing read-only stub. The client self-heal now also covers the mid-session case (the current session's sidecar disappearing), not just boot. ([#​2782](nesquena/hermes-webui#2782), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.247`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051247--2026-06-03--Release-HO-stage-q19--coerce-reasoning-effort-to-model-supported-levels) [Compare Source](nesquena/hermes-webui@v0.51.246...v0.51.247) ##### Fixed - A globally-configured reasoning effort (`agent.reasoning_effort`) is now **coerced to the closest level the active model/provider actually supports** before each request, instead of being sent verbatim and rejected. For example `openai-codex` `gpt-5` rejects `max` (now degraded to `xhigh`) and `o1`/`o3`/`o4` only accept `low`/`medium`/`high` (so `max`/`xhigh` degrade to `high`). Coercion only ever steps *down* to a supported level (never escalates), and `none`/unset are preserved. The model/provider effort-capability filter is applied consistently across the heuristic, models.dev metadata, GitHub Copilot, and LM Studio detection paths. ([#​3505](nesquena/hermes-webui#3505), [@​franksong2702](https://github.com/franksong2702)) ### [`v0.51.246`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051246--2026-06-03--Release-HN-stage-q18--WebUI-rename-syncs-to-agent-statedb) [Compare Source](nesquena/hermes-webui@v0.51.245...v0.51.246) ##### Fixed - Renaming a session in the WebUI now writes the new title through to the agent's `state.db`, so the TUI and CLI no longer keep showing the old name. The `/api/session/rename` handler now calls `_sync_session_title_to_insights()` (gated on the `sync_to_insights` setting) — exactly like the sibling `/api/session/title/regenerate` handler already did. ([#​3225](nesquena/hermes-webui#3225), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.245`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051245--2026-06-03--Release-HM-stage-q17--messaging-source-badge-in-chat-topbar) [Compare Source](nesquena/hermes-webui@v0.51.244...v0.51.245) ##### Fixed - Messaging sessions (Telegram, Discord, WeChat, etc.) now show their platform source badge in the **chat-pane topbar**, not just the sidebar. The topbar badge was gated on `is_cli_session`, which is intentionally `false` for messaging sources, so the badge silently disappeared once you opened the session. The gate is removed; a recovered native session whose sidecar stamps `source_label: "WebUI"` is still left un-badged (it isn't a foreign source). ([#​3338](nesquena/hermes-webui#3338), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.244`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051244--2026-06-03--Release-HL-stage-q16--workspace-OS-import-drop--composer-drop-zone-polish) [Compare Source](nesquena/hermes-webui@v0.51.243...v0.51.244) ##### Added - **Drop OS files/folders onto a specific workspace folder row or breadcrumb segment** to upload into that directory (not only the current directory). OS folder drops are traversed via `webkitGetAsEntry`/`readEntries` and their nested structure is preserved on upload. Composer `@path` drags ([#​1097](nesquena/hermes-webui#1097)), the internal tree-move ([#​3402](nesquena/hermes-webui#3402)), and OS-drop isolation ([#​3411](nesquena/hermes-webui#3411)) are all preserved. ([#​3402](nesquena/hermes-webui#3402), [#​3424](nesquena/hermes-webui#3424), [@​pamnard](https://github.com/pamnard)) ##### Fixed - The composer drop-zone overlay no longer looks garbled when you drag a workspace file (or OS file) over the footer. Previously the translucent overlay let the textarea, attach/mic icons, and model/profile chips bleed through and collide with the hint text. The overlay is now a clean, fully-opaque box with a single centered, context-aware label — **"Drop to insert workspace reference"** when dragging a workspace file (which inserts an `@path` reference) vs **"Drop files to attach"** for an OS file (which attaches it to the message). ### [`v0.51.243`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051243--2026-06-03--Release-HK-stage-q15--drag-to-move-files-within-the-workspace) [Compare Source](nesquena/hermes-webui@v0.51.242...v0.51.243) ##### Added - You can now **drag a file or folder in the workspace tree onto another folder row (or a breadcrumb segment) to move it** within the workspace. A new `POST /api/file/move` performs the move server-side, confined to the workspace root (`safe_resolve` on both source and destination, rejects `..` destinations, and refuses to move a folder into itself or a descendant). Name collisions and no-op moves are handled, and the drop handlers use `stopPropagation` so the existing composer `@path` drag ([#​1097](nesquena/hermes-webui#1097)) and OS-file upload-on-drop ([#​3411](nesquena/hermes-webui#3411)) are unchanged. ([#​3402](nesquena/hermes-webui#3402), [#​3422](nesquena/hermes-webui#3422), [@​pamnard](https://github.com/pamnard)) ### [`v0.51.242`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051242--2026-06-03--Release-HJ-stage-q14--Graphite-skin) [Compare Source](nesquena/hermes-webui@v0.51.241...v0.51.242) ##### Added - New **Graphite** appearance skin — a quiet, neutral-gray "workbench" alternative to the default gold/cream, selectable from Settings → Appearance (and `/theme skin graphite`). All visual changes are scoped to `data-skin="graphite"` so the default appearance is unchanged; the skin ships both light and dark palettes built on the existing CSS-variable token system (no new dependency or build step). Tightens typography, shadows, active-sidebar spacing, and code-block framing, and uses a neutral gray palette rather than an olive-tinted one. ([#​3440](nesquena/hermes-webui#3440), [@​t3chn0pr13st](https://github.com/t3chn0pr13st)) ### [`v0.51.241`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051241--2026-06-03--Release-HI-stage-q13--New-Chat-returns-to-your-unsent-draft-after-visiting-history) [Compare Source](nesquena/hermes-webui@v0.51.240...v0.51.241) ##### Fixed - Starting a **New Chat** draft, peeking at a previous conversation, then clicking **New Chat** again no longer loses your unsent prompt. Zero-message New Chat sessions are intentionally hidden from the sidebar, so after you navigated away there was no way back to the empty session that held your draft — New Chat just created another fresh empty session and the draft was stranded. The New Chat entrypoint now remembers the candidate empty draft session (a single `localStorage` pointer) and, before creating a fresh session, re-validates it through `/api/session` and routes back only if it is still a safe empty draft (zero messages, no active stream, no pending message, not worktree-backed, matching profile, and a non-empty server-side `composer_draft`). The composer draft is also flushed to the server before a session switch so typing and immediately navigating away can't drop it. Clearing the draft (e.g. after sending) clears the pointer, so an emptied draft never traps you on New Chat. ([#​3333](nesquena/hermes-webui#3333), [#​3471](nesquena/hermes-webui#3471), [@​starGazerK](https://github.com/starGazerK)) ### [`v0.51.240`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051240--2026-06-03--Release-HH-stage-q12--mobile-swipe-up-stops-streaming-auto-scroll) [Compare Source](nesquena/hermes-webui@v0.51.239...v0.51.240) ##### Fixed - On mobile/touch devices you can now swipe up to stop the auto-scroll-during-streaming behavior. Previously the stream snapped back to the bottom on every token and there was no way to read earlier content while a response was arriving: `_recordNonMessageScrollIntent()` only detected upward intent on the wheel path (`typeof e.deltaY === 'number'`), but touch events carry no `deltaY`, so a finger swipe never unpinned the view. The handler now tracks the `touchstart` Y position and treats a `touchmove` that moves the finger up by >8px as upward-scroll intent — the same authoritative unpin (`_messageUserUnpinned`) the wheel path uses — so auto-follow stops until you scroll back to the bottom or tap the ↓ button. ([#​3470](nesquena/hermes-webui#3470), [@​cnogrin](https://github.com/cnogrin)) ### [`v0.51.239`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051239--2026-06-03--Release-HG-stage-q10--ignore-SIGPIPE-so-a-dropped-client-cant-kill-the-server) [Compare Source](nesquena/hermes-webui@v0.51.238...v0.51.239) ##### Fixed - The server no longer dies silently when a client drops the connection mid-response. Python's default action for `SIGPIPE` is `Term`, so a single broken-pipe `socket.send()` in any `ThreadingHTTPServer` worker thread (browser tab closed mid-stream, network drop, mobile backgrounding, a dropped long-poll, an `/api/updates/check` timeout) could terminate the entire WebUI process — no exception, no log, no `/health` response. `server.py` now sets `SIGPIPE` to `SIG_IGN` at import time: the kernel surfaces the broken pipe as a catchable `BrokenPipeError`, the per-request handler unwinds, the connection closes, and the server keeps serving. The handler is `getattr`-guarded so it is a no-op on Windows, where `SIGPIPE` does not exist (preserves native-Windows support, [#​1952](nesquena/hermes-webui#1952)) (salvaged from [#​3407](nesquena/hermes-webui#3407), [@​PatrickNoFilter](https://github.com/PatrickNoFilter)). ### [`v0.51.238`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051238--2026-06-03--Release-HF-stage-q9--New-Conversation-hits-the-fast-path-on-cold-start) [Compare Source](nesquena/hermes-webui@v0.51.237...v0.51.238) ##### Fixed - Clicking **New Conversation** on a cold start no longer hangs for 3–4s on a catalog rebuild. `POST /api/session/new`'s fast path (`_resolve_compatible_session_model_state`) returns immediately only when the request carries both a `model` and a truthy `model_provider`; on a cold/unhydrated dropdown the client sent `model_provider=null`, so the request fell into `get_available_models()` and rebuilt the full catalog (the "first click slow, later clicks fast" asymmetry from [#​2518](nesquena/hermes-webui#2518)). `newSession()` (`static/sessions.js`) now falls back to `window._activeProvider` (then the previous session's `model_provider`) when the dropdown option carries no provider, so the first click takes the fast path too. **Two guards keep this safe:** (1) a slash-qualified (`gemini/…`) or `@provider:model` slug already carries a foreign provider namespace from a prior backend, so the fallback deliberately leaves `model_provider=null` for those; (2) even a *bare* model can carry a known family prefix (`gpt`→openai, `claude`→anthropic, `gemini`→google) — if that family maps to a different provider than the fallback we'd attach, `model_provider` is left null too. Both cases preserve the server slow-path's family-aware cross-provider repair rather than silently re-pointing the new session at the wrong backend ([#​2518](nesquena/hermes-webui#2518) follow-up, [@​franksong2702](https://github.com/franksong2702)). ### [`v0.51.237`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051237--2026-06-03--Release-HE-stage-q8--reconcile-early-cancel-against-live-worker-state) [Compare Source](nesquena/hermes-webui@v0.51.236...v0.51.237) ##### Fixed - Cancelling a live turn immediately after sending now reliably stops the worker and settles the session to a cancelled state, instead of leaving the UI showing a running spinner over a blank session page. The bug was an early-cancel race: the browser SSE could detach (removing the entry from `STREAMS`) before the worker was fully reflected there, so `cancel_stream()` returned early and never interrupted the agent. `cancel_stream()` now falls back to the live active-run registry (`ACTIVE_RUNS`) and the session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")` and the session is cleaned up. Relatedly, `/api/session` now reports run-journal active state from the live active-run registry rather than treating any persisted `active_stream_id` as proof the worker is still alive ([#​3475](nesquena/hermes-webui#3475), [@​franksong2702](https://github.com/franksong2702)). ### [`v0.51.236`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051236--2026-06-03--Release-HD-stage-q7--native-Windows-support-for-bootstrap-and-terminal) [Compare Source](nesquena/hermes-webui@v0.51.235...v0.51.236) ##### Added - Native Windows support for `bootstrap.py` and the embedded terminal ([#​1952](nesquena/hermes-webui#1952)). Hermes WebUI already ran on Windows when invoked as `python server.py` directly; this unblocks the supported `python bootstrap.py` path. `api/terminal.py` no longer hard-imports the POSIX-only `fcntl`/`termios`/`select` at module load — they're guarded behind `_TERMINAL_SUPPORTED = sys.platform != "win32"`, and the embedded-terminal entry points raise `NotImplementedError` (or no-op) on Windows, following the existing optional-feature guard pattern (`api/turn_journal.py`, `api/providers.py`). The bootstrap native-Windows block becomes a warning instead of a hard `RuntimeError`; auto-install (which shells out to `/bin/bash`) still errors clearly on native Windows (WSL is unaffected), and the foreground launch path uses `subprocess.Popen` + exit on Windows (where `os.execv` spawns rather than replaces the process, orphaning it from a supervisor) instead of `os.execv`. POSIX behavior is unchanged on every path ([#​1952](nesquena/hermes-webui#1952), [@​rodboev](https://github.com/rodboev)). ### [`v0.51.235`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051235--2026-06-03--Release-HC-stage-q5--no-duplicate-transcript-replay-on-repeated-questions-after-compression) [Compare Source](nesquena/hermes-webui@v0.51.234...v0.51.235) ##### Fixed - The chat transcript no longer accumulates duplicate messages after multiple context-compression cycles when the user asks similar (or identical) questions across turns. `_find_current_user_turn` (`api/streaming.py`) located the slice point for the current turn's new messages by scanning `result_messages` for the user text — but after compression `result_messages` carries the full conversation history, so a *first*-match scan returned an **older** turn's index, making the merge re-append the entire replayed history from that point (observed: a 137-message session where 89 were duplicate replays, burying the real new messages). It now returns the **last** matching user turn, so the candidate slice begins at the current turn and the replayed history is not re-appended. To stay correct when the agent loop appends synthetic `role:"user"` continuation prompts (e.g. "Continue" / empty-recovery nudges) after the real turn, an exact (strong) match is preferred over a later substring (weak) match — so a synthetic continuation can't anchor the merge past the real turn and drop the assistant/tool output in between. Behavior on the no-match path (fall back to the last user index) is unchanged ([#​3468](nesquena/hermes-webui#3468), [@​jasonjcwu](https://github.com/jasonjcwu)). A regression test pins the unit behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant (each verified to fail against the pre-fix logic). ### [`v0.51.234`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051234--2026-06-03--Release-HB-stage-q4--duplicate-instance-startup-guard--remote-terminal-workspace-paths) [Compare Source](nesquena/hermes-webui@v0.51.233...v0.51.234) ##### Fixed - The server now refuses to start when a live instance is already responding on the configured port, instead of silently sharing it (a Windows/macOS hazard where `SO_REUSEADDR` semantics let two processes bind 8787 at once, [#​3289](nesquena/hermes-webui#3289)). Rather than globally disabling `SO_REUSEADDR` (which would brick legitimate fast restarts — `ctl.sh restart` and the `os.execv` self-update path rebind immediately and would hit the TIME\_WAIT window), startup now runs a live-listener probe (`_abort_if_already_serving`): a TCP connect + `GET /health` with a 2s timeout. A live instance answers and startup aborts with a clear message; a dying instance whose socket still lingers in the kernel backlog accepts the connection but never responds, so the probe times out and startup proceeds — preserving fast restart. On Windows, `SO_EXCLUSIVEADDRUSE` is set in a `server_bind()` override to get true exclusive binding (POSIX keeps the inherited `allow_reuse_address = True`) ([#​3289](nesquena/hermes-webui#3289), [@​rodboev](https://github.com/rodboev)). - Remote/SSH terminal profiles can now use target-side workspace paths that don't exist on the WebUI host. Workspace validation/resolution previously `stat()`-ed every path against the WebUI server's local filesystem, so a `terminal.cwd` (or session workspace) living on the remote target was rejected as nonexistent. For profiles whose terminal backend is non-local, paths **under the configured `terminal.cwd`** now pass validation without a server-local existence check, and stale server-local `last_workspace` values are ignored unless they fall under the remote cwd. Local profiles are unchanged — the bypass only fires for remote backends and only for paths contained within `terminal.cwd` ([#​3486](nesquena/hermes-webui#3486), [@​dso2ng](https://github.com/dso2ng)). ### [`v0.51.233`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051233--2026-06-03--Release-HA-stage-q3--session-truncate-keepcount-guard-against-silent-transcript-loss) [Compare Source](nesquena/hermes-webui@v0.51.232...v0.51.233) ##### Fixed - `POST /api/session/truncate` no longer silently wipes a session transcript on a negative `keep_count`, and no longer returns an HTTP 500 on a non-numeric one. `keep_count` fed a bare `int()` straight into the destructive `s.messages = s.messages[:keep]` slice followed by `s.save()`, so a negative value sliced as `messages[:-N]` — **deleting the most recent N messages and persisting the result to disk** (e.g. `keep_count=-5` on a 3-message session wiped the entire transcript and returned HTTP 200). `keep_count` is now validated before the slice — non-integer → `400 "keep_count must be an integer"`, negative → `400 "keep_count must be non-negative"` — mirroring the guard the sibling `/api/session/branch` handler already applies (`keep_count=0` keeps its existing "clear all messages" meaning) ([#​3472](nesquena/hermes-webui#3472), [@​Mubashirrrr](https://github.com/Mubashirrrr)). ### [`v0.51.232`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051232--2026-06-03--Release-GZ-stage-q2--cron-endpoint-query-param-guards--Japanese-locale-translations) [Compare Source](nesquena/hermes-webui@v0.51.231...v0.51.232) ##### Fixed - The cron output (`/api/crons/output`) and cron recent (`/api/crons/recent`) endpoints no longer return a confusing HTTP 500 on a malformed numeric query param. A non-numeric `limit` (e.g. `?limit=abc`) or `since` previously let `int()`/`float()` raise `ValueError` up to the top-level handler; both are now parsed defensively (falling back to their defaults). The cron-output `limit` is also clamped to `[1, 500]` so a negative value can't reach the newest-first `files[:limit]` slice as `files[:-n]` (which would drop the oldest entries — or return an empty list when the magnitude exceeds the count — instead of the newest outputs), mirroring the guard `_handle_cron_run_detail` already uses ([#​3473](nesquena/hermes-webui#3473), [@​Mubashirrrr](https://github.com/Mubashirrrr)). ##### Changed - Japanese (`ja`) locale: translated 80 previously-untranslated UI strings (MCP server controls, tool summaries, and related toasts) from their English fallbacks to Japanese, with all `${…}` interpolation placeholders preserved. No locale keys added or removed ([#​3480](nesquena/hermes-webui#3480), [@​koshikai](https://github.com/koshikai)). ### [`v0.51.231`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051231--2026-06-03--Release-GY-stage-q1--model-extras-tail-resolution--plugins-tab-auto-hide--search-depth-guard--symlink-home-suggestions) [Compare Source](nesquena/hermes-webui@v0.51.230...v0.51.231) ##### Fixed - `/model <name>` can now select a model that lives in the **truncated `extra_models` tail** of a large provider catalog, completing the [#​3368](nesquena/hermes-webui#3368) fix that v0.51.229 left half-done. On Nous-style catalogs with >25 models the picker renders only a featured subset as `<option>` entries and pushes the rest into `extra_models`; the `/model` resolver previously matched only against the rendered `sel.options`, so a bare model living only in the extras tail (e.g. `xiaomi/mimo-v2.5` alongside the featured `xiaomi/mimo-v2.5-pro`) was un-selectable and produced a misleading "did you mean -pro?" toast. A new `_buildModelCandidates()` (`static/commands.js`) now builds the candidate set from the full `/api/models` catalog (featured `models` + `extra_models`) — the same complete list the CLI and `/model` autocomplete use — and an extras-only winner is injected via `_ensureModelOptionInDropdown()` before selection so the correct `model` + `model_provider` persist end-to-end. The [#​3437](nesquena/hermes-webui#3437) tier-guard is fully preserved: a genuinely off-catalog versioned name still refuses to snap to a `-pro`/`-flash` tier and shows the suggestion toast ([#​3368](nesquena/hermes-webui#3368), [@​nesquena-hermes](https://github.com/nesquena-hermes); with [@​garyd9](https://github.com/garyd9), confirmation [@​yutaotie](https://github.com/yutaotie)). - The **Plugins** tab in Settings is now auto-hidden when no plugins are installed (`/api/plugins` returns `empty: true`), and deep-linking to the hidden plugins pane falls back to the Conversation section. The tab reappears automatically when plugins are detected ([#​3457](nesquena/hermes-webui#3457), [@​pix0127](https://github.com/pix0127)). - `GET /api/sessions/search?...&depth=<x>` no longer returns a confusing HTTP 500 on a non-numeric `depth` (e.g. `?depth=deep`) and no longer silently excludes the newest messages on a negative `depth` (which sliced as `messages[:-n]`). `depth` is now parsed defensively and clamped to `>= 0` (0 keeps its existing "search the full transcript" meaning), mirroring the guard sibling handlers already use ([#​3474](nesquena/hermes-webui#3474), [@​Mubashirrrr](https://github.com/Mubashirrrr)). - Workspace path autocomplete now expands `~/` suggestions even when the WebUI process home path is a symlink or alias of the trusted home root, so prefixes like `~/Doc` still list home-directory matches instead of returning an empty dropdown. The typed `~` target is now resolved before the trust comparison ([#​3433](nesquena/hermes-webui#3433), [@​sjh9714](https://github.com/sjh9714)). </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/813
Thinking Path
style.css/boot.js/index.html/i18n.js) plus server allowlist inapi/config.py.What Changed
static/style.css—:root.dark[data-skin="zeus"]palette block (dark-only, no light variant) using#0F0F0F–#181818near-blacks. Borders and focus rings use gold tints (rgba(255,215,0,0.06–0.4)). Chat message bodies receive a subtle dark surface background distinct from the main bg — assistant messages use a near-black#181818, user messages a warm gold-tinted#1C1600. Seven component overrides (topbar, sidebar, active session, composer, assistant/user message bodies, modals).static/boot.js—_SKINSentry with swatches['#FFD700','#FFBF00','#1A1A00']. Avoids#000000as a swatch since it disappears on dark picker backgrounds.static/index.html—zeus:1in the pre-paint skin whitelist to prevent flash on first load.static/i18n.js— added to all 10 localecmd_themehelp strings.api/config.py— added to_SETTINGS_SKIN_VALUESserver-side allowlist.THEMES.md,CHANGELOG.md— docs.tests/test_zeus_skin.py— 6 tests mirroring the Geist Contrast pattern: 4-file registration, dark surfaces, accent preservation via gold-tinted borders/focus rings, active-session gold highlight, modal override coverage, i18n locale count.Why It Matters
/theme skin:zeus.UI media
Dark mode — active conversation:
Dark mode — settings panel:
Verification
node --check static/boot.js— cleannode --check static/i18n.js— cleanpython3 -m py_compile api/config.py— cleangit diff --check— cleanmaster(v0.51.195) — skin picker exposes Zeus;/theme skin:zeusactivates correctly; persists across reload.Risks / Follow-ups
panels.js/ behavior changes — strictly skin-only per the AGENTS.md one-logical-change rule.