From b438976507c33564f77b22e89ad9515e38bcedc4 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 3 Jul 2026 23:04:41 +0000 Subject: [PATCH] gate-fix #5467 r2: force render when recovering from skeleton/error-banner DOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex re-gate found 2 more false-skips (DOM state OUTSIDE the signature path): - CORE: profile-switch skeleton stays visible if the new payload signature matches the last one (empty/same-shaped profile) — skip fires over the skeleton. - SILENT: stale 'Could not load conversations' banner persists when a transient fetch failure heals with identical rows. Fix: capture _hadSessionListSkeleton + _hadSessionListLoadError BEFORE clearing them, and disallow the identical-signature skip when either was set. + regression test. (messages.js:1462 finding is a base-comparison artifact for #5472/#5479/#5484, not in #5467's diff.) --- static/sessions.js | 12 +++++++++++- tests/test_webui_external_refresh_frontend.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/static/sessions.js b/static/sessions.js index 4d80fbbe..4775da99 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4436,6 +4436,11 @@ function _applySessionListPayload(sessData, projData){ _syncSessionAttentionSoundState(_allSessions); _pruneLineageReportCacheToVisibleSessions(_allSessions); _allProjects = projData.projects||[]; + // Capture the recovering-from-error state BEFORE clearing it: the error banner + // DOM was rendered outside the signature path, so if this payload heals with + // rows identical to the last render, the identical-signature skip below would + // leave the stale "Could not load conversations" banner on screen. (Codex #5467) + const _hadSessionListLoadError = !!_sessionListLoadError; _sessionListLoadError = null; _sessionListHasLoadedOnce = true; _markPollingCompletionUnreadTransitions(_allSessions); @@ -4457,6 +4462,7 @@ function _applySessionListPayload(sessData, projData){ // holds the CURRENT profile's rows. Clear the skeleton flag right before painting so this // authoritative render replaces the profile-switch skeleton — while unrelated renders that // fire before this point stay blocked by the guard in renderSessionListFromCache(). + const _hadSessionListSkeleton = _sessionListSkeletonActive; _sessionListSkeletonActive = false; // No-op fast path: if this payload renders identically to what is already on // screen (the common case for idle polls) and no entrance animation is @@ -4465,9 +4471,13 @@ function _applySessionListPayload(sessData, projData){ // renderSessionListFromCache directly and are unaffected. Guarded by the same // conditions renderSessionListFromCache bails on, so a bailed render never // caches a signature that would suppress the next real repaint. (#5455 WS2.4) + // NEVER skip when recovering from a skeleton or error-banner DOM state: those + // are rendered outside the signature path, so an identical-signature match + // would leave the skeleton/error on screen instead of the real list. (Codex #5467) const _canRenderNow = !_renamingSid && !_sessionActionMenu; + const _mustForceRender = _hadSessionListSkeleton || _hadSessionListLoadError; const _renderSig = _sessionListRenderSignature(); - if(_canRenderNow && !_sessionListRefreshAnimationPending && _renderSig && _renderSig===_lastSessionListRenderSig){ + if(_canRenderNow && !_mustForceRender && !_sessionListRefreshAnimationPending && _renderSig && _renderSig===_lastSessionListRenderSig){ // Preserve the per-refresh INFLIGHT cleanup that renderSessionListFromCache // would otherwise perform, then skip only the DOM rebuild. if(typeof _purgeStaleInflightEntries==='function') _purgeStaleInflightEntries(); diff --git a/tests/test_webui_external_refresh_frontend.py b/tests/test_webui_external_refresh_frontend.py index 03816568..62a4140e 100644 --- a/tests/test_webui_external_refresh_frontend.py +++ b/tests/test_webui_external_refresh_frontend.py @@ -176,6 +176,25 @@ def test_session_list_render_signature_changes_on_pending_running_and_attention( assert "JSON.stringify([" in block +def test_session_list_render_signature_does_not_skip_recovering_from_skeleton_or_error(): + """The render-skip must never fire when recovering from a skeleton or the + 'Could not load conversations' banner — both are rendered OUTSIDE the + signature path, so an identical-signature match (empty/same-shaped profile, + or a transient fetch failure healing with identical rows) would leave the + skeleton/error DOM on screen instead of the real list. (Codex #5467 re-gate) + """ + start = SESSIONS_JS.find("function _applySessionListPayload(") + assert start != -1 + body = SESSIONS_JS[start:start + 6000] + assert "const _hadSessionListSkeleton = _sessionListSkeletonActive;" in body + assert "const _hadSessionListLoadError = !!_sessionListLoadError;" in body + assert "const _mustForceRender = _hadSessionListSkeleton || _hadSessionListLoadError;" in body + assert "!_mustForceRender" in body, "the force flag must gate the identical-signature skip" + # captured BEFORE the respective clears + assert body.index("const _hadSessionListSkeleton") < body.index("_sessionListSkeletonActive = false;") + assert body.index("const _hadSessionListLoadError") < body.index("_sessionListLoadError = null;") + + def test_pwa_pull_to_refresh_refreshes_session_list_not_page_when_available(): assert "window.refreshSessionList('pull', {force:true, refreshActive:true})" in UI_JS assert "Promise.resolve(window.refreshSessionList('pull', {force:true, refreshActive:true})).catch(()=>{}).finally(_ptrReset)" in UI_JS