Skip to content

Add Graphite appearance skin - #3440

Closed
t3chn0pr13st wants to merge 1 commit into
nesquena:masterfrom
t3chn0pr13st:codex/graphite-theme
Closed

t3chn0pr13st wants to merge 1 commit into
nesquena:masterfrom
t3chn0pr13st:codex/graphite-theme

Conversation

@t3chn0pr13st

@t3chn0pr13st t3chn0pr13st commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI already has separate theme and skin axes, and the UI/UX guide asks theme work to use the existing CSS variable system without adding a build step or framework.
  • The requested improvement was a Codex-like neutral appearance that feels quieter than the default gold/cream skin while staying native to Hermes WebUI.
  • A skin is the smallest reviewable surface for this: it keeps the default appearance unchanged, avoids a new dependency, and lets users opt in from existing settings.
  • The implementation therefore adds Graphite as a selectable skin and scopes all visual changes to data-skin="graphite".
  • Follow-up polish tightened typography, shadows, active sidebar spacing, code-block framing, and the light palette so the skin is neutral gray rather than olive-tinted.

What Changed

  • Register Graphite in the settings skin picker and config validation.
  • Add Graphite to /theme help text.
  • Add light and dark Graphite CSS palettes using the existing token system.
  • Scope system typography, composer chrome, sidebar active-row styling, code block framing, tool-card tone, and reduced shadows to the Graphite skin.
  • Neutralize the light Graphite palette to remove olive-tinted surfaces and keep it closer to a clean Codex-like gray/white workbench.
  • Add regression tests for registration, palette tokens, non-green accent behavior, typography, code-frame continuity, and /theme i18n/help coverage.

Why It Matters

The default Hermes skin remains unchanged, but users who prefer a quieter developer-console look can now select Graphite without custom CSS. The skin follows the project’s no-build-step design: plain CSS variables, existing JS registration, and focused tests.

Contract Routing

Task type: UI / UX / theme-skin addition.

Touched areas:

  • static/style.css
  • static/boot.js
  • static/i18n.js
  • static/index.html
  • api/config.py
  • theme/skin regression tests

Relevant public docs:

  • CONTRIBUTING.md
  • docs/CONTRACTS.md
  • docs/UIUX-GUIDE.md
  • DESIGN.md
  • THEMES.md

Scope boundaries:

  • No default theme behavior changes.
  • No new frontend framework, bundler, build step, dependency, or asset pipeline.
  • No change to the theme axis; Graphite is a skin on the existing skin axis.
  • No global typography or composer changes outside data-skin="graphite".

Evidence needed before claiming done:

  • Automated registration/config/i18n tests.
  • Local browser verification in both light and dark Graphite.
  • Before/after UI evidence for reviewer-visible UI changes.

No intentional contract change is made. This PR extends the existing skin list while preserving the current theme/skin architecture.

UI Evidence

Local browser verification was performed with Graphite in light and dark modes, including the chat view, sidebar active rows, composer, code blocks, and settings registration.

Before: default light skin.

Default light skin before

After: Graphite light skin.

Graphite light skin after

The screenshots use the same local session, viewport, scroll position, code block, sidebar, and composer. Computed light tokens were checked during capture:

  • Default light: --bg #FEFCF7, --sidebar #FAF7F0, no data-skin.
  • Graphite light: --bg #FFFFFF, --sidebar #F3F3F3, data-skin="graphite".

Verification

After rebasing on upstream v0.51.230 / 2d20d336:

node --check static/boot.js
node --check static/i18n.js
python3 -m py_compile api/config.py
uv run --with pytest pytest tests/test_graphite_skin.py tests/test_issue2462_theme_i18n.py -q
rm -f uv.lock
git diff --check

Result: 9 passed.

Additional local checks:

  • Selected Graphite in a running local WebUI.
  • Verified computed light tokens: --bg #FFFFFF, --sidebar #F3F3F3, --code-bg #F1F1F1, --user-bubble-bg #EFEFEF.
  • Verified the light palette no longer uses the previous olive-tinted surface tokens.

Risks / Follow-ups

  • This is a visible UI addition, so before/after screenshots are included above; maintainers may still request narrower/mobile evidence if they want to review responsive details.
  • The skin is intentionally scoped and opt-in; users who do not select Graphite should see no visual changes.
  • Future visual regression coverage could add screenshot-based tests if the project adopts that workflow.

Model Used

  • Provider/model/mode: OpenAI Codex GPT-5.5 xhigh
  • Notable tool use: local repository inspection, browser-based visual verification, pytest/node/python checks, GitHub CLI for PR updates.

@t3chn0pr13st
t3chn0pr13st force-pushed the codex/graphite-theme branch from f273294 to c552935 Compare June 2, 2026 21:26
@t3chn0pr13st

Copy link
Copy Markdown
Contributor Author

It looks like this
image

@t3chn0pr13st
t3chn0pr13st force-pushed the codex/graphite-theme branch 4 times, most recently from d5a9684 to 01255b7 Compare June 3, 2026 08:45
@t3chn0pr13st
t3chn0pr13st force-pushed the codex/graphite-theme branch from 01255b7 to e171b2e Compare June 3, 2026 09:11
@t3chn0pr13st

Copy link
Copy Markdown
Contributor Author

