Skip to content

feat: add Geist Contrast skin - #2521

Merged
4 commits merged into
nesquena:masterfrom
intellectronica:feat/contrast-geist-skin
May 20, 2026
Merged

4 commits merged into
nesquena:masterfrom
intellectronica:feat/contrast-geist-skin

Conversation

@intellectronica

@intellectronica intellectronica commented May 18, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

This PR adds a narrowly scoped Geist Contrast skin to Hermes WebUI without changing the theme/mode model. It keeps the skin as a selectable visual style that works with both light and dark themes.

The skin key is geist-contrast; the user-facing label is Geist Contrast.

What Changed

  • Adds the Geist Contrast skin tokens and scoped component overrides.
  • Registers geist-contrast in the early boot allowlist, runtime skin picker, server settings validation, and /theme help text.
  • Documents the new skin in the theme docs, UI/UX guide, README, and changelog.
  • Adds focused regression coverage for skin registration, contrast-critical dark-mode affordances, neutral selected/sidebar states, and the command help skin list.

Verification

  • python -m pytest tests/test_geist_contrast_skin.py tests/test_issue2462_theme_i18n.py tests/test_sprint26.py -q
    • 20 passed in 2.21s
  • Browser-verified with an isolated WebUI server and extensions disabled:
    • dark mode using hermes-theme=dark, hermes-skin=geist-contrast
    • light mode using hermes-theme=light, hermes-skin=geist-contrast
    • Appearance picker shows Geist Contrast selected
  • Verified the screenshot run loaded only the skin supplied by this PR.

Screenshots

Dark

Hermes WebUI using the Geist Contrast skin in dark mode

Light

Hermes WebUI using the Geist Contrast skin in light mode

Appearance picker

Appearance settings with the Geist Contrast skin selected

Model Used

Implemented with Fnord via GPT-5.5.

@intellectronica
intellectronica force-pushed the feat/contrast-geist-skin branch from ddf3018 to 8672b69 Compare May 18, 2026 11:29
@intellectronica intellectronica changed the title feat: add Contrast Geist skin feat: add Geist Contrast skin May 18, 2026
@intellectronica
intellectronica force-pushed the feat/contrast-geist-skin branch from 8672b69 to c097ba5 Compare May 18, 2026 11:37
@intellectronica
intellectronica marked this pull request as ready for review May 18, 2026 11:42
@nesquena-hermes nesquena-hermes added the ux User experience / visual polish label May 18, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Skin registration looks clean. Reading static/boot.js:1204 (the _SKINS entry), static/index.html:20 (the early boot allowlist), api/config.py:4167 (server validation), and the CSS at static/style.css:296-464, the geist-contrast key is wired end-to-end and consistent. The value:'geist-contrast' field plus the (s.value||s.name).toLowerCase() change in _VALID_SKINS and _buildSkinPicker is the right move — it lets the user-facing label keep the space while the storage/data-attr key stays hyphenated.

One concrete bug I want to flag before merge, plus a couple of optional notes.

Bug: /theme geist-contrast slash command won't match this skin

cmdTheme in static/commands.js:665-707 still derives its skin allowlist from s.name.toLowerCase() only — it does not respect the new s.value field:

