feat(start.ps1): expand hermes-agent candidate paths for Windows installers#2805
feat(start.ps1): expand hermes-agent candidate paths for Windows installers#2805Koraji95-coder wants to merge 6 commits into
Conversation
…ena#1952) Combined PR for nesquena#1952 per maintainer's option (a) suggestion: - README.md: paragraph below the existing "Native Windows is not supported" warning linking @markwang2658's community-maintained no-Docker / no-WSL2 setup, including the memory delta (~330 MB native vs ~1080 MB with WSL2+Docker) cited in the issue thread. - start.ps1 (new, 142 lines): PowerShell launcher that bypasses bootstrap.py's platform refusal and invokes server.py directly. Mirrors start.sh's discovery (load .env, find Python, locate agent dir, set env defaults). - CHANGELOG.md: ### Added entry for start.ps1, ### Documentation entry for the README link. No bootstrap.py change (deferred to a separate larger PR). See PR body for Thinking Path / What Changed / Why It Matters / Verification / Risks / Model Used.
Three fixes from copilot-pull-request-reviewer[bot]: - start.ps1 .env loader: switch the "already-set env var" guard from a truthy check to an explicit $null check. Empty-string env vars are falsey in PowerShell, so the old guard would mis-skip and overwrite an intentionally-empty pre-set var from the .env file. - start.ps1 hermes-agent-not-found error: switch from single-quoted to double-quoted (interpolated) message so $env:USERPROFILE and the sibling-dir path render as real paths instead of literal strings. Pre-resolve both candidate paths into local vars for clarity. - README.md community-guide paragraph: restructure the dense one-liner into a bulleted list (memory delta with "varies by config" caveat, what works, known limitations) AND clarify the WSL2 relationship: WSL2 is needed once for first-time venv creation; after that, start.ps1 runs natively with no WSL2 in the daily loop.
Surfaced by the maintainer's requested fresh-checkout smoke test on PR nesquena#2783. The original `if (-not $AgentDir)` guard correctly handled the unset-env-var case (falls through to candidate auto-discovery), but did NOT validate the env var when it was explicitly set. A stale or typo'd HERMES_WEBUI_AGENT_DIR pointing at a missing folder would silently progress into `& $Python $serverPath`, leading to a confusing exit 9009 from the Microsoft Store python3 stub (or a missing-imports crash on a real python). Now: when the env var is set we `Test-Path (Join-Path $AgentDir 'hermes_cli')` first. Missing -> Write-Error + exit 1 with a clear message telling the user to either unset (to use auto-discovery) or fix the path. Same validation the candidate-discovery loop already applies to its own paths — just extended to the explicit-override case. Verified on a fresh `gh repo clone Koraji95-coder/hermes-webui` checkout of this branch: PS> $env:HERMES_WEBUI_AGENT_DIR = 'C:\does\not\exist' PS> .\start.ps1 -Port 38913 Write-Error: ...HERMES_WEBUI_AGENT_DIR is set to ...but no hermes_cli/ folder exists there. Unset the variable to fall back to auto-discovery, or fix the path. PS> $LASTEXITCODE 1
…allers Maintainer follow-up on PR nesquena#2783: the auto-discovery list only checked `%USERPROFILE%\.hermes\hermes-agent` and `../hermes-agent`, but the official Windows installer puts hermes-agent under `%LOCALAPPDATA%\hermes\hermes-agent`, and MSI installs commonly land in `Program Files`. Users hitting `start.ps1` from a fresh clone of an installer-deployed agent currently have to set `HERMES_WEBUI_AGENT_DIR` manually — which is the exact friction the PR was trying to remove. Adds three new candidate paths to the discovery list: - `%LOCALAPPDATA%\hermes\hermes-agent` (official Windows installer) - `%PROGRAMFILES%\hermes\hermes-agent` (MSI to Program Files) - `%PROGRAMFILES(X86)%\hermes\hermes-agent` (MSI to x86 path) Order matters: existing `%USERPROFILE%\.hermes\hermes-agent` stays first (user-local installs win), then the three new system-wide paths, then the sibling-of-RepoRoot `../hermes-agent` developer-checkout case stays last. Same `Test-Path (Join-Path \$c 'hermes_cli')` validation applies to every candidate. Also restructures the not-found error message to enumerate every searched path via `\$candidates -join ', '` — when discovery fails the user now sees the exact list checked, so it's obvious where to drop the agent or which `HERMES_WEBUI_AGENT_DIR` value to set. Stacked on top of nesquena#2783. Once nesquena#2783 lands, this rebases cleanly onto master.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds native Windows support guidance and a PowerShell launcher to run Hermes WebUI without bootstrap.py on Windows, plus documentation updates describing the community-supported path.
Changes:
- Added
start.ps1PowerShell launcher that loads.env, discovers Python + hermes-agent, sets env defaults, and runsserver.py. - Updated README with links to a community-maintained native Windows setup guide and notes/limitations.
- Updated changelog entries describing the new launcher and Windows discovery paths.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| start.ps1 | Introduces a Windows-native launcher with env loading, Python/agent discovery, and direct server.py execution. |
| README.md | Adds native Windows community guide links and notes about native vs WSL2 usage. |
| CHANGELOG.md | Documents the addition of start.ps1 and Windows agent discovery behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ll-guard+changelog
Three Copilot-found issues from the PR review:
1. `start.ps1:94` and `start.ps1:107` used `Test-Path (Join-Path ... 'hermes_cli')` without `-PathType Container`. A file (not a directory) named `hermes_cli` at any candidate root would pass the check, even though every downstream use of `$AgentDir` treats it as a directory. Both sites now use `-PathType Container`.
2. `start.ps1:99-104` built `$candidates` as a 5-entry literal array, two of which (`${env:ProgramFiles}` and `${env:ProgramFiles(x86)}`) can be null/empty on 32-bit Windows or in constrained environments. `Join-Path` throws on a null Path. Switched to incremental list construction with an `if ($root)` guard for the three system-wide candidates; `%USERPROFILE%` is always set on standard Windows so it stays unguarded, and the dev-checkout sibling is path-derived (not env-based) so it's also unguarded.
3. `CHANGELOG.md:9` entry implied `start.ps1` was being extended in this PR. Because nesquena#2805 stacks on nesquena#2783, the full diff Copilot reviews shows `start.ps1` as newly added — making the original phrasing ambiguous. Rewrote the entry to be stack-position-independent: lists the discovery paths added without claiming the file is new or existing, mentions the `-PathType Container` validation and the conditional-build hardening.
Did NOT address Copilot's two WSL2-related comments (`start.ps1:16` header,
`README.md:140`) in this commit — those lines are from nesquena#2783's scope, not
nesquena#2805's. Will surface as a separate small docs-fix PR per the maintainer's
"keep diffs focused" guidance.
Verification:
- `[System.Management.Automation.Language.Parser]::ParseFile` returns no
syntax errors against the updated start.ps1
- Both `Test-Path -PathType Container` sites match the same validation
contract the candidate loop already used at the override-validation site
- Conditional foreach is verified to produce a 4-entry list on a standard
64-bit Windows machine (USERPROFILE + LOCALAPPDATA + Program Files +
Program Files (x86)) and degrades gracefully if any of the three
system-wide roots is unset
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scovery
Surfaced by smoke-testing this PR on a fresh foundry-side checkout.
PowerShell sometimes runs as a 32-bit (WOW64) process on 64-bit
Windows — e.g., from certain shell-chain configurations (Git Bash →
pwsh through specific launchers, or some 32-bit IDE-spawned shells).
In that mode `$env:ProgramFiles` is redirected to
`C:\Program Files (x86)` by Windows, so the existing two-entry loop:
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramFiles}, ${env:ProgramFiles(x86)}))
produced TWO identical `C:\Program Files (x86)\hermes\hermes-agent`
candidates AND silently missed the real `C:\Program Files\hermes\hermes-agent`
where MSI-installed hermes-agent actually lives.
Two changes:
1. Add `${env:ProgramW6432}` to the loop. ProgramW6432 is the canonical
override that Windows guarantees points at the 64-bit Program Files
regardless of process bitness. On a native 64-bit process, it
equals `$env:ProgramFiles` (so we may pick up a duplicate, handled
below). On a WOW64 process, it's the only way to reach
`C:\Program Files`.
2. Add `$candidates = $candidates | Select-Object -Unique` after the
list is built. Collapses any same-path collisions regardless of
which env-var combination caused them — defensive against future
env-var weirdness too (constrained sandboxes, custom Windows builds).
Verified end-to-end:
- BEFORE the fix, smoke test on a WOW64 pwsh 7.5.4 showed candidates 3 + 4
both = `C:\Program Files (x86)\hermes\hermes-agent`. Real `C:\Program Files`
never checked.
- AFTER the fix, same shell shows 5 distinct candidates: USERPROFILE,
LOCALAPPDATA, ProgramW6432 (`C:\Program Files`), ProgramFiles(x86), sibling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Smoke-tested on a Windows 11 Pro machine with hermes-agent installed at
|
SummaryRead the discovery block at the PR's HEAD (commit dbebbed) and walked through the smoke-test transcript above. The WOW64 finding is a real bug that would have shipped silently; the Code reference
$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86)...
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}DiagnosisWOW64 bug + fix: correct and well-targeted. Windows' filesystem-and-environment redirection layer (WoW64) maps
Enumerated not-found error: Smoke-test transcriptThe transcript above (Test 1 confirming Stacking noteStacked on #2783 alongside #2806 and #2807 - this is the most substantive of the three follow-ups (real bug fix surfaced by smoke testing, not just hardening or docs). Worth landing. VerificationI did not execute the script (cron policy is read-only on PR worktrees). The WOW64 redirect behavior is documented Windows API surface; the fix is verifiable by inspection. The |
1 similar comment
SummaryRead the discovery block at the PR's HEAD (commit dbebbed) and walked through the smoke-test transcript above. The WOW64 finding is a real bug that would have shipped silently; the Code reference
$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86)...
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}DiagnosisWOW64 bug + fix: correct and well-targeted. Windows' filesystem-and-environment redirection layer (WoW64) maps
Enumerated not-found error: Smoke-test transcriptThe transcript above (Test 1 confirming Stacking noteStacked on #2783 alongside #2806 and #2807 - this is the most substantive of the three follow-ups (real bug fix surfaced by smoke testing, not just hardening or docs). Worth landing. VerificationI did not execute the script (cron policy is read-only on PR worktrees). The WOW64 redirect behavior is documented Windows API surface; the fix is verifiable by inspection. The |
…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.
…allers (#2805) Squashed from 3 author commits onto current master (the 3 base commits from already-shipped #2783 were filtered out by the squash): - 6822cbb feat: expand hermes-agent candidate paths - 6f42353 Copilot review: PathType+null-guard+changelog - dbebbed handle WOW64 ProgramFiles redirection Authorship preserved. CHANGELOG entry merged into batch stamp commit.
…eading WSL2-venv-portability claim (#2806) Squashed from 3 author commits onto current master (3 base commits from already-shipped #2783 were filtered out by the squash). #2805's expanded candidate-path discovery + PathType Container check preserved from prior stage commit. Authorship preserved. CHANGELOG entry merged into batch stamp commit.
…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.124) (#634) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.108` → `0.51.124` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.124`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051124--2026-05-24--Release-CV-stage-batch6--3-PR-Windows-only-stack--agent-paths--docs--port-hardening) [Compare Source](nesquena/hermes-webui@v0.51.123...v0.51.124) ##### Added - **PR [#​2805](nesquena/hermes-webui#2805 by [@​Koraji95-coder](https://github.com/Koraji95-coder) — `start.ps1`: expand hermes-agent candidate paths for Windows installers. The launcher now searches `$env:USERPROFILE\.hermes\hermes-agent`, the dev-checkout sibling, and the Windows installer roots (`$env:LOCALAPPDATA\hermes\hermes-agent`, `${env:ProgramW6432}\hermes\hermes-agent`, `${env:ProgramFiles}\hermes\hermes-agent`, `${env:ProgramFiles(x86)}\hermes\hermes-agent`) with `Select-Object -Unique` to collapse WOW64 ProgramFiles redirection collisions on 32-bit PowerShell processes. Adds `-PathType Container` to the `HERMES_WEBUI_AGENT_DIR` guard so a file named `hermes_cli` doesn't false-positive. Null-guards `${env:ProgramFiles(x86)}` for constrained environments where it's missing. Zero impact on Linux/macOS — file is `start.ps1`, never loaded by `start.sh` or `bootstrap.py`. ##### Documentation - **PR [#​2806](nesquena/hermes-webui#2806 by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Native Windows venv path corrected in `start.ps1` doc-comment and `README.md`. The previous text suggested "run bootstrap.py inside WSL2 once to create the venv, then this script can use that venv" — but a WSL2-created venv is `venv/bin/python` (ELF) and cannot be invoked by native Windows Python. The corrected guidance is to create a Windows venv natively (`python -m venv venv` from PowerShell), then `start.ps1` auto-discovers `venv\Scripts\python.exe`. WSL2 remains useful as a parallel install for the full `bootstrap.py` + Linux runtime path. ##### Hardened - **PR [#​2807](nesquena/hermes-webui#2807 by [@​Koraji95-coder](https://github.com/Koraji95-coder) — `start.ps1`: `HERMES_WEBUI_PORT` env-var parsing uses `[int]::TryParse` + range guard (1-65535) instead of a bare `[int]` cast that threw `InvalidCastException` with no context on typos or accidental shell expansion. Server-process exit code is captured into `$script:serverExitCode` and emitted via `exit` AFTER the `try/finally` cleanup, so `Pop-Location` always runs (avoids leaving the caller stuck at `$RepoRoot` in interactive or dot-sourced sessions). Also drops a non-functional `@args` splat that PowerShell doesn't populate under `[CmdletBinding()]` — the launcher's existing use case is env-var-driven, no pass-through args needed. ### [`v0.51.123`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051123--2026-05-24--Release-CU-stage-batch5--2-PR-low-risk-batch--gzipETag-static-caching--Open-in-VS-Code) [Compare Source](nesquena/hermes-webui@v0.51.122...v0.51.123) ##### Performance - **PR [#​2779](nesquena/hermes-webui#2779 by [@​v2psv](https://github.com/v2psv) — Static asset serving negotiates gzip, emits ETags, and uses `immutable` cache headers for fingerprinted URLs. `_serve_static()` in `api/routes.py` previously sent every `/static/*` response with `Cache-Control: no-store` and no `Content-Encoding`, so a page reload over a slow link re-downloaded the full \~2.4 MB JS+CSS shell on every visit. The fix layers three changes inside the same function: (1) gzip the body when the client opts in via `Accept-Encoding`, gated to compressible MIME types and files >1 KB; (2) emit a weak ETag derived from `(size, mtime_ns)` and short-circuit conditional GETs to `304 Not Modified`; (3) send `Cache-Control: public, max-age=31536000, immutable` when the URL carries a non-empty `?v=…` fingerprint (the `__WEBUI_VERSION__` token already substituted by the index template and referenced from `static/sw.js`'s `SHELL_ASSETS`), falling back to `public, max-age=300` otherwise. Raw bytes, compressed bytes, and ETags are cached in-process keyed by `(size, mtime_ns)` so a redeploy is picked up without a restart, while missing/random paths never enter the cache and image/font types skip gzip to avoid wasted CPU on already-compressed payloads. Measured against an asyncio TCP proxy that injects RTT + bandwidth caps for representative VPN scenarios: cold loads improve 2.7-3.1× (e.g. 80 ms RTT / 10 Mbps WireGuard goes from 4.0 s to 1.3 s), warm reloads improve 3.3-4.0× via 304 responses, and bytes-on-the-wire drop 74% on cold loads. Loopback (already fast) still benefits 2.4×. Scope is strictly `/static/*`: `/api/*`, `/stream`, `/`, `/index.html`, `/session/*`, and login/auth routes are served by independent handlers and continue to send `no-store` exactly as before — no change to CSRF, session payloads, SSE buffering, or login flows. 11 regression tests pin gzip negotiation, ETag/304 round-trip including `Vary: Accept-Encoding`, fingerprint-driven cache policy including empty `?v=`, image/tiny-file skip rules, redeploy invalidation, and the existing path-traversal sandbox. ##### Added - **PR [#​2787](nesquena/hermes-webui#2787 by [@​munim](https://github.com/munim) — "Open in VS Code" action in workspace file browser (resolves [#​2735](nesquena/hermes-webui#2735)). Right-clicking any file, folder, or the workspace root now shows an **Open in VS Code** menu item alongside the existing Reveal in File Manager action. The action calls a new `POST /api/file/open-vscode` endpoint which resolves the workspace-relative path via the existing `safe_resolve` traversal guard, then launches VS Code via `subprocess.Popen` (fire-and-forget, consistent with `_handle_file_reveal`). The endpoint resolves the executable via `shutil.which()` first, then falls back to a hardcoded list of common install locations (macOS: `/usr/local/bin/code` and the app-bundle CLI; Linux: `/usr/bin/code`, `/snap/bin/code`; Windows: `%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd` and the `%PROGRAMFILES%` variants) so the action works even when the server process inherits a minimal PATH. Configurable via a new optional `vscode` block in `config.yaml`: `command` overrides the default `code` executable; `host_path_prefix` + `container_path_prefix` enable Docker/container host-path translation. If the command cannot be found anywhere, a descriptive error is returned instead of a bare OS error. i18n keys `open_in_vscode` and `open_in_vscode_failed` added with full translations in all 10 locales. 26 new tests in `tests/test_2735_open_in_vscode.py` pin source wiring, command-resolution logic, i18n completeness, translated strings, and live endpoint error paths. ### [`v0.51.122`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051122--2026-05-24--Release-CT-stage-batch4--4-PR-low-risk-batch--stale-cache-tail--inflight-UI--segment-flush--reasoning-accumulator) [Compare Source](nesquena/hermes-webui@v0.51.121...v0.51.122) ##### Fixed - **PR [#​2802](nesquena/hermes-webui#2802 by [@​ai-ag2026](https://github.com/ai-ag2026) — Drop stale inactive cached user tails when `/api/session` reloads a conversation whose saved sidecar already ends on an assistant answer. Supersedes [#​2733](nesquena/hermes-webui#2733) (held due to async-compression interaction): the new guard adds a `len(cached_messages) <= len(disk_messages)` filter so it never fires when the cache has genuine new concurrent edits beyond the disk state — only when the cache has an unsaved user row past the saved assistant tail. Adds `api/models._inactive_cache_tail_needs_disk_check()` + `_cache_has_stale_unsaved_user_tail()` helpers and 5 new tests in `tests/test_webui_state_db_reconciliation.py`. Previously-held test `test_session_compress_async_reports_stale_session_guard` now passes (verified). Closes umbrella [#​2361](nesquena/hermes-webui#2361) partially. - **PR [#​2796](nesquena/hermes-webui#2796 by [@​ai-ag2026](https://github.com/ai-ag2026) — Clear stale inflight UI state before starting a new send so blocked composer busy-state from failed/incomplete prior turns doesn't divert new turns into the invisible queue. Five-commit squashed fix: (1) drop stale optimistic sidebar rows once canonical session data arrives, (2) clear stale busy state before send via `_clearStaleBusyStateBeforeSend()`, (3) preserve server idle rows over stale optimistic local rows, (4) let `/api/chat/start` survive non-fatal pre-start UI errors via `_runOptionalPreStartUiStep()`, (5) keep those warnings console-only instead of throwing. Adds `_shouldKeepLocalOnlyOptimisticSessionRow()` in `static/sessions.js` and 8 new tests in `tests/test_inflight_send_start_race.py`. Closes [#​2795](nesquena/hermes-webui#2795). Authorship preserved via `--author`. - **PR [#​2777](nesquena/hermes-webui#2777 by [@​b3nw](https://github.com/b3nw) — Flush pending render before segment reset at tool/interim\_assistant boundaries so live tokens that arrived in the 66ms rAF throttle window don't get lost from the DOM when `_resetAssistantSegment()` clears `assistantBody`. New `_flushPendingSegmentRender()` helper writes via `smd`, `renderMd`, or `esc` fallback (same paths as `_doRender`) only when `_renderPending` is true. Completed transcripts were never affected — `renderMessages` rebuilds from the full `assistantText` accumulator on `done`. Adds `tests/test_issue2713_streaming_segment_flush.py`. Closes [#​2713](nesquena/hermes-webui#2713). - **PR [#​2778](nesquena/hermes-webui#2778 by [@​b3nw](https://github.com/b3nw) — Reset reasoning accumulator per turn and prefer `reasoning_content` over `reasoning` on read. Two related bugs: (1) `reasoningText` was initialized once when the SSE stream opened and never reset between turns, so the `done` event would assign the union of every turn's reasoning to the last assistant message in multi-turn agent sessions; now reset at both turn boundaries (`tool` + `interim_assistant`). (2) `static/ui.js renderMessages` preferred `m.reasoning` (potentially corrupted by bug 1) over `m.reasoning_content` (the clean per-turn backend value); the fallback now reads `m.reasoning_content || m.reasoning`. Updates `tests/test_streaming_race_fix.py` to scope the reconnect-accumulator guard to the `_wireSSE` preamble only (turn-boundary resets inside event listeners are intentional). Adds `tests/test_issue2565_reasoning_accumulation.py`. Closes [#​2565](nesquena/hermes-webui#2565). ### [`v0.51.121`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051121--2026-05-24--Release-CS-stage-batch3--4-PR-low-risk-batch--statedb-merge--display-counts--compression-marker--Windows-launcher) [Compare Source](nesquena/hermes-webui@v0.51.120...v0.51.121) ##### Fixed - **PR [#​2788](nesquena/hermes-webui#2788 by [@​Carry00](https://github.com/Carry00) — Prevent `state.db` messages being silently dropped during sidecar merge. Two related bugs were combining to discard historical messages: (1) `get_state_db_session_messages()` was selecting `role, content, timestamp` but NOT `id`, so every row was assigned a `("legacy", ...)` merge key instead of `("message_id", ...)`; (2) when a WebUI-origin session was continued via another Hermes surface (Gateway, CLI), the reader was always hitting the *active* profile's `state.db` rather than the session's own profile. Symptom: a 189-message session showed only 50 in the WebUI. Fix: include `id` in the SELECT when the column exists, and accept an optional `profile=` arg so cross-profile reads use the right database. Both callers in `api/routes.py handle_get` now thread `profile=getattr(s, 'profile', None)` through. - **PR [#​2797](nesquena/hermes-webui#2797 by [@​ai-ag2026](https://github.com/ai-ag2026) — Align messaging session display counts with deduped display messages. The `message_count` returned by `/api/session` is the display coordinate space used for pagination and the header badge. Messaging-thread `state.db` metadata can carry raw duplicate transport rows (blank assistant separators between Discord/Slack thread turns) that `_merged_session_messages_for_display()` intentionally dedupes for rendering. The advertised count was the raw row count, so the frontend expected phantom messages after dedupe — `len(display_msgs) < message_count` triggered "load older" UI states that immediately returned nothing. Fix: `raw["message_count"] = _merged_message_count` for messaging sessions, computed from the same merge that produced the displayed messages. Adds `tests/test_gateway_sync.py::test_messaging_session_message_count_matches_deduped_display_messages` covering the regression. - **PR [#​2803](nesquena/hermes-webui#2803 by [@​simjak](https://github.com/simjak) — Compression-summary cards no longer use ordinary tool output that merely mentions context compression. The streaming auto-compression path was using a local broad substring matcher that fired on any message containing the strings "context compaction" / "context compression" / "context was auto-compressed" / "active task list was preserved across context compression", including skill/tool JSON output and ordinary user discussion about compaction. The strict predicate at `api/compression_anchor._is_context_compression_marker()` was already correctly scoped to synthetic marker prefixes on non-tool messages. Fix: expose the strict predicate as `is_context_compression_marker()` (public name) and route `api/streaming._is_context_compression_marker` through it as a backward-compatible alias. Tool/skill output that mentions compression no longer seeds `compression_anchor_summary` cards. ##### Added - **PR [#​2783](nesquena/hermes-webui#2783 by [@​Koraji95-coder](https://github.com/Koraji95-coder) — Native Windows launcher and community-guide README link (squashed from 3 commits). `start.ps1` is a PowerShell equivalent of `start.sh` that bypasses `bootstrap.py`'s `ensure_supported_platform()` refusal and invokes `server.py` directly on native Windows. It mirrors `start.sh`'s discovery (load optional `.env` with the same readonly-var filter for `UID`/`GID`/`EUID`/`EGID`/`PPID`, find Python via `HERMES_WEBUI_PYTHON` env → `python3` → `python` → `py`, validate `HERMES_WEBUI_AGENT_DIR` on disk before use, prefer the agent's `venv\Scripts\python.exe`, set `HERMES_WEBUI_HOST` / `HERMES_WEBUI_PORT` / `HERMES_WEBUI_STATE_DIR` / `HERMES_HOME` defaults). The README adds a community-maintained native Windows setup section pointing to [@​markwang2658](https://github.com/markwang2658)'s `hermes-windows-native-guide` and `hermes-windows-native` repos with the documented memory delta (\~330 MB native vs \~1080 MB WSL2+Docker). Closes both halves of [#​1952](nesquena/hermes-webui#1952). Assumes Python + agent venv are already set up — first-time setup still needs WSL2 once to create the venv (`bootstrap.py` still refuses on native Windows). ### [`v0.51.120`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051120--2026-05-24--Release-CR-stage-batch2--3-PR-low-risk-batch--Bedrock-provider--update-check-past-tag--CORS-preflight) [Compare Source](nesquena/hermes-webui@v0.51.119...v0.51.120) ##### Added - **PR [#​2786](nesquena/hermes-webui#2786 by [@​munim](https://github.com/munim) — Surface AWS Bedrock as a configurable provider in the WebUI model picker. `api/config.py` registers `"bedrock": "AWS Bedrock"` in `PROVIDER_LABELS`, adds 6 default Bedrock model IDs (Claude Opus 4.7 / 4.6 / 4.5, Sonnet 4.6 / 4.5, Haiku 4.5) to `DEFAULT_MODELS["bedrock"]`, and teaches `_build_configured_model_badges()` to detect Bedrock when both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are present (IAM-style auth, not single-API-key). Static fallback list is overridden at runtime by `hermes_cli.models.provider_model_ids("bedrock")` when the live AWS model list is reachable. Adds `tests/test_issue2720_bedrock_model_picker.py` with 11 test cases covering registry, defaults, env-detection, and runtime override. Resolves [#​2720](nesquena/hermes-webui#2720). ##### Fixed - **PR [#​2789](nesquena/hermes-webui#2789 by [@​munim](https://github.com/munim) — Update check no longer falsely reports "Up to date" when HEAD has moved hundreds of commits past the latest tag. The hermes-agent repository keeps committing to master between tagged releases, and the old `_check_repo_release()` returned `behind=0` (since `current_tag == latest_tag`) and stopped — so the user saw "Up to date" while the working tree was hundreds of commits behind. The fix: when `behind == 0`, run `git describe --tags --always`; if the result contains the `-N-gSHA` suffix (HEAD past tag), return `None` so `_check_repo_branch()` runs and reports the real commit gap. Adds 8 new test cases in `tests/test_updates.py` covering past-tag detection, equal-tag-and-HEAD pass-through, untagged-repo behavior, and the agent-cadence [#​2653](nesquena/hermes-webui#2653) scenario. Resolves [#​2653](nesquena/hermes-webui#2653). - **PR [#​2790](nesquena/hermes-webui#2790 by [@​weidzhou](https://github.com/weidzhou) — Add `do_OPTIONS()` handler in `server.py` so CORS preflight requests return `200 OK` with appropriate `Access-Control-Allow-*` headers instead of `501 Not Implemented`. Browsers sending a preflight OPTIONS for cross-origin API calls previously hit the BaseHTTPRequestHandler default and the entire CORS exchange was blocked. The handler narrowly responds only to OPTIONS — no broader CORS posture change to other endpoints. Resubmit of closed [#​2750](nesquena/hermes-webui#2750) (which bundled unrelated session-index changes); this PR is the minimal preflight-only split that [@​nesquena-hermes](https://github.com/nesquena-hermes) and [@​AJV20](https://github.com/AJV20) requested. ### [`v0.51.119`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051119--2026-05-24--Release-CQ-stage-batch1--3-PR-low-risk-batch--tool-cards--404-recovery--Hepburn-skin) [Compare Source](nesquena/hermes-webui@v0.51.118...v0.51.119) ##### Fixed - **PR [#​2801](nesquena/hermes-webui#2801 by [@​ai-ag2026](https://github.com/ai-ag2026) — Preserve settled tool cards across stream completion. The streaming `done` handler now derives anchored settled tool cards from message-level tool metadata (`message.tool_calls`, `message._partial_tool_calls`, or `content[].type === 'tool_use'`) when present, instead of unconditionally falling back to session-level `d.session.tool_calls`. The fallback could overwrite the per-message anchors after pagination/windowing because session-level coordinates may not line up with the active message array, causing tool cards to disappear on the final `done` render. Fixes [#​2613](nesquena/hermes-webui#2613), complements [#​2777](nesquena/hermes-webui#2777) (which covers pending-segment flushes at tool/interim boundaries). Adds `tests/test_streaming_markdown.py::test_done_handler_prefers_message_tool_metadata_for_settled_render` to lock the precedence. - **PR [#​2808](nesquena/hermes-webui#2808 by [@​chouzz](https://github.com/chouzz) — Recover deterministically from boot-time `/session/{id}` 404s (Option A for [#​2798](nesquena/hermes-webui#2798)). When `loadSession()` hits a 404 during boot-time restore (`!currentSid`), `static/sessions.js` now always clears `localStorage['hermes-webui-session']`, strips the stale URL with `history.replaceState(null, '', '/')`, and rethrows so boot falls through to empty-state recovery. The previous condition required the stale id to match `localStorage`, so a stale `/session/{id}` URL with empty `localStorage` (post state-reset) could leave the UI stuck on "Session not available in web UI." Fixes [#​2798](nesquena/hermes-webui#2798). ##### Added - **PR [#​2799](nesquena/hermes-webui#2799 by [@​gavinssr](https://github.com/gavinssr) — Add Hepburn skin (magenta-rose palette derived from the Hepburn TUI theme). Full light + dark palette under `:root[data-skin="hepburn"]` / `:root.dark[data-skin="hepburn"]`, registered in `static/boot.js` `_SKINS` and whitelisted in `static/index.html`'s inline skin gate. As part of this PR `loadSettingsPanel()` in `static/panels.js` now prefers `localStorage.getItem('hermes-skin')` over `settings.skin` when populating the skin picker (DOM truth → settings fallback), so the picker matches what the user actually sees after the inline gate has already resolved legacy aliases. ### [`v0.51.118`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051118--2026-05-22--Release-CP-stage-pr2773--1-PR-hotfix--v051117-brick-fix-chat-input-restored) [Compare Source](nesquena/hermes-webui@v0.51.117...v0.51.118) ##### Fixed - **PR [#​2773](nesquena/hermes-webui#2773 by [@​nesquena-hermes](https://github.com/nesquena-hermes) — fix(chat): rename `_inflightStateLimits()` in `static/ui.js` to `_getInflightStateLimits()` so it no longer collides with the `window._inflightStateLimits` config object set in `static/boot.js`. Closes [#​2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#​2766](nesquena/hermes-webui#2766)) declared a top-level helper with the same name as a window-attached config object; because top-level `function foo(){…}` declarations in classic (non-module) scripts attach to `window`, boot.js's `window._inflightStateLimits = {…}` assignment overwrote the function reference before any session could send. Every new chat broke on first `send()` with `TypeError: _inflightStateLimits is not a function`, leaving v0.51.117 effectively unusable. Renamed the function only (the public-ish window key is unchanged) and updated all 4 call sites. \*\*New regression test `tests/test_window_function_collision.py` scans every static JS file for top-level `function NAME()` declarations whose name is also the target of `window.NAME = {…}` / `= <number>`, the exact shape that broke [#​2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#​2771](nesquena/hermes-webui#2771) (`_inflightStateLimits` in v0.51.117). The test fails loudly with a precise file:name diagnostic if the bug class returns. Verified end-to-end against the live browser before merge: `_getInflightStateLimits()` returns the limits object and `saveInflightState()` persists to localStorage without throwing. ### [`v0.51.117`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051118--2026-05-22--Release-CP-stage-pr2773--1-PR-hotfix--v051117-brick-fix-chat-input-restored) [Compare Source](nesquena/hermes-webui@v0.51.116...v0.51.117) ##### Fixed - **PR [#​2773](nesquena/hermes-webui#2773 by [@​nesquena-hermes](https://github.com/nesquena-hermes) — fix(chat): rename `_inflightStateLimits()` in `static/ui.js` to `_getInflightStateLimits()` so it no longer collides with the `window._inflightStateLimits` config object set in `static/boot.js`. Closes [#​2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#​2766](nesquena/hermes-webui#2766)) declared a top-level helper with the same name as a window-attached config object; because top-level `function foo(){…}` declarations in classic (non-module) scripts attach to `window`, boot.js's `window._inflightStateLimits = {…}` assignment overwrote the function reference before any session could send. Every new chat broke on first `send()` with `TypeError: _inflightStateLimits is not a function`, leaving v0.51.117 effectively unusable. Renamed the function only (the public-ish window key is unchanged) and updated all 4 call sites. \*\*New regression test `tests/test_window_function_collision.py` scans every static JS file for top-level `function NAME()` declarations whose name is also the target of `window.NAME = {…}` / `= <number>`, the exact shape that broke [#​2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#​2771](nesquena/hermes-webui#2771) (`_inflightStateLimits` in v0.51.117). The test fails loudly with a precise file:name diagnostic if the bug class returns. Verified end-to-end against the live browser before merge: `_getInflightStateLimits()` returns the limits object and `saveInflightState()` persists to localStorage without throwing. ### [`v0.51.116`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051116--2026-05-22--Release-CN-stage-pr2676--1-PR--per-skill-enabledisable-toggle-in-Skills-panel-CLI-parity-with-hermes-skills-config) [Compare Source](nesquena/hermes-webui@v0.51.115...v0.51.116) ##### Added - **PR [#​2676](nesquena/hermes-webui#2676 by [@​lucasrc](https://github.com/lucasrc) — Each skill in the Skills panel now has a toggle pill (enabled/disabled) so users can turn individual skills on or off directly from the WebUI without editing `config.yaml`. Achieves parity with the existing `hermes skills config` CLI subcommand (interactive TUI that toggles `skills.disabled` in config). The disabled state is mirrored through to `skills.platform_disabled.webui` when that key is present. Disabled skills remain visible in the panel (muted via `opacity: .45`) instead of being filtered out, so users can re-enable them later. New endpoint: `POST /api/skills/toggle` validates the skill exists in the filesystem before mutating config, wraps the YAML read-modify-write under the existing `_cfg_lock` for thread safety, and calls `reload_config()` so the change takes effect immediately. Toggle pill uses theme variables (`--accent-bg-strong`, `--accent`, `--border`, `--muted`, `--accent-text`) so it adapts automatically to each skin: gold for default, red for ares, blue for poseidon, purple for sisyphus, grey for mono — verified empirically across light + dark variants. i18n keys (`skill_enabled`, `skill_disabled`, `skill_toggle_failed`) translated across all 10 locales. Default-state safety: fresh installs (no `skills.disabled` key in config) return `disabled: False` for every skill — no regression risk for new users. ### [`v0.51.115`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051115--2026-05-22--Release-CM-stage-pr2731--1-PR--clarify-prompt-collapseexpand-with-chevron-icon-polish) [Compare Source](nesquena/hermes-webui@v0.51.114...v0.51.115) ##### Added - **PR [#​2731](nesquena/hermes-webui#2731 by [@​Michaelyklam](https://github.com/Michaelyklam) — Clarification prompts now include a compact Collapse/Expand control so users can temporarily shrink a blocking decision card and reread the chat context behind it before responding. The toggle uses Lucide chevron icons (chevron-down expanded → click to collapse, chevron-up collapsed → click to expand) and a small circular pill matching the existing composer-button design language. The collapsed card sits cleanly above the composer at every tested viewport (desktop 1920×1080, mobile iPhone 14 390×844) without edge clipping. New clarification prompts still open expanded so users notice them. ### [`v0.51.114`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051114--2026-05-22--Release-CL-stage-407--1-PR--update-check-recovery-from-remote-re-tags) [Compare Source](nesquena/hermes-webui@v0.51.113...v0.51.114) ##### Fixed - **PR [#​2758](nesquena/hermes-webui#2758 by [@​nesquena-hermes](https://github.com/nesquena-hermes) — fix(updates): pass `--force` to `git fetch --tags` in `api/updates.py` so the WebUI's release-tracking update check can recover from a remote re-tag (e.g. a release tag that was force-pushed to a new commit after a squash-merge). Without `--force`, plain `git fetch origin --tags` returns `! [rejected] vX.Y.Z (would clobber existing tag)` and the entire update path (check, force-apply, normal-apply) jams indefinitely — neither the periodic check nor manual "Check now" nor the Update button can recover. Three fetch call sites were patched (`_check_repo`, `apply_force_update`, `apply_update`) to use `--tags --force`; the WebUI never pushes tags, so deferring to the remote's view is the right contract. Closes [#​2756](nesquena/hermes-webui#2756). ### [`v0.51.113`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051113--2026-05-22--Release-CK-stage-406--1-PR--composer-model-picker-lag-fix--hard-refresh-recovery) [Compare Source](nesquena/hermes-webui@v0.51.112...v0.51.113) ##### Fixed - **PR [#​2743](nesquena/hermes-webui#2743 by [@​franksong2702](https://github.com/franksong2702) — Composer model picker now opens immediately from the existing static option list while the dynamic `/api/models` catalog hydrates in the background, instead of blocking the click on the catalog request. A just-selected session model also survives a hard refresh that interrupts the async `/api/session/update` POST: the selection is staged into `sessionStorage` (keyed by session\_id, 10-minute TTL) before the async update flies, and `loadSession()` re-applies the pending pick on next session restore and retries the persistence call. Tests pin the new ordering: visible picker render before `await`, pending-state save before `await api('/api/session/update')`, and pending-state replay before the first `syncTopbar()` projects server metadata. ### [`v0.51.112`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051112--2026-05-22--Release-CJ-stage-405--1-PR--session-model-authoritative-across-restore) [Compare Source](nesquena/hermes-webui@v0.51.111...v0.51.112) ##### Fixed - **PR [#​2737](nesquena/hermes-webui#2737 by [@​ai-ag2026](https://github.com/ai-ag2026) — Keep the session model authoritative when a restored session is reactivated. Previously, stale browser-cached picker state could override an active conversation's model in four scenarios: (1) on initial boot when `localStorage` had a different model preference than the active session, (2) on hard refresh when `S._bootReady` revealed the composer chip before the live catalog hydrated, (3) when the session's model wasn't in the current provider catalog (the static/default fallback silently rewrote `S.session.model`), (4) when starting a new session whose model wasn't in the static HTML dropdown. The fix: `loadSession()` now requests `resolve_model=1` so backend normalization happens synchronously with metadata; boot model hydration prefers the active session over `localStorage`; hard refresh re-runs the model dropdown hydration before `_bootReady`; a new `_ensureModelOptionInDropdown()` helper injects a `data-custom='1'` option for models not in the catalog instead of silently rewriting `S.session.model` to the default. 100 LOC of new pytest regression coverage pinning each behavior. ### [`v0.51.111`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051111--2026-05-22--Release-CI-stage-404--1-PR--keep-statedb-replays-out-of-sidecar-tail) [Compare Source](nesquena/hermes-webui@v0.51.110...v0.51.111) ##### Fixed - **PR [#​2746](nesquena/hermes-webui#2746 by [@​ai-ag2026](https://github.com/ai-ag2026) — Prevent replayed state.db rows from being appended after an already-correct sidecar transcript tail. `merge_session_messages_append_only()` previously tried to skip state.db rows replaying the sidecar, but two edge cases leaked through: (1) the final row of a replayed sidecar prefix was not skipped because the replay index had reached the sidecar sequence length, and (2) a replayed middle segment was not considered prefix replay, so old state.db rows could be appended after the saved assistant tail. That made `/api/session` appear to end on an old user prompt even when the saved sidecar already ended on the real assistant answer. The fix tracks per-(role, content) visible-occurrence counts in the sidecar and uses that as a replay budget when comparing state.db rows; legitimate repeated messages from state.db are still preserved. `_has_visible_duplicate()` is kept as a thin wrapper around the new `_matching_visible_duplicate()` for backwards compatibility. Regression test covers both full-replay and middle-segment replay shapes. ### [`v0.51.110`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051110--2026-05-22--Release-CH-stage-403--2-PR-batch--default-personality-from-config--sort-configured-providers-to-top) [Compare Source](nesquena/hermes-webui@v0.51.109...v0.51.110) ##### Added - **PR [#​2747](nesquena/hermes-webui#2747 by [@​s010mn](https://github.com/s010mn) — `new_session()` now reads `display.personality` from `config.yaml` as the default for new conversations. Previously every new session started with `personality=None` and required an explicit `/personality <name>` slash command. Values `'none'`, `'default'`, `'neutral'`, and empty string are treated as no-personality. Case-insensitive — `personality: Taleb` normalizes to `taleb`. Config-read is wrapped in try/except so malformed config falls back to the prior behavior rather than crashing session creation. The `/personality` slash command still works for per-session overrides. - **PR [#​2683](nesquena/hermes-webui#2683 by [@​jasonjcwu](https://github.com/jasonjcwu) — Sort providers so configured/custom entries appear first in both the model picker dropdown (`api/config.py::get_available_models`) and the Settings providers panel (`api/providers.py::get_providers`). Priority order: (1) the active provider, (2) `custom:*` providers from `custom_providers` config, (3) providers with configured API keys (credential pool or `config.yaml`), (4) all others alphabetical. Eliminates scrolling past 25+ unconfigured providers to find the one in active use. ### [`v0.51.109`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051109--2026-05-22--Release-CG-stage-402--2-PR-batch--sidebar-action-menu-click-stability--chat-panel-sidebar-resync-after-navigation) [Compare Source](nesquena/hermes-webui@v0.51.108...v0.51.109) ##### Fixed - **PR [#​2741](nesquena/hermes-webui#2741 by [@​ai-ag2026](https://github.com/ai-ag2026) — Keep the sidebar conversation actions menu open while session-list refreshes, stream updates, or panel-resync repairs arrive. Previously the three-dot menu beside chat titles could be torn down before the user finished clicking it because `renderSessionListFromCache()` rebuilt the row DOM (and the fixed-position menu's anchor) without checking whether the menu was open. The new early-return at the top of the refresh keeps the menu stable; destructive menu actions explicitly close the menu before they fire, so dismissal still works as expected. - **PR [#​2736](nesquena/hermes-webui#2736 by [@​ai-ag2026](https://github.com/ai-ag2026) — Resync the chat sidebar after returning from Settings/Logs/other panels. The session list is virtualized, and the browser can clamp the preserved scrollTop during a panel transition; without a render after the chat view is visible again, stale virtual spacer/header DOM remained until the next manual scroll. The new `_resyncChatSidebarAfterPanelSwitch()` helper runs one guarded `requestAnimationFrame` after the panel becomes visible, bails if a rename input or action menu is open, and uses no polling. </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/634
…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.
Thinking Path
start.ps1for native Windows, mirroringstart.sh's discovery shape%USERPROFILE%\.hermes\hermes-agent(user-local install) and../hermes-agent(dev-checkout layout)start.ps1from a fresh checkout where hermes-agent was installed via the official Windows installer (which places it under%LOCALAPPDATA%\hermes\hermes-agent) or via MSI (typicallyProgram Files) currently have to setHERMES_WEBUI_AGENT_DIRmanuallystart.ps1was trying to removeWhat Changed
start.ps1candidate list expands from 2 → 5 paths, built incrementally with null-guards:%USERPROFILE%\.hermes\hermes-agent%LOCALAPPDATA%\hermes\hermes-agentif ($root)%PROGRAMFILES%\hermes\hermes-agentif ($root)%PROGRAMFILES(X86)%\hermes\hermes-agentif ($root)— null on 32-bit Windows../hermes-agent$RepoRoot; unguardedEach candidate is validated as a directory via
Test-Path … -PathType Container— a file (not a directory) namedhermes_cliat any candidate root no longer passes the check. Applied uniformly at both the env-var-override-validation site (start.ps1:94) and the candidate-loop site (start.ps1:111).Not-found error now enumerates every searched path via
$candidates -join ', ', so when discovery fails the user sees the exact list checked.Diff:
start.ps1 | 14 +++++++-----,CHANGELOG.md | 1 +(entry under existing[Unreleased]/### Added).Why It Matters
The official Windows installer for hermes-agent ships it to
%LOCALAPPDATA%\hermes\hermes-agent. Without this PR, every user of that installer who clones hermes-webui and runsstart.ps1has to either:HERMES_WEBUI_AGENT_DIR, re-run%USERPROFILE%\.hermes\hermes-agentAdding the three system-wide candidate paths means installer-deployed agents auto-discover; restructuring the error message means when discovery fails on a machine with no agent installed, the user sees all paths that were checked. The conditional-build hardening means the launcher degrades gracefully on 32-bit Windows where
${env:ProgramFiles(x86)}is null andJoin-Pathwould otherwise throw. The-PathType Containervalidation means a stray file namedhermes_clican't pass the discovery check and lead to a confusing import error later.Verification
start.ps1parses clean via[System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$errs)— zero errorsCHANGELOG.mdentry under[Unreleased]/### Added, immediately above the existing PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 entry, preserves chronological orderingstart.ps1is "newly added") AND post-feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783-merge (wherestart.ps1is "extended")%LOCALAPPDATA%\hermes\hermes-agent, noHERMES_WEBUI_AGENT_DIRset — expected: discovery finds the agent andstart.ps1launchesserver.py. Happy to coordinate with anyone who has the installer-deployed agent layout available; can run on a fresh foundry-side checkout if useful — let me know if you'd like before/after screenshots in a follow-up comment.if ($root)correctly skips the missing${env:ProgramFiles(x86)}withoutJoin-Paththrowing. Same coordination offer.Repo doesn't have PowerShell-specific tests (test suite is pytest-based for the Python side), so the PowerShell-only changes here can't be CI-tested in the existing
tests/layout. If you'd like a Pester test added alongside this PR (or a follow-up), say the word.Risks / Follow-ups
-PathType Containertightening only narrows what passes — anyone whosehermes_cliwas previously a file (impossible by design but theoretically possible) would now be caught earlier instead of later.start.ps1doesn't exist onmasteryet; this PR depends on feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 landing first. Once feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 merges, rebase is clean (additive changes only).start.ps1:16header andREADME.md:140— Copilot flagged it as misleading (a WSL2-created venv is Linux-only, not portable to Windows Python). Out of this PR's scope; will file a separate small docs-fix PR.Test-Pathis called against; no auth, path-handling, uploads, or streaming behavior changes.Model Used
[regex]::Escape($anchor)match-once-or-fail before applying. Final human review (catching the initial PR-body format mismatch withCONTRIBUTING.md) before push.