Light theme updated
image_2026-06-03_12-24-08

nesquena-hermes added a commit that referenced this pull request Jun 3, 2026
## Release v0.51.242 — Release HJ (stage-q14)

UX-approved via Telegram (Nathan — dark/light/mobile screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. |

### Why it's safe
- **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`).
- Opt-in; a user has to select it. Default experience unchanged.

### Test-robustness fix (absorbed)
The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened).

### Gate
- Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss)
- Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram

Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.242 (Release HJ) via release PR #3500 — thank you @t3chn0pr13st! 🙏

The Graphite skin is on master and tagged — a clean, neutral-gray workbench alternative, selectable from Settings → Appearance. Nathan approved it via screenshots (dark/light/mobile). All CSS is properly scoped to data-skin="graphite" so the default appearance and other skins are untouched (Codex verified zero bleed).

One absorbed adjustment: your new graphite scoped selectors land in style.css before the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (test_issue677, test_issue856_pinned_indicator_layout, test_workspace_panel_session_list). I made those 3 tests anchor on the canonical unscoped rule (start-of-line) instead of the first .selector match — now robust against this and any future skin, and verified they still pass on clean master CSS. No change to your skin CSS was needed. Authorship preserved via Co-authored-by + CHANGELOG credit.

Closing as merged-via-release-stage.

eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 4, 2026
…➔ 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. ([#&#8203;2481](nesquena/hermes-webui#2481), [@&#8203;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 [#&#8203;2491](nesquena/hermes-webui#2491) lands). ([@&#8203;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. ([#&#8203;3433](nesquena/hermes-webui#3433), [@&#8203;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. ([#&#8203;3328](nesquena/hermes-webui#3328), [@&#8203;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. ([#&#8203;2974](nesquena/hermes-webui#2974), [@&#8203;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. ([#&#8203;2782](nesquena/hermes-webui#2782), [@&#8203;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. ([#&#8203;3505](nesquena/hermes-webui#3505), [@&#8203;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. ([#&#8203;3225](nesquena/hermes-webui#3225), [@&#8203;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). ([#&#8203;3338](nesquena/hermes-webui#3338), [@&#8203;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 ([#&#8203;1097](nesquena/hermes-webui#1097)), the internal tree-move ([#&#8203;3402](nesquena/hermes-webui#3402)), and OS-drop isolation ([#&#8203;3411](nesquena/hermes-webui#3411)) are all preserved. ([#&#8203;3402](nesquena/hermes-webui#3402), [#&#8203;3424](nesquena/hermes-webui#3424), [@&#8203;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 ([#&#8203;1097](nesquena/hermes-webui#1097)) and OS-file upload-on-drop ([#&#8203;3411](nesquena/hermes-webui#3411)) are unchanged. ([#&#8203;3402](nesquena/hermes-webui#3402), [#&#8203;3422](nesquena/hermes-webui#3422), [@&#8203;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. ([#&#8203;3440](nesquena/hermes-webui#3440), [@&#8203;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. ([#&#8203;3333](nesquena/hermes-webui#3333), [#&#8203;3471](nesquena/hermes-webui#3471), [@&#8203;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. ([#&#8203;3470](nesquena/hermes-webui#3470), [@&#8203;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, [#&#8203;1952](nesquena/hermes-webui#1952)) (salvaged from [#&#8203;3407](nesquena/hermes-webui#3407), [@&#8203;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 [#&#8203;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 ([#&#8203;2518](nesquena/hermes-webui#2518) follow-up, [@&#8203;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 ([#&#8203;3475](nesquena/hermes-webui#3475), [@&#8203;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 ([#&#8203;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 ([#&#8203;1952](nesquena/hermes-webui#1952), [@&#8203;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 ([#&#8203;3468](nesquena/hermes-webui#3468), [@&#8203;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, [#&#8203;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`) ([#&#8203;3289](nesquena/hermes-webui#3289), [@&#8203;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` ([#&#8203;3486](nesquena/hermes-webui#3486), [@&#8203;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) ([#&#8203;3472](nesquena/hermes-webui#3472), [@&#8203;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 ([#&#8203;3473](nesquena/hermes-webui#3473), [@&#8203;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 ([#&#8203;3480](nesquena/hermes-webui#3480), [@&#8203;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 [#&#8203;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 [#&#8203;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 ([#&#8203;3368](nesquena/hermes-webui#3368), [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes); with [@&#8203;garyd9](https://github.com/garyd9), confirmation [@&#8203;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 ([#&#8203;3457](nesquena/hermes-webui#3457), [@&#8203;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 ([#&#8203;3474](nesquena/hermes-webui#3474), [@&#8203;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 ([#&#8203;3433](nesquena/hermes-webui#3433), [@&#8203;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
@t3chn0pr13st

Copy link
Copy Markdown
Contributor Author

Thank you! I'm really glad the Graphite skin was useful. I appreciate you shipping it through the release stage and preserving the credit. Happy to have contributed!

SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
## Release v0.51.242 — Release HJ (stage-q14)

UX-approved via Telegram (Nathan — dark/light/mobile screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| nesquena#3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. |

### Why it's safe
- **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`).
- Opt-in; a user has to select it. Default experience unchanged.

### Test-robustness fix (absorbed)
The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened).

### Gate
- Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss)
- Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram

Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants