Fixes two SKIP_ONBOARDING bugs and eliminates 10 permanently-skipped integration tests.
- SKIP_ONBOARDING=1 now honoured unconditionally (no longer gated on chat_ready)
- apply_onboarding_setup refuses to write config/env files when SKIP_ONBOARDING is set
- TestMediaEndpointIntegration (6) and TestOnboardingGateIntegration (4): collection-time
skip guards removed; server reachability checked at runtime with fail() not skip()
Tests: 1327 passed, 0 skipped.
Admin merge — self-built PR, Nathan authorized full merge process in session.
Admin merge — docs-only follow-up: CHANGELOG entry, version badge v0.50.64, one new test. No code logic. Nathan authorized end-to-end merge in session.
Squash-merges PR #584 by @aronprins.
Drops the meta row (message count, model slug, source-tag badge) from every sidebar session item. Each session now renders as a single title line — visible session count roughly doubles at typical viewport height.
Changes merged verbatim from contributor branch, plus maintainer additions:
- CHANGELOG entry for v0.50.64
- Version badge bump to v0.50.64
- New test: test_relative_time_today_bucket (closes minor coverage gap from code review)
Co-authored-by: aronprins <aronprins@users.noreply.github.com>
Squash-merges PR #578 (rebased from #574 by @renheqiang + #575 by @nesquena-hermes). MCP server toolsets now included in WebUI sessions; onboarding wizard no longer fires for non-standard providers. 1331 tests pass. Nathan override applied for self-built #575.
When a custom_providers entry in config.yaml has a 'name' field (e.g. 'Agent37'),
the web UI model picker now uses that name as the group header instead of the
generic 'Custom' label.
Previously all custom_providers entries were bucketed under 'custom' which
rendered as 'Custom' in the dropdown optgroup — losing the named identity the
user set up during onboarding.
Changes:
- Track named custom providers as 'custom:<slug>' keys internally so multiple
named providers can coexist as separate groups
- When building model groups, emit each named provider under its own display
name (e.g. 'Agent37') rather than falling through to the generic label
- Unnamed entries (no 'name' field) still fall back to the 'Custom' group
- When all entries are named, the bare 'Custom' bucket is suppressed
Adds 7 tests covering single named provider, multiple named providers,
multiple models in same named provider, unnamed fallback, and mixed cases.
Fixes#557
BUG-1 (CRITICAL): messages.js line 522 — mismatched quote in
setComposerStatus('Reconnecting…') caused JS syntax error on the
reconnect path.
BUG-2 (HIGH): messages.js line 491 — broken template literal
'\\n\\n*{d.hint}*' restored to '\n\n*${d.hint}*'. Error hint
text was non-functional (missing $ prefix and escaped newlines).
BUG-3 (HIGH): messages.js — showApprovalCard(pending, pendingCount),
_approvalCurrentId, and approval_id in respondApproval() were removed,
regressing the simultaneous approval queue fix from PR #546. Restored
all three, including the '1 of N pending' counter and poll passthrough.
BUG-4 (LOW): api/streaming.py — MiniMax thinking delimiter regex
missing closing pipe: <|channel> -> <|channel|> in both
_strip_thinking_markup() and _looks_invalid_generated_title().
ALSO: test_issue487b.py docstring changed to raw string to fix
DeprecationWarning for invalid escape sequence '\s'.
Changes _pending from a single overwriting dict value to a list,
so parallel tool calls each get their own approval slot.
api/routes.py:
- Wraps submit_pending() to append to a list and assign a stable
approval_id (uuid4) to each entry.
- _handle_approval_pending() returns the first queued entry plus
pending_count so the UI can show '1 of N'.
- _handle_approval_respond() pops by approval_id (falls back to
oldest entry for backward-compat with old clients).
- Backward-compat: legacy single-dict values in _pending are
handled without crashing.
static/messages.js:
- respondApproval() sends approval_id in the POST body.
- showApprovalCard() accepts pendingCount, shows '1 of N pending'
counter when multiple approvals are queued.
- _approvalCurrentId tracks the approval_id of the displayed card.
- Poll loop passes pending_count to showApprovalCard.
static/index.html:
- Adds approvalCounter element for the '1 of N' display.
tests/test_approval_queue.py:
- 14 tests: static-analysis checks (Python + JS + HTML),
functional tests that inject two simultaneous approvals and
verify both are surfaced and independently resolvable.
Extends _sanitize_messages_for_api() with a two-pass approach:
1. Collect all tool_call_ids declared in assistant messages (handles
both OpenAI 'id' and Anthropic 'call_id' field names).
2. Drop any tool-role messages whose tool_call_id was not declared
by a preceding assistant message.
Strictly-conformant providers (Mercury-2/Inception, newer OpenAI
models) reject histories with orphaned tool results with a 400 error:
'Message has tool role, but there was no previous assistant message
with a tool call.' This can happen when histories are edited, when
switching between providers, or when partial messages are stored.
Adds 13 regression tests covering: valid roundtrip preservation,
multiple tool calls, partial orphan filtering, Anthropic call_id,
edge cases (None tool_calls, missing tool_call_id, non-dict entries).
When a user switches the model via the model picker while a session has
existing messages, a toast now informs them: 'Model change takes effect
in your next conversation'. This prevents confusion when the model
dropdown updates visually but the running conversation continues with
the original model.
Implementation: 4-line addition in modelSelect.onchange in boot.js,
after the existing provider-mismatch warning. Checks S.messages.length
(the reliable in-memory array) and guards showToast with typeof.
Synthesized from PRs #516 (armorbreak001), #517 and #518 (cloudyun888).
Placement follows #518's correct boot.js approach. Reference corrected
from S.session.messages to S.messages (always initialized by loadSession).
4 new tests in test_provider_mismatch.py::TestModelSwitchToast.
Co-authored-by: armorbreak001 <armorbreak001@users.noreply.github.com>
Co-authored-by: cloudyun888 <cloudyun888@users.noreply.github.com>
- sessions.js _formatSourceTag(): return null for unrecognised tags
instead of raw string — prevents legacy 'N/A' values from surfacing
- sessions.js metaBits push: guarded with _stLabel null check so only
known platform labels appear in the session metadata line
- sessions.js [SYSTEM:] title fallback: drop raw s.source_tag middle
term, fall back directly to 'Gateway' for unknown sources
7 new tests in test_issue429.py.
1 updated test in test_sprint40_ui_polish.py (new guarded push pattern).
Closes#429
Bug: the autolink pass stashed <a> tags (via _al_stash) before running,
but did not stash <img> tags. When  was converted to an <img>
tag by the image pass, the subsequent autolink regex matched the URL
inside src="..." and wrapped it in <a href="...">url</a>, producing
src="<a href="...">url</a>" — a completely broken image source.
Fix: extend the _al_stash regex from:
(<a\b[^>]*>[\s\S]*?<\/a>)
to:
(<a\b[^>]*>[\s\S]*?<\/a>|<img\b[^>]*>)
This stashes both <a> and self-closing <img> tags before autolink runs,
then restores them after, so the URL inside src= is never touched.
Adds 7 regression tests in tests/test_issue487b.py.
- test_sprint45.py: compute SETTINGS_FILE lazily via _get_settings_file() so it
reads HERMES_WEBUI_TEST_STATE_DIR at call time (not at import time, when conftest
hasn't yet set the env var). Fixes test isolation across all 1078 tests.
- test_sprint45.py: use auth cookie in teardown when clearing password post-test.
- test_sprint45.py: remove test_synced_version_strings (checks local-patch version).
- static/i18n.js: add zh missing keys: onboarding_password_will_replace,
onboarding_password_keep_existing, onboarding_password_remains_disabled.
- server.py: revert server_version to HermesWebUI/0.50.38 (matches master).
* fix: relax workspace trust boundary to user home directory
The previous restriction required workspaces to be under DEFAULT_WORKSPACE
(/home/hermes/workspace), which blocked all profile-specific workspaces
(~/CodePath, ~/General, ~/WebUI, ~/Camanji, etc.) since each profile uses
a different directory under home.
New boundary: any directory under Path.home() is trusted.
This still blocks /etc, /tmp, /var, /root, /usr and all paths outside the
user's home, while allowing any legitimate workspace under ~/
Also updates test assertions from 'trusted workspace root' to 'outside'
since the new error message says 'outside the user home directory'.
* fix: workspace trust uses home-dir + saved-list, not single ancestor
Three-layer trust model that works cross-platform and multi-workspace:
1. BLOCKLIST: /etc, /usr, /var, /bin, /sbin, /boot, /proc, /sys, /dev, /root,
/lib, /lib64, /opt/homebrew — always rejected, even if somehow saved
2. HOME CHECK: any path under Path.home() is trusted — covers ~/CodePath,
~/hermes-webui-public, ~/WebUI, ~/General, ~/Camanji simultaneously;
Path.home() is cross-platform (Linux ~/..., macOS ~/..., Windows C:\Users\...\...)
3. SAVED LIST ESCAPE HATCH: if a path is already in the saved workspace list,
it's trusted regardless of location — covers self-hosted deployments where
workspaces live outside home (/data/projects, /opt/workspace, etc.)
None/empty → DEFAULT_WORKSPACE (always trusted, validated at startup)
* docs: v0.50.35 release — version badge and CHANGELOG
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix(workspace): restrict session workspaces to trusted roots
* fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root
_profile_default_workspace() reads the agent's terminal.cwd which may differ
from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which
respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent
with how new_session() seeds the initial workspace.
* docs: v0.50.34 release — version badge and CHANGELOG
---------
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: workspace panel close button — no duplicate X on desktop, mobile X respects file preview
Two bugs fixed in the workspace right panel:
1. Duplicate X on desktop (bug): #btnClearPreview (the X icon) was always
visible alongside #btnCollapseWorkspacePanel (the chevron), producing two
close controls at once. Fixed in syncWorkspacePanelUI() — on desktop, the X
is now hidden when no file preview is open (display:none), and only shown
when the user is viewing a file. The chevron remains as the sole close
control in browse mode.
2. Mobile X collapses panel instead of dismissing file (bug): .mobile-close-btn
was calling closeWorkspacePanel() directly, which collapsed the whole panel
even when a file was open. Changed to handleWorkspaceClose(), which already
has the correct two-step logic: clear preview first, close panel only if
no preview is visible.
Files changed:
- static/boot.js: syncWorkspacePanelUI() hides btnClearPreview on desktop
when hasPreview is false, guarded by !isCompact so mobile is unaffected
- static/index.html: mobile-close-btn onclick changed from
closeWorkspacePanel() to handleWorkspaceClose()
- tests/test_sprint44.py: 10 new regression tests
- tests/test_mobile_layout.py: updated test_workspace_close_button_present()
to accept handleWorkspaceClose() as the valid onclick target
* fix: widen test_server_delete_invalidates_index window to 1200 chars
The test extracted a 600-char window starting from the session/delete
handler to check for SESSION_INDEX_FILE. Commit 3cc5839 added session_id
character validation and path traversal guards before the unlink call,
pushing SESSION_INDEX_FILE to ~764 chars from the match — beyond the
600-char limit, causing the test to fail on CI.
Widened the window to 1200 chars, which accommodates any reasonable
amount of guard code before the SESSION_INDEX_FILE.unlink() call.
* docs: v0.50.33 release — version badge and CHANGELOG
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: delegate all live model fetching to agent's provider_model_ids()
Previously _handle_live_models() maintained its own per-provider logic:
- anthropic, google, gemini returned 'not_supported' (hardcoded exclusions)
- openai-codex had a custom branch (added in v0.50.30)
- openai/copilot had hardcoded base URLs
- other providers fell through to a generic /v1/models fetch
Now the handler delegates entirely to hermes_cli.models.provider_model_ids(),
which is the agent's authoritative resolver:
- anthropic: live fetch via /v1/models with correct API-key or OAuth headers
- copilot: live fetch from api.githubcopilot.com/models with Copilot headers
- openai-codex: Codex OAuth endpoint + ~/.codex/ cache fallback
- nous: live fetch from Nous inference portal
- deepseek, kimi-coding: generic OpenAI-compat /v1/models
- opencode-zen/go: OpenCode live catalog
- openrouter: curated static list (live returns 300+ which is overwhelming)
- google/gemini, zai, minimax: static list (non-standard or Anthropic-compat endpoints)
- any others: graceful static fallback
Also removed the client-side skip guard in _fetchLiveModels() (ui.js) that
blocked live fetching for anthropic, google, and gemini.
The hardcoded model lists in _PROVIDER_MODELS remain as the fallback when
credentials are missing or network is unavailable — they are never shown
when live data is available.
* docs: v0.50.31 release — version badge and CHANGELOG
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: route openai-codex live model fetch through agent's get_codex_model_ids()
Previously _handle_live_models() grouped openai-codex with openai and sent a
request to https://api.openai.com/v1/models, which returns 403 because Codex
auth is OAuth-based via chatgpt.com, not a standard API key. The live fetch
silently failed and the UI showed only the hardcoded static list.
Now: openai-codex has a dedicated early-exit branch that calls
hermes_cli.codex_models.get_codex_model_ids() — the same path the agent CLI
uses. It resolves models in order: live Codex API (if OAuth token available) >
~/.codex/ local cache > DEFAULT_CODEX_MODELS. This means:
- If the user has a valid Codex OAuth session, the UI gets the exact model list
their subscription provides (e.g. gpt-5.2, gpt-5.3-codex-spark that aren't
in the hardcoded list)
- If the OAuth session is expired, falls back to local ~/.codex/ cache
- Always has DEFAULT_CODEX_MODELS as final fallback
Also: improved label generation for Codex model IDs (GPT-5.4 Mini vs GPT 5 4 Mini).
Added 1 structural regression test.
* docs: v0.50.30 release — version badge and CHANGELOG
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: correct tool call card rendering on session load
Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):
1. loadSession() sanitized messages (B9 filter) but did NOT update
the session-level tool_calls array's assistant_msg_idx references.
Since compact() returns only sanitized messages and recomputes
tool_calls with indices into the compacted array, the original
assistant_msg_idx values became stale/misaligned.
2. loadSession() then assigned the broken session-level tool_calls
directly to S.toolCalls. This prevented renderMessages()'s fallback
path (which derives tool_calls from per-message tool_calls using
correct sanitized-array indices) from ever running.
Fix:
- Keep full sanitization loop with index remapping for session-level
tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
set S.toolCalls=[] so renderMessages() uses the fallback derivation
from per-message tool_calls, which already have correct indices.
* test: add 8 regression tests for issue #401 tool call index remapping
* docs: v0.50.29 release — version badge and CHANGELOG
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: expand openai-codex model catalog to match agent DEFAULT_CODEX_MODELS
The _PROVIDER_MODELS["openai-codex"] catalog only listed codex-mini-latest,
so the model dropdown for profiles using openai-codex provider (e.g. CodePath)
showed only that one entry — even when the profile's saved default_model was
gpt-5.4 or another standard Codex model.
Updated to match DEFAULT_CODEX_MODELS from hermes_cli/codex_models.py:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- gpt-5.2-codex
- gpt-5.1-codex-max
- gpt-5.1-codex-mini
- codex-mini-latest (kept, relabeled as 'Codex Mini (latest)')
Also adds 2 regression tests: catalog includes gpt-5.4, display name correct.
* docs: v0.50.28 release — version badge and CHANGELOG
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>