diff --git a/CHANGELOG.md b/CHANGELOG.md index 1870b5a9..8990028d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] +## [v0.50.254] — 2026-05-01 + +### Fixed +- **API 500 regression on /api/sessions, /api/memory: `_combined_redact` TypeError** (#1394, closes #1394) — PR #1387 follow-up `fc88981` started passing `force=True` to `redact_sensitive_text()`, but older hermes-agent builds don't accept the `force` kwarg. Every redaction call on the hot path crashed with `TypeError`, degrading the entire API to 500 errors. `_combined_redact` now wraps the call in `try/except TypeError` and falls back to the no-kwarg call. The local fallback (ghp_/sk-/hf_/AKIA) still runs unconditionally, so coverage doesn't regress. (`api/helpers.py`) @bergeouss — PR #1400, fixes #1394 + +- **Code block tree-view: newlines stripped from data-raw, jsyaml retry loop missing** (#1397, closes #1397) — Two bugs in the JSON/YAML tree-view renderer. (1) Browsers normalize newlines to spaces inside HTML attribute values (HTML spec); the `data-raw` attribute on `.code-tree-wrap` lost every newline, so multi-line YAML/JSON came out as single-line tree views. Fixed by encoding `\n` as ` ` before writing the attribute. (2) When jsyaml hadn't loaded yet, `initTreeViews()` set `data-tree-init=1` immediately and bailed — the lazy-load callback never re-invoked init, leaving the block in raw view forever. Fixed by removing `data-tree-init` and calling `_loadJsyamlThen(initTreeViews)` to retry after load. (`static/ui.js`) @bergeouss — PR #1400, fixes #1397 + +- **Credential permission fixer respects HERMES_HOME_MODE and HERMES_SKIP_CHMOD** (#1389, closes #1389) — `fix_credential_permissions()` was unconditionally forcing 0600 on every credential file in `HERMES_HOME` at startup. Docker setups that intentionally use group bits (e.g. `HERMES_HOME_MODE=0640` for shared volumes) had their declared mode silently overridden. Now `HERMES_SKIP_CHMOD=1` bypasses the fixer entirely; when `HERMES_HOME_MODE` is set, the fixer only strips world bits (0o007) and preserves operator-declared group access. (`api/startup.py`) @bergeouss — PR #1400, fixes #1389 + +- **Sidebar session click is now instant on mouse, drag-aware on touch** (#1398) — clicking a chat in the sidebar previously had a 300ms delay on every device to disambiguate single-tap from double-tap-rename. Mouse users perceived this as lag. Now the delay is 0 for `pointerType==='mouse'` and stays 300ms for touch (where it's needed for tap-vs-drag disambiguation). Adds pointermove drag detection: movement >5px from pointerdown marks the gesture as a drag, cancels the pending tap timer, suppresses hover highlighting via a `.dragging` class, and clears 50ms after release so the row doesn't flash hover mid-scroll. (`static/sessions.js`, `static/style.css`) @JKJameson — PR #1398 + +- **Per-tab session URL anchors via `/session/`** (#1392) — replaces the cross-tab `localStorage['hermes-webui-session']` active-session bus with per-tab URL ownership. Each tab anchors its active conversation in the path (`/session/`), so two tabs viewing different sessions can no longer yank each other around when localStorage changes. The `` script in `static/index.html` stops at the `/session/` marker so subpath mounts (`/myapp/session/`) still resolve assets correctly; all `new URL('api/...', location.href)` calls migrated to `document.baseURI||location.href` for the same reason. New helpers `_sessionIdFromLocation()`, `_sessionUrlForSid()`, `_setActiveSessionUrl()` in `sessions.js`. Lineage-aware active highlighting (`_sessionLineageContainsSession`) keeps a forked session highlighted even when collapsed inside a parent lineage row. The `popstate` handler navigates between sessions via browser back/forward but refuses to switch mid-stream (`S.busy` guard, mirroring the cross-tab storage handler). The cross-tab storage handler was deliberately defanged so it only re-renders the sidebar — it no longer force-loads the new sid into the current tab. (`api/routes.py`, `static/boot.js`, `static/commands.js`, `static/index.html`, `static/messages.js`, `static/sessions.js`, `static/terminal.js`, `static/ui.js`, `static/workspace.js`, `tests/test_session_cross_tab_sync.py`, `tests/test_session_lineage_collapse.py`) @dso2ng — PR #1392 + +### Changed +- **Settings toggle: "Show CLI sessions" → "Show non-WebUI sessions"** (#1407) — the old label was misleading: the feature surfaces conversations from CLI, Telegram, Discord, Slack, WeChat, and other non-WebUI channels — not just CLI. The new label captures the actual scope. Pure rename across all 8 locales (en, zh, zh-Hant, ru, es, de, pt, ko); underlying logic untouched. Reordered channel examples by global adoption (Telegram, Discord, Slack first; WeChat de-emphasized). (`static/i18n.js`, `static/index.html`, `tests/test_korean_locale.py`) @franksong2702 — PR #1407 + +- **`popstate` handler refuses to switch sessions mid-stream** — Opus pre-release follow-up. Mirrors the same `S.busy` guard the cross-tab storage handler had. A user mid-stream who absent-mindedly hits browser Back used to lose their active turn (PR #1392 introduced the popstate listener without the guard). Now shows a toast and stays on the current session. 1 regression test in `test_v050254_opus_followups.py`. (`static/sessions.js`) + + ## [v0.50.253] — 2026-05-01 ### Added diff --git a/static/sessions.js b/static/sessions.js index b460c6d7..4072f42c 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1733,7 +1733,15 @@ if(typeof window!=='undefined'){ window.addEventListener('storage', (e) => { void _handleActiveSessionStorageEvent(e); }); window.addEventListener('popstate', () => { const sid=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null; - if(sid && (!S.session || S.session.session_id!==sid)) void loadSession(sid); + if(!sid || (S.session && S.session.session_id===sid)) return; + // Refuse to switch sessions mid-stream — same UX guard the storage-event + // handler had. A user mid-turn who hits browser Back should NOT lose the + // active stream. They can hit Back again once the turn ends. + if(S.busy){ + if(typeof showToast==='function') showToast('Finish the current turn before switching sessions.',3000); + return; + } + void loadSession(sid); }); } diff --git a/tests/test_v050254_opus_followups.py b/tests/test_v050254_opus_followups.py new file mode 100644 index 00000000..9e862c1b --- /dev/null +++ b/tests/test_v050254_opus_followups.py @@ -0,0 +1,35 @@ +"""Regression tests for v0.50.254 Opus pre-release follow-ups. + +Apr 2026 v0.50.254 batch added per-tab session URL anchors (#1392). Opus advisor +flagged that the new popstate handler was missing the same `S.busy` guard the +storage-event handler had — a user mid-stream who hits browser Back would lose +their active turn the same way cross-tab churn used to do. Adds the guard. +""" + +from __future__ import annotations + +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] + + +def test_popstate_handler_guards_busy_state(): + """Browser back/forward must not switch sessions while a stream is live. + + The new `popstate` handler in `static/sessions.js` (added by #1392) has to + mirror the `S.busy` guard that the cross-tab storage handler had. Otherwise + a user mid-stream who absent-mindedly hits Back will get yanked out of their + active turn — exactly the regression the storage-event guard was added to + prevent. + """ + src = (REPO / "static" / "sessions.js").read_text(encoding="utf-8") + popstate_idx = src.find("addEventListener('popstate'") + assert popstate_idx != -1, "popstate handler missing from sessions.js" + # Look at the next ~600 chars of the handler body. + body = src[popstate_idx : popstate_idx + 600] + assert "S.busy" in body, ( + "popstate handler must check S.busy before calling loadSession() — " + "otherwise mid-stream users lose their turn when they hit browser Back. " + "Mirror the same guard the cross-tab storage handler had." + ) + assert "loadSession" in body, "popstate handler must call loadSession when allowed"