Bundles 2 PRs:
- #1366 fix: guard finalizeThinkingCard with session ID check (with pre-release fix)
- #1367 fix(clarify-sse): stale-detector health timer (Opus SHOULD-FIX from v0.50.249)
Pre-release fix on #1366: the contributor's guard depends on
liveAssistantTurn.dataset.sessionId, but no code in the repo sets
that attribute. Without the fix, the guard would always early-return
(undefined !== sid is always true), breaking the streaming UI
completely — every assistant turn's thinking card would stay open
forever. Added per-site stamps at all 3 places that create
liveAssistantTurn in static/ui.js, plus a regression test that fails
any future creation site that forgets the stamp.
Without this check, switching browser tabs while a stream is running
causes finalizeThinkingCard() to operate on the wrong session's
thinking card DOM — the card belongs to the stream that started it,
not the session currently displayed in the tab. The guard ensures
finalize only runs when the live assistant turn's session matches
the current session.
Co-authored-by: Josh <josh@fyul.link>
- api/streaming.py SSE payload now falls back to agent.model_metadata.get_model_context_length when compressor doesn't supply context_length (mirrors the session-save fallback shipped in v0.50.247).
- api/streaming.py also falls back to s.last_prompt_tokens to avoid using the cumulative input_tokens counter.
- static/ui.js tracks rawPct separately from pct and shows '(context exceeded)' tooltip when rawPct > 100 instead of misleading '100% used (0% left)'.
- static/messages.js clears 'Uploading...' composer status after upload completes.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Frontend companion to backend fix in v0.50.246 (#1341 + a5c10d5).
Default context window to 128K when usage.context_length is falsy.
Show '(est. 128K)' label when using the default.
Use input_tokens as fallback for last_prompt_tokens.
Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
From PR #1338. Already independently APPROVED by nesquena before being absorbed into v0.50.246.
CHANGELOG entries from this PR were dropped during squash (the v0.50.245 section is already
shipped); they will be re-added under [v0.50.246] in the release commit.
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
release: v0.50.243
Batch release of 2 PRs.
- #1301 — fix: remove PRIMARY chip badge + add Claude Opus 4.7 label
Drops the chip-projected configured-model badge added in #1287 (chip
width 235px → 164px). Adds Claude Opus 4.7 label entries so the picker
no longer renders "Claude Opus 4 7" (missing dot).
Independently reviewed and approved by nesquena (commit c0bbd23).
- #1297 (@franksong2702) — fix: preserve cron output response snippets
Fixes#1295. /api/crons/output now preserves the ## Response section
when a large skill dump appears in the prompt section; falls back to
file tail when no marker exists.
Tests: 3254 passed, 2 skipped, 3 xpassed.
Independently reviewed and approved by nesquena (commit b262e4d).
The post-stream renderMd() in static/ui.js only handled #, ##, ### — lines starting with #### through ###### fell through and emitted as literal text after streaming finalized.
Extend the heading replacer chain to cover h4-h6, ordered longest-first, so ###### cannot be partially captured by the shorter ### rule. Add the matching .msg-body h4/h5/h6 CSS rules (and data-font-size variants) so the new tags inherit the same visual rhythm as h1-h3.
Adds 3 node-driven tests in test_renderer_js_behaviour.py pinning all six heading levels and the longest-first replacer order.
Closes#1258
Addresses reviewer feedback on #524 — the compress affordance was only
reachable via hover (desktop). Mobile users can now tap the context ring
button to toggle the tooltip and access the compress button.
- CSS: add .ctx-tooltip-active class with opacity + pointer-events
- JS: tap-to-toggle handler on ctxIndicator with outside-click dismiss
- aria-hidden toggled correctly for accessibility
Ref: #1223 review comment
- Add 512 KB cap for inline diff rendering to prevent DOM bloat on large patch files
- Add diff_error and diff_too_large i18n keys in all 7 locales for clear error messages
- Improve error state to show explanatory message instead of just filename
- Addresses reviewer feedback on file size cap and missing diff_error i18n key
- Fenced code blocks with diff/patch lang hint render with colored lines
(green +lines, red -lines, italic @@ hunks)
- MEDIA:.patch/.diff files render inline instead of download link
(async fetch via loadDiffInline() in post-render pipeline)
- CSS: diff-block, diff-line, diff-plus/minus/hunk classes
- i18n: diff_loading key in all 7 locales
- 12 tests: renderer, MEDIA inline, CSS classes, i18n parity
Closes#483
When context usage reaches 50% (yellow), a subtle hint button appears
in the context ring tooltip suggesting /compress. At 75%+ (red), the
hint intensifies with a warning style.
Clicking the button pre-fills /compress into the composer and focuses
it, so the user can add a focus topic or just hit send. No auto-fire
— the user stays in control.
- static/ui.js: conditional visibility + click handler in _syncCtxIndicator
- static/index.html: ctxCompressBtn element inside ctxTooltip
- static/style.css: muted button style, red variant for ctx-high
- static/i18n.js: ctx_compress_hint / ctx_compress_action in all 7 locales
Closes#524
The inline rename via double-click (nameEl.ondblclick) was not updating
the _expandedDirs and _dirCache when renaming a directory, unlike the
context-menu rename path (_inlineRenameFileItem) which already had this
logic. This could cause the tree view to show stale expand state after
a directory was renamed via double-click.
The file tree already supported file rename (double-click), file delete
(button), and create file/folder. This adds the missing directory
operations:
Backend:
- _handle_file_delete now supports directories when recursive=true
(uses shutil.rmtree instead of blocking with an error)
Frontend:
- Right-click context menu on all file/directory items with Rename
and Delete options (follows the project context menu pattern)
- Directory delete button (x) with confirmation dialog
- _inlineRenameFileItem() for renaming dirs via context menu prompt
- Expanded-dir cache is updated on rename/delete to stay consistent
- Context menu auto-positions within viewport bounds
i18n: delete_dir_confirm, rename_title, rename_prompt in all 7 locales
Closes#1104
- Drop btnCancel element and all JS show/hide call sites across
boot.js, messages.js, sessions.js, ui.js (superseded by single
primary action button)
- Remove .cancel-btn CSS rules including mobile media-query override
- Route updateSendBtn() title/aria-label through t() with English
fallbacks; add composer_send/queue/interrupt/steer/stop keys to all
7 locales (en, ru, es, de, zh, zh-Hant, ko)
- Branch disabled-state tooltip on reason: clarify lock, compression
running, or idle-empty, each with its own i18n key
- Update test_sprint10 / test_sprint36 to reflect single-button model:
assert btnSend present and id="btnCancel" absent; replace
test_hides_cancel_button with test_clears_composer_status
- Restore deepseek-chat-v3-0324 and deepseek-reasoner with '(legacy)' labels;
these are deprecated 2026-07-24 but still live until then
- Fix zai (Z.AI/GLM) default_base_url: use /api/paas/v4 instead of /api/coding/paas/v4;
the coding plan path is for the glmcode custom provider, not the general API
- Update test assertions to match
- Remove deepseek-chat-v3-0324 (DeepSeek V3) and deepseek-reasoner (R1)
from _MODEL_LIST, _PROVIDER_MODELS, static/index.html, and static/ui.js
- Keep only deepseek-v4-flash and deepseek-v4-pro
- These old model IDs are deprecated since 2026-07-24
Add deepseek-v4-flash and deepseek-v4-pro model entries to:
- api/config.py (_MODEL_LIST and _PROVIDER_MODELS)
- static/index.html (model dropdown)
- static/ui.js (static label map)
These are the latest DeepSeek models with 1M context window,
replacing the legacy deepseek-chat/deepseek-reasoner (deprecated 2026-07-24).
When multiple providers expose the same bare model ID (e.g. two custom
providers both listing gpt-5.4), the model picker cannot distinguish
them — both rows appear active and clicking the other provider's copy
is a no-op.
Fix:
- Add _deduplicate_model_ids() post-process in api/config.py that
detects duplicate bare model IDs across groups and prefixes
collisions with @provider_id: so each entry is globally unique
- Update norm() regex in static/ui.js to strip @provider: prefixes
for fuzzy matching, so existing sessions with bare model IDs still
restore correctly
- First occurrence stays bare for backward compatibility with sessions
that already store the bare model name
- Update test_model_resolver to be dedup-aware
Closes#1228
When context usage reaches 50% (yellow), a subtle hint button appears
in the context ring tooltip suggesting /compress. At 75%+ (red), the
hint intensifies with a warning style.
Clicking the button pre-fills /compress into the composer and focuses
it, so the user can add a focus topic or just hit send. No auto-fire
— the user stays in control.
- static/ui.js: conditional visibility + click handler in _syncCtxIndicator
- static/index.html: ctxCompressBtn element inside ctxTooltip
- static/style.css: muted button style, red variant for ctx-high
- static/i18n.js: ctx_compress_hint / ctx_compress_action in all 7 locales
Closes#524
Batch release v0.50.232 — 4 fixes.
## PRs included
| PR | Author | Fix |
|---|---|---|
| #1192 | @nesquena-hermes | Model chip fuzzy-match false positive (#1188) |
| #1193 | @nesquena-hermes | openai-codex not detected in model picker (#1189) |
| #1196 | @nesquena-hermes | Workspace files blank after second empty-session reload |
| #1197 | @bergeouss | Session timestamps wrong with server/client clock drift (#1144) |
All four PRs independently reviewed and approved by @nesquena.
## Integration fixes applied
**#1193:** Updated misleading comment — `OPENAI_API_KEY` does NOT authenticate the default Codex OAuth endpoint (that uses `chatgpt.com/backend-api/codex` and requires a separate OAuth flow). The comment now accurately states the known limitation. Also replaced a fragile 400-char source-scan test with an isolation-safe unit test. Note: OAuth-authenticated users already get detected via `hermes_cli.auth` — this fix only addresses the env-var fallback path.
## Test results
**2764 passed, 2 skipped** (macOS-only workspace tests). Browser QA: **21/21**. `/api/sessions` confirmed returning `server_time` and `server_tz` fields.
Batch release v0.50.231 — 3 fixes.
## PRs included
| PR | Author | Fix |
|---|---|---|
| #1186 | @nesquena (Claude Code) | macOS `/etc` symlink bypass in workspace blocked-roots |
| #1187 | @nesquena-hermes | Workspace panel stuck closed after empty-session reload |
| #1190 | @bergeouss | Fenced code content leaking into markdown passes (#1154) |
All three PRs were independently reviewed and approved by @nesquena.
## Test results
**2729 passed, 2 skipped** (2 macOS-only tests correctly skipped on Linux). Browser QA: **21/21**.
## Key fix notes
**#1186:** `_workspace_blocked_roots()` now returns both literal and `Path.resolve()` forms of each blocked root. macOS symlinks (`/etc → /private/etc`) previously let a resolved candidate slip past the literal check. New `_is_blocked_system_path()` helper with `/var/folders` and `/var/tmp` carve-outs for pytest temp dirs.
**#1187:** Regression from #1182 — `syncWorkspacePanelState()` force-closed on any no-session state. Now only closes in `'preview'` mode. Both boot paths restore localStorage panel pref before sync.
**#1190:** Fenced code blocks are now stashed as `\x00P<n>\x00` tokens through ALL markdown passes (list/heading/table regexes), restored at the very end. Previously, diff hunks and markdown headings inside code blocks triggered those regexes, injecting `<ul>/<li>/<h>` tags that broke `</pre>` closure.
Merged as v0.50.226.
Integration branch absorbed @aronprins's original PR #1141 with one reviewer fix from @nesquena (`1d11646`: queue hide tooltip updated to reference the queue pill, not the removed titlebar badge).
**Full gate results:**
- 2595 tests passing ✅
- Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14) ✅
- Independent review: APPROVED by @nesquena ✅
Thank you @aronprins for the clean PR — the titlebar is properly restored.
* feat: attention state for broken cron jobs + Korean i18n (#1133, @franksong2702)
* fix: pytest state isolation for direct session saves (#1136, @franksong2702)
* fix(#1095): image thumbnails in composer + lightbox in chat (#1135)
* fix(css): restore cron attention + detail-alert rules overwritten by style.css merge (absorb)
* docs: v0.50.225 release notes and version bump
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>