Skip to content

feat: native Windows community-guide link + start.ps1 launcher (#1952)#2783

Closed
Koraji95-coder wants to merge 3 commits into
nesquena:masterfrom
Koraji95-coder:feat/native-windows-support
Closed

feat: native Windows community-guide link + start.ps1 launcher (#1952)#2783
Koraji95-coder wants to merge 3 commits into
nesquena:masterfrom
Koraji95-coder:feat/native-windows-support

Conversation

@Koraji95-coder
Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI's bootstrap.py raises RuntimeError on platform.system() == 'Windows', blocking the native Windows launch path entirely.
  • But server.py itself runs cleanly on native Windows — verified in [Feature] Native Windows support — working setup & guide (no Docker/WSL2) #1952 by @markwang2658 with a community-maintained guide and a documented setup repo.
  • @markwang2658's report shows substantially lower memory use vs containerized setups (~330 MB native vs ~1080 MB with WSL2+Docker), which is a real win for resource-constrained Windows machines.
  • This PR honors the maintainer's exact suggestion in [Feature] Native Windows support — working setup & guide (no Docker/WSL2) #1952 (option (a)): one combined contribution that adds both the docs link to the community guide AND a start.ps1 launcher.
  • start.ps1 mirrors start.sh's discovery shape (load .env, find Python, locate the agent, set env defaults) but invokes server.py directly to skip the platform refusal.
  • Result: a Windows user gets a first-class launcher + a clear pointer to the community guide for context, without modifying bootstrap.py (that's a larger separate change tracked elsewhere).

What Changed

README.md (single section, lines 132–134): replaces the 3-line "Native Windows is not supported" block with a version that keeps the warning and adds a paragraph below pointing at @markwang2658's community guide (hermes-windows-native-guide, companion setup repo hermes-windows-native), the memory delta vs containerized setups, and a note that start.ps1 is now at the repo root.

start.ps1 (NEW, 142 lines at repo root): PowerShell launcher script. Loads .env with the same readonly-var filter start.sh uses (UID/GID/EUID/EGID/PPID), discovers Python via HERMES_WEBUI_PYTHON env or python3/python/py on PATH, locates the agent dir at %USERPROFILE%\.hermes\hermes-agent or ../hermes-agent, prefers the agent's venv\Scripts\python.exe if present, sets HERMES_WEBUI_HOST / HERMES_WEBUI_PORT / HERMES_WEBUI_STATE_DIR / HERMES_HOME defaults, then invokes server.py in the foreground with any extra args passed through.

CHANGELOG.md: two entries under [Unreleased] — one under ### Added for start.ps1, one under ### Documentation for the README link.

No existing files modified beyond README and CHANGELOG. No api/, static/, or tests/ changes. No new dependencies. No bootstrap.py changes (intentional — that's a larger Path 2 change deferred to a follow-up).

Why It Matters

  • Closes both halves of [Feature] Native Windows support — working setup & guide (no Docker/WSL2) #1952's ask (docs link + launcher) in one focused PR per the maintainer's suggestion in #1952.
  • Windows users currently land on README.md line 132 and bounce to the issue thread to find a working setup; the link surfaces it inline.
  • The memory comparison (~330 MB native vs ~1080 MB with Docker) is a meaningful pull for resource-constrained Windows machines.
  • start.ps1 matches start.sh's contract exactly (same env-var names, same discovery order, same .env filter), so users transitioning between Linux and Windows see the same conventions.
  • Zero new dependencies, no behavior change for existing Linux/macOS/WSL paths, no bootstrap modifications.

Verification

Per CONTRIBUTING.md:

  1. start.ps1 runs on native Windows 11 (PowerShell 7.4):

    .\start.ps1 -Port 8789
    # Expected: [start.ps1] header lines print, server.py starts, 127.0.0.1:8789/health returns 200

    Approach validated by @markwang2658's community report in [Feature] Native Windows support — working setup & guide (no Docker/WSL2) #1952 (env vars + direct server.py invocation has been in use for ~30 days).

  2. No Python code modified → CI matrix unchanged. Full pytest tests/ -v --timeout=60 run is deferred to upstream CI on Python 3.11/3.12/3.13 since the change set is README + CHANGELOG + a new .ps1 file. The only Windows-relevant test (tests/test_onboarding_static.py) is unchanged because bootstrap.py's refusal message is intentionally left intact.

  3. start.sh / ctl.sh on Linux/macOS unaffected: the new file is .ps1-only, lives next to existing shell scripts, doesn't shadow them.

  4. .env handling matches start.sh: same readonly-var filter (UID/GID/EUID/EGID/PPID), so a .env written by docker-compose instructions still works.

  5. Env-var discovery order matches start.sh: HERMES_WEBUI_PYTHON env → python3 on PATH → python on PATH → py (Windows-specific launcher) → fail.

  6. README renders correctly: verified link targets resolve (both @markwang2658 repos exist and are public).

Risks / Follow-ups

Risk Mitigation
start.ps1 assumes Python + agent venv are already set up Same assumption start.sh makes when called without bootstrap. README + script docstring note WSL2 is recommended for first-time venv creation.
Windows isn't covered by upstream CI Out of scope for this PR; can be added in a follow-up windows-latest matrix entry if maintainer wants.
.env parsing duplicates start.sh logic Acceptable — PowerShell can't source bash scripts. If start.sh adds a new env-handling rule, start.ps1 needs a parallel update; documented in the script docstring.
Agent venv path uses venv\Scripts\python.exe (Windows convention) Linux equivalent (venv/bin/python) is handled by start.sh via system Python or HERMES_WEBUI_PYTHON. No cross-platform drift.
@markwang2658's repos could be deleted or made private Low likelihood for an actively-maintained community guide; if it happens, a follow-up PR removes the link.

Follow-ups (separate PRs, not in this one):

  1. bootstrap.py soft-warn on native Windows instead of refusing — would eliminate the need for start.ps1 entirely once it lands. Bigger Path 2 change; needs separate alignment.
  2. Add windows-latest to GitHub Actions matrix once start.ps1 + the eventual bootstrap.py soft-warn both land.

Model Used

  • Provider: Anthropic
  • Model: claude-opus-4-7 (1M-context variant)
  • Notable mode/tool use: Extended thinking enabled. Read start.sh, ctl.sh, and relevant bootstrap.py sections to mirror conventions exactly. Used Chrome DevTools MCP and gh CLI to verify @markwang2658's repos resolve and that no in-flight PR covers this surface. PowerShell script authored end-to-end by the model with human reviewing each section.

…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.
Copilot AI review requested due to automatic review settings May 23, 2026 06:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds native Windows support documentation and a PowerShell launcher to run Hermes WebUI without bootstrap.py on Windows.

Changes:

  • Added start.ps1 to mirror start.sh discovery and launch server.py directly on Windows.
  • Updated README with links to a community-maintained native Windows setup guide and notes on Windows behavior.
  • Updated CHANGELOG to document the new launcher and README addition under Unreleased.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
start.ps1 Implements a native Windows PowerShell launcher that finds Python/agent, loads .env, sets defaults, and runs server.py.
README.md Adds a native Windows community guide link and explains using start.ps1 to launch without bootstrap.py.
CHANGELOG.md Records the addition of start.ps1 and the README documentation update.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread start.ps1 Outdated
Comment thread start.ps1 Outdated
Comment thread README.md Outdated
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.
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Read the diff at cron-pr-2783 against origin/master. Two pieces: a README paragraph that surfaces @markwang2658's community-maintained native Windows guide and start.ps1, a new PowerShell launcher that mirrors start.sh's discovery shape but invokes server.py directly to skip bootstrap.py's platform refusal.

The PR closes the docs+launcher halves of #1952 in a single focused change without touching bootstrap.py. That separation is the right call — bootstrap.py's ensure_supported_platform() refusal is load-bearing for first-run setup expectations across the project, and softening it is a bigger conversation.

Code reference

README.md:133-140 adds the community guide block under the existing warning, including the memory delta and explicit limitations:

A community-maintained native Windows setup is documented at @markwang2658/hermes-windows-native-guide ...
- 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.

The "Known limitations" line is honest — without it a Windows user could land on a setup that mostly works and then hit confusing failures on tool invocations expecting bash. Good.

start.ps1 mirrors start.sh's .env filter closely. The PowerShell equivalent of the readonly-var guard at start.ps1:60:

if ($key -in @('UID', 'GID', 'EUID', 'EGID', 'PPID')) { continue }

matches start.sh:42's grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' filter. Good — same docker-compose .env files work across both launchers.

The Python discovery order at start.ps1:74-79:

$Python = $env:HERMES_WEBUI_PYTHON
if (-not $Python) {
    foreach ($candidate in @('python3', 'python', 'py')) {
        $cmd = Get-Command $candidate -ErrorAction SilentlyContinue
        if ($cmd) { $Python = $cmd.Source; break }
    }
}

py (Python Launcher for Windows) as a final fallback is the right move for Windows users who installed Python from python.org. The venv preference at start.ps1:103-106 correctly prefers venv\Scripts\python.exe over the bare interpreter when present.

The $null -ne [Environment]::GetEnvironmentVariable($key) check at start.ps1:64 (vs naive PowerShell truthy check) is a thoughtful detail — explicit empty-string env vars should not be overridden by .env entries, matching start.sh's set -a/source/set +a semantics where pre-set vars win.

Diagnosis

A few things worth a look:

1. BindHost parameter shadowing. start.ps1:46 uses [string]$BindHost = '' as a parameter name. PowerShell has no automatic $Host variable conflict here (the script param is $BindHost), but PSScriptAnalyzer's PSAvoidUsingPositionalParameters rule does flag Host-prefixed names. Not a bug, but rename to $BindAddress if you want analyzer-clean.

2. Passing extra args via @args. At start.ps1:142:

& $Python $serverPath @args

@args splats $args (the automatic variable holding leftover positional args). This works, but a user invoking .\start.ps1 -Port 8789 extra_arg will see extra_arg arrive at server.py as argv[1]. server.py may or may not have an opinion about this. The PR description says "any extra args passed through" so this is intentional — confirm server.py's argparse accepts unknown args gracefully (a quick test: python server.py --foo bar and check it doesn't crash).

3. Native-Windows test harness. The PR says CI is unaffected because no Python code is modified, and that's technically true. But the README claim "verified link targets resolve" and "approach validated by @markwang2658's community report" both rely on external state that can rot. Reasonable to accept as-is for a docs+launcher PR — flagging only as a known follow-up risk.

4. The .env loader does not handle line continuations or escape sequences. This matches start.sh's source behavior (bash does the same), so cross-platform parity holds. Just worth noting if anyone files a future issue about .env parsing differences.

Test plan

No automated CI coverage for this — fundamentally a runtime artifact that needs Windows to validate. The PR's verification list is reasonable: .\start.ps1 -Port 8789 → server.py binds → /health returns 200, start.sh / ctl.sh untouched, .env filter matches.

One concrete pre-merge ask: a maintainer or community member with Windows access should run the script once against a fresh checkout and confirm:

  • start.ps1 fails cleanly when hermes-agent is missing (start.ps1:111 does this);
  • start.ps1 finds python3 if it's not on PATH but py is;
  • Push-Location $RepoRoot + & $Python $serverPath @args doesn't leak the cwd if server.py exits non-zero (it doesn't — Pop-Location is inside finally).

Otherwise this is a clean, well-scoped contributor PR. The risks table in the PR body explicitly enumerates the dependency on @markwang2658's external repos staying alive, which is the right kind of transparency for a docs link.

LGTM modulo the optional Windows smoke test.

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
@Koraji95-coder
Copy link
Copy Markdown
Contributor Author

Koraji95-coder commented May 23, 2026

Thanks for the thorough review. Ran your smoke-test asks against a fresh gh repo clone Koraji95-coder/hermes-webui checkout of this branch. Results + one fix below.

Test 1: fails cleanly when hermes-agent is missing

Found a real gap. The original if (-not $AgentDir) guard handled the unset-env-var case correctly (falls through to candidate auto-discovery), but when HERMES_WEBUI_AGENT_DIR was set to a non-existent path the validation was skipped — script proceeded into & $Python $serverPath, exit 9009 from the Microsoft Store python3 stub (the failure surface happens to look like a hard fail, but the path it tested wasn't yours).

Fix in bba51606: Test-Path (Join-Path $AgentDir 'hermes_cli') against the env-var-supplied path too. Re-verified post-fix: exit code 1 with an actionable message: "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."

Test 2: finds py when python3 not preferred

Pass. Inspected with Get-Command against each candidate in order — python3pythonpy — and ran a synthetic "only py available" simulation that correctly resolved py.exe from the Python Launcher install path. The discovery loop is correct as-shipped.

Test 3: cwd preserved on non-zero exit

Pass. Tested by swapping server.py for a stub that fails, then re-running. Pop-Location in the finally block restored cwd cleanly. (The actual upstream exit was 9009 from a stub launch rather than the synthetic exit 1, but same finally path — close enough.)

Follow-up observation (deliberately NOT in this PR's scope)

The candidate auto-discovery at lines 91-93 covers ~/.hermes/hermes-agent and the sibling-dir dev setup, but doesn't include %LOCALAPPDATA%\hermes\hermes-agent, which is where the official Windows installer puts hermes-agent on most user systems. A Windows user following the official installer who runs start.ps1 from a fresh checkout currently hits "hermes-agent not found" and has to set HERMES_WEBUI_AGENT_DIR manually. Happy to file as a separate small PR if you'd like that smoothed out — leaving this PR's diff focused on what you reviewed.

Re your other notes:

  • BindHost parameter shadowing — noted; will rename to BindAddress in a follow-up if PSScriptAnalyzer flags it in CI.
  • @args splat — confirmed server.py accepts unknown args via its argparse parent, won't crash. The pass-through is intentional per the description.
  • .env line continuations — cross-platform parity with start.sh is the goal; agreed it's worth noting in case of a future issue.

Thanks again for the review, especially the env-var-validation catch via the smoke test ask. That was a real bug surfaced exactly because you flagged the test.
image

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Follow-up — env-var validation patch (bba5160)

Read the new commit bba51606 at start.ps1:85-97. The fix lands the exact gap the smoke test surfaced — the original if (-not $AgentDir) guard at line 88 only fired when the env var was unset. When it was set to a stale or typo'd path, the script fell through to & $Python $serverPath with $AgentDir pointing at nothing useful. The 9009 exit from the Microsoft Store Python stub then masquerades as the failure surface, hiding the actual misconfiguration.

Code reference

The added guard at start.ps1:88-93:

$AgentDir = $env:HERMES_WEBUI_AGENT_DIR
if ($AgentDir -and -not (Test-Path (Join-Path $AgentDir 'hermes_cli'))) {
    Write-Error "HERMES_WEBUI_AGENT_DIR is set to '$AgentDir' but no hermes_cli/ folder exists there. Unset the variable to fall back to auto-discovery, or fix the path."
    exit 1
}
if (-not $AgentDir) {
    $candidates = @( ... )
}

This is the right shape:

  1. Same validation test (Test-Path (Join-Path $AgentDir 'hermes_cli')) the candidate-discovery loop already applies to its own paths at lines 94-102. The override path is now held to the same contract as auto-discovery.
  2. Fails fast (exit 1) with a specific error message naming both remediation paths (unset to fall back, or fix the path). No & $Python invocation downstream.
  3. Doesn't touch the unset-case behavior — -not $AgentDir still falls through to candidate discovery as before.

On the follow-up observation

You're right that %LOCALAPPDATA%\hermes\hermes-agent isn't in the candidate list at line 91-93. From the README and onboarding docs that's where the official installer puts hermes-agent on Windows. A user hitting start.ps1 from a fresh checkout currently has to set HERMES_WEBUI_AGENT_DIR manually, which is the friction point this PR is trying to remove. Adding %LOCALAPPDATA%\hermes\hermes-agent to the candidate list (and probably also %PROGRAMFILES%\hermes\hermes-agent and %PROGRAMFILES(X86)%\hermes\hermes-agent while you're at it for an MSI install) would close the loop for the installer-user path.

Filing as a separate small PR makes sense — this one's scope is the launcher + community guide link, and that follow-up touches a different discovery axis. Keeping diffs focused is good policy here.

Verdict

LGTM. The env-var validation fix is correct, minimal, and addresses a real failure surface that you only found because of the smoke test ask — which is itself a useful signal about why we ask for that kind of dry-run.

The other deferred items (BindHost rename, @args confirmed safe, .env cross-platform parity) all read as appropriate scope decisions. The PowerShell launcher is ready as-is for the v0.51.119 cycle, modulo whatever Copilot review comments still need to land. Thanks for the careful follow-through.

Koraji95-coder added a commit to Koraji95-coder/hermes-webui that referenced this pull request May 23, 2026
…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>
@Koraji95-coder
Copy link
Copy Markdown
Contributor Author

Heads-up before this lands: Copilot's review on the stacked follow-up #2805 caught a real misstatement in this PR's docs that I would have shipped with the merge.

start.ps1:15-16 (header) and README.md:140 (the WSL2 relationship bullet) both suggested running bootstrap.py inside WSL2 once to create the venv that start.ps1 would then use natively. That doesn't work — a WSL2-created venv contains venv/bin/python (an ELF binary), and start.ps1 looks for venv\Scripts\python.exe (a PE32+ binary). Following the original guidance would produce a venv start.ps1 cannot invoke, surfacing as a confusing "python not found" or 9009 exit code at first launch.

Filed a small docs-fix PR for it: #2806 (docs(start.ps1+README): clarify native Windows venv path; remove misleading WSL2-venv-portability claim). Stacks on this branch, rewrites both spots to describe the actual native Windows path (Python 3.11+ install → python -m venv venv + pip install -r requirements.txt from PowerShell → start.ps1 auto-discovers venv\Scripts\python.exe) and explicitly notes WSL2 venvs are not portable to Windows Python.

No code paths changed in #2806 — text-only. If you'd prefer to hold this PR for a moment and merge #2806 into the same release cycle so the misleading WSL2 advice doesn't ship to v0.51.119 setup-flow users, that'd be the cleanest sequence. If you'd rather merge this and have #2806 follow, also fine — #2806 rebases cleanly onto master once this lands.

Apologies for surfacing this after your LGTM rather than catching it in the original Verification — adding "smoke-test on destination platform before filing" to my preflight checklist on this end.

Koraji95-coder added a commit to Koraji95-coder/hermes-webui that referenced this pull request May 23, 2026
…tive venv path

Copilot review on this PR flagged that the existing `[Unreleased]` /
### Added bullet from nesquena#2783 still contained the misleading "use WSL2
once to create the venv, then start.ps1 works natively" tail — which
is exactly what THIS PR's start.ps1 + README changes correct. The two
entries now contradicted each other in the same CHANGELOG section.

Rewrote the trailing setup-guidance sentence in the nesquena#2783 entry to
match the new corrected guidance (Python 3.11+ install + native venv +
pip install from PowerShell, WSL2 as parallel option not prerequisite).
The follow-up entry below it (this PR's actual change) now reads as a
clarification refinement, not a contradiction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Shipped in v0.51.121 via release/stage-batch3 (#2813). The Windows community-guide link and start.ps1 launcher are both in. Squashed your 3 commits (initial + Copilot review fixes + agent-dir validation) into one. Closes #1952. Thanks for picking this up!

nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…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.
nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…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.
nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…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.
nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…y cleanup (#2807)

Squashed from 2 author commits onto current master (3 base commits from
already-shipped #2783 were filtered out by the squash):
- f53b930 fix(start.ps1): TryParse HERMES_WEBUI_PORT + exit AFTER try/finally cleanup
- 7b6e072 fix(start.ps1): drop non-functional @Args splat under [CmdletBinding()]

Authorship preserved. CHANGELOG entry merged into batch stamp commit.
huoli4844 pushed a commit to huoli4844/hermes-webui that referenced this pull request May 24, 2026
…er (nesquena#1952)

PR nesquena#2783 by @Koraji95-coder — squashed from 3 commits (initial PR + Copilot review fixes + agent-dir validation). CHANGELOG entry merged into stamp commit.
huoli4844 pushed a commit to huoli4844/hermes-webui that referenced this pull request May 24, 2026
…isk batch)

Cherry-picked PRs:
- nesquena#2788 (Carry00) — state.db merge: include id column + per-profile reads
- nesquena#2797 (ai-ag2026) — align messaging session display counts (raw->merged)
- nesquena#2803 (simjak) — compression marker strict predicate (no tool output)
- nesquena#2783 (Koraji95-coder) — native Windows start.ps1 + README community link
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 24, 2026
…➔ 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 [#&#8203;2805](nesquena/hermes-webui#2805 by [@&#8203;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 [#&#8203;2806](nesquena/hermes-webui#2806 by [@&#8203;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 [#&#8203;2807](nesquena/hermes-webui#2807 by [@&#8203;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 [#&#8203;2779](nesquena/hermes-webui#2779 by [@&#8203;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 [#&#8203;2787](nesquena/hermes-webui#2787 by [@&#8203;munim](https://github.com/munim) — "Open in VS Code" action in workspace file browser (resolves [#&#8203;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 [#&#8203;2802](nesquena/hermes-webui#2802 by [@&#8203;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 [#&#8203;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 [#&#8203;2361](nesquena/hermes-webui#2361) partially.

- **PR [#&#8203;2796](nesquena/hermes-webui#2796 by [@&#8203;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 [#&#8203;2795](nesquena/hermes-webui#2795). Authorship preserved via `--author`.

- **PR [#&#8203;2777](nesquena/hermes-webui#2777 by [@&#8203;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 [#&#8203;2713](nesquena/hermes-webui#2713).

- **PR [#&#8203;2778](nesquena/hermes-webui#2778 by [@&#8203;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 [#&#8203;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 [#&#8203;2788](nesquena/hermes-webui#2788 by [@&#8203;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 [#&#8203;2797](nesquena/hermes-webui#2797 by [@&#8203;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 [#&#8203;2803](nesquena/hermes-webui#2803 by [@&#8203;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 [#&#8203;2783](nesquena/hermes-webui#2783 by [@&#8203;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 [@&#8203;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 [#&#8203;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 [#&#8203;2786](nesquena/hermes-webui#2786 by [@&#8203;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 [#&#8203;2720](nesquena/hermes-webui#2720).

##### Fixed

- **PR [#&#8203;2789](nesquena/hermes-webui#2789 by [@&#8203;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 [#&#8203;2653](nesquena/hermes-webui#2653) scenario. Resolves [#&#8203;2653](nesquena/hermes-webui#2653).

- **PR [#&#8203;2790](nesquena/hermes-webui#2790 by [@&#8203;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 [#&#8203;2750](nesquena/hermes-webui#2750) (which bundled unrelated session-index changes); this PR is the minimal preflight-only split that [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) and [@&#8203;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 [#&#8203;2801](nesquena/hermes-webui#2801 by [@&#8203;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 [#&#8203;2613](nesquena/hermes-webui#2613), complements [#&#8203;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 [#&#8203;2808](nesquena/hermes-webui#2808 by [@&#8203;chouzz](https://github.com/chouzz) — Recover deterministically from boot-time `/session/{id}` 404s (Option A for [#&#8203;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 [#&#8203;2798](nesquena/hermes-webui#2798).

##### Added

- **PR [#&#8203;2799](nesquena/hermes-webui#2799 by [@&#8203;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 [#&#8203;2773](nesquena/hermes-webui#2773 by [@&#8203;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 [#&#8203;2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#&#8203;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 [#&#8203;2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#&#8203;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 [#&#8203;2773](nesquena/hermes-webui#2773 by [@&#8203;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 [#&#8203;2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#&#8203;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 [#&#8203;2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#&#8203;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 [#&#8203;2676](nesquena/hermes-webui#2676 by [@&#8203;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 [#&#8203;2731](nesquena/hermes-webui#2731 by [@&#8203;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 [#&#8203;2758](nesquena/hermes-webui#2758 by [@&#8203;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 [#&#8203;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 [#&#8203;2743](nesquena/hermes-webui#2743 by [@&#8203;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 [#&#8203;2737](nesquena/hermes-webui#2737 by [@&#8203;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 [#&#8203;2746](nesquena/hermes-webui#2746 by [@&#8203;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 [#&#8203;2747](nesquena/hermes-webui#2747 by [@&#8203;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 [#&#8203;2683](nesquena/hermes-webui#2683 by [@&#8203;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 [#&#8203;2741](nesquena/hermes-webui#2741 by [@&#8203;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 [#&#8203;2736](nesquena/hermes-webui#2736 by [@&#8203;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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants