Commit Graph

689 Commits

Author SHA1 Message Date
bsgdigital a2d7f311be fix(streaming): prevent dropped characters in incremental smd path (#960)
Detect prefix desync between current display text and already-streamed text, then rebuild the streaming-markdown parser from full content to avoid character loss during live rendering. Add regression assertions for the new desync guard.

Made-with: Cursor

Co-authored-by: bsgdigital <bsg@bsgdigital.com>
2026-04-24 11:04:32 -07:00
nesquena-hermes c06ec43f17 chore: v0.50.193 CHANGELOG (#958)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.193
2026-04-24 11:04:26 -07:00
bsgdigital e5cf9c5910 fix(streaming): strip malformed DSML function_calls tags (#958)
Handle DeepSeek DSML variants including truncated and spaced tag forms, and sanitize thinking-card text so leaked XML fragments never render. Add regression tests for DSML edge cases and thinking-card sanitization.

Made-with: Cursor

Co-authored-by: bsgdigital <bsg@bsgdigital.com>
2026-04-24 11:04:16 -07:00
nesquena-hermes 70de09290c chore: v0.50.192 CHANGELOG (#951)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.192
2026-04-24 11:04:09 -07:00
ruxme f109592cb0 perf: add defer to all local script tags (#951)
All 10 local <script> tags now use the defer attribute, allowing the
browser to download them in parallel during HTML parsing instead of
blocking the DOM sequentially. Execution order is preserved.

Before: scripts loaded one-at-a-time, each blocking DOM construction
After:  scripts downloaded in parallel, executed in order after DOM ready

Fixes slow sidebar session list rendering on initial page load.

Co-authored-by: 陳俊宇 <chenjunyu@chenjunyudeMacBook-Air-7.local>
2026-04-24 11:03:59 -07:00
nesquena-hermes d339200b5b chore: v0.50.191 CHANGELOG (#948)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.191
2026-04-24 11:03:52 -07:00
starship-s 0a91e3cb02 fix: identify WebUI sessions as webui platform (#948)
* fix: use webui platform for webui sessions

* test: harden WebUI platform hint regression coverage
2026-04-24 11:03:42 -07:00
nesquena-hermes cb41075bd2 chore: v0.50.190 CHANGELOG (.venv #949)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.190
2026-04-24 10:45:33 -07:00
xingyue 91703e3e54 fix(config): add .venv discovery paths in _discover_python (#949) 2026-04-24 10:45:23 -07:00
nesquena-hermes 396537c624 chore: v0.50.189 CHANGELOG (#961 csp)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-24 10:45:09 -07:00
Basit Mustafa b072a6887c fix(csp): add explicit manifest-src 'self' directive (#961)
PR #920 added static/manifest.json and sw.js for PWA support. The CSP
in _security_headers() had no explicit manifest-src directive, so browsers
fell back to default-src 'self' and emitted a console warning on every page
load. The fallback is functionally correct but non-compliant with CSP Level 3
best practice of declaring each directive explicitly.

Adds manifest-src 'self' before base-uri. No origin set is changed.
Regression test added alongside existing CSP coverage in test_pwa_manifest_csp.py.

Co-authored with Claude Sonnet 4.6 / Anthropic.
2026-04-24 10:44:46 -07:00
nesquena-hermes 27e69c404a chore: v0.50.189 CHANGELOG (csp #961)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.189
2026-04-24 10:44:34 -07:00
nesquena-hermes dbc9c910a8 chore: v0.50.188 CHANGELOG (btw fix #950)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.188
2026-04-24 10:44:10 -07:00
bergeouss 23e9070fc5 fix(btw): use correct SSE endpoint /api/chat/stream (#950)
The /btw command was completely non-functional because attachBtwStream()
connected to /api/stream which doesn't exist — the server SSE handler
lives at /api/chat/stream. This caused an immediate 404 on every /btw
request.

Closes #945

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:43:44 -07:00
nesquena-hermes e0257d81d5 chore: v0.50.187 CHANGELOG entry for breakpoint fix (#956)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.187
2026-04-24 09:13:35 -07:00
nesquena-hermes 533edbcae0 fix(ui): close 641-767px rail/hamburger breakpoint gap (#956)
At 641-767px the sidebar was in a no-mans-land: hamburger hidden (<=640 only)
and rail also hidden (>=768 only). Users could still navigate via the sidebar-nav
tabs inside the sidebar, but the rail was absent unnecessarily.

Changing the rail breakpoint from min-width:768px to min-width:641px closes the
gap. The sidebar slide-in behavior (position:fixed, hamburger toggle) stays at
<=640px only, so the mobile UX is unchanged. At 641-767px the rail now appears
alongside the persistent sidebar.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-24 09:13:00 -07:00
nesquena-hermes 885f1fa349 chore: v0.50.186 CHANGELOG entry for three-column layout (#899)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.186
2026-04-24 09:06:50 -07:00
Aron Prins 970bc1d3fd refactor(ui): three-column layout with left rail + main-view migration (#899)
refactor(ui): three-column layout with left rail + main-view migration (#899)

Unifies the shell into a three-column layout (rail + sidebar + main) matching the
hermes-desktop reference, and migrates every per-item detail/edit surface into a
shared main-view canvas with consistent headers, empty states, and action buttons.

Changes:
- New desktop-only left rail (48px) with 8 nav tabs (chat/tasks/skills/memory/workspaces/profiles/todos/settings)
- Persistent app titlebar (replaces per-chat topbar), active conversation title shown
- All panel detail/create/edit views migrated to #mainSkills, #mainTasks, #mainSettings, #mainWorkspaces, #mainProfiles, #mainMemory
- Settings moved out of modal into main-view page; ESC closes it
- YAML frontmatter rendered in collapsible <details> block in skill detail
- Toasts repositioned from bottom-center to top-right with theme-aware success/error/warning/info variants
- Composer workspace chip split into two-button group: files-icon toggles file panel, label opens workspace picker
- .settings-menu → .side-menu / .side-menu-item (generalised, shared by memory and settings panels)
- i18n: ~25 new keys across en/ru/es/de/zh/zh-Hant for all new form labels, placeholders, and empty states
- Mobile: hamburger in titlebar, slide-in sidebar; box-shadow removed from sidebar
- New regression test: tests/test_settings_navigation_and_detail_refresh.py (9 tests)

Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-24 09:05:25 -07:00
nesquena-hermes 061af78cde v0.50.185: /btw stream hardening + .venv bootstrap + /reasoning toast (#935 #939 #941 #942)
* fix(bootstrap): discover .venv layout in agent_dir (closes #938) (#941)

* fix(btw): harden _streamDone flag — defensive ordering + session guard + stream_end coverage (#935)

* fix(btw): align /reasoning toast prefix with BRAIN const (#939)

* docs: v0.50.185 release notes, update test counts to 2107

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.185
2026-04-23 23:25:45 -07:00
nesquena-hermes 87d4136a43 fix(ui): move reasoning chip after model chip in composer footer (#937)
Reasoning is a sub-setting of the model (applies only to models that
support it), so the model should come first. This also keeps the model
chip in a stable position regardless of whether reasoning is active.

Order was: Profile → Workspace → Reasoning → Model
Order now:  Profile → Workspace → Model → Reasoning

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 19:43:41 -07:00
nesquena-hermes ce9aec1640 chore: v0.50.184 release notes (#936)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.184
2026-04-23 19:38:48 -07:00
nesquena-hermes 1a9dba7844 fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934)
* fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933)

* fix(ui): resize handler symmetry + lock regressions for PR #934 fixes

Two small additions on top of the core PR:

1. Resize handler now re-positions the reasoning dropdown when the window
   resizes while it's open, matching the existing model-dropdown branch.
   Without this, resizing while the dropdown is open leaves it aligned to
   the pre-resize chip position — fine in practice (most resizes close the
   dropdown via the global click handler) but inconsistent with the
   model-dropdown sibling.

2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10
   tests locking all four fixes in place so they can't silently regress:

   - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it)
   - Dropdown is grouped with the other composer-level dropdowns
   - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji)
   - _applyReasoningChip() body doesn't include 🧠
   - cmdReasoning calls _applyReasoningChip(eff) directly with the
     server-confirmed effort, not syncReasoningChip() (stale cache)
   - _streamDone flag declared, set in done handler, checked in onerror
   - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive)
   - resize handler re-positions composerReasoningDropdown

Full suite: 2056 passed, 0 failed.

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>
2026-04-23 19:18:51 -07:00
nesquena-hermes 06bedc8e23 Merge pull request #932 from nesquena/pr-929-review
feat(commands): /background, /btw slash commands + undo button + reasoning chip
v0.50.183
2026-04-23 18:33:50 -07:00
Nathan Esquenazi 63b0207604 fix(background): wire completion hook + keep running tasks in tracker
The /background feature was fundamentally non-functional as shipped —
two coupled bugs kept results from ever reaching the user:

1. complete_background() was defined but NEVER called.  The
   _handle_background thread ran _run_agent_streaming and then exited;
   no hook signalled the task tracker that the work was done.  Every
   background task stayed in status="running" forever and
   get_results() (which filters to done-only) always returned [].

2. get_results() called _BACKGROUND_TASKS.pop(parent_sid, []) which
   removed the ENTIRE list — including tasks still in flight.  Even if
   bug #1 were fixed, the first frontend poll during a long-running
   task would drop the task from the tracker, and
   complete_background()'s loop would iterate over an empty list when
   the worker eventually finished — the result would still be lost.

Fix:

- api/background.py::get_results now retains running tasks in the
  dict; only done ones are popped and returned.
- api/routes.py::_handle_background wraps _run_agent_streaming in an
  inline worker (_run_bg_and_notify) that, after streaming completes,
  reloads the hidden bg session, extracts the last non-error assistant
  message, and calls complete_background(parent_sid, task_id, answer).
  Worker also best-effort unlinks the hidden bg session file so
  SESSION_DIR doesn't accumulate debris.
- Exception safety: any failure in _run_agent_streaming or the
  post-processing path still calls complete_background with a fallback
  sentinel so the frontend's polling loop doesn't hang forever.

Added 5 regression tests in tests/test_background_tasks.py:
- running tasks survive get_results polls
- done tasks are returned and removed
- poll → complete → poll round-trip surfaces the answer (this is the
  original bug's reproduction path)
- empty parent is cleaned up
- static check: _handle_background's worker calls complete_background
  and uses Session.load to extract the answer

Full suite: 2023 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:32:47 +00:00
nesquena-hermes 9c69b646ff feat(commands): /background, /btw slash commands + undo button + reasoning chip
Rebased onto master after #931 (aux title routing) to resolve streaming.py conflict.
All changes from both PRs are cleanly integrated.

2088 tests passing (2065 master + 23 from #931).

Co-authored-by: bergeouss <bergeouss@gmail.com>
2026-04-24 01:24:51 +00:00
nesquena-hermes 57222c70e7 Merge pull request #931 from nesquena/pr-925-review
fix(streaming): respect auxiliary.title_generation config for session titles
v0.50.182
2026-04-23 18:22:44 -07:00
nesquena-hermes 14a1924796 fix(streaming): respect auxiliary.title_generation config for session titles
- _aux_title_configured(): returns True when provider/model/base_url is set
- _aux_title_timeout(): reads configured timeout, falls back to 15.0s default
- _generate_llm_session_title_via_aux: use_agent_model kwarg preserves old behavior
- Missing llm_invalid_aux fallback now triggers agent-model retry
- 23 new tests in tests/test_title_aux_routing.py — all pass

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-04-24 01:07:02 +00:00
nesquena-hermes 36da37ff13 Merge pull request #930 from nesquena/feat/vendor-smd-0.2.15
chore: vendor streaming-markdown@0.2.15, remove CDN dependency
v0.50.181
2026-04-23 18:06:26 -07:00
nesquena-hermes b14ea4f9f6 chore: vendor streaming-markdown@0.2.15, remove CDN dependency
Self-hosts smd.min.js (12,586 bytes, sha384 verified against npm tarball).
App works fully offline/air-gapped. Static server correctly serves static/vendor/*.

Co-authored-by: bsgdigital <bsgdigital@users.noreply.github.com>
2026-04-24 01:05:20 +00:00
nesquena-hermes ff970ec844 Merge pull request #923 from nesquena/feat/917-streaming-markdown
Merging feat/917-streaming-markdown. 2065 tests pass. APPROVED by @nesquena. Pre-existing QA harness failure on master confirmed (not a regression).
v0.50.180
2026-04-23 17:43:40 -07:00
Nathan Esquenazi b563484a56 fix(smd): strip javascript:/data:/vbscript: URLs — smd does not sanitize schemes
streaming-markdown@0.2.15 preserves arbitrary URL schemes in href/src.
Verified with a Node + jsdom harness:

  IN : [click](javascript:alert(1))
  OUT: <p><a href="javascript:alert(1">click</a>)</p>        ← XSS vector

Confirmed unsafe for: javascript:, vbscript:, data:text/html, file://.
The library uses only safe DOM primitives (createElement/appendChild/
createTextNode — no innerHTML/eval), so <script> tags are escaped as
text, but URL-scheme filtering is absent. The existing renderMd() path
implicitly filtered to http(s) via its regex, so this is a regression
the moment streaming markdown is enabled.

Attack path: agent echoes prompt-injection content containing a
markdown link with javascript: href → smd renders it live → user clicks
during the streaming window → JS executes in webui origin → session
cookie, API calls, etc.

Fix: walk the live DOM after each parser_write (and again after
parser_end) and remove href/src attributes whose scheme isn't on the
safe allowlist (http, https, mailto, tel, and relative/anchor paths).
Blocked anchors keep their text content but lose href; blocked images
lose src and get data-blocked-scheme="1" for debugging.

Harness confirms all 10 tested cases behave correctly — javascript:,
vbscript:, data:text/html, file:// all stripped; https://, /path,
#anchor, mailto:, tel: all preserved.

Added 5 regression tests in TestSmdUrlSchemeSanitization that lock:
  - the sanitize helper exists
  - the allowlist regex permits https? and forbids javascript/vbscript/data:
  - _smdWrite invokes sanitize after parser_write
  - _smdEndParser invokes sanitize after parser_end
  - the sanitizer covers both <a href> and <img src>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:28:40 -07:00
nesquena-hermes 89b0c8eb41 feat: incremental streaming markdown via streaming-markdown (v0.50.180, #917)
Co-authored-by: bsgdigital
2026-04-23 23:09:08 +00:00
nesquena-hermes a3647570fb fix: persist onboarding_completed for CLI-configured users on first chat_ready (#922)
* fix: persist onboarding_completed for CLI-configured users on first chat_ready (v0.50.179, #921)

Co-authored-by: bsgdigital

* fix(onboarding): don't 500 the status endpoint if save_settings fails

The #921 persist call `save_settings({"onboarding_completed": True})` in
get_onboarding_status() raises if the settings.json write fails
(read-only filesystem, disk full, permission error). That turns every
/api/onboarding/status call into a 500 until the disk is writable,
which is much worse UX than losing the persistence-across-restart guard.

Wrapped in try/except so persistence becomes best-effort. The function
still sets settings["onboarding_completed"] = True in memory on success,
and `completed` reflects `config_auto_completed` on this request either
way, so the user sees the right state even when the write fails — only
the next-restart protection degrades.

Added regression test that patches save_settings to raise OSError and
asserts the endpoint still returns completed=True without raising.

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>
v0.50.179
2026-04-23 15:46:02 -07:00
nesquena-hermes 1011918d50 feat: add PWA support (manifest, service worker, install prompt) (#920)
* feat: add PWA support (manifest, service worker, install prompt) (v0.50.178, #911)

Co-authored-by: bsgdigital
Closes #685

* fix(sw): await caches.match() before `|| fallback` so offline HTML actually shows

The offline-navigation fallback was dead code:

    return caches.match('./') || new Response('<html>...</html>', ...);

`caches.match()` returns a Promise, and Promise objects are always truthy
in a `||` check — so the `new Response(...)` branch was never taken. On
actual offline, `caches.match('./')` resolves to undefined (no cache hit
for the root), the SW returns undefined, and the browser falls back to
its own default offline page. The custom "Hermes requires a server
connection" HTML was unreachable.

Fix by threading the match through `.then()` so the resolved value (not
the Promise object) feeds the `||`:

    return caches.match('./').then((cached) => cached || new Response(...));

Added 13 regression tests in tests/test_pwa_manifest_sw.py covering:
- manifest.json validity + required PWA fields + icon existence
- sw.js cache-version placeholder + API/stream bypass + correct offline
  pattern (explicitly rejects the broken `|| new Response` shape so it
  can't regress)
- /manifest.json + /sw.js routes serve correct Content-Type,
  Cache-Control, Service-Worker-Allowed headers and inject WEBUI_VERSION
- index.html links manifest, registers SW, has iOS PWA meta tags

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>
v0.50.178 v0.50.177
2026-04-23 15:14:21 -07:00
nesquena-hermes 07caaec6ef fix(mobile): adapt settings dialog and message controls for mobile screens (#919)
* fix(mobile): adapt settings dialog and message controls for mobile screens (#915)

Co-authored-by: bsgdigital

* fix(mobile): adapt settings dialog and message controls for mobile screens (v0.50.177, #915)

Co-authored-by: bsgdigital

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 15:12:07 -07:00
nesquena-hermes 1175ee363f fix(models): duplicate dropdown entries, stale default model, lowercase injected label (#907 #908 #909) (#918)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.176 v0.50.175
2026-04-23 14:41:06 -07:00
nesquena-hermes 5b923a9502 fix: harden session persistence and per-session lock handling during streaming (v0.50.175, #910) (#910)
Co-authored-by: starship-s

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-23 14:25:43 -07:00
nesquena-hermes 5082f426f2 fix: correct interleaved streaming order (Text → Thinking → Tool → Text) (#913)
* fix: correct interleaved streaming order (Text → Thinking → Tool → Text)

During live streaming, tool cards were inserted before their associated
thinking cards instead of after them. The root cause was that
appendLiveToolCard's anchor selector didn't include .thinking-card-row,
so finalized thinking cards were skipped when finding the insertion point.

Changes:
- messages.js: Add segment splitting (segmentStart/_freshSegment) so each
  text segment after a tool call renders only its own slice, not the full
  accumulated text. Sync thinking card render in reasoning handler to
  avoid rAF race with tool events. Guard removeThinking() to preserve
  finalized cards when reasoningText is active.
- ui.js: Add .thinking-card-row to appendLiveToolCard anchor selector so
  tool cards land after finalized thinking. Add anchor-based positioning
  to appendThinking for correct interleaved placement. Clean up empty
  spinner-only thinking rows in finalizeThinkingCard. Add 3-dot waiting
  indicator (toolRunningRow) after tool cards for visual feedback.
- style.css: Scope blinking cursor to last live-assistant segment only.
  Add spacing for toolRunningRow.

* chore: CHANGELOG for v0.50.174

---------

Co-authored-by: bsgdigital <bsgdigital@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.174
2026-04-23 13:23:43 -07:00
nesquena-hermes 537c8271db fix(renderer): ordered list items always showed 1. — emit value= on each li (#886) (#904)
* fix(renderer): ordered list items always showed 1. — emit value= on each <li> (#886)

Root cause: when LLMs output numbered lists with blank lines between items,
renderMd()'s paragraph-splitter (split(/\n{2,}/)) breaks the markdown into
one chunk per item. The ordered-list regex then wraps each item in its own
<ol>, and since each <ol> restarts at 1, the rendered output is always 1. 1. 1.

Fix: capture the original number from each list line and emit value="N" on
every <li>. The HTML spec guarantees that value= overrides the <ol> counter,
so even items in separate <ol> containers display their correct ordinal.

6 regression tests in tests/test_886_ordered_list_numbering.py.
1958 tests pass.

* chore: add v0.50.173 CHANGELOG entry for ordered list fix

---------

Co-authored-by: Hermes Bedrock Fix <hermes-fixes@local>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.173
2026-04-23 12:15:56 -07:00
nesquena-hermes 9dd6e3f338 fix(cancel): preserve partial streamed response on Stop Generation (#893) (#902)
* fix(cancel): preserve partial streamed response on Stop Generation (#893)

* docs(cancel): fix misleading comment — partial message is NOT _error=True

The outer comment block claimed `_error=True so _sanitize_messages_for_api()
strips it from future conversation history`, but the actual append call
sets only `_partial=True` (correctly matching the inner comment six lines
below and the PR description). Updated the outer comment to match reality
so a future reader doesn't try to "fix" the code to match the wrong comment.

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>
v0.50.172
2026-04-23 11:16:59 -07:00
nesquena-hermes 4089972b09 fix(models): preserve @nous: prefix in settings + fix cross-namespace 404 for Nous (#895 #894) (#901)
* fix(models): preserve @nous: prefix in settings + fix cross-namespace 404 for Nous (#895 #894)

* fix(review): persist bare form for CLI compatibility + picker smart-match

The PR persisted `@nous:anthropic/claude-opus-4.6` verbatim to config.yaml
to make the Settings picker match its dropdown options (which carry the
`@nous:` prefix after #885). That fixes the WebUI picker but introduces a
cross-tool regression: hermes-agent's CLI reads `config.yaml -> model.default`
directly and passes it to the provider API verbatim. For aggregator providers
(Nous is one — see hermes_cli/model_normalize.py `_AGGREGATOR_PROVIDERS`),
`normalize_model_for_provider` is skipped entirely (run_agent.py:887), so
the literal `@nous:anthropic/...` string flows to the Nous API, which rejects
it — breaking every user who runs `hermes` in the terminal right after
saving via WebUI.

Fix the tension at the picker rather than the persistence: the existing
`_findModelInDropdown()` smart matcher already normalises both sides
(lowercase, strip namespace prefix, dashes→dots) so a saved bare
`anthropic/claude-opus-4.6` resolves to the `@nous:anthropic/claude-opus-4.6`
option automatically. Applied this in panels.js via `_applyModelToDropdown()`.

Changes:
  api/config.py         revert the @-prefix preservation; persist the
                        resolved bare/slash form (CLI-compatible)
  static/panels.js      Settings picker uses _applyModelToDropdown()
                        instead of raw `.value =` so saved bare forms
                        still select the matching @nous: option
  tests                 test renamed + asserts bare persisted form;
                        new test locks the smart-matcher contract

This also improves behaviour for a dormant case not flagged in #895: a user
who set their default via `hermes model X` and opens Settings for the first
time used to see a blank picker (bare form vs prefixed options). Now the
smart matcher finds the right option, so the "open Settings → save → bare
form in config.yaml" round-trip is stable for both CLI- and WebUI-origin
saves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update CHANGELOG v0.50.171 — bare-form persistence + picker smart-match

---------

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>
v0.50.171
2026-04-23 10:44:10 -07:00
nesquena-hermes 498156a3e8 fix(settings): show live models in default model picker and apply to new chats (#872) (#900)
* fix(settings): show live models in default model picker and apply to new chats (#872)

Two related bugs:
1. Settings > Preferences > Default Model dropdown only showed static models
   from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7)
   were missing. Now calls _fetchLiveModels() on the settings picker too.
2. New chats ignored the saved default model preference — they always used the
   chat-header dropdown value (which reflects the previous session's model).
   Now newSession() uses the saved default_model and syncs the dropdown.

Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models
can be applied to any <select> element (chat-header or settings picker).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction

The tests searched for og.dataset.provider, _isPortalFetch, and openrouter
exclusion patterns inside _fetchLiveModels(). These were extracted into
_addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to
check _addLiveModelsToSelect first, falling back to _fetchLiveModels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add multi-tab note on window._defaultModel

Clarifies that window._defaultModel is per-page-load and not synced
across browser tabs, following maintainer feedback on #889.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: CHANGELOG for v0.50.170

* chore: trigger PR refresh after rebase

---------

Co-authored-by: fr33m1nd <bergeouss@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
v0.50.168 v0.50.169 v0.50.170 v0.50.167 v0.50.166
2026-04-23 09:58:15 -07:00
bergeouss cd01e4d5ba feat(models): live-first model fetching for all OpenAI-compat providers (#892)
* feat(models): live-first model fetching for all OpenAI-compat providers (#871)

The WebUI model picker relied on hardcoded _PROVIDER_MODELS as primary
source for providers like zai, minimax, mistralai, xai, openai-codex,
deepseek, and gemini. These lists go stale — new models don't appear
until someone manually updates the dict.

Add an OpenAI-compat /v1/models fetch fallback in _handle_live_models()
that fires when provider_model_ids() is unavailable or returns []. The
resolution chain is now:

  1. hermes_cli.provider_model_ids() (agent's live fetch)
  2. Custom providers from config.yaml
  3. Direct /v1/models fetch for known OpenAI-compat endpoints
  4. Static _PROVIDER_MODELS as last-resort offline fallback

Covers: zai, minimax, mistralai, xai, openai-codex, deepseek, gemini.

Uses urllib (stdlib) — no new dependencies. Static lists remain as
offline fallback so the UI always shows something.

Closes #871

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(models): address review feedback on live fetch (#892)

Five changes from nesquena-hermes review:

1. Move _OPENAI_COMPAT_ENDPOINTS to module level — avoid dict
   reconstruction per request
2. Document urllib blocking behavior — 8s timeout acceptable because
   server is threaded and frontend enriches in background
3. Add TODO comment for TTL-based caching follow-up
4. Remove openai-codex from endpoint map — same endpoint as base
   openai provider, already covered by provider_model_ids()
5. Restrict API key lookup to provider-scoped and model.api_key only
   — remove top-level api_key fallback to prevent cross-provider
   key leakage

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 09:45:46 -07:00
Pavol Biely 96c97c5e0e fix: remove hardcoded chinese title heuristics (#887)
* fix: remove hardcoded chinese title heuristics

* fix: use english placeholder for non-latin fallback titles
2026-04-23 09:45:34 -07:00
Joe Maples ae7be6deba fix(docker): Install all dependencies for agent (#897) 2026-04-23 09:45:28 -07:00
bergeouss bd443c4862 fix(markdown): stash code blocks with attributes and multiline content (#890) (#891)
The _ob_stash regex in renderMd() used (<code>[^<]*</code>) which failed
to match <code class="language-sql"> tags (attributes) and couldn't capture
multiline content. Code blocks leaked into the bold/italic pipeline,
corrupting SQL/C# comments into <strong><em> tags and producing &lt;
artifacts.

Replace with (<code\b[^>]*>[\s\S]*?</code>) to handle attributes and
multiline content correctly.

Closes #890

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 09:45:20 -07:00
nesquena-hermes b82954ee70 feat(ui): session attention indicators — streaming spinner, unread dot, timestamps (#856)
Closes #856. Co-authored-by: Frank Song <138988108+franksong2702@users.noreply.github.com>
Reviewed-by: nesquena (709bd37 — test isolation fix also included)
v0.50.165
2026-04-23 09:05:57 -07:00
nesquena-hermes 666d385c03 fix: Nous static models use @nous: prefix — v0.50.164 (#885)
fix: Nous static models use @nous: prefix — v0.50.164 (#885)

Follow-up to #854 / PR #870. The previous fix made Nous static IDs
slash-prefixed and added a portal-guard branch to resolve_model_provider().
This tightens the static list to use the explicit @nous: prefix, matching
the format of live-fetched models after ui.js's _fetchLiveModels() portal-
prefix step.

The @provider:model branch in resolve_model_provider() is more explicit and
reliable than the portal-guard fallback. Both static and live-fetched paths
now converge on the same resolver output — and as a side effect, the dedup
check in _fetchLiveModels() now correctly identifies static entries as already
present, eliminating duplicate entries in the dropdown for Nous users.

Verified: all 29 Nous models in the browser dropdown carry @nous: prefix,
routing confirmed correct via resolve_model_provider() for all 4 static IDs,
1941 tests passing.

Closes #854.
v0.50.164
2026-04-22 22:56:21 -07:00
nesquena-hermes d39d30a213 fix: correct message ordering after task cancellation — v0.50.163 (#883)
fix: correct message ordering after task cancellation — v0.50.163 (#883)

Fixes the message-ordering glitch from #882: clicking Cancel while the
agent is responding could cause a subsequent response to render above
the "*Task cancelled.*" marker.

Root cause: the cancel handler pushed the marker only to local S.messages
without persisting to the server. When the done event fired shortly after
and replaced S.messages from server state, the marker disappeared from
client state while the next response anchored to the server-authoritative
position.

Fix has three parts:
- Server (cancel_stream): append *Task cancelled.* to session.messages
  with _error:True + timestamp, then save. _error ensures
  _sanitize_messages_for_api() strips it from conversation_history on
  the next agent turn, so the LLM never sees it as a prior assistant
  turn. Precedent: same flag used for the apperror marker at line 1343.
- Client (SSE cancel handler): fetch /api/session instead of pushing
  locally (same pattern as the done handler). Falls back to local push
  if the fetch fails.
- Tests: fix test window width for cancel handler (1200→dynamic); add
  two regression tests pinning _error flag and _sanitize invariant.

1941 tests passing.

Co-authored-by: piliang <piliang1@jd.com>
v0.50.163
2026-04-22 22:17:40 -07:00
Frank Song 62c56175b7 feat(workspaces): autocomplete trusted workspace paths — v0.50.162 (PR #880 by @franksong2702, closes #616)
Adds GET /api/workspaces/suggest endpoint and autocomplete dropdown in the Spaces panel. Suggestions limited to trusted roots (home, saved workspaces, boot default). Keyboard nav, Tab completion, hidden dir support. Symlink-escape and dotdot-escape invariants locked by regression tests.
v0.50.162
2026-04-23 02:35:58 +00:00