Surfaces the Hermes agent's LLM Wiki as a read-only knowledge-base
observability section in Insights (status counts + browsable read-only
pages). Unconfigured shows a clear "Unavailable" empty state.
Strictly read-only, auth-gated. The wiki file-serving path is hardened
against the full traversal/symlink/TOCTOU class (Codex + Opus review, each
vector regression-tested + toothless-checked):
- _llm_wiki_page_files resolves the wiki root once as the trust base;
skips a section whose resolved path escapes the root; requires each *.md
target's RESOLVED path under both real root and real section; rejects
dot-prefixed segments on lexical AND resolved-relative paths (closes
symlink-to-hidden/out-of-tree + symlinked-section escapes). Browse + read
share this allowlist.
- /api/wiki/page rejects abs paths + a real '..' SEGMENT (legit v1..v2.md
still opens); requires the resolved target in the allowlist; reads the
resolved path with O_NOFOLLOW; and fstat-verifies the open fd's
(st_dev, st_ino) against the identity captured at allowlist time — closing
final-component AND parent-directory symlink-swap TOCTOU races. Any
mismatch/vanish -> clean 404, never a 500.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
The WebUI gateway chat backend routed every browser chat turn through the
gateway's /v1/runs API whenever the gateway advertised approval/runs
support (gateway_chat.py capability gate). The runs API creates an isolated
Api_Server session per turn, so mid-conversation context was lost between
messages. Routing through /v1/runs is now gated behind an explicit opt-in
(_gateway_use_runs_api_enabled: HERMES_WEBUI_GATEWAY_USE_RUNS_API env var or
webui_gateway_use_runs_api config key, env-over-config precedence, default
off). The capability gate now requires BOTH the opt-in flag AND gateway
approval support before using /v1/runs; otherwise normal chat stays on
/v1/chat/completions, preserving the session via X-Hermes-Session-Id.
Operators who want the in-gateway-chat approval-events flow re-enable it
with the flag.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Docker images and pip installs have no .git directory, so _check_repo
returned None and the frontend interpreted both update-target parts being
falsy as "Up to date" — a stale Docker image would misleadingly look
current. _check_repo now returns a minimal sentinel dict ({name, behind:
None, no_git: True}) instead of None so the Settings panel can distinguish
"can't check" from "up to date", and _formatUpdateTargetStatus /
checkUpdatesNow surface a localized "Can't check for updates" state
(settings_update_no_git, added across all 13 locale blocks). In mixed
deployments where one component has a git checkout and the other does not,
the valid git-side update info is still shown. The sentinel's behind: None
flows safely through the existing notification/summary builders, which all
gate on int(info.get('behind') or 0) > 0.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
When the model dropdown rebuilds (panel switch, provider refresh, or a
live-model fetch that times out and serves a fallback catalog), a
cross-provider selection could vanish from the refreshed catalog and the
browser would snap the <select> back to its first <option> (the default
model). _reconcileModelDropdownSelection now routes each selection branch
through an _applyOrEnsure helper that delegates to
_ensureModelOptionInDropdown, re-injecting the chosen model as a custom
option when it is missing from the rebuilt catalog instead of returning
null. Branch order and per-branch model/provider arguments are unchanged.
Maintainer note: simplified the contributor's helper to delegate straight
to _ensureModelOptionInDropdown (which already applies-first-then-injects),
removing a redundant double scan; re-pointed the
test_populate_model_dropdown_reconciles_selection_after_rebuild string
assertions to the new form (the behavioral node-driver tests for boot-default
precedence and in-page selection preservation are unchanged and still pass).
Co-authored-by: Ed <edou@edoudoux.com>
Ships @dso2ng's fix: /api/session now reconciles stale CLI source flags
against the state.db WebUI-origin row, and _isExternalSession() refuses to
classify an explicit WebUI-source session as external — so a WebUI-native
transcript with a stale sidecar isn't force-reloaded like an imported one.
Rebased onto master (contributor's net code-diff applied; byte-identical to
PR head). CHANGELOG stamped v0.51.474.
Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
Opus review note: the substring match in _stale_prefix_matches_prior_user_context
is intentionally loose, but correctness depends on the caller's
suffix-equals-entire-submitted-turn invariant (cleaning can only ever rewrite a
flagged row to the user's actual current turn — never drop legitimate content).
Comment-only; no behavior change.
- Rename test_handler_skips_attach_when_text_present to
test_handler_skips_preventDefault_when_text_present: asserts
early-return on text-only clipboards and that preventDefault is
gated on !hasText (the new contract)
- Add test_handler_attaches_image_when_both_text_and_image_present:
positive assertion that addFiles() is reached when both text and
image items are in the clipboard (the headline fix)
Addresses review feedback: tests now reflect the new behavior matrix
instead of the old if(!imageItems.length || hasText) return guard.
The paste handler (boot.js) bailed entirely when the clipboard contained
both text and image items, preventing image attachment in the most common
real-world scenario: right-click → Copy Image from browsers, Notes, Slack,
etc. — these almost always carry a text/html or text/plain representation
alongside the image.
Previous behavior (from #1620 fix):
- clipboard has text only → normal text paste (correct)
- clipboard has image only → attach image (correct)
- clipboard has BOTH → text pasted, image silently dropped (wrong)
New behavior:
- clipboard has text only → normal text paste (unchanged)
- clipboard has image only → preventDefault + attach image (unchanged)
- clipboard has BOTH → let text paste normally AND attach image
The key change: preventDefault() is now only called when no text is
present (true screenshot paste), so the browser handles text insertion
naturally while addFiles() runs regardless.
Fixes the regression introduced by the #1620 fix. Closes#1620.
test_fade_text_effect_uses_dynamic_window_check used PANELS_JS.index(needle,
300000) to skip the payload-builder occurrence and land on the listener block.
The #3850 searchable-settings additions shifted byte offsets so the magic 300000
now lands on the payload-builder occurrence instead → false failure (the actual
listener code is unchanged + correct). Anchor on the listener-specific
'fadeTextCb=...\n if(fadeTextCb){' instead. No production code change.
Codex+Opus review fixes:
1. Index query missed the common toggle shape <label><input><span data-i18n>>
— only label[data-i18n] was indexed, so most checkbox settings (Virtualize
long transcripts, Browser notifications, etc.) were silently unsearchable.
Broaden to 'label[data-i18n], label [data-i18n], label' and make
_resolveSettingsField resolve any [data-i18n] node to its .settings-field.
2. Stale async-render race: _beginSettingsPanelSession now bumps
_settingsSearchSeq + clears the input/results, and the outside-click dismiss
handler also bumps the seq, so an in-flight index build from a prior session
can't paint into the dropdown. + 4 regression tests.
Ships @rodboev's searchable-settings feature (#3850/#3936): a search box in
Settings that finds any field across all 5 sections with breadcrumbs +
click-to-navigate+highlight+clear. Rebased fresh onto master (post v0.51.470),
5 feature files byte-identical to PR head, re-stamped v0.51.471 (was v0.51.469,
collided with the shipped transcript-virtualization release).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Virtualization (#500/#4214) caused a scroll-up flicker on long sessions with
tall tool-call rows (variable-height anchor oscillation), distinct from the
stream-completion jump fixed in #4325. Per maintainer decision, flip the
'Virtualize long transcripts' preference to experimental/opt-IN (default OFF)
for everyone until the scroll behavior is reliable.
- api/config.py: virtualize_transcript default True->False; add
virtualize_transcript_optin marker (both bool-allowlisted); load_settings
force-off migration — a stored true WITHOUT the post-flip opt-in marker is
reset to off, so 100% of existing users land on off; an explicit opt-in
(marker present) is honored.
- boot.js / panels.js: default-off semantics (===true), persist the opt-in
marker on enable, checkbox not pre-checked.
- index.html / i18n.js (en): relabel experimental + flicker warning.
- Tests: rewrite the #4325 toggle assertions for the opt-in contract + 4 new
migration tests (unset/stale-true/explicit-optin/marker-without-true).
Phase B (root-cause scroll-stability) tracked separately. Thanks @b3nw.
The opt-out toggle's change handler wrapped renderMessages in a silent
empty catch. Keep the defensive try (a toggle must never wedge the
settings handler) but console.warn on failure so a broken render is
diagnosable instead of swallowed.
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.
Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve#4325.
Render symlinks in the workspace file tree with a distinct chain-link
icon (vs regular file/folder icons), and show "Symlink → {target}" on
hover with long targets middle-elided. Directory and file symlinks are
both handled; regular entries keep their normal icons.
Rebased onto master (v0.51.466) and code-diff verified byte-identical to
the contributor's PR head before staging.
Co-authored-by: Rod Boev <rod.boev@gmail.com>