diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feebddb..341a2d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.592] — 2026-06-22 — Release UY (skip gateway poll when hidden/streaming + smd re-init) + +### Fixed + +- **The sidebar's gateway-poll fallback no longer burns CPU re-rendering while you're streaming or the tab is hidden, and long streamed responses render more efficiently.** The 5s gateway-poll fallback now skips its `renderSessionList` pass when the tab is hidden or a turn is actively streaming (it catches up immediately on tab refocus via a one-time `visibilitychange` listener, so no gateway updates are dropped). Separately, when the streaming markdown parser was torn down by a prior segment but `window.smd` is still available, the live renderer now recreates the parser on the cleared element instead of falling back to repeated full-`innerHTML` rebuilds — avoiding O(n²) DOM churn on long responses. Thanks @akrhin. (#4730) + ## [v0.51.591] — 2026-06-22 — Release UX (stop transcript jumping to first message on completion) ### Fixed diff --git a/static/messages.js b/static/messages.js index cf15b25b..2c1a02d0 100644 --- a/static/messages.js +++ b/static/messages.js @@ -3352,6 +3352,18 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ : _stripXmlToolCalls(assistantText.slice(segmentStart)); if(_smdParser){ _smdWrite(displayText); + } else if(window.smd){ + // Parser was nulled out (e.g. by a prior segment end) but smd is + // available — recreate it on the existing element. Uses the non-fade + // renderer to match standard rendering, avoiding O(n²) innerHTML + // churn on long responses (#4704). Clear any content the renderMd() + // fallback already wrote first: _smdNewParser resets _smdWrittenText to + // '' but does NOT clear the element, so a following _smdWrite(displayText) + // would append the full accumulated segment ON TOP of the existing + // fallback render and duplicate the live text. + assistantBody.innerHTML=''; + _smdNewParser(assistantBody, false); + if(_smdParser) _smdWrite(displayText); } else if(renderMd){ assistantBody.innerHTML=renderMd(displayText); } else { diff --git a/static/sessions.js b/static/sessions.js index 062fd639..1867768e 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4101,7 +4101,23 @@ if(typeof window!=='undefined') window.refreshSessionList = refreshSessionList; function startGatewayPollFallback(ms){ const intervalMs = Math.max(5000, Number(ms) || _gatewayFallbackPollMs); if(_gatewayPollTimer) clearInterval(_gatewayPollTimer); - _gatewayPollTimer = setInterval(() => { renderSessionList({deferWhileInteracting:true}); }, intervalMs); + _gatewayPollTimer = setInterval(() => { + // Skip poll when tab is hidden or a stream is active — saves CPU + // and avoids redundant DOM renders during active streaming (#4704). + if(typeof document !== 'undefined' && document.hidden) return; + if(typeof S !== 'undefined' && (S.busy || S.activeStreamId)) return; + renderSessionList({deferWhileInteracting:true}); + }, intervalMs); + // Visibility catch-up: refresh immediately when tab re-gains focus, + // so no gateway updates are dropped during hidden-skip periods. + if(typeof document !== 'undefined' && !document._hermesGatewayPollVisibilityHook){ + document.addEventListener('visibilitychange', () => { + if(!document.hidden && typeof renderSessionList === 'function'){ + void renderSessionList({deferWhileInteracting:false}); + } + }); + document._hermesGatewayPollVisibilityHook = true; + } } function stopGatewayPollFallback(){