Commit Graph

146 Commits

Author SHA1 Message Date
nesquena-hermes bbdacdca5c fix: context window indicator overflow (#1356)
- 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>
2026-04-30 21:32:45 +00:00
fxd-jason 1df89e7a52 fix(ui): show context indicator percentage without explicit context_length (#1349)
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>
2026-04-30 18:31:30 +00:00
nesquena-hermes d4b055c30b fix(streaming+ui): preserve user message on cancel + persist activity-panel expand state (#1298)
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>
2026-04-30 16:18:41 +00:00
nesquena-hermes 1fa740d32f feat(chat): render fenced code blocks in user messages (#1325)
From PR #1335.

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-04-30 16:18:02 +00:00
nesquena-hermes fbe84d26e6 fix(ui+pwa): avoid stale Mermaid render errors and bust cached static asset URLs on every release
From PR #1337.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 16:18:01 +00:00
nesquena-hermes 4683a4a0d0 fix(models): default model rehydration when providers share slash-qualified IDs (#1313)
From PR #1326.

Co-authored-by: hacker2005 <chen20057275@outlook.com>
2026-04-30 15:24:35 +00:00
nesquena-hermes e86de0aff3 fix(ui): show configured fallback models missing from catalog
From PR #1322.

Co-authored-by: renatomott <renato.mott@gmail.com>
2026-04-30 15:24:34 +00:00
nesquena-hermes 1ccd958e23 fix(ui): avoid duplicate header copy buttons (#1096)
From PR #1324.

Co-authored-by: Dennis Soong <dso2ng@gmail.com>
2026-04-30 15:24:32 +00:00
nesquena-hermes 3f838fc31a release: v0.50.244 (#1308)
release: v0.50.244

Batch release of 4 PRs:

- #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API.
  Per-message speaker button + auto-read toggle + voice/rate/pitch in
  Settings. localStorage-only state. Closes #499.

- #1304 — Stale saved session 404 cleanup + structured api() errors.
  Salvaged from #1084. Independently approved on 358275e.

- #1306 — Cmd/Ctrl+K works while a conversation is busy.
  Salvaged from #1084. Independently approved on 2e8a239.

- #1307 — Sienna skin (warm clay & sand earth palette).
  Salvaged from #1084. Independently approved on 5cd79c8.

Tests: 3290 passed, 2 skipped, 3 xpassed, 0 failures (was 3254; +36 tests).

Independently reviewed and approved by nesquena (commit 47f0e0d). End-to-end
trace verified the TTS flow; security audit confirmed SpeechSynthesisUtterance
is plain-text-only with no XSS surface; behavioural harness confirmed
_stripForTTS handles all 12 markdown-stripping cases; bounds clamping on
rate/pitch verified; opt-in behavior verified.
2026-04-29 21:34:27 -07:00
nesquena-hermes ded9b7e1c4 release: v0.50.243 (#1302)
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).
2026-04-29 21:06:30 -07:00
nesquena-hermes 0ad95cb16a release: v0.50.241 (#1293)
release: v0.50.241

Batch release of 4 PRs:

- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
  speed controls and HTTP byte-range streaming. PDF/media previews in
  workspace file browser. Composer tray inline players for audio/video.
  (Rebased from #1232.)

- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
  the model picker, carried through to the composer chip. Persists through
  on-disk model cache.

- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
  Settings; inline Saving / Saved / Failed status. Font size now persists
  to config.yaml. Refs #1003.

- #1294 (@franksong2702) — Normalize agent session source metadata
  (raw_source / session_source / source_label) through /api/sessions and
  gateway watcher SSE snapshots. Existing source_tag / is_cli_session
  fields preserved. Refs #1013.

Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).

Independently reviewed and approved by nesquena (commit d1738f6).
2026-04-29 19:54:07 -07:00
nesquena-hermes 33a145a669 release: v0.50.240
## Release v0.50.240

Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures).

---

### Added

- **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282
- **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482
- **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479
- **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485
- **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481
- **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568
- **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281
- **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268
- **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269

### Fixed

- **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266
- **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278
- **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267
- **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273

---

### Test results

```
3199 passed, 2 skipped, 3 xpassed in 72.79s
```

### PRs on hold (not included)

#1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-29 17:42:32 -07:00
Hermes Agent 8e546c0273 Merge remote-tracking branch pr/1260 into stage/batch-v0.50.239 2026-04-29 15:55:51 +00:00
Hermes Agent 4ee80425f2 Merge remote-tracking branch 'refs/remotes/pr/1229' into stage/batch-v0.50.238 2026-04-29 15:17:57 +00:00
Brian f65f488635 fix(renderer): render h4-h6 markdown headings (####, #####, ######)
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
2026-04-29 23:15:59 +08:00
Hermes Agent 2bb0af49f2 Merge remote-tracking branch pr/1254 into stage/batch-v0.50.238 2026-04-29 15:10:22 +00:00
bergeouss 6a17e4cc0c fix(ui): add touch toggle support for context tooltip on mobile
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
2026-04-29 04:59:00 +00:00
Frank Song 60a4cb057e Add embedded workspace terminal 2026-04-29 04:35:11 +00:00
bergeouss 29a31a6e26 fix(484): lazy-load js-yaml CDN for YAML tree view, add parse_failed_note i18n 2026-04-29 04:34:27 +00:00
bergeouss 49a2a424d5 feat: collapsible JSON/YAML tree viewer (#484)
- Fenced code blocks with json/yaml lang get Tree/Raw toggle
- Recursive DOM builder (_buildTreeDOM) with type-colored values
  (green strings, blue numbers, amber booleans, muted nulls)
- Auto-collapse at depth 2+, default tree for >=10 lines blocks
- YAML parsing via js-yaml (lazy, CDN-loaded)
- CSS: tree-view, tree-node, collapsible, type-colored classes
- i18n: tree_view, raw_view keys in all 7 locales
- 14 tests: renderer, types, collapse, CSS, i18n

Closes #484
2026-04-29 04:34:26 +00:00
Frank Song 6f37da38a6 Clarify model scope in composer and settings 2026-04-29 04:33:29 +00:00
bergeouss 1f602b47ec fix: add file size cap and error i18n keys for diff viewer (#1234)
- 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
2026-04-29 04:33:25 +00:00
bergeouss 9a371f06f5 feat: inline diff/patch viewer (#483)
- 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
2026-04-29 04:33:25 +00:00
bergeouss 63dee0a87c feat(#524): add compress affordance to context ring tooltip
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
2026-04-29 04:33:09 +00:00
bergeouss 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.
2026-04-29 04:31:59 +00:00
bergeouss 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.
2026-04-29 04:31:58 +00:00
bergeouss 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
2026-04-29 04:31:58 +00:00
starship-s 03b7714f65 docs: note Lucide source for composer icons 2026-04-29 04:31:56 +00:00
starship-s 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
2026-04-29 04:31:55 +00:00
starship-s b57134bf2b ui: reflect explicit busy slash command in send button 2026-04-29 04:31:55 +00:00
starship-s 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)
2026-04-29 04:31:54 +00:00
starship-s 96182e5f51 fix: keep busy-input send available on mobile 2026-04-29 04:31:54 +00:00
fxd-jason 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
2026-04-29 04:31:16 +00:00
fxd-jason 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
2026-04-29 04:31:15 +00:00
fxd-jason 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).
2026-04-29 04:31:14 +00:00
bergeouss 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
2026-04-29 04:31:11 +00:00
yzp12138 f35d7786e5 fix: send image uploads as native multimodal inputs 2026-04-28 23:18:51 +08:00
bergeouss 0a0513c9d3 feat(#524): add compress affordance to context ring tooltip
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
2026-04-28 10:47:12 +00:00
nesquena-hermes 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>
2026-04-27 21:39:30 -07:00
nesquena-hermes 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.
2026-04-27 18:40:13 -07:00
nesquena-hermes 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.
2026-04-27 17:43:36 -07:00
nesquena-hermes 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)!
2026-04-27 16:27:03 -07:00
nesquena-hermes 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)!
2026-04-27 15:28:19 -07:00
nesquena-hermes 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
2026-04-27 13:34:59 -07:00
nesquena-hermes 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.
2026-04-27 11:43:32 -07:00
nesquena-hermes 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>
2026-04-26 21:04:38 -07:00
nesquena-hermes 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>
2026-04-26 15:29:02 -07:00
nesquena-hermes 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>
2026-04-26 10:36:59 -07:00
nesquena-hermes 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 &gt;/&lt;/&amp; 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
  &gt;-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 &gt; prefix
  and &gt;-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>
2026-04-25 23:08:59 -07:00
nesquena-hermes 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 04e7b53 changed the blockquote prefix-strip regex from
  /^>[ \t]?/   (consume "> ", "\t>", or just ">")
to
  /^>[\t]?/    (only consume "\t>" or just ">")

The space character was dropped from the character class. Since
practically every blockquote an LLM produces is "> " (greater-than
followed by a space), this leaves a leading space artifact on every
stripped blockquote line. Worse, the leading space breaks the
list-detection regex `^(?:  )?[-*+] ` inside the new `_applyBlockquotes`
helper — that regex requires either zero or two leading spaces, never
one — so the new "list inside blockquote" feature never fired for
the canonical input shape `> - item`.

Reproducer (against the actual ui.js via node, before the fix):
  > Hello world         → <blockquote> Hello world</blockquote>
                                       ^ phantom leading space
  > Steps:              → <blockquote>Steps:
  > - one                  - one
  > - two                  - two</blockquote>
                          ^ literal text, NOT a <ul>; lists-in-quote feature broken
  > - [x] done          → blockquote with literal "[x] done", no checkbox span

Tests passed despite the bug because tests/test_blockquote_rendering.py
and tests/test_renderer_comprehensive.py validate against a Python
mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e.
the mirror is correct, the JS is not, and the static-mirror tests
can't catch the divergence. Same shape of bug as commit 94d63d0
(phantom <br> in trailing line) where the mirror was right and the JS
was wrong.

Fix: restore the space character in the strip regex's character class.

Add tests/test_renderer_js_behaviour.py — 11 tests that drive the
ACTUAL renderMd via node and assert on rendered output for the most
common LLM shapes (single-line quote, multi-line quote, list inside
quote, task list inside quote, nested >>>, strikethrough inside and
outside quote, top-level task list, quote followed by heading,
multi-paragraph quote with list, CRLF normalisation).

Verified: the buggy regex makes 6 of those 11 tests fail; the corrected
regex makes all 11 pass.

Suite: 2354 passed, 0 new failures.

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

* Collapse agent session compression chains

* Restore upstream changelog entries

* fix(agent_sessions): bubble active compression chains to top by tip last_activity

The original PR merge kept the chain head's id/title/started_at and overrode
id/model/message_count/ended_at/end_reason from the tip — but did NOT override
last_activity. Since the projected list is sorted by last_activity DESC and
the WebUI sidebar surfaces updated_at = last_activity, an actively-used
compression chain whose tip is being edited NOW would sort by the ROOT's
old last_activity and fall below recently touched standalone sessions.

Reproducer (with the harness against actual code, before the fix):
  - root: started 30 days ago, last msg 30 days ago
  - tip:  started 28 days ago (parent_session_id=root), last msg 5 seconds ago
  - standalone: last msg 2 days ago

  Sidebar order with original PR:
    [0] standalone  (48h ago)
    [1] active_tip  (last_activity=root's 720h ago)  ← wrong

  Sidebar order after fix:
    [0] active_tip  (last_activity=tip's 0h ago)     ← correct
    [1] standalone  (48h ago)

This matches Hermes Agent's own list_sessions_rich projection at
hermes_state.py:903-909, which overrides "last_active" from the tip
exactly so that the agent CLI's session list orders the same way.

Add ``last_activity`` to the merge-from-tip key list, update the existing
test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to
expect tip-derived updated_at, and add
test_compression_chain_bubbles_to_top_by_tip_activity locking in the
bubble-to-top invariant — without this regression test the previous
behaviour passed CI because no test exercised the sort order against a
mixed set of chains and standalone sessions.

The chain head's started_at (created_at) and title remain preserved, so
users can still find the conversation by its original date and name.

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

* docs: v0.50.216 release notes and version bump

Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix.

* chore: gitignore local-only review harness directory

Adds .local-review/ to .gitignore so renderer drivers, sample inputs,
fixture builders, and other reviewer scratch files do not accidentally
get committed. Nothing under that path is ever shared in the repo;
keeping the entry tracked makes the boundary explicit for any future
contributor who creates the directory locally.

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

* Keep reasoning chip visible for None effort

* test(reasoning): pin chip render output via node, not just source regex

The PR's static checks in test_reasoning_chip_btw_fixes.py validate the
shape of _applyReasoningChip (no display='none' literal, the right
classList.toggle call exists, the right label literals are in the
function body) but pass even if the runtime detail is wrong — for
example if `inactive` were inverted, _normalizeReasoningEffort
mishandled whitespace, or _formatReasoningEffortLabel returned the
wrong literal for an unknown input.

Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive
the actual _applyReasoningChip() via node and assert on the rendered
DOM state for each effort value:

  TestChipAlwaysVisible
    - empty / null  -> "Default" label, inactive=true
    - "none"        -> "None" label, inactive=true
    - "low"/"high"  -> verbatim label, inactive=false
  TestNormalizationEdgeCases
    - "NONE"        -> normalises to "None"
    - "  none  "    -> trims and normalises
    - unknown junk  -> falls through visible, never hidden
  TestTitleAttributeAccessibility
    - title attribute carries the human-readable label for tooltip /
      screen-reader use

Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught).
Against this PR's ui.js: 11/11 pass.

This pattern (drive the actual JS via node) caught two regex-only
regressions in PR #1073 where the Python mirror was correct while the
JS was broken. Same protection added here so the chip-visibility
contract can't silently break in a future refactor.

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

* docs: add #1074 to v0.50.216 changelog, bump test count to 2428

* fix(i18n): restore broken Unicode in Russian and Spanish steer strings

Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally
stripped the `\u` prefix from Unicode escape sequences in two locales,
producing garbled literal hex strings visible to users:

  Spanish (es):
    - cmd_steer:                   correcci00f3n  → corrección
    - cmd_steer_fallback:          2014 en cola   → — en cola
    - busy_steer_fallback:         2014 en cola   → — en cola
    - settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección
    - settings_busy_input_mode_steer: correcci00f3n  → corrección

  Russian (ru):
    - settings_desc_busy_input_mode: the entire Cyrillic string was
      replaced with raw 4-hex-char code-points without the \u prefix
      (041e043f... instead of actual Cyrillic). Decoded:
      "Определяет поведение при отправке сообщения во время работы
      агента. Очередь ждёт; Прерывание отменяет и начинает заново;
      Steer внедряет коррекцию без прерывания."

Fix: write the correct characters directly (UTF-8 is the file encoding
so embedding them literally is cleaner than \u escapes for long text).

All other locales (en, de, zh, zh-Hant) were not affected — confirmed
by grepping for bare hex run-ons in the updated file.

Verified: node --check static/i18n.js passes; full pytest suite green
(2365 passed, 47 skipped).

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

* docs: remove duplicate compression chain entry from [Unreleased]

---------

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>
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-25 21:06:31 -07:00