mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-06-02 15:10:41 +00:00
9fabd12e41b17eb6282ccf5713643bf55ffb0c5e
120 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8c24b24dcd |
feat: upload and extract zip/tar archives into workspace (#525)
- Add extract_archive() with zip-slip and tar-slip protection - New /api/upload/extract endpoint for archive uploads - Auto-detect archive files (.zip, .tar.gz, .tgz, .bz2, .xz) - Archives extracted into named subfolder (avoids overwrites) - Workspace file tree auto-refreshes after extraction - Archive extensions added to file picker accept list - i18n: archive_extracted key in all 7 locales Security: path traversal blocked via resolve() prefix check, matching existing safe_resolve_ws() sandbox pattern. |
||
|
|
9c57d36156 |
fix: update expanded dirs cache on double-click directory rename
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. |
||
|
|
38df294af9 |
feat(#1104): workspace directory CRUD — delete, rename, context menu
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 |
||
|
|
03b7714f65 | docs: note Lucide source for composer icons | ||
|
|
9d5480565f |
fix: remove deprecated btnCancel; localise composer tooltips with disabled reason branching
- 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 |
||
|
|
b57134bf2b | ui: reflect explicit busy slash command in send button | ||
|
|
be291498cf |
ui: swap composer action icons to Lucide (ISC-licensed)
- queue: list-end (append to queue) - interrupt: skip-forward (jump ahead) - steer: compass (course correction) |
||
|
|
96182e5f51 | fix: keep busy-input send available on mobile | ||
|
|
26f51b7190 |
fix: address review feedback — restore V3 as legacy, fix zai base_url
- 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 |
||
|
|
568a913615 |
chore: remove deprecated DeepSeek V3/R1 models, keep only V4
- 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 |
||
|
|
9df01c6167 |
feat: add DeepSeek V4 Flash and V4 Pro models
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). |
||
|
|
a8101d98f7 |
fix(models): deduplicate model IDs across provider groups (#1228)
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 |
||
|
|
7189416969 |
fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes (#1206)
fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes v0.50.235 (#1203 — profile switch workspace/model/chip, 3 bugs + flaky test): - switch_profile now reads target profile's workspace directly (thread-local bypass) - invalidate_models_cache() after profile switch (model dropdown staleness) - syncTopbar() updates chip before early-return (no-session path) v0.50.234 (#1201/#1205 — XSS hardening + workspace security): - renderMd() full HTML attribute sanitizer replacing tag-name-only allowlist - Delegated image lightbox (removes all inline onclick) - macOS /etc → /private/etc symlink bypass fixed - /System /Library added to blocked workspace roots - Legacy /api/chat workspace trust gap closed Both PRs independently reviewed. 2787/2787 tests. QA harness 20/20 + 11/11 API checks. Co-authored-by: Brendan Schmid <bschmidy10@Wilson.bschmidy10> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> |
||
|
|
3780df9428 |
fix: batch v0.50.232 — fuzzy match, codex detection, workspace reload, timestamp sync (#1198)
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. |
||
|
|
e61a405add |
fix: batch v0.50.231 — macOS symlink bypass, workspace panel, fenced code leak (#1194)
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. |
||
|
|
a091be6a8e |
fix: batch v0.50.229 — session perf, ephemeral sessions, iOS zoom (#1183)
Merged as v0.50.229. 2678 tests passing. Browser QA 21/21. All three PRs were independently reviewed and approved by @nesquena with reviewer commits pulled in: - #1181 (#1158): `d974388` (stale-response race in _loadOlderMessages) - #1182: `7e20006` (full-scan fallback path consistency) - #1180: `a5ad154` (regression test for iOS zoom threshold) Thanks @jasonjcwu (#1158)! |
||
|
|
ef26d19549 |
fix: batch v0.50.228 — renderer, model race, tool card, empty session, .env (#1179)
Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser. **Fix verifications:** - #1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅ - #1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅ - #1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅ - #1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅ - #1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅ Thanks @bsgdigital (#1150) and @bergeouss (#1178)! |
||
|
|
8b8ff3328a |
fix: batch triage — 12 contributor PRs (v0.50.227) (#1168)
Merged as v0.50.227. 2634 tests passing, browser QA 21/21 (desktop + mobile). Full attribution below. Thanks to all 12 contributors: @jundev0001 (#1138), @franksong2702 (#1142, #1157, #1162), @dso2ng (#1143), @bergeouss (#1145, #1146, #1156, #1159), @jasonjcwu (#1149), @ccqqlo (#1161), @frap129 (#1165) Two fixes applied during integration and two more by the independent reviewer (@nesquena): - messages.js: per-turn cost delta capture order (#1159) - workspace.py: symlink target blocked-roots check + HOME sanity guard (#1149, #1165) - panels.js: cron unread counter bookkeeping (in-loop increment bug) - tests/test_symlink_cycle_detection.py: register workspace before session/new |
||
|
|
dca8624454 |
fix(ui): restore rail-era app titlebar state (v0.50.226) (#1163)
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. |
||
|
|
5192ca5de5 |
v0.50.225: cron attention, image lightbox, pytest isolation (#1137)
* 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> |
||
|
|
fc0152b2fc |
v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* 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> |
||
|
|
27b17a8fc8 |
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* 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> |
||
|
|
498b51bfc6 |
v0.50.218: chat bubble overflow, project color picker, blockquote renderer (#1085)
* fix(css): add overflow-wrap:anywhere to chat bubbles — prevents long URL overflow (#1080) * fix(projects): rename now works via dblclick timer guard + right-click color picker (#1078) * fix(renderer): block-level constructs inside blockquotes now render Fenced code blocks, headings, horizontal rules, and ordered lists inside blockquotes now render correctly. Six related bugs documented in blockquote-rendering-bugs.md were collapsed into one architectural fix in renderMd(). Bugs fixed (all 6): 1. Fenced code blocks inside blockquotes -- > prefixes leaked into the <pre> body and the blockquote got fragmented around the rendered code, sometimes leaving raw <pre>/<div class="pre-header"> as visible text. 2. Blank > continuation lines fragmented multi-paragraph blockquotes into separate <blockquote> elements with literal > between them. 3. ## headings inside blockquotes rendered as literal "##" text. 4. Numbered lists inside blockquotes rendered as plain prose. 5. Complex blockquote (mixed headings + code + list + inline code) collapsed into a monospace blob with raw markdown syntax leaking everywhere. 6. Horizontal rules (---) inside blockquotes rendered as literal text. Root cause: The per-line passes for fenced code, headings, hr, ordered lists all ran BEFORE the blockquote handler and could not match lines that started with >, so by the time blockquote stripping ran those constructs had already been mishandled. Fix: A new blockquote pre-pass at the top of renderMd(): - Walks lines fence-aware so > -prefixed lines inside non-blockquote code fences (e.g. shell prompts in bash code blocks) are not miscaptured as a blockquote. - Groups consecutive > -prefixed lines, strips the > prefix, and recursively calls renderMd() on the stripped content. The recursive call handles all block-level constructs (fenced code, headings, hr, ordered/unordered lists, nested blockquotes) using the same pipeline. - Wraps the rendered HTML in <blockquote> and stashes it with a \x00Q token. Restored at the very end of renderMd() so no later pass can mangle the inner HTML. The old _applyBlockquotes regex-replace is removed entirely along with its limited inline branches for nested blockquotes and unordered lists. Behaviour change: Blockquotes now produce CommonMark-compliant <p> wrapping for text content (was: bare text directly inside <blockquote>). The visual output is the same in browsers but the HTML structure is now standard. Tests: - 14 new behavioural tests in tests/test_renderer_js_behaviour.py drive the actual renderMd() via node and lock all 6 bug fixes. - .local-review/test_blockquote_bugs.js -- node harness covering the same scenarios, runnable manually for fast iteration. - 2407/2408 tests pass (1 pre-existing macOS-only failure deselected). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(renderer): entity decode before blockquote pre-pass + CSS margin fix - Move the >/</& entity-decode to run at the very top of renderMd(), before the blockquote pre-pass. Previously decode() ran at line 756 (after the pre-pass at line 697), so LLM output containing >-encoded blockquotes was never matched by the pre-pass. - Add .msg-body blockquote p{margin:0} and .preview-md blockquote p{margin:0} so the new CommonMark-compliant <p> wrapping inside blockquotes doesn't add extra vertical spacing. Prior shape (bare text) had no default p-margins. - Add Node-driven tests: TestBlockquoteEntityEncodedInput covers > prefix and >-encoded fenced code inside blockquotes. - Add struct test: TestBlockquotePrePassOrdering::test_entity_decode_runs_before_blockquote_pre_pass locks decode < _bq_stash ordering in ui.js. Fixes found during Opus independent review of #1083. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: v0.50.218 release notes, test count 2458, roadmap update --------- 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> |
||
|
|
58ad315dca |
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075)
* fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly
MIME_MAP was missing entries for .html and .htm. The server fell back to
Content-Type: application/octet-stream, which browsers refuse to render as
HTML in an iframe — causing a blank white preview.
The rest of the pipeline was already correct: the iframe exists in
static/index.html, openFile() in static/workspace.js routes .html to
showPreview('html'), and _handle_file_raw() in api/routes.py sets the
correct CSP sandbox header when ?inline=1 is present. The only missing
piece was the MIME type.
* test(workspace): lock in MIME_MAP entry for .html/.htm
PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py
to fix the blank workspace HTML preview iframe. Without a direct
assertion on the MIME_MAP entries, the fix could silently regress
(the existing test_779_html_preview.py tests cover the iframe wiring,
the inline=1 query handling, and the CSP sandbox header — but none of
them touch MIME_MAP itself).
Add a single regression test that asserts MIME_MAP['.html'] and
MIME_MAP['.htm'] are both 'text/html' so any future removal of those
entries fails CI immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(composer): raise .approval-card.visible z-index above .queue-card
.queue-card has z-index:2. .approval-card.visible had no z-index, so the
queue flyout would render on top of the approval card when both were visible
simultaneously — obscuring the Allow/Deny buttons.
Fix: add z-index:3 to .approval-card.visible so approvals always render
above the queue flyout. Approval is a blocking, security-relevant interaction
and must never be obscured by passive UI elements.
* test(composer): pin approval-card z-index > queue-card invariant
PR #1071 raises .approval-card.visible to z-index:3 so the security-
relevant Allow / Deny buttons stay clickable when the queue flyout is
also open. Without a regression test, a future CSS edit could silently
drop the z-index back below queue-card (z-index:2) and reintroduce the
bug — there is no automated UI test covering this stacking interaction.
Add a focused regex check that pins the invariant:
.approval-card.visible z-index must be strictly greater than
.queue-card z-index.
Modeled on the existing CSS-regex regression style in
tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: intercept /steer /interrupt /queue before busy-mode routing in send()
Root cause: slash commands entered while the agent is busy never reached
the command dispatcher. send() enters the busy block and returns early at
line ~50, so the slash-command intercept (~line 56) is never reached.
The text was queued as a plain message. When it drained after the turn
ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream,
and showed "No active task to stop."
Fix: at the top of the busy block, before checking busyMode, check if the
text starts with / and is one of the three control commands. If so, dispatch
the handler immediately and return. This lets the user type /steer, /interrupt,
or /queue at any time — including while the agent is mid-stream — and have
them execute against the live session.
Two new regression tests added:
- test_slash_commands_intercepted_before_busymode_routing: verifies the
intercept appears before the busyMode routing in the busy block
- test_steer_intercept_calls_handler_directly: verifies the intercept calls
_bc.fn(_pc.args) and returns, not queues
* test(busy-intercept): pin sync input-clear before await in slash intercept
PR #1072's intercept clears the msg input before awaiting the handler.
Order matters: if the await happens first (or if the clear is moved
inside the handler), the input still shows '/steer foo' for the duration
of the await. A reflexive second Enter press during that window — common
while waiting for the toast — re-runs send(): either re-fires the
handler (double-steer) or, if the turn just ended, falls through to the
non-busy slash dispatcher and drops a confusing "No active task to stop."
Add test_steer_intercept_clears_input_before_await pinning the order so
this UX invariant cannot silently regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: update steer i18n and settings copy — steer no longer interrupts
With the real /steer implementation (agent.steer() via /api/chat/steer),
steer injects a correction mid-turn WITHOUT interrupting the current stream.
The previous copy said "falls back to interrupt", "Steer (interrupt + send)",
etc. — accurate only for the old placeholder, not the real implementation.
Changes across all 6 locales (en/ru/es/de/zh/zh-Hant):
cmd_steer: "falls back to interrupt" removed
settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction"
cmd_steer_fallback: "interrupted" → "queued for next turn"
busy_steer_fallback: "interrupted instead" → "queued for next turn"
settings_desc_busy_input_mode: "currently falls back to interrupt" removed
Also:
static/index.html: inline fallback text updated to match
static/commands.js: internal comment clarified (fallback = queue+cancel,
not "interrupt mode" which implies the primary action)
* fix(renderer): group consecutive blockquote lines into single element
Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs:
1. `.+` required at least one character — bare `>` lines (blank
continuation lines) did not match and passed through as literal `>`
2. Each matching line became its own `<blockquote>` element — a 10-line
blockquote produced 10 stacked `<blockquote>` tags with no grouping
3. When a fenced code block sat inside a blockquote, the fence-stash
pass consumed the code content and left orphaned `>` lines that the
old `.+` pattern could not match
Fix: replace the single-line regex with a group-based approach that matches
one or more consecutive `>` lines as a single block, strips the `>` prefix
from each line, passes each non-empty line through inlineMd(), turns blank
`>` lines into `<br>`, and wraps the entire group in one `<blockquote>`.
14 regression tests added covering:
- Single-line blockquotes (regression)
- Multi-line grouping (2 and 10 lines)
- Two separate blockquotes staying separate
- Bare `>` and `>text` (no space) edge cases
- Blank continuation lines → <br>
- Bold / italic / inline-code inside blockquotes
- Blockquote followed by normal paragraph
* fix(renderer): drop empty trailing line from blockquote match
The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.
Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.
Reproducer:
'> Hello\n\nThe rest of the message.'
→ '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
^^^ phantom <br>
Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).
Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
- input ending with \n
- quote followed by paragraph (the real-world case)
- multi-line quote ending with \n
- quote with blank continuation + trailing \n (internal <br> stays,
trailing <br> does not)
Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes
Five additional fixes on top of the blockquote grouping from the initial commit:
1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows
line endings do not produce stray \r characters in rendered output
2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use
inside blockquotes/lists) and the outer pass (for plain paragraphs).
Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped.
3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐
via task-done/task-todo span wrappers. Checks [X] (uppercase) too.
4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its
own <blockquote> element rather than passing through as literal >.
Implemented by extracting the blockquote rule into _applyBlockquotes()
which calls itself recursively on the stripped inner content.
5. Lists inside blockquotes: > - item now renders <ul><li> inside the
blockquote instead of a literal "- item" string. Task list items work
inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>).
Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF
strip at the top of renderMd pushed the autolink regex past the old limit.
68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py
covering all constructs, edge cases, and combinations.
* fix(renderer): restore space in blockquote prefix-strip regex
Commit
|
||
|
|
6c343aff84 |
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056)
* feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS catalog so they appear in the model picker for the openai, openai-codex, and copilot providers. Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent * fix(models): add gpt-5.5-mini to copilot provider catalog * fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044) Mermaid's built-in 'dark' and 'default' themes inject an @import for fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src only allows cdn.jsdelivr.net, so this request is blocked on every diagram render, filling the console with CSP errors. Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables block of mermaid.initialize() in renderMermaidBlocks(). This suppresses Mermaid's external font import and uses the page's existing font stack. Avoids adding fonts.googleapis.com to the CSP — no new external dependency, no font FOUT, consistent with the rest of the UI typography. 3 regression tests added in tests/test_1044_mermaid_csp_font.py. 2215/2215 tests passing. * fix(onboarding): non-standard provider/path cluster (#1029) * fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045) The pageshow handler added for #822 only cleared the session search filter and re-rendered the session list. This left the rest of the layout chrome (topbar, rail icons, workspace panel, resize handles, gateway SSE) in the stale bfcache DOM state, causing a broken layout (oversized search icon, uninitialized rail) that required a hard refresh to fix. Fix: extend the pageshow handler to re-run the full set of layout sync calls that the boot IIFE runs on a fresh page load: syncTopbar() — restores model chip, title, topbar state syncWorkspacePanelState() — restores workspace panel open/closed _initResizePanels() — reattaches panel resize drag listeners startGatewaySSE() — reconnects the gateway SSE watcher (bfcache-persisted connections are dead) All four calls are typeof-guarded for safe degradation if a helper is not yet defined. The existing #822 fixes (sessionSearch clear + renderSessionListFromCache) are preserved unchanged. loadSession() is intentionally NOT re-called — it would cause message flicker; the sync calls above are sufficient to restore visual state. 7 regression tests added in tests/test_1045_bfcache_layout_restore.py. 2219/2219 tests passing. * fix(bfcache): also close open dropdowns on bfcache restore (#1045) Additional symptom noted in issue #1045: bfcache freezes the DOM including any open dropdown/popover state. The thinking-level selector (and other composer dropdowns) left open when navigating away would appear open without user interaction on tab restore. Extend the pageshow handler to call all four named close functions before the layout sync: closeModelDropdown() — composer model selector closeReasoningDropdown() — thinking/reasoning effort selector closeWsDropdown() — workspace chip dropdown closeProfileDropdown() — profile switcher dropdown All calls are typeof-guarded, matching the style of the layout sync calls already in the handler. 2 new tests (9 total in test_1045_bfcache_layout_restore.py): - pageshow closes all four named dropdowns - dropdown closes appear before layout sync calls (clean state first) 2221/2221 tests passing. * fix(bfcache): remove _initResizePanels() — bfcache preserves listeners * fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test * fix(sessions): use cron job name as session title when available (#1032) * fix(test): add id column to messages table in cron title test fixture * fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block * fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038) When an auth session expires, the server returns a 302→/login for page requests. In a normal browser this works fine, but in an iOS PWA running in standalone mode the redirect navigates out of the PWA shell into Safari, leaving the app permanently stuck on 'Authentication required' with no recovery path. Fix: intercept 401 responses client-side before surfacing any error. - workspace.js api(): check res.status===401 first; call window.location.href='/login' and return immediately (no throw) - ui.js: add _redirectIfUnauth() helper; wire into all direct fetch() calls that bypass api() — api/models, api/models/live, api/upload All fetch paths that could receive a 401 now redirect cleanly within the PWA frame rather than opening Safari. 6 regression tests added in tests/test_1038_pwa_auth_redirect.py. 2175/2175 tests passing. * fix(pwa): preserve current URL in ?next= param on 401 redirect * fix(test): update 401-redirect assertion to accept ?next= URL format * feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by the login success handler (always redirected to ./). _safeNextPath() validates and returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs, // protocol-relative URLs, backslash variants, and control characters. 4 new regression tests added. * Implement session agent cache for AIAgent reuse Added session agent cache to reuse AIAgent across messages. * Implement agent caching for session management * Implement session agent eviction on session deletion Added session agent eviction to prevent turn count leakage in recycled sessions. * docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27) * docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210 Three entries in the [Unreleased] section are duplicates of items now listed under v0.50.210: - Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP - bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore - iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL The original drafts landed in [Unreleased] when individual PRs (#1047, #1048, #1043) were approved; the v0.50.210 release-notes commit then added the same items under the version section without removing the [Unreleased] copies. Drop the duplicates so users reading the CHANGELOG don't see the same fix listed twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: qxxaa <mrhanoi@outlook.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3ce7844a7a |
feat(queue): Codex-style message queue flyout above composer (#1040)
* chore: apply pending #965 queue flyout patches on local master Queue flyout implementation (PR #965 — pending merge) applied on top of upstream v0.50.205. Features: - Queue card slides up from behind composer (approval-card pattern) - Lucide icons via li(), CSS class system, no inline SVG dumps - Drag-to-reorder by _queued_at timestamp (survives re-renders) - Inline contenteditable edit with focus guard and blur-commit - Combine preserves first item files, merge immediate (no 200ms race) - Files/model compact badges per item - Hide/expand via header chevron + composer pill + titlebar chip - All 3 expand paths sync correctly - border-bottom CSS order fixed, fingerprint improved, _dragTs guards CF CSP domains also applied (deployment-specific, not in upstream PR). * fix(queue): harden merge closure, toggleQueue sid, and drain flash - mergeBtn _doMerge now reads live queue (_getSessionQueue) instead of stale closure q - toggleQueue reads activeSid from S.session at call time, not captured param - updateQueueBadge defers chips.innerHTML='' by 360ms so slide-out transition completes before content clears * style(queue): contain:paint on inner, pill fade-in animation * feat(queue): pill outside composer, compact collapsed state matching card width - Move #queuePill out of .composer-box to between .composer-flyout and .composer-box - Pill styled as compact queue-card-inner (same border, radius:14px 14px 0 0, no border-bottom) - Pill width matches card inner: max-width:calc(var(--msg-max)-40px), centered - Pill stays visible until user re-expands or queue drains (updateQueueBadge no longer hides pill when card is manually collapsed) - Remove all queue-active/queue-pill-active composer modifications — composer untouched - Fix: mergeBtn reads live queue not stale closure - Fix: toggleQueue uses S.session.session_id at call time not captured param - Fix: chips.innerHTML deferred 360ms on drain to avoid empty-card flash * fix(queue): collapsed state persists + cross-session DOM isolation - Add _queueCollapsed[sid] flag: set by hideBtn, cleared by pill expand / queue drain - _renderQueueChips respects flag — no longer reopens card when new message queued while collapsed - updateQueueBadge else-branch: DOM mutations now gated on sid===active session - _syncQueueTitlebar only fires for active session in else-branch - Fixes Opus/Codex-identified bugs: pill auto-reopen and cross-session DOM corruption * fix(queue): proper pill wrapper matching queue-card structure - Add .queue-pill-outer div wrapper (max-width:var(--msg-max); padding:0 20px) identical to .queue-card outer — positions pill button at exact card-inner width - .queue-pill button fills slot with width:100% - Removes hardcoded 740px — width is derived correctly from the same CSS variables the card uses, scales with --msg-max across all viewports - JS toggles .show on pillOuter (parentElement), not on pill button directly --------- Co-authored-by: Basit Mustafa <basit.mustafa@gmail.com> |
||
|
|
ad8e10304c |
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> |
||
|
|
12a8c051fb |
fix: inject full workspace path into agent context for uploaded files (#997)
fix: inject full workspace path into agent context for uploaded files (#997) Uploaded files (drag-and-drop or paperclip) were saved correctly to the workspace but the agent message only contained the bare filename — `photo.jpg` instead of the full path. The agent couldn't call `read_file` or `vision_analyze` without a full path. `uploadPendingFiles()` now returns `{name, path}` objects from `/api/upload` (`data.path` was always returned, just never threaded through). The agent message gets the full absolute path; all display surfaces (badges, session history, INFLIGHT state, POST body) continue showing only the bare filename. Three fixes absorbed during review: - Second `saveInflightState()` call was passing raw `{name,path}` objects instead of the `uploadedNames` string array (INFLIGHT localStorage corruption on page reload) - `attachLiveStream()` was being called with the raw object array; changed to pass `uploadedNames` so the `done` handler receives strings, not objects - `attachLiveStream` `done` handler referenced `uploadedNames` which is out of scope there (ReferenceError on every upload success); fixed to use the `uploaded` param Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Closes #996 |
||
|
|
e62338d3a0 |
fix(queue): drain correct session queue after cross-session stream completion (#964)
When a session finishes streaming while the user has switched to a different session, setBusy(false) was draining S.session.session_id (the currently *viewed* session) instead of the session that actually finished. Queued follow-up messages were silently dropped. Root cause: setBusy() has no context about which session triggered it. The activeSid closure variable inside attachLiveStream() knew the right session but was not propagated. Fix: add _queueDrainSid module global (null by default). Stream done and error handlers set it to activeSid immediately before calling setBusy(false). setBusy(false) reads and clears _queueDrainSid, falling back to S.session if it is unset (the common case where the user hasn't switched away). Handlers patched: done event, start-call error handler, stream_end/stream_stop reconnection fallback, and max-retry error exit. Co-authored with Claude Sonnet 4.6 / Anthropic. |
||
|
|
6333a06524 |
perf(ui): cache renderMessages per session, skip O(n) rebuild on back-navigation (#963)
renderMessages() tears down and rebuilds every message's DOM from scratch on every call — renderMd() (markdown parse), Prism highlight, and KaTeX per message, O(n) total. With large sessions the main thread blocks for 1-5 seconds on each call. A Chrome perf trace (78s, many open sessions) showed: - 9,373ms of GC across 34,049 GC events (sustained, not burst) - Peak 273 messages.js FunctionCalls/second - 4.7s, 3.5s, 3.2s main-thread blocks from repeated renderMessages invocations The render bottleneck is unaddressed by PR #959 (which improves the network/ parse leg of session switching, not the render leg). Fix: a session-keyed innerHTML cache. After a full rebuild, the rendered HTML is stored against the session_id + message count. When switching back to a session that was already rendered with the same count, the DOM is restored from cache (fast innerHTML set + re-highlight) instead of rebuilt from scratch. Guard: the cache is only used on cross-session navigation (sid !== current). In-session updates (new messages, edits, tool_complete, stream events) always get a full rebuild — no stale content is ever shown. Cache is capped at 30 sessions and evicts oldest-first to bound memory. Co-authored with Claude Sonnet 4.6 / Anthropic. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 < 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> |
||
|
|
e3607855b1 |
fix: poll /health after update instead of blind setTimeout — v0.50.158 (closes #874)
Replaces blind setTimeout reload with /health polling loop. Banner shows restart status with manual Reload button. Works behind reverse proxies. 25 regression tests. |
||
|
|
201235d807 |
fix: live-fetched portal models route through configured provider — v0.50.153 (closes #854)
_fetchLiveModels() applies @provider: prefix to model IDs from portal providers. |
||
|
|
256b3fbbdf |
fix: image_generate renders inline + auto-title strips thinking preamble — v0.50.152 (closes #853, #857)
MEDIA: restore renders all https:// URLs as img (closes #853). _strip_thinking_markup strips Qwen3 plain-text reasoning preambles (closes #857). |
||
|
|
5fa731ea4a |
release: v0.50.151 — credential_pool provider detection + Ollama Cloud support (PR #820 by @starship-s)
Surfaces providers added via credential_pool in the model dropdown. Ambient gh-cli tokens suppressed. _apply_provider_prefix helper extracted. Ollama Cloud display name + dynamic model list. looksLikeBareOllamaId heuristic tightened. Test isolation fixed. PR #820 by @starship-s. |
||
|
|
1239129ae2 |
fix(models): stale cross-provider model no longer shows as unavailable in picker (closes #829)
* 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> |
||
|
|
859602340e |
fix: streaming race conditions (#631) + blank-page workspace binding (#804)
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. |
||
|
|
8f1f582caf |
fix: BYOK/custom provider models missing from WebUI model dropdown (#815)
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. |
||
|
|
a4d59b9e6c |
fix: update banner — conflict recovery path + server self-restart after update (#816)
* 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> |
||
|
|
811424a87b |
feat(reasoning): full /reasoning CLI parity — show|hide + effort levels via config.yaml (#812)
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(). |
||
|
|
bbc917a5c6 |
fix(renderer): stop " mangling inside code blocks (#801)
Closes #801. Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com> |
||
|
|
94a04ddd40 |
fix(ui): persist session queue to sessionStorage across page refresh (#768)
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 |