* fix(models): stale cross-provider model no longer shows as unavailable in picker
Two bugs allowed an openai/gpt-5.4-mini stale session model to appear as
'(unavailable)' under a custom provider group for users who never configured
OpenAI (#829).
Backend (api/routes.py): _resolve_compatible_session_model() had a blanket
early-return for active_provider in {custom, openrouter} that skipped all
normalization regardless of whether any catalog group could route the model's
prefix. A custom_providers-only user with a stale openai/... session model
was never corrected. Fixed: only skip normalization when the model prefix is
actually routable (matches a catalog group provider_id, or an openrouter
group is present that can route any provider/model).
Frontend (static/ui.js): renderSession() injected a bare <option> (not in
any <optgroup>) for models not found in the dropdown. renderModelDropdown()
rendered bare options without emitting a group heading, so they visually
inherited the last rendered provider heading — making the stale model appear
to belong to the custom provider group. Fixed: silently reset to the first
available model and fire a PATCH to persist the correction instead of
injecting a misleading (unavailable) option.
5 new tests in test_provider_mismatch.py cover:
- stale openai model cleared when custom_providers-only + no default_model
- stale openai model cleared when custom_providers-only + default_model set
- openrouter model preserved when openrouter group present
- custom/ namespace always preserved
- ui.js no longer injects model_unavailable option
* fix(ui): declare modelSel locally in syncTopbar reset path; fix test assertion
- Use const modelSel=$('modelSelect') instead of undeclared sel in the
stale-model reset branch of syncTopbar() (caught in Opus review)
- Fix test assertion: or → and for model_unavailable key absence check
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(ui): clear session search on boot + autocomplete=off — prevents bfcache from restoring stale filter (closes#822)
* fix(ui): add pageshow handler for true bfcache restore case (#822 completion)
The original PR's two fixes cover fresh page loads and hard reloads —
but the bug the issue describes happens on *bfcache restore* (Chrome's
back-forward cache). The async boot IIFE does NOT re-run when the
browser restores a page from bfcache; the DOM is restored in place,
including any stale #sessionSearch value. The boot-time clear has no
effect there.
`autocomplete="off"` is a hint that Chrome and others sometimes honour
for bfcache but is not reliable for user-typed values (as opposed to
autofill candidates).
Add a pageshow event listener that checks event.persisted === true and,
on that path only, clears #sessionSearch and re-renders from cache.
Fresh loads skip the listener (persisted=false) and continue to be
handled by the boot IIFE.
Also added tests/test_session_search_bfcache_822.py with 7 tests:
- autocomplete="off" present on the input
- boot-time clear runs before the first renderSessionList
- pageshow listener registered
- handler guards on event.persisted
- handler clears the search field and triggers a re-render
Full suite: 1745 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): surface gateway SSE failures and add polling fallback
- add a JSON probe mode for the gateway SSE endpoint
- detect watcher-unavailable 503s from the browser
- fall back to periodic session refresh with a toast
- add probe payload tests and endpoint coverage
Fixes#635
* fix(sessions): surface gateway SSE failures and add polling fallback (#826)
Absorbed from PR #826 by @cloudyun888 (fixes#635).
When the gateway watcher thread is not running, the browser now shows a
toast notification and falls back to 30-second periodic polling for session
sync. Previously the SSE failure was completely silent with no user feedback.
Changes from original PR:
- Deleted misplaced test_gateway_sse_probe_unit.py (was at repo root, not
discovered by `pytest tests/`); unit tests moved into tests/test_gateway_sync.py
- _gateway_sse_probe_payload now checks watcher._thread.is_alive() rather
than just watcher is not None — a watcher instance with a dead poll thread
now correctly reports unavailable and activates the polling fallback
- probeGatewaySSEStatus catch(e) now starts the polling fallback on network
error rather than silently swallowing the failure
- Added 5 unit tests covering all watcher-alive/dead/missing/disabled branches
Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com>
* cleanup(gateway): public is_alive() + dedup probe/live watcher-alive check + changelog
Three small cleanups on top of @cloudyun888's PR #826 absorption:
1. Add GatewayWatcher.is_alive() public accessor so routes.py doesn't
reach into the private _thread attribute. The existing private-
attribute check stays as a defensive fallback for any older in-
memory instance or test double that doesn't implement the full API.
2. Dedupe the watcher_alive computation in _handle_gateway_sse_stream:
the live-SSE path now calls _gateway_sse_probe_payload(...) and reads
its watcher_running field instead of re-deriving the same logic
inline. Keeps probe and SSE in sync automatically.
3. CHANGELOG trailer was (#826, fixes#635, @cloudyun888) — this PR is
#828, so updated to (#828, absorbs PR #826 by @cloudyun888, fixes
#635) matching the repo convention for absorbed PRs (see #805).
Added two regression tests:
- test_gateway_watcher_is_alive_public_method — covers the three
lifecycle states (before start, while running, after stop).
- test_probe_payload_prefers_public_is_alive — asserts the probe
uses watcher.is_alive() rather than poking _thread when the
public method exists.
Full suite: 1735 passed, 0 new failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: cloudyun888 <269269188+86cloudyun-afk@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#631. Closes#804.
Bug A (thinking card below answer / double render / stuck cursor): trailing rAF after 'done'
inserted a duplicate live-turn wrapper into already-settled DOM. Fixed via _streamFinalized flag
+ cancelAnimationFrame in all terminal handlers (done/apperror/cancel/_handleStreamError) +
_scheduleRender guard. All three reported symptoms were the same root cause.
Bug B (accumulator reset): original fix reset assistantText/reasoningText inside _wireSSE on reconnect.
Reverted — server uses one-shot queue.Queue(), no replay on reconnect, reset would wipe valid
pre-drop content causing data loss. Bug A fix alone resolves all symptoms.
#804 (blank page workspace): syncWorkspaceDisplays uses S._profileDefaultWorkspace as fallback;
workspace chip enabled when hasWorkspace (not hasSession); promptNewFile/promptNewFolder/
switchToWorkspace/promptWorkspacePath auto-create session on blank page; boot.js hydrates
_profileDefaultWorkspace from /api/settings before any session exists.
Opus max-effort review + Nathan independent review + full browser QA. 1765/1765 tests.
Closes#815.
Three root causes fixed:
1. Provider aliases (z.ai/x.ai/google/grok/claude/aws-bedrock/dashscope/~25 more) not
normalized before _PROVIDER_MODELS lookup — provider fell to empty else-branch while
TUI worked (it normalizes at startup). Fixed via _resolve_provider_alias() + inlined
_PROVIDER_ALIASES table in api/config.py.
2. Silent ImportError in original normalization: 'from hermes_cli.models import
_PROVIDER_ALIASES' inside try/except silently failed without hermes-agent on sys.path
(CI, minimal installs). The inlined table fixes this — normalization now works
regardless of whether hermes-agent is installed.
3. /api/models/live?provider=custom now falls back to custom_providers entries from
config.yaml when provider_model_ids() returns empty.
Also: provider_id on every group in /api/models response for deterministic JS optgroup
matching (no substring false positives). 17 targeted tests, 1725/1725 full suite.
* fix: update banner conflict recovery + server self-restart after update (#813#814)
* fix(update): restart must wait for in-flight update + reset force button on retry
Two defects in the update banner flow found during review of PR #816:
1. Two-target race (webui + agent sequential)
The client posts targets sequentially: webui succeeds and schedules
a restart timer (2 s delay); client then posts agent; server begins
agent fetch+pull; at T=2 s the restart timer fires os.execv mid-pull,
killing the agent update and closing the client connection. User
sees "Update failed (agent): Failed to fetch" even though webui did
update, and the agent repo is in an unknown partial state.
Fix: _schedule_restart() now blocks on _apply_lock before calling
os.execv. If a second update is in flight when the timer fires, the
restart thread waits until it completes. If nothing is in flight the
lock acquire is instant, so no-op updates still restart immediately.
2. Stale force-update button across retries
_showUpdateError sets btnForceUpdate to display:inline-block when
res.conflict / res.diverged. Nothing resets it on the next retry,
so a subsequent non-conflict error (e.g. network) leaves the stale
force button visible pointing at the previous target.
Fix: applyUpdates() now hides the force button and clears its
data-target at the start of each attempt.
Tests:
- test_schedule_restart_waits_for_apply_lock: holds _apply_lock from a
helper thread, verifies execv is delayed until the lock is released.
- test_schedule_restart_still_fires_when_no_update_in_flight: sanity
check that the common path still works with no contention.
- test_apply_updates_resets_force_button_at_start: regression guard
that the reset appears before the update loop begins.
Full suite: 1683 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(update): hold _apply_lock through execv + fix banner error layout
Two fixes from Opus review:
1. TOCTOU gap in _schedule_restart (api/updates.py): the original pattern
acquired _apply_lock, released it, then called os.execv — leaving a brief
window where a new update could start between release and execv. Fixed by
moving os.execv inside the 'with _apply_lock:' block so the process is
replaced while still holding the lock; no new update can acquire it.
2. Banner CSS layout (static/index.html): #updateError was a direct flex child
of .update-banner (display:flex row), so long error messages sat inline
between #updateMsg and the buttons instead of below the message.
Wrapped #updateMsg + #updateError in a flex-column container so errors
stack vertically under the status line.
* docs: add v0.50.134 CHANGELOG entry
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#461
Adds full /reasoning CLI parity to the WebUI slash command system:
- /reasoning show|on → window._showThinking = true; writes display.show_reasoning to config.yaml (same key as CLI); mirrors to settings.json for boot.js
- /reasoning hide|off → same in reverse; re-renders immediately
- /reasoning none|minimal|low|medium|high|xhigh → POST /api/reasoning → writes agent.reasoning_effort to config.yaml; takes effect next turn (matching CLI semantics)
- /reasoning (no args) → GET /api/reasoning → live status toast from config.yaml
- Autocomplete shows all 8 options: show|hide|none|minimal|low|medium|high|xhigh
- Profile-isolated: _get_config_path() is thread-local so per-profile settings never bleed across
- Boot hydration: window._showThinking initialised from settings.json show_thinking on page load
- Inspect.signature guard in streaming.py so older hermes-agent builds don't TypeError
28 new tests, 1708/1708 total passing. Full browser QA on port 8789 with isolated state. CLI/config.yaml sync verified with hermes_constants.parse_reasoning_effort().
Fixes the multi-client profile isolation bug (#798).
- get_hermes_home_for_profile(): pure path resolver, validates name against
_PROFILE_ID_RE (rejects path traversal), never mutates os.environ or globals
- new_session() accepts explicit profile= param from POST body (S.activeProfile),
short-circuits the process-level _active_profile global
- streaming handler resolves HERMES_HOME from s.profile instead of the global
- sessions.js sends profile: S.activeProfile in every new-session POST
10 tests in tests/test_issue798.py including concurrency and traversal coverage.
Co-authored-by: nesquena <nesquena@users.noreply.github.com>
The v0.50.124 Docker build failed with:
cannot create /apptoo/api/_version.py: Permission denied
Root cause: 'USER hermeswebuitoo' is set before 'COPY . /apptoo', but
COPY without --chown creates files owned by root. The subsequent RUN
step (which writes api/_version.py) runs as hermeswebuitoo and has no
write permission to the root-owned api/ directory.
Fix: COPY --chown=hermeswebuitoo:hermeswebuitoo so the unprivileged user
owns the app files and can write _version.py at build time.
Regression from #790.
Co-authored-by: nesquena-hermes <hermes@nesquena.com>
* fix: bootstrap.py loads REPO_ROOT/.env so direct invocation matches start.sh
When users run 'python3 bootstrap.py' directly (the primary documented
entry point in README), HERMES_WEBUI_HOST, HERMES_WEBUI_PORT and other
.env settings were silently ignored because the shell-level 'source .env'
in start.sh was never executed.
Add _load_repo_dotenv() in bootstrap.py that reads REPO_ROOT/.env into
os.environ before DEFAULT_HOST / DEFAULT_PORT are evaluated at module
level. Uses unconditional assignment matching 'set -a; source .env'
shell semantics. Only loads the repo .env (bootstrap config) — not
~/.hermes/.env, which the server still loads independently at startup
for provider credentials.
Reported in #730 by @leap233 who had HERMES_WEBUI_HOST=0.0.0.0 and
HERMES_WEBUI_PORT=18787 in the webui .env; running bootstrap.py directly
caused the server to ignore both settings.
Tests: 15 new tests in tests/test_bootstrap_dotenv.py covering the
full loader (key=value, comments, blank lines, quoted values, no-file,
unreadable-file, overwrite semantics, values with = signs) and structural
assertions that _load_repo_dotenv() is called before DEFAULT_HOST/PORT.
1613 tests total.
* fix: address review feedback on PR #791
- bootstrap.py: document overwrite semantics and 'export' note in docstring
- bootstrap.py: handle 'export FOO=bar' prefix (strip before splitting on =)
- bootstrap.py: print warning to stderr on .env parse failure (not silent swallow)
- bootstrap.py: add side-effect comment at _load_repo_dotenv() call site
- CHANGELOG.md: restore v0.50.124 and v0.50.123 headers (were merged into
v0.50.125 section, making three consecutive ### Fixed blocks with no ## header
between them)
- tests: fix test_noop_when_dotenv_unreadable to assert warning is emitted
- tests: tighten test_does_not_set_empty_values with concrete assertion
- tests: add test_export_prefix_stripped
- tests: remove dead _import_bootstrap_with_env() helper (never called)
1614 tests total
---------
Co-authored-by: nesquena-hermes <hermes@nesquena.com>
* fix: dynamic version badge — read from git tag, never hardcoded
The settings panel showed v0.50.87 and the HTTP Server: header said
HermesWebUI/0.50.38 — both hardcoded strings that drift further behind
with every release because there was no mechanism to keep them in sync.
Changes:
- api/updates.py: add _run_git() (moved before _detect_webui_version),
_detect_webui_version(), and WEBUI_VERSION module constant resolved
once at import time via 'git describe --tags --always --dirty'.
Fallback chain: git → api/_version.py → 'unknown'.
- api/routes.py: inject webui_version into GET /api/settings response
so the frontend can read it without a separate API call.
- static/panels.js: loadSettingsPanel() populates .settings-version-badge
from settings.webui_version — one line after the existing api() call.
- static/index.html: replace stale hardcoded 'v0.50.87' with '—'
placeholder; JS overwrites it as soon as the settings panel opens.
- server.py: replace hardcoded 'HermesWebUI/0.50.38' server_version with
'HermesWebUI/' + WEBUI_VERSION.lstrip('v') — stays in sync automatically.
- Dockerfile: add ARG HERMES_VERSION=unknown and write api/_version.py
so Docker images (where .git is excluded) still show the correct tag.
- .github/workflows/release.yml: pass build-args: HERMES_VERSION=${{ github.ref_name }}
to the Docker build step on tag pushes.
- .gitignore: exclude api/_version.py (generated by Docker/CI, never committed).
No manual 'update the version badge' step is required going forward.
Tagging is sufficient — the badge and HTTP header update automatically.
Tests: 18 new tests in tests/test_version_badge.py covering the full
resolution chain, /api/settings injection, HTML placeholder, JS wiring,
and server.py import. 1596 tests pass total.
* fix: address review feedback on PR #790
- api/updates.py: replace exec() with regex parse for api/_version.py
(no supply-chain risk from build artifact; exec unnecessary for one assignment)
- api/updates.py: cap git describe timeout at 3s (was 10s — import-time
stall on NFS/.git would block server startup unnecessarily)
- server.py: lstrip('v') → removeprefix('v') (lstrip strips chars not prefix)
- server.py: emit bare 'HermesWebUI' when version is 'unknown' rather than
'HermesWebUI/unknown' (log aggregators expect semver-ish suffix or none)
- CHANGELOG.md: add v0.50.124 entry for this user-visible change
- tests: rename exec-error test to reflect regex behaviour; add tests for
removeprefix usage and unknown-version header guard (1598 tests total)
---------
Co-authored-by: nesquena-hermes <hermes@nesquena.com>
set_hermes_default_model() calls reload_config() which resyncs _cfg_mtime,
so the mtime check inside get_available_models() never fires and the POST
response returns the stale cached default. Explicitly drop the TTL cache
after reload so the next read recomputes. Fixes the CI failure in
test_default_model_updates_hermes_config which the prior teardown-only
fix in this PR did not actually address.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At the @media(max-width:900px) breakpoint both .close-preview and .mobile-close-btn were visible simultaneously. Since boot.js wires both to handleWorkspaceClose(), only the mobile-close-btn needs to show at that width. Adds .close-preview{display:none} to the 900px media block.
Fixes#781
cancel_stream() now pops STREAMS/CANCEL_FLAGS/AGENT_INSTANCES and clears session.active_stream_id immediately after signalling cancel. Fixes sessions permanently stuck at 409 when the agent thread is blocked in a bad tool call. Session cleanup runs outside STREAMS_LOCK to preserve lock ordering.
Fixes#653
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Queued follow-up messages now survive page refresh. Persisted atomically in queueSessionMessage/shiftQueuedSessionMessage. On reload: if agent still active, queue is silently hydrated (done handler drains it); if idle, first entry is restored as a composer draft with a toast. Stale entries discarded.
Fixes#660
- quota_exhausted error type: distinguishes credit exhaustion from rate limits
- Streaming errors persisted to session file so they survive page reload
- _error flag excludes persisted errors from subsequent LLM API calls
- stream_end and title SSE events use original session_id (not s.session_id which rotates during context compaction)
Fixes#739, #652, #653
Removes the bubble_layout toggle from Settings, all persistence, CSS, i18n strings, and the UI docs demo. The CSS was already effectively dead. Users with a saved bubble_layout value in settings.json get a clean migration via _SETTINGS_LEGACY_DROP_KEYS.
Credit: @aronprins (PR #760 / #777)
Co-authored-by: aronprins <aronprins@users.noreply.github.com>
Removes split-brain where WebUI Settings persisted default_model separately from Hermes runtime config.yaml. New POST /api/default-model endpoint writes to config.yaml. Existing saved values migrated on first load.
Fixes#761
Co-authored-by: aronprins <aronprins@users.noreply.github.com>
Adds sub-argument suggestions for /model, /personality, /reasoning slash commands. /reasoning is now discoverable from the first slash. Keyboard navigation pre-selects the first item. Fixes bug where no-arg commands (/clear, /new, /stop, etc.) would loop the dropdown on selection.
Fixes#632
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Adds compact/detailed toggle for the session list sidebar. Compact is the default (no behavior change for existing users). Detailed mode shows message count and model; profile names only appear when mixing sessions across profiles.
Fixes#673
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
## Summary
Rebased on behalf of @aronprins from fork branch `codex/dark-user-bubbles`. Two asset-only commits (PR screenshot add/remove) were dropped; the two code commits are applied cleanly on top of current master (v0.50.110).
### What changed
**Dark-mode user bubbles** (`static/style.css`):
- `:root.dark` now overrides `--user-bubble-bg`/`--user-bubble-border` to `var(--accent-bg-strong)` (a 15% opacity tint) — keeps the bubble visually subdued in dark skins instead of a glaring bright accent fill
- Removes 6 per-skin `--user-bubble-text` hacks (ares, mono, slate, poseidon, sisyphus, charizard); text falls back to `var(--text)` which is already correct in dark mode
- Adds `--user-bubble-placeholder` token; edit-area box-shadow now uses `--focus-ring` instead of hardcoded `rgba(255,255,255,.15)`
**Thinking card collapsibility** (`static/ui.js` + `static/style.css`):
- `_thinkingMarkup()` now includes `onclick` toggle and chevron affordance, matching the compression reference card pattern
- `.thinking-card-header` gets `display:flex; gap:8px` for proper icon/label/chevron alignment
**Tests**: 2 new in `test_bugbatch_apr2026.py` (dark bubble token contract + no-per-skin-hack assertion), 2 updated in `test_ui_card_animation.py` (flex header layout + onclick pattern).
1520 passed. QA 20/20. Browser verified: dark mode bubble uses subtle tint, thinking card toggles correctly.
(credit: @aronprins)
Squash merge of PR #717 — rebased on behalf of @franksong2702.
## What it does
Fixes#680. Footer chrome (timestamps, copy, edit, regenerate) is now hover-only for both user and assistant message rows, consistent throughout the conversation. The last assistant turn keeps cumulative usage visible at rest; timestamp and actions are revealed inline on hover in the same row.
Key changes:
- `static/ui.js`: new `_formatMessageFooterTimestamp()` (local timezone, cross-day fuller format); `timeHtml` no longer gated to user-only; last assistant usage moved from separate `.msg-usage` div to inline `.msg-usage-inline` span in the footer
- `static/style.css`: `.msg-foot-with-usage` class + rules; assistant footer opacity changed from 0.45 to 0 (hover-only); `:focus-within` alongside `:hover` for keyboard users
- `api/streaming.py`: `_restore_reasoning_metadata()` now preserves `_ts`/`timestamp` for unchanged historical messages
- `tests/test_sprint49.py`: 8 new tests covering rendering contract, hover CSS, timestamp preservation
Tests: 1518 passed. QA: 20/20. Browser verified. Reviewed and approved by @nesquena and @aronprins.
## Summary
Fixes the ordering-dependent test failure in `test_custom_provider_display_name.py` (issue #754).
**Root cause:** `_models_with_cfg()` patches `config.cfg` then calls `get_available_models()`. That function checks `config.yaml`'s mtime on every call — if it has changed since the last `reload_config()`, it calls `reload_config()` again, which reads from disk and silently overwrites the patch. Any test that writes `config.yaml` (e.g. via `save_settings()`) before this test runs changes the mtime and triggers the reload.
**Fix:** Pin `config._cfg_mtime` to the current `config.yaml` mtime before calling `get_available_models()`, then restore it in the `finally` block. This is the same pattern already used in `test_model_resolver.py` (lines 249, 393).
**Also restores `_cfg_mtime`** in the `finally` block so the patch leaves no side effects on subsequent tests.
## Tests
1510 passed — the previously-flaky test now passes regardless of which tests ran before it.
Closes#754
## Summary
Follow-up to #751/#752. Code review identified a case where `_normalize_session_model_in_place` could call `session.save()` (which triggers a full session index rebuild) for sessions with `model: null` or missing model field.
Root cause: `_resolve_compatible_session_model(None)` returns `(default_model, True)` when a default exists — which was interpreted as "changed, needs save." But there's nothing to correct for a session with no model; the default is just a fallback for display purposes, not a cross-provider correction worth persisting.
Fix: capture `original_model` before calling `_resolve_compatible_session_model`. Only call `session.save()` if `original_model` was non-empty and actually changed.
Adds a test asserting `save_calls == []` when `session.model is None`.
No behavior change for sessions with a real model (the primary use case of #751 is unaffected).
## Summary
Regression fix for #751.
Models with custom or unrecognized prefixes (e.g. `custom-provider/my-model`, `test/import-model`) were being incorrectly replaced with the active provider default. Root cause: `_normalize_provider_id("custom-provider")` matched the `"custom"` prefix and returned `"custom"`, which ≠ `active_provider` → normalization fired.
Two-part fix:
1. Add `"custom"` and `"openrouter"` to the `model_provider` exclusion set in `_resolve_compatible_session_model` (parallel to the existing `active_provider` guard)
2. Return `""` for unknown prefixes in `_normalize_provider_id` so the `if model_provider` truthiness check safely short-circuits
Adds a regression test covering `custom-provider/`, `test/`, `my-local-llm/`, and `lmstudio-community/` prefixes.
## Tests
1499 passed, 0 failures (was 2 failures before this fix)
## Summary
Rebased-on-behalf of @likawa3b (originally PR #748 — stale base).
Sessions can outlive provider changes. When an old session still points to a model from a previous provider (e.g. `gemini-3.1-pro-preview` after switching the agent to OpenAI Codex), starting a chat hits the wrong backend and fails silently.
This PR adds a lightweight normalization pass:
- `_normalize_provider_id()` maps common prefixes to canonical provider IDs
- `_resolve_compatible_session_model()` checks the session model's provider against `active_provider` and returns the default model if they differ
- `_normalize_session_model_in_place()` is called at GET `/api/session` — corrects and persists stale models once
- Chat start also normalizes via `_resolve_compatible_session_model()` and returns `effective_model` in the response
- `messages.js` applies `effective_model` back to the UI/localStorage/dropdown if set
Closes#748
## Tests
1498 passed (2 pre-existing ordering failures unrelated to this PR; 5 new tests added in `test_provider_mismatch.py`).
**Original author:** @likawa3b
## Summary
Rebased-on-behalf of @franksong2702 (originally PR #728 — had CHANGELOG conflict after #747 merged).
Moves `#cmdDropdown` from outside `composer-box` to inside it, so the `position:absolute` anchor is scoped to the composer width rather than the full chat panel. CSS updated to use `bottom:calc(100% + 4px)` and `width:auto;max-width:100%` for clean upward positioning.
Closes#633
## Changes
- `static/index.html` — moved `cmd-dropdown` div inside `composer-box`
- `static/style.css` — updated `.cmd-dropdown` positioning (remove `margin-bottom`, use `bottom:calc(100% + 4px)`, add `width:auto;max-width:100%`)
- `tests/test_sprint50.py` — 2 new structural tests verifying DOM position and CSS rules
## Tests
1493 passed, 1 warning (2 new tests added)
**Original author:** @franksong2702
fix(ui): restrict edit to latest user message (#747)
Only the latest user turn shows the pencil/edit affordance. Older user
messages remain read-only (copy + timestamp still work). Avoids the
misleading implication that historical messages can be lightly edited
when the actual action truncates the session and restarts the
conversation from that point.
Closes#744
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Fixes gateway port exposure, workspace path expansion, HERMES_WEBUI_STATE_DIR default, and adds three-container reference config with dashboard. All ports localhost-bound by default.
Fixes and extends PR #683 (MatzAgent). recognition.start() is now a real call. _isRecording race guard added with correct reset in all paths. localStorage persistence of fallback flag. Closes#683.
Co-authored-by: MatzAgent <MatzAgent@users.noreply.github.com>
Fixes the /compress reference card showing only a short 3-line summary immediately after compression. Now prefers the persisted compaction message (full handoff) over the raw API summary, matching what is shown after page reload. Closes#695.
- Live search input in model dropdown (filter by name or ID)
- Provider group headers preserved in filtered view
- Clear button, Escape-to-close, No models found empty state
- i18n EN/ES/zh-CN strings
- CSS uses var(--accent) consistent with current theme system
- zh-CN double-escape fix included
- Provider headers regression fix included
- 1423 tests pass
Co-authored-by: mmartial <mmartial@users.noreply.github.com>