ci(windows): add native-Windows startup E2E workflow#2811
ci(windows): add native-Windows startup E2E workflow#2811Koraji95-coder wants to merge 2 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 first-class support for running Hermes WebUI on native Windows by introducing a PowerShell launcher, documenting the community native-Windows setup, and adding CI coverage to prevent Windows-specific startup regressions.
Changes:
- Add
start.ps1to launchserver.pydirectly on Windows (bypassingbootstrap.py’s Windows refusal) with.envloading and agent discovery. - Update README with links and notes for the community-maintained native Windows setup.
- Add a GitHub Actions workflow that boots the app on
windows-latestand probes/healthand/.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
start.ps1 |
New native Windows launcher that mirrors start.sh discovery and runs server.py directly. |
README.md |
Adds community native Windows guide links and notes, including mention of start.ps1. |
CHANGELOG.md |
Documents the new launcher and Windows startup CI workflow. |
.github/workflows/native-windows-startup.yml |
Adds Windows E2E startup check that creates a venv, starts the server, and probes endpoints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (-not $Python) { | ||
| foreach ($candidate in @('python3', 'python', 'py')) { | ||
| $cmd = Get-Command $candidate -ErrorAction SilentlyContinue | ||
| if ($cmd) { $Python = $cmd.Source; break } |
There was a problem hiding this comment.
Out-of-scope for this PR — the py fallback discussed lives in start.ps1 which is added by PR #2783. This PR (the CI workflow) only includes start.ps1 in its diff because it stacks on #2783 against master. Once #2783 merges, this PR rebases to drop those lines. Valid concern though — worth tracking as a follow-up to start.ps1 itself; @('py','-3') or splitting $PythonCommand / $PythonArgs is the right shape.
|
|
||
| Push-Location $RepoRoot | ||
| try { | ||
| & $Python $serverPath @args |
There was a problem hiding this comment.
Duplicate of the comment on line 79 — same out-of-scope reasoning. The py launcher concern lives in start.ps1 (PR #2783's territory), not this CI workflow PR.
| - name: Start server (background) via start.ps1 | ||
| shell: pwsh | ||
| run: | | ||
| $job = Start-Job -ScriptBlock { | ||
| param($repo) | ||
| Set-Location $repo | ||
| pwsh -NoLogo -File .\start.ps1 | ||
| } -ArgumentList $PWD |
There was a problem hiding this comment.
Addressed in 02f05339 — added a 'Provision hermes-agent (sibling install)' step that pip-installs hermes-agent into a sibling venv, copies hermes_cli from site-packages so start.ps1's Test-Path hermes_cli -PathType Container guard passes, and exports HERMES_WEBUI_AGENT_DIR for start.ps1 to consume. Falls back to an empty hermes_cli dir if site-packages doesn't expose it — server.py would then surface a clearer runtime error if the CLI is actually needed at startup, which is the right signal.
| $job = Start-Job -ScriptBlock { | ||
| param($repo) | ||
| Set-Location $repo | ||
| pwsh -NoLogo -File .\start.ps1 | ||
| } -ArgumentList $PWD |
There was a problem hiding this comment.
Addressed in 02f05339 — replaced Start-Job with Start-Process -PassThru capturing the wrapper PID. The cleanup step now runs taskkill /PID $env:SERVER_PID /T /F which walks the process tree (/T) and force-terminates (/F). Also added belt-and-suspenders: kill anything still bound to port 8787 if the taskkill missed something. Also captured start.ps1 stdout/stderr to $RUNNER_TEMP and dump them on /health timeout so future failures have actionable signal.
| - name: Stop background server | ||
| if: always() | ||
| shell: pwsh | ||
| run: | | ||
| if ($env:JOB_ID) { | ||
| Stop-Job -Id $env:JOB_ID -ErrorAction SilentlyContinue | ||
| Remove-Job -Id $env:JOB_ID -Force -ErrorAction SilentlyContinue | ||
| } |
There was a problem hiding this comment.
Same fix as above — addressed in 02f05339. The taskkill /T /F tree-walk is in the always() cleanup step and captures the spawned server.py child process via the wrapper PID.
| a fresh bootstrap. For first-time setup, run bootstrap.py inside | ||
| WSL2 once to create the venv, then this script can use that venv. |
There was a problem hiding this comment.
Already resolved upstream — PR #2806 corrected this guidance in start.ps1's .DESCRIPTION block (the WSL2 venv claim was replaced with the native Windows python -m venv venv setup). #2806 has been merged. This PR shows the stale text only because it stacks on #2783 against master; once #2783 rebases onto master, the corrected text will appear.
| - **Memory:** community-measured ~330 MB native vs ~1080 MB with WSL2+Docker (varies by configuration). | ||
| - **What works:** chat, workspace browser, session management, all themes. | ||
| - **Known limitations:** some POSIX-style file paths surface in the workspace browser; bash-assuming agent tools may not work natively. | ||
| - **WSL2 relationship:** WSL2 is recommended *once* for first-time venv creation (since `bootstrap.py` currently refuses on native Windows). After the venv exists, `start.ps1` at the repo root runs the WebUI natively by invoking `server.py` directly — no WSL2 needed for day-to-day use. |
There was a problem hiding this comment.
Already resolved upstream — PR #2806 split the dense WSL2 bullet into a 'Native Windows setup' bullet (python -m venv venv → pip install -r requirements.txt → pwsh .\start.ps1) and a separate WSL2-relationship one-liner that explicitly notes a WSL2-built venv is a Linux venv that won't be invokable by native Windows Python. #2806 has been merged. The stale text shows in this PR's diff only because it stacks on #2783 against master.
Two workflow-specific issues caught by Copilot's review:
1. **hermes-agent not provisioned.** start.ps1 errors out if it can't
find %USERPROFILE%\.hermes\hermes-agent or a ../hermes-agent sibling.
Added a "Provision hermes-agent (sibling install)" step that:
- pip-installs hermes-agent into a sibling directory's venv
- copies hermes_cli from site-packages to satisfy start.ps1's
Test-Path hermes_cli -PathType Container guard
- exports HERMES_WEBUI_AGENT_DIR for start.ps1 to consume
Falls back to creating an empty hermes_cli dir if the package
layout doesn't expose hermes_cli — server.py will then surface a
clearer runtime error if the CLI is actually needed.
2. **Stop-Job doesn't tree-kill the spawned server child.** Replaced
the Start-Job wrapper with Start-Process -PassThru capturing the
wrapper PID, then `taskkill /PID $SERVER_PID /T /F` walks the
process tree in the always() cleanup step. Belt-and-suspenders:
also kill anything still bound to port 8787.
Also captured start.ps1's stdout/stderr to $RUNNER_TEMP and dumped
them on /health timeout so future failures have actionable signal
instead of an opaque "60s timeout" message.
Timeout-minutes bumped 10 → 12 to accommodate the hermes-agent venv
install step (which can add ~30-60s on cold cache).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ws-only stack) Cherry-picked PRs (all by @Koraji95-coder): - #2805 — expand hermes-agent candidate paths for Windows installers - #2806 — clarify native Windows venv path; remove WSL2-venv-portability claim - #2807 — TryParse HERMES_WEBUI_PORT + exit AFTER try/finally cleanup - #2811 — native-Windows startup E2E CI workflow All 4 PRs were branched off #2783 (now shipped in v0.51.121). Squash-merged each PR's unique changes onto current master with conflict resolution. Authorship preserved on every commit. Zero impact on Linux/macOS runtime — file scope is start.ps1, README.md (Windows section), and a new Windows-CI workflow that only runs on PRs touching start.ps1/requirements.txt/etc.
|
Hi @Koraji95-coder — held this one out of the v0.51.124 batch after running the workflow against itself on release/stage-batch6. The workflow's Could you rework the provisioning step? Two viable options:
Option 1 is probably what we want — it catches WOW64/path/PowerShell regressions without depending on a full agent install. The other 3 PRs (#2805/#2806/#2807) shipped in v0.51.124 today; this one stays open with the |
…k, option 1) Per @nesquena-hermes review on nesquena#2811: hermes-agent isn't published to PyPI, so `pip install hermes-agent` finds nothing and start.ps1's hermes_cli guard correctly bails out — leaving the previous workflow unable to self-validate against release/stage-batch6. This rework adopts option 1 from the review: drop the pip install, stub a hermes_cli/ directory with a minimal __init__.py next to the sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and assert that none of its own Write-Error guards (no Python, no agent dir, bad port, missing hermes_cli, missing server.py) appeared in stderr. /health is no longer probed — the server cannot boot on a stub, and full-boot regressions stay covered by the Linux jobs and docker-smoke.yml. Scope intentionally narrower than the original: this workflow validates start.ps1's PowerShell syntax + path discovery only. The exact bug class PR nesquena#2805 caught (WOW64 ProgramFiles redirect) would now light up red here pre-merge, which is the reason this gate exists. Paths filter trimmed to `start.ps1` + the workflow itself; the broader list (requirements.txt / bootstrap.py / server.py) was inherited from the original full-boot scoping and isn't relevant for a path-discovery- only run. Verification: workflow runs on this PR via its own pull_request trigger. The first CI run on this branch IS the verification. CHANGELOG updated under [Unreleased] with a single bullet sized to the surrounding density.
02f0533 to
6636cd4
Compare
The path-discovery step succeeds on the first run, but the cleanup
step exits non-zero because `taskkill /PID 5560 /T /F` returns 128
("process not found") when server.py has already exited on the mock
hermes_cli stub. That's the expected steady state for this mock-only
workflow, not a failure.
Two-line fix: reset `$global:LASTEXITCODE = 0` after the taskkill
call, and explicit `exit 0` at the end of the step so any other
external-command exit codes don't bubble up. The try/catch wrapper
didn't help because taskkill writes its diagnostic to stderr without
raising a PowerShell exception — `catch` never fired.
Run 26352805510 on this branch shows the failure shape: "OK: start.ps1
path discovery - all guards passed." in the verify step, then
"ERROR: The process '5560' not found." in the cleanup step. Path
discovery is what this workflow exists to validate; cleanup just has
to not fail the job.
|
Reworked per your option 1 — pushed
Scope is now strictly: PowerShell syntax + Also rebased onto current Verification: run 26352903974 on this branch went green — both "Native Windows startup" and "Tests" passed on Ready for your review when you have a moment. Edited 2026-05-24: corrected a broken verification link (was pointing at the fork's actions URL with the wrong run id; PR runs live on the upstream repo). The right URL is the one shown above. Caught by a human reviewer on our side before you saw the stale link. |
Ready for re-review —
|
|
Shipped in v0.51.127 (Release CY) — https://github.com/nesquena/hermes-webui/releases/tag/v0.51.127 Thanks for the contribution! Your changes are now live on master. Closing this PR as merged-via-stage-branch. |
…k, option 1) Per @nesquena-hermes review on nesquena#2811: hermes-agent isn't published to PyPI, so `pip install hermes-agent` finds nothing and start.ps1's hermes_cli guard correctly bails out — leaving the previous workflow unable to self-validate against release/stage-batch6. This rework adopts option 1 from the review: drop the pip install, stub a hermes_cli/ directory with a minimal __init__.py next to the sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and assert that none of its own Write-Error guards (no Python, no agent dir, bad port, missing hermes_cli, missing server.py) appeared in stderr. /health is no longer probed — the server cannot boot on a stub, and full-boot regressions stay covered by the Linux jobs and docker-smoke.yml. Scope intentionally narrower than the original: this workflow validates start.ps1's PowerShell syntax + path discovery only. The exact bug class PR nesquena#2805 caught (WOW64 ProgramFiles redirect) would now light up red here pre-merge, which is the reason this gate exists. Paths filter trimmed to `start.ps1` + the workflow itself; the broader list (requirements.txt / bootstrap.py / server.py) was inherited from the original full-boot scoping and isn't relevant for a path-discovery- only run. Verification: workflow runs on this PR via its own pull_request trigger. The first CI run on this branch IS the verification. CHANGELOG updated under [Unreleased] with a single bullet sized to the surrounding density.
…➔ 0.51.134) (#650)
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.124` → `0.51.134` |
---
> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/567) for more information.
---
### Release Notes
<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>
### [`v0.51.134`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051134--2026-05-25--Release-DF-stage-batch16--single-PR-Windows-path-defaults)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.133...v0.51.134)
##### Fixed
- **PR [#​2897](https://github.com/nesquena/hermes-webui/issues/2897)** by [@​chouzz](https://github.com/chouzz) — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes [#​2840](https://github.com/nesquena/hermes-webui/issues/2840).
### [`v0.51.133`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051133--2026-05-25--Release-DE-stage-batch15--6-PR-contributor-batch--aux-task-validation--workspace-artifact-gating--update-apply-guard--Joplin-auth-header--prefill-cache-guard--notes-drawer-i18n)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.132...v0.51.133)
##### Fixed
- **PR [#​2891](https://github.com/nesquena/hermes-webui/issues/2891)** by [@​franksong2702](https://github.com/franksong2702) — Auxiliary model settings now reject unknown task slots instead of allowing arbitrary keys under `config.yaml`'s `auxiliary` block. Valid slots and the `__reset__` sentinel continue to work.
- **PR [#​2892](https://github.com/nesquena/hermes-webui/issues/2892)** by [@​franksong2702](https://github.com/franksong2702) — Workspace Artifacts now keeps read-only tool paths out of the "files changed" list by gating structured path extraction to known file-mutation tools.
- **PR [#​2893](https://github.com/nesquena/hermes-webui/issues/2893)** by [@​franksong2702](https://github.com/franksong2702) — Update Now no longer reports success or enters the restart wait flow when no WebUI or Agent update target is selected.
- **PR [#​2895](https://github.com/nesquena/hermes-webui/issues/2895)** by [@​franksong2702](https://github.com/franksong2702) — Cached WebUI agents no longer overwrite `prefill_messages` with an empty list when a later request does not include explicit prefill context.
- **PR [#​2894](https://github.com/nesquena/hermes-webui/issues/2894)** by [@​franksong2702](https://github.com/franksong2702) — Joplin notes drawer API calls now send the Web Clipper token in an `Authorization` header instead of placing it in the request URL query string.
- **PR [#​2896](https://github.com/nesquena/hermes-webui/issues/2896)** by [@​franksong2702](https://github.com/franksong2702) — Third-party notes drawer copy now uses localized strings in the supported non-English locale bundles (it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr) instead of reusing the English defaults.
### [`v0.51.132`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051132--2026-05-24--Release-DD-stage-batch14--4-PR-replayed-context--interrupted-response--shutdown-affordance--passkey-opt-in)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.131...v0.51.132)
##### Added
- **PR [#​2859](https://github.com/nesquena/hermes-webui/issues/2859)** by [@​AJV20](https://github.com/AJV20) — Optional passkey/WebAuthn sign-in for password-protected WebUI instances. Authenticated users can register/remove passkeys from Settings -> System, and `/login` shows a passwordless sign-in button only after a passkey exists. Password auth remains the default-off bootstrap and recovery path. **Opt-in default-off behind `HERMES_WEBUI_PASSKEY=1` env var or `webui_passkey_enabled: true` config flag** — when disabled, the UI block hides, all 6 `/api/auth/passkey/*` endpoints return 404, and `is_auth_enabled()` ignores any pre-existing credential file so the auth posture cannot silently flip if the flag is unset later.
- **PR [#​2824](https://github.com/nesquena/hermes-webui/issues/2824)** by [@​gavinssr](https://github.com/gavinssr) — A "Stop server" affordance in Settings → System that gracefully shuts down the local WebUI server. Useful when WebUI was launched via `./ctl.sh start` or the native macOS/Windows app and the user wants to stop it without context-switching to a terminal. Confirmation dialog before the actual shutdown. The `/api/shutdown` route is CSRF-gated and intended for local-loopback use. Originally a title-bar button; relocated to Settings per the project's deep-UX rule (default-hidden for niche destructive actions on always-visible surfaces).
##### Fixed
- **PR [#​2685](https://github.com/nesquena/hermes-webui/issues/2685)** by [@​LumenYoung](https://github.com/LumenYoung) — Prevent replayed context in chat reconciliation and metering. When a WebUI session is recovered (e.g., after a process restart, network drop, or browser reload), the sidebar/`state.db` reconciliation logic walks the sidecar transcript in order and only skips rows that can actually be aligned with the remaining sidecar context. The prior set-membership check was too broad: a legitimate fresh message that happened to share a key with any older repeated short message in the sidecar was mis-classified as already-seen and dropped from the replay, leading to lost context and inconsistent metering. Also caps the per-turn live-tool-prompt token estimate at 12,000 to prevent unbounded growth on bursts of large tool reads before exact provider accounting overrides.
- **PR [#​2739](https://github.com/nesquena/hermes-webui/issues/2739)** by [@​ai-ag2026](https://github.com/ai-ag2026) — Clarify `Response interrupted` recovery markers so they report that the live response stream stopped instead of asserting that the WebUI process restarted. The recovery path now records distinct interruption causes for real process restarts, stream/run split-brain, and lost worker bookkeeping; browser-side SSE transport failures show a separate `Connection interrupted` message, client-side `BrokenPipeError` disconnects no longer get logged as server 500s, and chat/gateway SSE errors emit rate-limited (30 events / 60s / 4KB body cap), sanitized client diagnostics to `/api/client-events/log` for future root-cause checks. The stream-status `terminal_state` value for lost-worker bookkeeping changes from `stale-from-restart` to `lost-worker-bookkeeping`, matching the new non-restart wording.
##### Notes
- **6,532 pytest passed** sequentially before Opus pass + locale parity fix; full re-run pending after Opus SHOULD-FIX patches.
- **Opus Advisor verdict: SHIP-WITH-SHOULD-FIXES applied.** Zero MUST-FIX. Four SHOULD-FIX items patched inline before tag:
- `/api/auth/status` now gates `passkeys_enabled` / `passwordless_enabled` on the feature flag (fixes broken-affordance trap where passkey login button could show but endpoints returned 404)
- Settings → System Passkeys block now starts `display:none` and only reveals when the server confirms the flag is on AND credentials are accessible
- `/api/settings/save` refuses to set passwordless mode when the passkey feature flag is off (closes the auth-bypass path: user goes passwordless while flag on → admin unsets flag → restart serves WebUI fully unauthenticated)
- CHANGELOG entries added for PR [#​2685](https://github.com/nesquena/hermes-webui/issues/2685) and PR [#​2824](https://github.com/nesquena/hermes-webui/issues/2824) (both originally missing despite functional code changes)
- Deferred to follow-up: per-turn cumulative live-tool-prompt token cap ([#​2685](https://github.com/nesquena/hermes-webui/issues/2685) only added per-call cap; aggregate across many tool calls is a separate refactor).
- **i18n parity**: 7 new shutdown-affordance keys added across all 11 non-en locales (it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr) so locale parity tests pass on first run.
### [`v0.51.131`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051131--2026-05-24--Release-DC-stage-batch13--6-PR-notes-drawer--context-parity--PWA-swipe--locale-polish)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.130...v0.51.131)
##### Added
- **PR [#​2868](https://github.com/nesquena/hermes-webui/issues/2868)** by [@​AJV20](https://github.com/AJV20) — Installed/mobile PWA sessions now support an edge swipe from the left side of the screen to open the mobile sidebar drawer, while preserving the existing hamburger and overlay controls. PWA-standalone-gated, edge X<28px, vertical-tolerance 48px, interactive-target exclusion. Defends against accidental triggers from text selection or button taps.
- **PR [#​2527](https://github.com/nesquena/hermes-webui/issues/2527)** by [@​AJV20](https://github.com/AJV20) — Default-off, read-only Third-party notes drawer in the Memory panel. Lists configured note/knowledge MCP sources (Joplin, Obsidian, Notion, llm-wiki) when explicitly enabled via `webui_external_notes_sources` config or `HERMES_WEBUI_EXTERNAL_NOTES_SOURCES=1`. Automatic session recall unchanged. 4 API endpoints (`/api/notes/sources`, `/api/notes/search`, `/api/notes/item`, plus `external_notes_enabled` in memory\_read response) all gated behind the feature flag.
- **PR [#​2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@​AJV20](https://github.com/AJV20) — SSE stream runtime diagnostics in deep health checks: active stream count, subscriber totals, and offline buffered-event counts for stuck or slow WebUI chat investigations. Non-sensitive payload only.
- **PR [#​2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@​AJV20](https://github.com/AJV20) — WebUI session prefill parity for bounded JSON files. Browser-originated chat turns can load configured prefill context from `prefill_messages_file`, pass it to Hermes Agent as ephemeral model context, and surface a compact context status event in the chat UI without exposing prefill message bodies. WebUI intentionally does not execute `prefill_messages_script`; executable recall should go through the existing MCP/tool surface. Backward-compatible: degrades gracefully on older agent builds that don't support the `prefill_messages` kwarg.
##### Changed
- **PR [#​2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@​AJV20](https://github.com/AJV20) — Browser-surface session context is now attached to WebUI agent turns so the agent can distinguish a WebUI chat from messaging-platform transcripts. Context is ephemeral (not saved to history). WebUI progress guidance now preserves the normal Hermes messaging style instead of encouraging extra browser-only status chatter.
##### Fixed
- **PR [#​2865](https://github.com/nesquena/hermes-webui/issues/2865)** by [@​AJV20](https://github.com/AJV20) — New WebUI sessions no longer persist `display.personality` into per-session `Session.personality`; only explicit personality changes remain durable, preventing stale global display defaults from overriding profile-scoped session behavior. Closes [#​2845](https://github.com/nesquena/hermes-webui/issues/2845).
- **PR [#​2882](https://github.com/nesquena/hermes-webui/issues/2882)** by [@​ycj](https://github.com/ycj) — zh-CN (Simplified Chinese) session-time relative labels are now clearer: `${n}分钟前`, `${n}小时前`, `${n}天前`, and the more natural last-week phrasing `上周` instead of the previous bare-unit shorthand. Also corrects a small indentation glitch in the zh-TW (Traditional Chinese) locale. (Cherry-picked onto fresh stage with `Co-authored-by` attribution — original PR was based on stale master.)
- **PR [#​2873](https://github.com/nesquena/hermes-webui/issues/2873)** by [@​Charanis](https://github.com/Charanis) — The WebUI launcher (`ctl.sh` + `bootstrap.py`) now preserves environment variables that have already been resolved by the shell (for example `HERMES_WEBUI_PORT`, `HERMES_WEBUI_STATE_DIR`, `HERMES_WEBUI_HOST`) instead of letting a repo-level `.env` clobber them mid-launch. The `.env` keeps working as a default-only source for unset variables, gated by `HERMES_WEBUI_PRESERVE_ENV=1` set by the launcher subshell.
##### Notes
- **6,503 pytest passed** (sequential mode; the test infrastructure uses a single test server that doesn't support xdist parallelism — known limitation, tracked separately).
- **Opus Advisor verdict: SHIP-AS-IS.** Zero MUST-FIX. Three SHOULD-FIX items filed as follow-up issues (incomplete locale coverage for notes-drawer i18n keys, `_joplin_api_get` URL-token defense-in-depth, prefill `setattr` cache-reuse safety net).
- **[#​2527](https://github.com/nesquena/hermes-webui/issues/2527) i18n coverage**: 10 of the 11 non-en locales currently ship the English string `'Third-party notes'` for the drawer header. Since the drawer is default-off, user impact is zero today; follow-up issue tracks proper translations before any default-on transition.
### [`v0.51.130`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051130--2026-05-24--Release-DB-stage-batch12--3-PR-profile-isolation--boot-precedence--workspace-Artifacts-tab)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.129...v0.51.130)
##### Fixed
- **PR [#​2827](https://github.com/nesquena/hermes-webui/issues/2827)** by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Profile state-sync TLS-vs-thread fix (closes [#​2762](https://github.com/nesquena/hermes-webui/issues/2762)). When switching profiles via the WebUI cookie selector, session token-usage and title were being written to the *previously-active* profile's `state.db` instead of the cookie-switched one (sidecar messages + workspace files were already routed correctly; only the `state.db` sidecar sync leaked). Root cause: the cookie middleware sets `_tls.profile = '<cookie>'` on the HTTP request thread, but the daemon thread spawned in `_run_agent_streaming` doesn't inherit that TLS. When the streaming worker calls `sync_session_usage`, `_get_state_db → get_active_hermes_home → get_active_profile_name` finds no TLS profile, falls through to `_active_profile` (the process default), and opens the wrong DB. Fix plumbs the session's own `profile` field through `sync_session_usage(..., profile=...)` and `_get_state_db(profile=...)` rather than leaning on TLS that doesn't exist on the worker thread. Keeps the existing TLS path for callers that don't pass `profile=` explicitly, so external integrations don't regress. Also adds defensive `_validate_profile_name` rejecting `../etc`, leading-dash, whitespace, and over-long names (prevents path traversal via cookie tampering). Adds 11 regression tests covering explicit-profile honors, multi-thread profile preservation, unknown-profile-name fallback path, invalid-name refusal, and legacy-call-shape compatibility.
- **PR [#​2726](https://github.com/nesquena/hermes-webui/issues/2726)** by [@​starship-s](https://github.com/starship-s) — Boot model-default precedence follow-up (refines [#​2709](https://github.com/nesquena/hermes-webui/issues/2709)). The original v0.51.105 fix correctly preferred the profile/server default on fresh boot, but the implementation had two over-broad side effects flagged in post-merge review: (1) boot unconditionally cleared the persisted browser model state, even on restored sessions where that state should remain authoritative, and (2) `populateModelDropdown()` reapplied the default on every repopulate when no session model was present, which clobbered the in-page selection during ordinary dropdown refreshes. Fix is to gate the default-reapply behind an opt-in `{preferProfileDefaultOnFreshBoot: true}` parameter so boot keeps profile-default precedence, restored sessions keep their session model, and non-boot dropdown refreshes preserve the loaded session's model or the current in-page selection. Browser model state is no longer deleted just because the profile default wins this boot. Expanded the regression test coverage with a Node `select` / DOM shim that exercises the real `populateModelDropdown()` path for boot-default, restored-session, current-selection, and removed-model scenarios (+306 LOC tests).
##### Added
- **PR [#​2673](https://github.com/nesquena/hermes-webui/issues/2673)** by [@​AJV20](https://github.com/AJV20) — Workspace Artifacts tab (closes [#​2655](https://github.com/nesquena/hermes-webui/issues/2655)). New tab in the workspace panel that lists likely files mentioned, edited, or created during the active session. Prioritizes structured tool-call paths (file\_write, edit, patch, etc.), filters dependency/build noise (node\_modules, `__pycache__`, `.git`, lock files), and refreshes while live tool calls arrive. Artifact entries open through the existing workspace file preview flow. The MVP is frontend-scoped — backend ingestion uses the existing tool-call event stream rather than a new persistence path — so the maintainer can evaluate the UX before deciding whether artifact tracking should grow into a backend-backed feature. Refreshes alongside the file tree in `loadDir()` via a `typeof renderSessionArtifacts==='function'` guard so it composes cleanly with [#​2716](https://github.com/nesquena/hermes-webui/issues/2716)'s session stale-guard pattern. Adds `tests/test_issue2655_frontend.py`.
##### Notes
- **In-stage cherry-pick mechanics**: All 3 PRs were on stale-base merge-bases (master had advanced through 3 releases). Used `git apply --3way` of each PR's net delta vs its merge-base onto current stage HEAD, then resolved 2 small JS conflicts manually:
- `static/boot.js` ([#​2726](https://github.com/nesquena/hermes-webui/issues/2726) vs [post-#​2716](https://github.com/post-/hermes-webui/issues/2716) master): kept PR's parameterized `populateModelDropdown({preferProfileDefaultOnFreshBoot:true})` call (the whole point of [#​2726](https://github.com/nesquena/hermes-webui/issues/2726)) on top of master's [#​2716](https://github.com/nesquena/hermes-webui/issues/2716) hydration flow.
- `static/workspace.js` ([#​2673](https://github.com/nesquena/hermes-webui/issues/2673) vs [post-#​2716](https://github.com/post-/hermes-webui/issues/2716) master): kept master's `sessionId`-capture stale-session guard (closure-scoped sessionId check after `await`) AND added PR's `renderSessionArtifacts()` call to refresh the new Artifacts tab when the file tree updates. Wrapped in `typeof === 'function'` guard for defense-in-depth.
- **In-stage test fixes**: Patched 3 brittle source-string assertions to accept both [pre-#​2716](https://github.com/pre-/hermes-webui/issues/2716) and [post-#​2716](https://github.com/post-/hermes-webui/issues/2716) JS shapes (variable names changed during the cherry-pick, semantics preserved). Patched 1 schema mismatch in `tests/test_issue2762_state_sync_profile_kwarg.py::_read_session` helper — it queried `sessions.session_id` but the real `state.db` schema has `sessions.id` as primary key. Fix is mechanical: `SELECT id AS session_id` + `WHERE id = ?` so the helper queries the actual schema.
- Full pytest: pending re-run on this finalized stage. Touched-tests gate: 41 passed (covering [#​2827](https://github.com/nesquena/hermes-webui/issues/2827) + [#​2726](https://github.com/nesquena/hermes-webui/issues/2726) + [#​2673](https://github.com/nesquena/hermes-webui/issues/2673) surface areas).
- Agent self-verified: profile= kwarg threading on `_get_state_db` + `sync_session_usage`, production call site in `api/streaming.py:5078` passes `profile=getattr(s, 'profile', None)`, `populateModelDropdown` opt-in parameterization present, boot.js calls with `preferProfileDefaultOnFreshBoot:true`, workspace `renderSessionArtifacts()` defined + called.
### [`v0.51.129`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051129--2026-05-24--Release-DA-stage-batch11--4-PR-feature--perf-batch)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.128...v0.51.129)
##### Performance
- **PR [#​2836](https://github.com/nesquena/hermes-webui/issues/2836)** by [@​v2psv](https://github.com/v2psv) — HTTP/1.1 keep-alive for WebUI responses. Bumps `Handler.protocol_version` from the HTTP/1.0 default to `HTTP/1.1` so browsers can reuse TCP connections across normal API and static-file requests. Adds explicit `Content-Length` headers to hand-written responses that weren't already using shared `j()` / `t()` helpers. Adds `Content-Length: 0` to empty redirect / range-error responses. Switches SSE-style streaming endpoints from `Connection: keep-alive` to `Connection: close` (keep-alive is only safe when the response body is framed; SSE bodies have no fixed length). Significant first-paint / session-open improvements on high-RTT / VPN / proxied paths — author reports \~47% faster first paint and \~30-40% improvements on panel-load flows on a typical remote-host setup.
**Opus pre-release advisor caught one missing framing site** in the on-the-fly folder ZIP download path (`/api/folder/download`): the body has no known length, doesn't use chunked encoding, and was relying on HTTP/1.0 connection-close-equals-EOF. Under HTTP/1.1 this would have left clients hanging waiting for the next response after the central-directory bytes finished. Patched inline before tag: add `Connection: close` header to mirror the SSE-endpoint pattern. Opus verified this was the ONLY remaining streaming response in the codebase that needed the header — all 12 hand-written response paths + 8 SSE streams + j()/t() helpers + auth flow were already correctly framed by the PR.
##### Added
- **PR [#​2680](https://github.com/nesquena/hermes-webui/issues/2680)** by [@​mccxj](https://github.com/mccxj) — Auxiliary Models settings card in Settings → Preferences. Lets users configure per-task model routing for 9 canonical side-task slots: vision, web extract, compression, session search, approval, MCP tool reasoning, title generation, skills hub, curator. Each slot exposes a provider dropdown + model dropdown plus an "auto (use main model)" / "auto (use provider default)" pair so users can keep aux routing implicit when they don't care. New endpoints: `GET /api/model/auxiliary` returns current assignments; `POST /api/model/set` writes assignments (`scope=auxiliary` for aux slots, `scope=main` for the default chat model) and supports `task="__reset__"` to reset all slots back to auto. 16 new i18n keys added across all 12 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr — Turkish translations added in-stage to cover the sibling-PR collision with v0.51.127's Turkish locale baseline). 24 source-level test assertions covering HTML structure, JS logic, i18n parity, and route registration.
- **PR [#​2842](https://github.com/nesquena/hermes-webui/issues/2842)** by [@​AJV20](https://github.com/AJV20) — PWA polish for installed launches. New `static/pwa-startup.js` is loaded synchronously in `<head>` before the main UI bundle, so the page knows whether it's running standalone / in-browser / on iOS / offline before first paint. Marks `pwa-standalone`, `pwa-browser`, `pwa-ios`, `pwa-offline`, and short-lived `pwa-resumed` classes on `<html>`. Exposes `window.HermesPWA.{isStandalone, syncMode, launchAction, promptInstall}` helpers and captures `beforeinstallprompt` / `appinstalled` early enough that any future install-prompt UI can chain off them. Manifest gains app identity / scope / `display_override` (`window-controls-overlay` → `standalone` → `minimal-ui`) and a "New conversation" PWA App Shortcut. Service worker pre-caches the startup helper, switches navigation and shell-asset fetches to `cache: 'no-store'` before falling back to CacheStorage. Boot path wires `?source=pwa&action=new-chat` to start a fresh chat instead of reopening the last saved session. The viewport meta now sets `maximum-scale=1, user-scalable=no` for native-feel — acknowledged trade-off against WCAG 2.1 1.4.4 (Resize text), intentionally kept for the PWA-installed feel of this user base.
- **PR [#​2794](https://github.com/nesquena/hermes-webui/issues/2794)** by [@​Michaelyklam](https://github.com/Michaelyklam) — Runtime adapter route selection harness. Routes explicit adapter-mode chat starts through `build_runtime_adapter(...)` and keeps `legacy-direct` as the default `/api/chat/start` path. Continues the [#​1925](https://github.com/nesquena/hermes-webui/issues/1925) RFC slice progression: this is slice 4e, the default-off chat-start route-selection seam. Returns a bounded `501 Not Configured` response when `runner-local` is explicitly selected before a supervised runner client exists, instead of silently starting a legacy WebUI-owned run. New `_chat_start_response_from_run_start(...)` helper whitelists legacy-compatible chat-start response fields and keeps adapter-internal `run_id`, `status`, and `active_controls` out of public responses. Updates `docs/rfcs/hermes-run-adapter-contract.md` to mark [#​2744](https://github.com/nesquena/hermes-webui/issues/2744) shipped and define slice 4e.
##### Notes
- Full pytest: **6,467 passed / 6 skipped / 3 xpassed / 8 subtests passed**.
- Opus pre-release advisor reviewed all 7 risk areas (HTTP framing surface completeness, PWA startup ordering, sibling-PR `api/routes.py` interaction, service worker cache invalidation, viewport-meta trade-off, runtime adapter response shape, locale-counter brittleness). Verdict: **1 MUST-FIX patched inline** (folder ZIP `Connection: close` header), **0 inline SHOULD-FIX**, 1 follow-up suggested (`set_auxiliary_model` could validate `task` against `AUX_TASK_SLOTS` whitelist — auth-gated, low severity, filing as follow-up).
- Agent self-verified: protocol\_version bumped, SSE Connection-close + Content-Length plumbing, Auxiliary Models API surface (config + endpoints + frontend), PWA helpers + manifest shortcuts + display\_override, Runtime adapter wiring + whitelisting, i18n parity for all 12 locales on the 16 new aux keys.
- Browser-verified at 1920×1080: Auxiliary Models card renders correctly under Settings → Preferences, 9 task slots with provider/model dropdowns, "Reset all to auto" button, layout consistent with surrounding Settings cards, no clutter or clipping. PWA classes populate on `<html>` and HermesPWA namespace populates with 4 helpers as expected.
- In-stage commits added Turkish translations for [#​2680](https://github.com/nesquena/hermes-webui/issues/2680)'s 16 `settings_aux_*` / `settings_label_auxiliary_models` / `settings_desc_auxiliary_models` keys to close the sibling-collision gap with v0.51.127's Turkish locale ([#​2772](https://github.com/nesquena/hermes-webui/issues/2772)). Bumped `test_auxiliary_models_settings.py::test_all_locales_have_auxiliary_keys` from `count == 11` to `count == 12` (the locale set grew when Turkish landed).
### [`v0.51.128`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051128--2026-05-24--Release-CZ-stage-batch10--2-PR-perf--correctness-batch)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.127...v0.51.128)
##### Fixed
- **PR [#​2830](https://github.com/nesquena/hermes-webui/issues/2830)** by [@​franksong2702](https://github.com/franksong2702) — Pin state synchronization between persisted index and in-memory sessions (closes [#​2821](https://github.com/nesquena/hermes-webui/issues/2821)). Three coupled bugs:
- **Bug A (load-bearing):** `/api/session/pin` pre-snapshot used `getattr(session, "pinned", False)` which always returned `False` for dict-backed index rows from `all_sessions()`. With \~55-session profiles and LRU eviction churn, pinned counts routinely under-counted because the persisted snapshot was effectively empty. New `_session_field(session, field, default)` helper resolves both dict-backed and Session-object snapshots correctly.
- **Bug B:** Removed stale client-side `pinLimitReached` short-circuit in the sidebar action menu that could block pin clicks before the server saw them, based on `_allSessions` data that was stale mid-render. Server now enforces the cap; the toast surfaces the 400 response.
- **Bug C recovery:** Pin/unpin failure path (4xx response from `/api/session/pin`) now triggers `renderSessionList()` to refresh `_allSessions` from the server, so the sidebar never gets stuck on stale optimistic state.
Adds `tests/test_issue2821_session_pin_state_sync.py` (70 LOC) covering the `_session_field` helper, the persisted-pinned snapshot, the removed `pinLimitReached` reference, and the failure-catch refresh path. Companion fix to [#​2782](https://github.com/nesquena/hermes-webui/issues/2782) (server-side 404→200 transition for missing CLI-synced sessions) which remains out of scope.
##### Performance
- **PR [#​2716](https://github.com/nesquena/hermes-webui/issues/2716)** by [@​dobby-d-elf](https://github.com/dobby-d-elf) — Six independent perf nudges plus one correctness fix. nesquena-APPROVED on 2026-05-22 after a deep-review iteration; cherry-picked onto post-v0.51.127 master via 3-way apply with sibling-PR composition resolution.
- **Metadata-only `/api/session` correctness fix.** Refactors the prior inline reconciliation into `_metadata_only_message_summary(sid, profile=None)` helper that runs the full `merge_session_messages_append_only()` path. Pre-fix shortcut could over-count stale state.db replay rows that the merge intentionally filters out, producing false "transcript newer than loaded conversation" signals (same bug class as [#​2705](https://github.com/nesquena/hermes-webui/issues/2705) / [#​2686](https://github.com/nesquena/hermes-webui/issues/2686)). The new helper threads `profile=` through to `get_state_db_session_messages` to preserve [#​2827](https://github.com/nesquena/hermes-webui/issues/2827)'s TLS-vs-thread profile fix on background-thread reads.
- **Batched persisted-session checks in sidebar indexing.** One `SESSION_DIR.glob('*.json')` snapshot per call replaces per-row `_index_entry_exists()` filesystem lookups during `all_sessions()` pruning. Fallback to the per-row helper preserved when the glob raises.
- **Deferred render-cache signature.** `cachedRenderSignature` closes over the lookup-time signature so the cache STORE path reuses it without recomputing. `_messageRenderCacheSignature()` continues to include the content hash per [#​2692](https://github.com/nesquena/hermes-webui/issues/2692), preserving the cache-invalidation invariant.
- **Hoisted assistant tool-activity index.** Footer-rendering loop now uses an `O(1)` Set lookup instead of `S.toolCalls.some(...)` per message — \~30× fewer comparisons for a 100-message conversation with 30 tool calls.
- **Workspace stale-session guards.** `loadDir` and `_refreshGitBadge` in `static/workspace.js` capture `sessionId` at call time and check it after each `await` (including the catch path of `_refreshGitBadge` — without it, a late 404 from the previous session would hide the git badge on the current session).
- **Background model-catalog prime.** `_startBootModelDropdown` fires fire-and-forget on boot via `setTimeout(0)` so the live catalog hydrates without blocking. The existing `await` on the saved-session restore path is preserved (re-applies the saved session's model after hydration so the chip never shows the stale static default).
- **Failed hydration retryable.** `window._modelDropdownReady = null; throw e;` lets the next caller refetch instead of being stuck on a permanent failure.
Adds 76 LOC of new tests across `test_session_metadata_fast_path.py`, `test_webui_state_db_reconciliation.py`, `test_session_index.py`, `test_issue1539_provider_removal_dropdown_invalidation.py`, `test_issue1785_workspace_preview_breadcrumb.py`, `test_parallel_session_switch.py`.
##### Notes
- PR [#​2716](https://github.com/nesquena/hermes-webui/issues/2716) had been pending merge since 2026-05-22 due to a rebase blocker against the rapidly-advancing master (10+ intervening releases). Cherry-picked via `git apply --3way` of the PR's net delta vs its original merge-base (`f9302601`); 12 of 14 files applied cleanly. Two files had genuine conflicts requiring resolution: `api/routes.py` (took the PR's helper extraction AND added `profile=` threading to preserve [#​2827](https://github.com/nesquena/hermes-webui/issues/2827)'s fix), and `tests/test_webui_state_db_reconciliation.py` (kept BOTH master's pre-existing `test_api_session_reload_drops_stale_cached_user_tail_after_saved_assistant` AND the PR's new `test_metadata_fast_path_matches_reconciliation_for_restamped_replays` — they pin different invariants).
- Opus pre-release advisor reviewed all 6 risk areas (helper extraction correctness, sibling-PR composition, `Session.load` profile-safety, test coverage, deferred Bug D, stale-line-number cleanup nit). Verdict: **SHIP AS-IS** — no MUST-FIX, no inline SHOULD-FIX. Two follow-up issues to file post-tag (Bug D startup index rebuild perf; multi-profile state.db test for the `profile=` threading invariant).
- Full pytest: **6,434 passed / 6 skipped / 3 xpassed / 8 subtests passed** in 2m43s.
- Agent self-verified the producer→consumer channel for `_metadata_only_message_summary` with unmocked invocation against a real session-load path (per skill rule Trigger A + E for mocked-consumer test patterns).
- Closes: [#​2821](https://github.com/nesquena/hermes-webui/issues/2821) (pin state sync), and `get_state_db_session_summary` dead-code removed ([#​2716](https://github.com/nesquena/hermes-webui/issues/2716)).
### [`v0.51.127`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051127--2026-05-24--Release-CY-stage-batch9--7-PR-low-risk-batch--brick-class-Linux--brick-class-update-apply--composer-wide-screen--Turkish-locale--MCP-toggle--SSE-settlement--Windows-CI)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.126...v0.51.127)
##### Fixed
- **PR [#​2854](https://github.com/nesquena/hermes-webui/issues/2854)** by [@​nesquena-hermes](https://github.com/nesquena-hermes) — Embedded terminal opens then immediately closes with `[terminal closed]` on every Linux install past `71d8a8fb`. Root cause: `_terminal_shell_preexec_fn` set `PR_SET_PDEATHSIG=SIGTERM` on the PTY shell so orphans would die when WebUI crashed, but `PR_SET_PDEATHSIG` is **per-thread**, not per-process. WebUI uses `ThreadingHTTPServer`, so each HTTP request runs in its own short-lived worker thread; when the request handler returns and the worker thread exits, the kernel sees the pdeathsig-parent thread has died and SIGTERMs the PTY shell within \~10ms. macOS users were unaffected because `libc.prctl` doesn't exist there. Fix: drop the `preexec_fn` entirely; rely on `atexit.register(close_all_terminals)` for graceful shutdown and explicit `close_terminal` for user-driven close. Adds `tests/test_terminal_process_cleanup.py::test_pty_shell_survives_when_spawning_thread_exits` (real PTY shell spawned via worker thread, asserts shell alive after 500ms grace) plus static-check that `preexec_fn` cannot be re-introduced. Closes [#​2853](https://github.com/nesquena/hermes-webui/issues/2853).
- **PR [#​2855](https://github.com/nesquena/hermes-webui/issues/2855)** by [@​nesquena-hermes](https://github.com/nesquena-hermes) — "Update Now" loops for every user past the latest tag ([#​2846](https://github.com/nesquena/hermes-webui/issues/2846)). After [#​2758](https://github.com/nesquena/hermes-webui/issues/2758) the update check correctly fell through to branch comparison when `HEAD` had moved past the latest `v*` tag, but `_select_apply_compare_ref` still returned `tags[0]` — so `git pull --ff-only v2026.5.16` no-op'd, the server bounced, and the banner reappeared unchanged. `apply_force_update` had the same bug except worse (would `git reset --hard v2026.5.16` and rewind the checkout 254 commits). Fix: extract `_head_is_past_latest_tag(path, current_tag)` and have both check and apply paths consult it. Opus pre-release review caught a "case D" parameter-asymmetry drift (HEAD on older tag + commits + newer tag exists → predicate flipped between the two callsites) and patched the apply-side predicate to use `current_tag` + a `behind == 0` gate, exactly mirroring the check-side rule. Adds `test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_exists`. Closes [#​2846](https://github.com/nesquena/hermes-webui/issues/2846).
- **PR [#​2852](https://github.com/nesquena/hermes-webui/issues/2852)** by [@​ai-ag2026](https://github.com/ai-ag2026) — Chat `stream_end` handler now settles from the persisted session when `done` was not received or replayed, instead of leaving the active pane with live `Thinking` / assistant DOM and inflight state projected indefinitely. Reconnect / journal / replay paths can deliver `stream_end` without preceding `done`; the prior code treated `stream_end` as transport-only close. Duplicate / replayed `done` events are also made idempotent before completion sound / final render side effects. Opus pre-release review added a post-await race guard inside `_restoreSettledSession` to catch the case where a late `done` event runs the finalize path while the settlement is awaiting the `/api/session` roundtrip. Adds 4 new regression tests across `tests/test_1694_terminal_cleanup_ownership.py` covering both `stream_end`-without-`done` and duplicate-`done` paths.
- **PR [#​2811](https://github.com/nesquena/hermes-webui/issues/2811)** by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Native-Windows startup E2E workflow now self-tests on PR push (closes the [post-#​2783](https://github.com/post-/hermes-webui/issues/2783) gap where Windows-only regressions like the WOW64 ProgramFiles redirect could only be caught after release). Reworked per maintainer feedback to use a stub `hermes_cli/__init__.py` next to a sibling `hermes-agent/` folder rather than `pip install hermes-agent` (which is not on PyPI). Workflow runs `start.ps1` for 8s and asserts none of its `Write-Error` guards fired (no Python, no agent dir, bad port, missing `hermes_cli`, missing `server.py`). PowerShell syntax + path discovery is the testable surface; the server can't actually boot on a stub. `taskkill` exit-128 swallowed when the stub process is already gone.
##### Changed
- **PR [#​2812](https://github.com/nesquena/hermes-webui/issues/2812)** by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Composer max-width is now responsive on wide displays. Pre-change `.composer-box` had a fixed `max-width: 780px` that pinched footer chips (workspace name, model picker, reasoning chip, context ring) against each other on 1440p+ monitors. Switched to `max-width: clamp(780px, 60vw, 1100px)` — the 780px floor preserves byte-identical layout at 1280px (Aron's laptop reference width); 1440px viewports gain \~84px (864px composer); 1920px viewports gain \~320px (1100px composer cap). Mobile responsive logic untouched. Single-line CSS change in `static/style.css`.
##### Added
- **PR [#​2772](https://github.com/nesquena/hermes-webui/issues/2772)** by [@​vaur94](https://github.com/vaur94) — Complete Turkish (`tr`) locale across `static/i18n.js` (\~1,182 keys matching existing locale coverage). Adds Turkish login page strings in `api/routes.py` `_LOGIN_LOCALE`. Settings → Language now offers **Türkçe**; speech recognition uses `tr-TR`. Stage build absorbed a sibling-PR i18n collision with [#​2776](https://github.com/nesquena/hermes-webui/issues/2776) below (9 missing keys: `mcp_enable_server`, `mcp_disable_server`, `mcp_enabled_toast`, `mcp_disabled_toast`, `mcp_toggle_failed`, `open_in_vscode`, `open_in_vscode_failed`, `settings_label_ignore_agent_updates`, `settings_desc_ignore_agent_updates`) — Turkish translations added in-stage so locale-parity test passes. Closes [#​2537](https://github.com/nesquena/hermes-webui/issues/2537) as superseded (byzuzayli's earlier Turkish PR with narrower scope).
- **PR [#​2776](https://github.com/nesquena/hermes-webui/issues/2776)** by [@​roryford](https://github.com/roryford) — New `PATCH /api/mcp/servers/{name}` endpoint accepts `{"enabled": bool}`, writes `mcp_servers.<name>.enabled` to `config.yaml`, calls `reload_config()`, returns `{"ok": true, "name": "<name>", "enabled": <bool>}`. Each MCP server row in the panel now shows a clickable Enabled/Disabled toggle. Also fixes a pre-existing bug: `_handle_mcp_server_delete` and `_handle_mcp_server_update` were defined at line \~11656 but never wired into the HTTP router — DELETE wired into `handle_delete`, PUT wired via new `handle_put` / `do_PUT` in `server.py`. CORS preflight `Access-Control-Allow-Methods` updated to include `PUT` (Opus pre-release review nit). Adds 5 i18n keys to all 11 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr via in-stage parity fix). 7 new tests covering enable, disable, 404, empty-name, missing-field, response payload, URL-decoded names.
##### Notes
- Two PRs ([#​2854](https://github.com/nesquena/hermes-webui/issues/2854), [#​2855](https://github.com/nesquena/hermes-webui/issues/2855)) are brick-class fixes — every Linux install was unable to use the embedded terminal, and every install past the latest agent tag was stuck in an Update Now loop. They land in the same low-risk batch as cosmetic / locale / CI changes because both fixes are mechanical, well-tested, and the brick-class severity made deferring impossible.
- Opus pre-release advisor reviewed all 5 risk areas (PR\_SET\_PDEATHSIG removal, update apply path symmetry, MCP toggle wiring, composer clamp, stream\_end settlement). 1 MUST-FIX + 3 SHOULD-FIX all addressed inline before tag. Net: +69/-9 across 5 files for the Opus fixes.
- Full pytest: 6,424 passed / 6 skipped / 3 xpassed / 8 subtests passed.
- UX evidence for [#​2812](https://github.com/nesquena/hermes-webui/issues/2812) captured at 1280/1440/1920/mobile (iPhone 14 emulation); Telegram-approved.
- File a follow-up issue for pdeathsig-on-supervisor-thread hardening ([#​2854](https://github.com/nesquena/hermes-webui/issues/2854) deferred Option B) and French-locale `open_in_vscode` parity gap (predates this batch, Opus advisor flagged).
### [`v0.51.126`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051126--2026-05-24--Release-CX-stage-batch8--2-PR-low-risk-batch--kanban-markdown--live-activity-timeline)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.125...v0.51.126)
##### Added
- **PR [#​2819](https://github.com/nesquena/hermes-webui/issues/2819)** by [@​humayunak](https://github.com/humayunak) — Kanban task descriptions and comments now render as full GFM Markdown instead of plain-text. `_kanbanRenderMarkdown()` in `static/panels.js` rewrote the line-per-`<p>` wrapper as a block-parsing pipeline supporting headings, code blocks (fenced + indented), ordered/unordered lists, task lists with checkboxes, tables, blockquotes, horizontal rules, and strikethrough. `_kanbanRenderMarkdownInline()` gains `~~strikethrough~~` and tightens the italic regex to avoid mid-identifier `*` matches. CSS adds table borders, code-block background, checkbox styling, blockquote accent, and heading sizing scoped to `.hermes-kanban-md`. Frontend-only, scoped to the kanban panel. 95 existing kanban tests pass.
##### Changed
- **PR [#​2847](https://github.com/nesquena/hermes-webui/issues/2847)** by [@​AJV20](https://github.com/AJV20) — Live chat Activity disclosure now shows observable run telemetry instead of an empty `Thinking…` placeholder when no reasoning text is available (squashed from 2 author commits). New baseline rows surface run-start metadata (model, profile), `Waiting on model` / `Waiting on tool result` / `Working for …` status, tool start/finish in the timeline alongside the existing compact tool cards, and a `No recent activity for …` state after quiet periods. Frontend-only telemetry derived from existing stream events — no new backend event types. Adds `tests/test_live_activity_timeline.py` (4 tests). The compact/calm default Activity disclosure is preserved; it only becomes informative when expanded.
### [`v0.51.125`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051125--2026-05-24--Release-CW-stage-batch7--10-PR-low-risk-batch--UIUX-polish--bug-fixes--diagnostics)
[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.124...v0.51.125)
##### Fixed
- **PR [#​2839](https://github.com/nesquena/hermes-webui/issues/2839)** by [@​tn801534](https://github.com/tn801534) — Kanban worker log endpoint constructed URLs with a double query string (`?board=<slug>?tail=65536`) when a non-default board was active. The frontend was appending `?tail=65536` directly to a URL that already had `?board=...` from `_kanbanBoardQuery()`. Fix: pass `{tail: 65536}` as the `extra` argument to `_kanbanBoardQuery()` so it composes both params into a single valid query string. One-line, narrow scope.
- **PR [#​2832](https://github.com/nesquena/hermes-webui/issues/2832)** by [@​franksong2702](https://github.com/franksong2702) — Malformed HTTP request logging in `server.py` falls back to `"-"` for missing `command` or `path` instead of raising `AttributeError`. Defensive `getattr(self, 'command', None) or '-'` matches the pattern already used for `_req_t0` elsewhere in the handler. Adds `tests/test_issue2775_log_request.py` covering the malformed-request-before-path-assigned case.
- **PR [#​2818](https://github.com/nesquena/hermes-webui/issues/2818)** by [@​humayunak](https://github.com/humayunak) — Approval and clarify cards no longer steal focus from the composer textarea (`#msg`) when the user is mid-type. `showApprovalCard()` and `showClarifyCard()` now guard the `focus()` call on `document.activeElement !== $('msg')`, matching the pattern already used elsewhere for focus-sensitive paths. The clarify card also moves the focus call out of `setTimeout` for snappier UX. Silently dropped keystrokes during streaming are eliminated.
- **PR [#​2826](https://github.com/nesquena/hermes-webui/issues/2826)** by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Composer footer chip wraps no longer overlap at narrow widths (closes [#​2740](https://github.com/nesquena/hermes-webui/issues/2740)). The five chip wraps (`.composer-profile-wrap`, `.composer-ws-wrap`, `.composer-model-wrap`, `.composer-reasoning-wrap`, `.composer-toolsets-wrap`) had `flex: 0 1 auto` + `min-width: 0` so they would compress past their content's natural width when the composer narrowed, causing visual overlap of the profile / workspace / model / reasoning chips. Switched to `flex: 0 0 auto` via a single grouped selector. Each chip now keeps its natural width and the existing `overflow-x: auto` on `.composer-left` handles overflow via horizontal scroll. Default-width layout unchanged; only affects the overflow regime. Mobile-specific rules (already `flex: 0 0 auto`) untouched.
- **PR [#​2829](https://github.com/nesquena/hermes-webui/issues/2829)** by [@​franksong2702](https://github.com/franksong2702) — Workspace Markdown previews fall back to plain text for very large files (>64 KB or >1500 lines) instead of synchronously running the full rich Markdown renderer on the browser main thread, which could lock up the tab for several seconds on multi-megabyte `.md` files. Plain-text preview shows file size + line count in the status line so users know why rich rendering was bypassed; Edit mode still shows raw content as before. Closes [#​2823](https://github.com/nesquena/hermes-webui/issues/2823). Supersedes [#​2828](https://github.com/nesquena/hermes-webui/issues/2828) (same scope, less polished).
- **PR [#​2837](https://github.com/nesquena/hermes-webui/issues/2837)** by [@​franksong2702](https://github.com/franksong2702) — CSRF rejections now distinguish origin/proxy mismatches from expired session tokens, so provider-key removal and other protected requests show actionable diagnostics instead of the generic "Cross-origin request rejected" error. Adds `tests/test_issue2572_csrf_diagnostics.py` covering both failure modes.
- **PR [#​2834](https://github.com/nesquena/hermes-webui/issues/2834)** by [@​franksong2702](https://github.com/franksong2702) — Workspace Markdown `mailto:` and `tel:` links now render as clickable links, and sandboxed HTML preview links open outside the iframe (via injected `<base target="_blank">`) instead of navigating the preview into a browser-blocked page. Adds `tests/test_issue2768_workspace_links.py`.
- **PR [#​2838](https://github.com/nesquena/hermes-webui/issues/2838)** by [@​franksong2702](https://github.com/franksong2702) — Tasks panel surfaces a warning when the Hermes gateway is not configured or not running, so Docker users know scheduled jobs need the gateway daemon to tick while away. The single-container Docker boundary is also clarified in `docs/docker.md`. Adds `tests/test_issue2785_gateway_cron_guidance.py`.
##### Added
- **PR [#​2820](https://github.com/nesquena/hermes-webui/issues/2820)** by [@​tangerine-fan](https://github.com/tangerine-fan) — Clarify user choice is now echoed as a visible message in the conversation transcript. After the user responds to a clarify prompt, a synthetic user message with the chosen value is inserted into `S.messages` (marked `_clarify_response: true` so downstream consumers can filter if needed). Previously the choice was only visible in the transient clarify card; now the chat history preserves the decision.
- **PR [#​2843](https://github.com/nesquena/hermes-webui/issues/2843)** by [@​AJV20](https://github.com/AJV20) — New Settings preference "Ignore Agent updates" keeps WebUI update notices, banners, and update actions enabled while suppressing Hermes Agent update checks. Default `False` (current behavior). Useful when running an unreleased agent build or pinning to a specific agent commit.
</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/650
Thinking Path
start.ps1+ the native-Windows community-guide link. Right now there's no CI gate that exercises the native-Windows launch path, so regressions can land silently and only surface when a user tries to follow the README.$env:ProgramFilesredirect bug only post-filing, via manual smoke-testing. Same class of bug could regress at any later refactor with no signal until users complain.windows-latestworkflow that just runs the README's documented steps and asserts/healthreturns 200. Tiny, focused, becomes a permanent gate.docker-smoke.yml).What Changed
One new file:
.github/workflows/native-windows-startup.yml(75 lines).The workflow:
start.ps1/requirements.txt/bootstrap.py/server.py/ itself.actions/checkout+actions/setup-python@v5withpython-version: '3.11'.python -m venv venvvenv\Scripts\python.exe -m pip install -r requirements.txtpwsh -NoLogo -File .\start.ps1(in a backgroundStart-Job)http://localhost:8787/healthfor up to 60 s./and/healthreturn HTTP 200.always()cleanup.CHANGELOG entry under
[Unreleased]→### Added.No code changes. No new dependencies. Pure CI addition.
Why It Matters
\$env:ProgramFilesredirect bug from feat(start.ps1): expand hermes-agent candidate paths for Windows installers #2805 was caught only after the PR landed because nobody had a local sandbox+smoke step. This workflow runs that smoke automatically on every relevant PR going forward.start.ps1's venv discovery is human vigilance. After this lands, the README's documented steps become a CI gate — ifstart.ps1stops auto-discoveringvenv\Scripts\python.exe, this workflow fails before merge.Verification
actionlintlocally (no schema errors).python -m venv venv→pip install -r requirements.txt→pwsh .\start.ps1→ wait for/healthflow.PythonBA.dll) at process-launch time. Locally smoke-testing this workflow IN Windows Sandbox is therefore not possible without disabling WDAC (which defeats the sandbox's purpose). GitHub'swindows-latestrunners DON'T have WDAC enforcement, so the workflow runs cleanly there — which is the actual deployment target. The first CI run on this PR itself is the canonical verification.pull_requesttrigger fires on its own definition file). That's the verification.No automated test changes — this IS the test.
Risks / Follow-ups
start.ps1which is introduced by feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783. This PR targetsmasterso the file lands cleanly, but its CI step (`pwsh .\start.ps1`) only succeeds once feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 merges. Until then, if a PR touches the listed paths, the workflow will fail at the start.ps1 step with "file not found" — which is the right signal that start.ps1 isn't on master yet. The maintainer should decide whether to merge this before, after, or alongside feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783.windows-latestrunner performance. GitHub's Windows runners are slower than dev machines;pip install -r requirements.txtcan take 60–90 s on cold cache. The workflow's 60 s/healthpoll budget accommodates that. If first-time install ever exceeds the budget, the budget can be bumped in a tiny follow-up.Model Used
uv.exeand the Python.org installer'sPythonBA.dllboth blocked by Windows Defender Application Control on a fresh Windows Sandbox image). The CI workflow'swindows-latesttarget environment isn't subject to that restriction, so it's where verification happens.