async function cmdTheme(args){
  const themes=['system','dark','light'];
  const skins=(_SKINS||[]).map(s=>s.name.toLowerCase());   // ← "geist contrast" (space)
  ...
  const val=(args||'').toLowerCase().trim();
  ...
  if(skins.includes(val)){ ... }                             // ← never true for "geist-contrast"

So when a user types /theme geist-contrast (the form the docs and i18n strings advertise — see static/i18n.js:191, cmd_theme: '... default/ares/.../catppuccin/nous/geist-contrast'), val is geist-contrast but skins contains geist contrast and the picker falls through to the usage toast. The only way /theme succeeds for this skin today is if the user types /theme geist contrast, which is not how the command tokenizes argument strings.

Same one-line fix you already applied in boot.js works here:

const skins=(_SKINS||[]).map(s=>(s.value||s.name).toLowerCase());

That is genuinely all this needs; the rest of cmdTheme already calls _normalizeAppearance(..., val) which routes through the shared _VALID_SKINS set and is correct.

While you're in cmdTheme, the usage-toast at line 707 (showToast(t('theme_usage')+...+skins.join('|')+...)) will also emit the wrong label for this skin until the same change is made — users see ... | geist contrast | ... which they can't type back.

Optional notes

  1. THEMES.md got updated to "eleven named skins" which is right today, but consider switching to "the built-in skins ship as default, ares, ..." once the count starts changing more often. Tiny nit.

  2. The dark-mode active-tab icon override at static/style.css:399-402 stomps [data-lucide] to var(--accent-text). That value is #f5e65f, a pale yellow on #000. Contrast on the active rail/nav glyphs is borderline — looks fine on the screenshots but might want a real-world check on the mobile rail at 14 px. Not a blocker, just a touch point if anyone reports the active icon being hard to see.

  3. The five tests in tests/test_geist_contrast_skin.py are all string-match assertions over the source files — they exercise registration and rule presence but not the cmdTheme codepath. If you take the fix above, a quick assertion that 'geist-contrast' in (s.value||s.name for s in _SKINS) (or just a string match (s.value||s.name) in the commands.js source) would catch this regression next time someone touches the picker.

Test plan

After the one-line commands.js:667 change:

  • /theme geist-contrast from the composer → skin switches, toast t('theme_set')+'geist-contrast'.
  • /theme geist contrast → falls through to usage toast (expected; the space form is not a key).
  • Existing tests/test_geist_contrast_skin.py still green.
  • No other call sites of _SKINS.map(s=>s.name) need touching — grep confirms _buildSkinPicker and _VALID_SKINS are the only other consumers and both were already updated in this PR.

@intellectronica

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review — especially for catching the /theme geist-contrast mismatch.

I addressed the actionable bug by updating cmdTheme in static/commands.js to build the slash-command skin allowlist from (s.value || s.name).toLowerCase(), matching the boot-time and picker paths. That makes /theme geist-contrast resolve to the stored skin key and also keeps the usage toast from advertising the untypeable geist contrast label.

I also added a focused regression assertion in tests/test_geist_contrast_skin.py so this codepath stays pinned if _SKINS handling changes again.

Validation run:

  • python -m pytest tests/test_geist_contrast_skin.py tests/test_issue2462_theme_i18n.py tests/test_sprint26.py -q
    • 21 passed in 2.17s

I left the optional documentation wording and active-icon contrast notes unchanged for this follow-up because they were non-blocking observations rather than requested fixes, and the main bug/test coverage are now handled in the PR branch.

@intellectronica
intellectronica force-pushed the feat/contrast-geist-skin branch from 6bb93e3 to c0b951e Compare May 19, 2026 06:37
@intellectronica

intellectronica commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Updated after Eleanor's clarification.

  • Addressed the documentation request by changing THEMES.md away from the brittle hard-coded skin count wording. It now describes the built-in skin palette generically while still listing geist-contrast in the accepted /theme skin names and skin table.
  • Also updated docs/CONTRACTS.md so the appearance contract index lists the new geist-contrast skin alongside the current static/boot.js / static/style.css skin axis.
  • Kept the active-icon contrast unchanged: Eleanor revalidated the current contrast and wants it intentionally left as-is for this PR.
  • Rebased the PR branch onto current master (v0.51.92 / 71c7035) and resolved the CHANGELOG.md conflict from the release history moving ahead.
  • Validation run: uv run python -m pytest tests/test_geist_contrast_skin.py tests/test_issue2462_theme_i18n.py tests/test_sprint26.py -q21 passed in 1.87s.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Sign-off

Re-read the four commits at 52cdeac1 HEAD and the verification looks solid.

static/commands.js:667:

const skins=(_SKINS||[]).map(s=>(s.value||s.name).toLowerCase());

That matches the boot-time + picker paths exactly, and the usage toast at commands.js:707 now emits geist-contrast (the typeable form) since it reads from the same skins array. tests/test_geist_contrast_skin.py::test_geist_contrast_slash_theme_uses_skin_value_key pins both the allowlist line and the success-toast wording, so a regression here would surface immediately.

Doc changes also look right:

  • THEMES.md swap from "eleven named skins" → "built-in skins ship as named keys" is the right level of abstraction — it stops the doc from rotting on every new skin without losing the user-facing contract.
  • docs/CONTRACTS.md:64-68 appearance index now includes geist-contrast next to nous, which keeps the contracts inventory honest.

The rebase onto 71c7035 resolved the CHANGELOG.md conflict cleanly (the new entry under v0.51.92 reads fine). And the explicit decision to leave the dark-mode active-icon contrast as-is is fine on my end — that was a "touch point" note, not a blocker.

No further asks from me. Verdict: ready to merge once any final maintainer checks pass.

@nesquena-hermes
nesquena-hermes force-pushed the feat/contrast-geist-skin branch from 52cdeac to 86d4375 Compare May 20, 2026 00:13
@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 9c983e6 May 20, 2026
stevesu2021 pushed a commit to AIBusinessPlatformCenter/hermes-webui-original that referenced this pull request May 20, 2026
stevesu2021 pushed a commit to AIBusinessPlatformCenter/hermes-webui-original that referenced this pull request May 20, 2026
Unreleased section now reflects:
- PR nesquena#2598 live tool event dedup (AJV20)
- PR nesquena#2533 browser dashboard links (AJV20)
- PR nesquena#2607 messaging transcript dedup (AJV20)
- PR nesquena#2521 Geist Contrast skin (intellectronica)
- PR nesquena#2524 SSE runtime diagnostics endpoint (AJV20)

Removed merge markers and consolidated stray entries that leaked into the v0.51.94 release block.
stevesu2021 pushed a commit to AIBusinessPlatformCenter/hermes-webui-original that referenced this pull request May 20, 2026
PR nesquena#2521 (Geist Contrast skin) legitimately adds a scoped
`:root[data-skin="geist-contrast"] .theme-pick-btn.active` override that
appears earlier in style.css than the global `#mainSettings .theme-pick-btn.active`
rule. The naive substring search in tests/test_1059_settings_picker_active_state.py
found the skin-specific override first (which correctly uses --border2 for its
palette), failing the global assertion that wanted --accent.

Tighten both assertions to anchor on the `#mainSettings` selector prefix so
they always match the global rule regardless of how many skin-specific
overrides land in the file.
stevesu2021 pushed a commit to AIBusinessPlatformCenter/hermes-webui-original that referenced this pull request May 20, 2026
…assertion

PR nesquena#2521 (Geist Contrast skin) adds a scoped
`:root[data-skin="geist-contrast"] .session-item.active .session-title` rule
that legitimately uses its own palette values. The existing assertion in
test_sprint40_ui_polish.py matched on any line containing the
`.session-item.active .session-title` substring, picking up the skin-scoped
override and asserting against its palette.

Exclude lines containing `:root[data-skin=` from the base-rule scan so
skin-scoped overrides are free to use their own design tokens, while the
global rule still enforces var(--gold) / var(--accent-text).
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 20, 2026
… 0.51.95) (#569)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.92` → `0.51.95` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.95`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05195--2026-05-20--Release-BS-stage-388--5-PR-batch--live-tool-callback-event-dedup--browser-only-dashboard-links--messaging-transcript-merge-alignment--Geist-Contrast-skin--SSE-runtime-diagnostics)

[Compare Source](nesquena/hermes-webui@v0.51.94...v0.51.95)

##### Fixed

- **PR [#&#8203;2598](nesquena/hermes-webui#2598 by [@&#8203;AJV20](https://github.com/AJV20) — Surface live tool activity when Hermes Agent reports tools through its dedicated `tool_start_callback` / `tool_complete_callback` path, so browser chat shows the existing running tool cards instead of appearing idle until the final answer. The legacy `on_tool` callback path now early-returns for `tool.started` and `tool.completed` events when the structured callback path is already wired, preventing the same tool event from being emitted twice to the SSE stream.
- **PR [#&#8203;2533](nesquena/hermes-webui#2533 by [@&#8203;AJV20](https://github.com/AJV20) — Allow Settings → System to save public browser-only Official Hermes Dashboard links (for reverse-proxy URLs) without treating them as server-side probe targets. URL sanitization runs against the configured link before save; the dashboard probe is skipped for browser-only links.
- **PR [#&#8203;2607](nesquena/hermes-webui#2607 by [@&#8203;AJV20](https://github.com/AJV20) — Deduplicate messaging/CLI session transcript rows when the sidecar and state store encode the same no-id message with equivalent timestamps in different formats (e.g. `"10.0"` vs `10`), preventing repeated visible chat rows after session reconstruction. The messaging-display merge now reuses `api.models._session_message_merge_key(...)` instead of an ad-hoc dedup key, aligning with the existing append-only merge path.

##### Added

- **PR [#&#8203;2521](nesquena/hermes-webui#2521 by [@&#8203;intellectronica](https://github.com/intellectronica) — Add the Geist Contrast skin to the appearance picker. New light + dark variant pair with a high-contrast yellow-on-black accent and Geist editorial typography. Default unchanged — opt-in via Settings → Appearance → Skin → Geist Contrast. Slash command `/theme geist-contrast` now resolves correctly because the lookup matches against `skin.value` rather than `skin.name`. Documented in `THEMES.md` with a forward-compatible skin count (no hard-coded value).
- **PR [#&#8203;2524](nesquena/hermes-webui#2524 by [@&#8203;AJV20](https://github.com/AJV20) — Add non-sensitive SSE stream runtime diagnostics to deep health checks (`/health?deep=1`), including active stream count, subscriber totals, and offline buffered-event counts for stuck or slow WebUI chat investigations. Read-only telemetry; existing surfaces unchanged.

### [`v0.51.94`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05194--2026-05-19--Release-BR-stage-387--10-PR-full-sweep-batch--Slice-4b-runner-adapter-facade--folder-zip-download--partial-recovery-marker-dedupe--browser-api-client-side-timeout--auto-compression-card-rotation-finish--composer-draft-rollback-fix--metadata-count-reconciliation--active-session-refresh-on-external-sidecar-updates--indexed-context-metadata--gateway-queues-approval-peek)

[Compare Source](nesquena/hermes-webui@v0.51.93...v0.51.94)

##### Fixed

- **PR [#&#8203;2566](nesquena/hermes-webui#2566 by [@&#8203;bjb2](https://github.com/bjb2) — Add `GET /api/folder/download?session_id=...&path=...` streaming-zip endpoint with pre-flight 413 on size/file-count cap exceeded, `os.walk(followlinks=False)` plus per-symlink workspace-root resolution check, `allowZip64=True` for large files, and a "Download Folder" item in the workspace file context menu (dir items only). Configurable caps via `HERMES_WEBUI_FOLDER_ZIP_MAX_MB` (1024 default) and `HERMES_WEBUI_FOLDER_ZIP_MAX_FILES` (50000 default). `download_folder` i18n key added across all 11 locales with `// TODO: translate` fallback markers for non-en entries.
- **PR [#&#8203;2593](nesquena/hermes-webui#2593 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2592](nesquena/hermes-webui#2592)) — Deduplicate cancelled/recovered partial assistant markers using the full `(content, reasoning, partial tool calls)` payload instead of only non-empty text content. Tool-only failed turns no longer append identical empty-content `_partial` messages repeatedly. Full session loads collapse adjacent duplicate partial markers from already-bloated session files while preserving a `.partial-bak-<timestamp>` backup. New helpers `_partial_message_signature()` (api/streaming.py:2593-2622) + `_partial_marker_already_present()` (api/streaming.py:2625-2641) scope the dedup search to the current user turn only.
- **PR [#&#8203;2597](nesquena/hermes-webui#2597 by [@&#8203;dso2ng](https://github.com/dso2ng) (closes [#&#8203;2539](nesquena/hermes-webui#2539)) — Add a 30s default client-side timeout to the shared browser `api()` helper, with per-call `timeoutMs` overrides, `AbortController`-based cancellation, a timeout toast, and explicit 60s/120s ceilings for legitimately longer update flows. Body-read phase also raced against the timeout so a server that replies headers-OK and then stalls mid-JSON rejects cleanly. New `tests/test_api_timeout.py` covers default, override, abort, and body-read-stall paths.
- **PR [#&#8203;2601](nesquena/hermes-webui#2601 by [@&#8203;starship-s](https://github.com/starship-s) — Prevent the composer-draft rollback regression introduced by [#&#8203;2581](nesquena/hermes-webui#2581 active-session external-refresh polling. Adds `opts.preserveActiveInput` to `_restoreComposerDraft` and gates the overwrite on `current && current !== text`, keeping the guard co-located with the function that owns the contract. Backend `s.save(touch_updated_at=False)` for `/api/session/draft` so draft autosaves no longer falsely advance `updated_at` and trigger the refresh poll. Supersedes parallel-discovery PR [#&#8203;2602](nesquena/hermes-webui#2602).
- **PR [#&#8203;2603](nesquena/hermes-webui#2603 by [@&#8203;starship-s](https://github.com/starship-s) — Finish the running auto-compression card after the backend rotates the session id. The `compressed` SSE listener at `static/messages.js:1829-1862` used to early-return whenever `S.session.session_id !== activeSid`, but the `state` event listener at `:1656-1662` already rotates `window._compressionUi.sessionId` to the continuation id before `compressed` arrives. The strict active-session check is replaced with a cross-session safety check that still rejects mismatched events but no longer rejects the legitimate post-rotation `done` payload, so the elapsed-timer "compressing…" state no longer freezes after rotation completes.
- **PR [#&#8203;2604](nesquena/hermes-webui#2604 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2594](nesquena/hermes-webui#2594)) — Reconcile session metadata counts in the `/api/session?messages=0` fast path. Replaces the prior `max(sidecar_count, state_count)` heuristic with `len(merge_session_messages_append_only(sidecar_messages, state_db_messages))` so the metadata-only count matches the full-load count. Closes the followup issue filed against PR [#&#8203;2581](nesquena/hermes-webui#2581) / v0.51.93 — sidebar refresh polling no longer loops forever when `state.db` retains old rows that the append-only merge correctly filters out.
- **PR [#&#8203;2605](nesquena/hermes-webui#2605 by [@&#8203;LumenYoung](https://github.com/LumenYoung) (refs [#&#8203;2581](nesquena/hermes-webui#2581)) — Make the metadata-only `/api/session?messages=0&resolve_model=0` path return the persisted sidecar `message_count` from `Session._metadata_message_count` when no session-index entry exists, so the active-session external-refresh signal still trips on legacy sessions whose sidecar contains externally-appended content. Composed cleanly with [#&#8203;2604](nesquena/hermes-webui#2604) (the legacy-fallback applies only when the reconciled merged count is zero).
- **PR [#&#8203;2573](nesquena/hermes-webui#2573 by [@&#8203;espokaos-ops](https://github.com/espokaos-ops) (closes [#&#8203;2510](nesquena/hermes-webui#2510)) — Persist session-level approvals when a "Allow for this session" click lands while a stream is active and `_pending` is empty. The approval flow now peeks `_gateway_queues[sid]` to recover the queued `_ApprovalEntry`'s `pattern_keys` so `approve_session()` records the approval; the next dangerous command in the same session no longer asks again. Reduced scope to peek-only per prior review note; the `agent_session_key` round-trip plumbing was dropped (it was dead on the WebUI streaming path).

##### Added

- **PR [#&#8203;2599](nesquena/hermes-webui#2599 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Add the Slice 4b `RunnerRuntimeAdapter` facade — a protocol-translator client over a future runner/sidecar backend. The facade delegates `start_run`, `observe_run`, `get_run`, and control calls to an injected runner client, normalizes results into the existing `RunStartResult`/`RunEventStream`/`RunStatus`/`ControlResult` dataclasses, carries explicit `profile`/`workspace`/`model` payload fields, and returns bounded `unsupported` control results without owning `AIAgent`, stream lifecycle, cancel/approval/clarify queues, goal state, or cached-agent table. No route wiring, no default-on runner mode, no public response-shape change.
- **PR [#&#8203;2600](nesquena/hermes-webui#2600 by [@&#8203;LumenYoung](https://github.com/LumenYoung) (refs [#&#8203;2266](nesquena/hermes-webui#2266)) — Slimmer WebUI follow-up from the closed LCM/context-engine PR [#&#8203;2266](nesquena/hermes-webui#2266). Adds rendering and persistence for context-engine compression-anchor metadata (when present on a session or live compression event) including an "Indexed context" detail line on auto-compression cards. No agent-layer clone orchestration; WebUI-only metadata surface.

### [`v0.51.93`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05193--2026-05-19--Release-BQ-stage-386--10-PR-full-sweep-batch--RFC-Slice-4-runnersidecar-gate--workspace-tree-toggle-width-CSS-variable--settled-file-markdown-link-rendering--prompt-cache-coverage-percentage-fix--terminal-shell-shutdown-reap--configured-model-picker-provider-preservation--profile-aware-assistant-display-names--statedb-reconciliation-slice-1--queued-message-cross-session-drain-fix--stale-stream-writeback-supersede)

[Compare Source](nesquena/hermes-webui@v0.51.92...v0.51.93)

##### Fixed

- **PR [#&#8203;2580](nesquena/hermes-webui#2580 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2571](nesquena/hermes-webui#2571)) — Centralize the workspace-tree toggle slot width into a `--file-tree-toggle-width` CSS variable at `:root`, referenced from both `.file-tree-toggle` and `.file-tree-toggle-placeholder` so a future width adjustment can't silently desync the two rules. Closes the followup issue filed against PR [#&#8203;2563](nesquena/hermes-webui#2563) / v0.51.92.
- **PR [#&#8203;2576](nesquena/hermes-webui#2576 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) (closes [#&#8203;470](nesquena/hermes-webui#470)) — Preserve labeled `file://` links in settled markdown by rewriting them to `/api/media?path=...&inline=1` before the sanitizer drops them. The streamed and settled markdown paths are now symmetric on local-file anchors, while raw `file://` image sources continue to be blocked.
- **PR [#&#8203;2579](nesquena/hermes-webui#2579 by [@&#8203;starship-s](https://github.com/starship-s) (refs [#&#8203;2419](nesquena/hermes-webui#2419), [#&#8203;2421](nesquena/hermes-webui#2421)) — Fix the prompt-cache hit percentage to display the fraction of the prompt served from cache (`cache_read / prompt_total`) instead of the meaningless `cache_read / (cache_read + cache_write)`. New `api/usage.py` `prompt_cache_hit_percent()` helper matches Hermes Agent's log convention; UI labels updated across all locales.
- **PR [#&#8203;2582](nesquena/hermes-webui#2582 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2577](nesquena/hermes-webui#2577)) — Harden embedded workspace-terminal shell cleanup so graceful WebUI shutdowns close/reap every active PTY shell and the spawned shell receives a Linux parent-death signal (`PR_SET_PDEATHSIG`) if the WebUI process dies. The terminal close path now waits again after `SIGKILL` so timed-out shells don't remain unreaped.
- **PR [#&#8203;2583](nesquena/hermes-webui#2583 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Make assistant display names properly profile-aware. The saved assistant-name preference applies only to the literal `default` profile; named profiles use their own profile name. Centralizes `assistantDisplayName()` resolution across composer placeholder, `document.title` via `syncTopbar()`, message role labels via `_assistantRoleHtml()`, browser notifications, cancel-copy fallback, and empty-state on session delete.
- **PR [#&#8203;2584](nesquena/hermes-webui#2584 by [@&#8203;wirtsi](https://github.com/wirtsi) (closes [#&#8203;2585](nesquena/hermes-webui#2585)) — Prevent queued follow-up messages from draining into the wrong chat when the user switches sessions during the 120ms `setBusy(false)` drain window. The drain-time guard re-queues against `sid` (not the currently-viewed session) and `_sendInProgressSid` captures the activeSid at the commit point so the re-entrant `send()` path no longer reads a stale `S.session.session_id`.
- **PR [#&#8203;2587](nesquena/hermes-webui#2587 by [@&#8203;AJV20](https://github.com/AJV20) — Allow a still-running stream that was mistakenly marked interrupted by stale-pending recovery to replace its own recovery marker when it later finishes, while continuing to block stale writeback after any newer turn appends transcript content. Three new tests in `tests/test_session_sidecar_repair.py` cover the supersede-allowed and the two refuse cases.
- **PR [#&#8203;2588](nesquena/hermes-webui#2588 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2569](nesquena/hermes-webui#2569)) — Preserve the configured provider when choosing a configured model from the composer picker. `_getOptionProviderId()` now reads `data-provider` from temporary `<option data-custom="1">` rows (created by `selectModelFromDropdown` for configured models outside the native catalog), so the next send routes through the correct provider instead of falling back to whatever provider was already active.

##### Changed

- **PR [#&#8203;2581](nesquena/hermes-webui#2581 by [@&#8203;LumenYoung](https://github.com/LumenYoung) (refs [#&#8203;2194](nesquena/hermes-webui#2194)) — First recovery slice from the closed reconciliation PR [#&#8203;2194](nesquena/hermes-webui#2194). Routes streaming session reconstruction and sidebar metadata through the reconciled state.db/session-summary path with a metadata-only fast path for sidebar polls and a single-snapshot reuse on the streaming hot path. Includes the reviewer-requested `_new_turn_context_from_messages` extraction so both legacy and streaming paths share the `_drop_checkpointed_current_user_from_context` + casual-fresh-chat suppression behavior (refs [#&#8203;1217](nesquena/hermes-webui#1217) / [#&#8203;2308](nesquena/hermes-webui#2308)). 923 LOC across `api/models.py`, `api/routes.py`, `api/streaming.py`, `static/sessions.js` + four new test files; second-pass agent diff review LGTM after the streaming-path regression was caught and fixed.

##### Documentation

- **PR [#&#8203;2575](nesquena/hermes-webui#2575 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Advance the runtime-adapter RFC to the Slice 4 runner/sidecar planning gate after [#&#8203;2560](nesquena/hermes-webui#2560) shipped the queue-staging clarification. The RFC now marks queue routing as staged by default, defines Slice 4a as a docs/test contract before any runner code lands, and pins default-off feature-flagging, restart/reattach success criteria, control parity, profile/workspace payload isolation, and explicit non-goals for legacy-backend removal or server-side queue scheduler work.

</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/569
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
Unreleased section now reflects:
- PR nesquena#2598 live tool event dedup (AJV20)
- PR nesquena#2533 browser dashboard links (AJV20)
- PR nesquena#2607 messaging transcript dedup (AJV20)
- PR nesquena#2521 Geist Contrast skin (intellectronica)
- PR nesquena#2524 SSE runtime diagnostics endpoint (AJV20)

Removed merge markers and consolidated stray entries that leaked into the v0.51.94 release block.
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
PR nesquena#2521 (Geist Contrast skin) legitimately adds a scoped
`:root[data-skin="geist-contrast"] .theme-pick-btn.active` override that
appears earlier in style.css than the global `#mainSettings .theme-pick-btn.active`
rule. The naive substring search in tests/test_1059_settings_picker_active_state.py
found the skin-specific override first (which correctly uses --border2 for its
palette), failing the global assertion that wanted --accent.

Tighten both assertions to anchor on the `#mainSettings` selector prefix so
they always match the global rule regardless of how many skin-specific
overrides land in the file.
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…assertion

PR nesquena#2521 (Geist Contrast skin) adds a scoped
`:root[data-skin="geist-contrast"] .session-item.active .session-title` rule
that legitimately uses its own palette values. The existing assertion in
test_sprint40_ui_polish.py matched on any line containing the
`.session-item.active .session-title` substring, picking up the skin-scoped
override and asserting against its palette.

Exclude lines containing `:root[data-skin=` from the base-rule scan so
skin-scoped overrides are free to use their own design tokens, while the
global rule still enforces var(--gold) / var(--accent-text).
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Unreleased section now reflects:
- PR nesquena#2598 live tool event dedup (AJV20)
- PR nesquena#2533 browser dashboard links (AJV20)
- PR nesquena#2607 messaging transcript dedup (AJV20)
- PR nesquena#2521 Geist Contrast skin (intellectronica)
- PR nesquena#2524 SSE runtime diagnostics endpoint (AJV20)

Removed merge markers and consolidated stray entries that leaked into the v0.51.94 release block.
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
PR nesquena#2521 (Geist Contrast skin) legitimately adds a scoped
`:root[data-skin="geist-contrast"] .theme-pick-btn.active` override that
appears earlier in style.css than the global `#mainSettings .theme-pick-btn.active`
rule. The naive substring search in tests/test_1059_settings_picker_active_state.py
found the skin-specific override first (which correctly uses --border2 for its
palette), failing the global assertion that wanted --accent.

Tighten both assertions to anchor on the `#mainSettings` selector prefix so
they always match the global rule regardless of how many skin-specific
overrides land in the file.
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
…assertion

PR nesquena#2521 (Geist Contrast skin) adds a scoped
`:root[data-skin="geist-contrast"] .session-item.active .session-title` rule
that legitimately uses its own palette values. The existing assertion in
test_sprint40_ui_polish.py matched on any line containing the
`.session-item.active .session-title` substring, picking up the skin-scoped
override and asserting against its palette.

Exclude lines containing `:root[data-skin=` from the base-rule scan so
skin-scoped overrides are free to use their own design tokens, while the
global rule still enforces var(--gold) / var(--accent-text).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ux User experience / visual polish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants