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>
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 #2805 stacks on #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 #2783's scope, not
#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>
Maintainer follow-up on PR #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 #2783. Once #2783 lands, this rebases cleanly onto
master.
Surfaced by the maintainer's requested fresh-checkout smoke test on
PR #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
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.
Combined PR for #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.
Closes#2771.
v0.51.117 (PR #2766) introduced a top-level function _inflightStateLimits()
in static/ui.js that collided with the window._inflightStateLimits config
object set in static/boot.js. Because top-level function declarations in
classic (non-module) scripts attach to window, boot.js's assignment
overwrote the function reference, and every later _inflightStateLimits()
call threw TypeError. _compactInflightState() runs on every send(), so
no new chat session could be created — v0.51.117 is effectively unusable.
Reported by @jahilldev, with multiple users (@isma3iloiso, @theDanielJLewis,
@JHVenn) confirming the bug or reverting to v0.51.116.
Fix: rename the function to _getInflightStateLimits() — the window-attached
config key stays under its original name (unchanged for any downstream
code that reads it). Updates all 4 call sites in static/ui.js.
Tests:
- Update tests/test_inflight_storage_quota.py — the existing test
asserted 'function _inflightStateLimits()' in UI_JS as a positive
presence check, which certified the bug. Now asserts the renamed
function name is present AND the old colliding name is absent AND
no stale call sites remain.
- Add tests/test_window_function_collision.py — generalized regression
that scans every static JS file for top-level function declarations
whose name also appears as the target of 'window.X = {...}' or
'window.X = <number>'. This is the exact shape that broke #2715
(_pinnedSessionsLimit in v0.51.106) and #2771. Test fails with a
precise diagnostic naming the file and symbol if the bug class
returns. Confirmed test FAILS on current master (unfixed) and PASSES
on this branch.
Verified end-to-end against the live browser before commit:
- typeof window._inflightStateLimits === 'object' (config preserved)
- typeof window._getInflightStateLimits === 'function'
- _getInflightStateLimits() returns the limits object
- saveInflightState() persists to localStorage without throwing
Full pytest suite: 6308 passed, 6 skipped, 3 xpassed, 8 subtests passed.
Opus advisor: SHIP.
Original PR: #2676 by @lucasrc
Adds POST /api/skills/toggle endpoint that flips skills.disabled in
config.yaml, and a UI toggle in the Skills panel that shows all skills
(including disabled ones) with a per-skill on/off control.
- Backend: new endpoint validates skill exists in filesystem before
toggling. Read-modify-write wrapped in _cfg_lock for thread safety.
Writes through to platform_disabled.webui when present.
- Frontend: each skill-item now has a toggle switch; disabled skills
appear muted but still listed (previously they were filtered out).
- i18n: new toggle keys translated across all 9 non-English locales.
- Tests: round-trip test for disabled list normalization + toggle
endpoint behavior.
Squash-merged from contributor's branch (19 commits + 1 merge commit)
onto current master via the cherry-pick-stale-contributor-prs procedure.
- Replace text 'Collapse'/'Expand' button labels with Lucide chevron SVG
icons (chevron-down expanded → click to collapse, chevron-up collapsed
→ click to expand). Matches the iconographic design language of the
rest of the chrome (composer buttons, sidebar controls).
ARIA label + title attributes carry the same semantics for assistive
tech, so no accessibility regression vs. the text labels.
- Fix collapsed-card edge clipping at viewport bottom. Original
.clarify-card { bottom: -24px } was sized for the expanded card
(300-420px tall); adding a 72px collapsed variant pushed the header
below the parent's visible region. Override bottom to 8px and reduce
inner padding for the collapsed state so the entire header sits cleanly
inside the viewport at both desktop and mobile sizes (verified card
fits with ~115px margin desktop / ~125px margin mobile).
Per Nathan's 2026-05-22 UX feedback on the screenshot package.
Without --force, git fetch origin --tags refuses to overwrite divergent
local tags and returns 'would clobber existing tag', jamming the entire
WebUI update path indefinitely. The WebUI is a release-tracking consumer
that never pushes tags, so it should always defer to whatever the remote
says a release tag points to. Add --force to all three fetch-tag call
sites:
- _check_repo (the 'Check now' button + periodic check)
- apply_force_update (force-reset to remote HEAD)
- apply_update (stash + pull --ff-only)
Tests:
- Updated 3 existing tests in test_updates.py whose fake_git mocks
asserted the exact ['fetch', 'origin', '--tags'] args list.
- Updated 1 existing test in test_update_banner_fixes.py that asserted
the same shape for apply_update.
- Added 4 new regression tests:
- test_check_repo_fetches_tags_with_force
- test_apply_force_update_fetches_tags_with_force
- test_apply_update_fetches_tags_with_force
- test_check_repo_recovers_from_remote_retag (end-to-end,
proves the bare --tags fetch shape is no longer used)
Closes#2756.
When display.personality is set in config.yaml (e.g. personality: taleb),
new sessions now inherit it automatically instead of starting with
personality=None and requiring an explicit /personality command.
This makes the selected personality sticky across new conversations rather
than requiring per-session activation.
Behavior:
- display.personality values 'none', 'default', 'neutral', '' are treated
as no personality (personality=None), matching TUI gateway semantics.
- Config read is wrapped in try/except — if it fails, personality falls
back to None (no crash, no regression).
- Case-insensitive: 'Taleb' normalizes to 'taleb'.
The /personality slash command still works for per-session overrides as
before; this change only affects the initial default.