From 880085a09ea23a4d6ee531e6f8cf0292f8eb8a96 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 21 Apr 2026 22:11:32 -0700 Subject: [PATCH] fix(ui): clear session search on boot + autocomplete=off + pageshow bfcache handler (closes #822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): clear session search on boot + autocomplete=off — prevents bfcache from restoring stale filter (closes #822) * fix(ui): add pageshow handler for true bfcache restore case (#822 completion) The original PR's two fixes cover fresh page loads and hard reloads — but the bug the issue describes happens on *bfcache restore* (Chrome's back-forward cache). The async boot IIFE does NOT re-run when the browser restores a page from bfcache; the DOM is restored in place, including any stale #sessionSearch value. The boot-time clear has no effect there. `autocomplete="off"` is a hint that Chrome and others sometimes honour for bfcache but is not reliable for user-typed values (as opposed to autofill candidates). Add a pageshow event listener that checks event.persisted === true and, on that path only, clears #sessionSearch and re-renders from cache. Fresh loads skip the listener (persisted=false) and continue to be handled by the boot IIFE. Also added tests/test_session_search_bfcache_822.py with 7 tests: - autocomplete="off" present on the input - boot-time clear runs before the first renderSessionList - pageshow listener registered - handler guards on event.persisted - handler clears the search field and triggers a re-render Full suite: 1745 passed, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: nesquena-hermes Co-authored-by: Nathan Esquenazi Co-authored-by: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 9 ++ static/boot.js | 20 ++++ static/index.html | 2 +- tests/test_session_search_bfcache_822.py | 137 +++++++++++++++++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 tests/test_session_search_bfcache_822.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a801f83a..255366fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Hermes Web UI -- Changelog +## [v0.50.141] — 2026-04-22 + +### Fixed +- **Session list appears empty after browser reload / version update** — Chrome's + bfcache was restoring a prior search query into `#sessionSearch` on page restore, + causing `renderSessionListFromCache()` to silently filter out all sessions (including + newly created ones). Added `autocomplete="off"` to the search input and an explicit + value-clear at boot before the first render. Closes #822. (#829) + ## [v0.50.140] — 2026-04-22 ### Fixed diff --git a/static/boot.js b/static/boot.js index e4c58314..85637015 100644 --- a/static/boot.js +++ b/static/boot.js @@ -829,6 +829,11 @@ function applyBotName(){ _initResizePanels(); // Workspace panel restore happens AFTER loadSession so we know if // the session has a workspace — prevents the snap-open-then-closed flash (#576). + // Fix #822: clear any browser-restored value before first render. This + // covers fresh page loads and reloads. The bfcache restore case is handled + // separately below by a `pageshow` listener — the async IIFE here does NOT + // re-run when the browser restores the page from bfcache. + const _srch = document.getElementById('sessionSearch'); if (_srch) _srch.value = ''; const saved=localStorage.getItem('hermes-webui-session'); if(saved){ try{ @@ -851,3 +856,18 @@ function applyBotName(){ // Start real-time gateway session sync if setting is enabled if(typeof startGatewaySSE==='function') startGatewaySSE(); })(); + +// Fix #822 (bfcache path): when the browser restores the page from the +// back-forward cache, the async boot IIFE above does NOT re-run, but the +// DOM — including any stale value in #sessionSearch — IS restored. A +// prior search string would silently hide all sessions via the filter in +// renderSessionListFromCache(). Clear the field and re-render whenever +// the page is restored from cache (`event.persisted === true`). +window.addEventListener('pageshow', (event) => { + if (!event.persisted) return; // fresh loads are handled by the IIFE above + const _srch = document.getElementById('sessionSearch'); + if (_srch) _srch.value = ''; + if (typeof renderSessionListFromCache === 'function') { + try { renderSessionListFromCache(); } catch (_) {} + } +}); diff --git a/static/index.html b/static/index.html index a24e0443..968ed7b2 100644 --- a/static/index.html +++ b/static/index.html @@ -40,7 +40,7 @@ New conversation Cmd+K - +
diff --git a/tests/test_session_search_bfcache_822.py b/tests/test_session_search_bfcache_822.py new file mode 100644 index 00000000..ae715462 --- /dev/null +++ b/tests/test_session_search_bfcache_822.py @@ -0,0 +1,137 @@ +"""Tests for #822 — session list empty after browser reload / version update. + +Root cause (from Opus analysis): Chrome's bfcache restores a prior search query +into `#sessionSearch` on page restore; `renderSessionListFromCache()` reads that +field and applies it as a title filter — hiding every session. + +Fix: +- ``autocomplete="off"`` on the input hints to browsers not to restore the value +- Boot-time explicit `sessionSearch.value = ''` before the first render covers + fresh loads and hard reloads +- ``pageshow`` listener that checks ``event.persisted`` covers the true bfcache + restore case (where the async boot IIFE does NOT re-run) +""" +import pathlib +import re + +REPO = pathlib.Path(__file__).parent.parent + + +def read(rel): + return (REPO / rel).read_text(encoding='utf-8') + + +class TestSessionSearchAutocompleteAttribute: + """index.html must opt the session search input out of browser autocomplete/restore.""" + + def test_session_search_has_autocomplete_off(self): + src = read('static/index.html') + m = re.search(r']*id="sessionSearch"[^>]*>', src) + assert m, "#sessionSearch input tag not found in index.html" + tag = m.group(0) + assert 'autocomplete="off"' in tag or "autocomplete='off'" in tag, ( + "#sessionSearch must have autocomplete=\"off\" so browsers do not " + "restore a prior search query across reloads or bfcache restores (#822)" + ) + + +class TestBootClearsSessionSearch: + """Boot sequence must clear any browser-restored search value before + the first render, so the initial `renderSessionListFromCache` call sees + an empty filter and shows all sessions.""" + + def test_boot_clears_session_search_value_before_first_render(self): + src = read('static/boot.js') + # Must find a line that sets sessionSearch.value = '' at boot + assert re.search( + r"getElementById\(['\"]sessionSearch['\"]\)\s*;\s*if\s*\([^)]+\)\s*[^=]+\.value\s*=\s*['\"]{2}" + r"|sessionSearch[^=]+\.value\s*=\s*['\"]{2}", + src, + ), ( + "boot.js must clear #sessionSearch.value to '' before the first render " + "(before renderSessionList / loadSession) to avoid stale filter from " + "browser form restoration (#822)" + ) + + def test_boot_clear_is_before_first_render_call(self): + """The clear must precede the first renderSessionList call path so the + initial render shows an unfiltered list.""" + src = read('static/boot.js') + clear_pos = None + m = re.search(r"getElementById\(['\"]sessionSearch['\"]\)", src) + if m: + clear_pos = m.start() + assert clear_pos is not None, "session search clear not found in boot.js" + first_render_pos = src.find('renderSessionList()', clear_pos) + assert first_render_pos != -1, "renderSessionList() call not found after clear" + assert clear_pos < first_render_pos, ( + "sessionSearch clear must appear before the first renderSessionList() call" + ) + + +class TestPageShowBfcacheHandler: + """bfcache restore path: the async boot IIFE does NOT re-run when the + browser restores the page from back-forward cache, but the DOM — including + any stale value in #sessionSearch — IS restored. A `pageshow` listener + with `event.persisted` check is the only reliable way to clear on bfcache.""" + + def test_pageshow_listener_registered(self): + src = read('static/boot.js') + assert re.search( + r"addEventListener\(\s*['\"]pageshow['\"]", + src, + ), ( + "boot.js must register a `pageshow` event listener to handle " + "bfcache-restored page views (#822)" + ) + + def test_pageshow_handler_checks_event_persisted(self): + """Only bfcache restores set event.persisted=true; fresh loads have + it false and are already handled by the boot IIFE. Guarding prevents + clearing the search on every page show (which would wipe an in-progress + user filter if any other pageshow triggers happen).""" + src = read('static/boot.js') + m = re.search( + r"addEventListener\(\s*['\"]pageshow['\"].*?\}\s*\)", + src, + re.DOTALL, + ) + assert m, "pageshow listener body not found" + body = m.group(0) + assert 'persisted' in body, ( + "pageshow handler must guard on event.persisted so fresh loads " + "don't double-clear the field" + ) + + def test_pageshow_handler_clears_session_search(self): + src = read('static/boot.js') + m = re.search( + r"addEventListener\(\s*['\"]pageshow['\"].*?\}\s*\)", + src, + re.DOTALL, + ) + assert m + body = m.group(0) + assert 'sessionSearch' in body, ( + "pageshow handler must target #sessionSearch specifically" + ) + assert re.search(r"\.value\s*=\s*['\"]{2}", body), ( + "pageshow handler must set sessionSearch.value = ''" + ) + + def test_pageshow_handler_triggers_rerender(self): + """After clearing on bfcache restore, the cached DOM still shows the + filtered view. Re-rendering from cache with the now-empty filter + repopulates the list.""" + src = read('static/boot.js') + m = re.search( + r"addEventListener\(\s*['\"]pageshow['\"].*?\}\s*\)", + src, + re.DOTALL, + ) + assert m + body = m.group(0) + assert 'renderSessionListFromCache' in body or 'renderSessionList' in body, ( + "pageshow handler must re-render the list after clearing the filter " + "so the stale filtered DOM is replaced with the full list" + )