fix(ui): clear session search on boot + autocomplete=off + pageshow bfcache handler (closes #822)
* 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) <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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<span data-i18n="new_conversation">New conversation</span> <span style="font-size:10px;opacity:.5;margin-left:4px">Cmd+K</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="session-search"><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()"></div>
|
||||
<div class="session-search"><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()" autocomplete="off"></div>
|
||||
<div class="session-list" id="sessionList"></div>
|
||||
</div>
|
||||
<!-- Tasks (cron) panel -->
|
||||
|
||||
137
tests/test_session_search_bfcache_822.py
Normal file
137
tests/test_session_search_bfcache_822.py
Normal file
@@ -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'<input[^>]*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"
|
||||
)
|
||||
Reference in New Issue
Block a user