From 2a40ca4240cef135dbf3f7374292cb08ce155aad Mon Sep 17 00:00:00 2001 From: Brett Yelverton Date: Wed, 24 Jun 2026 16:36:32 +0000 Subject: [PATCH] fix(streaming): restore mobile scroll-jank guard during live renders A later streaming parse-cache change dropped the #MOBILESCROLL guard from the live-stream render tick while leaving the helper and CSS in place. On iOS PWA, Safari can paint a transient scrollTop=0 frame during streamed DOM rebuilds, forcing the reader to the first/oldest message. Re-add _fixMobileScrollJank() before streaming DOM work begins and pin it with a regression test so the third layer of the mobile scroll fix cannot be silently dropped again. --- static/messages.js | 3 +++ ...st_issue1360_streaming_scroll_hardening.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/static/messages.js b/static/messages.js index 50ca2ece1..211d1dd77 100644 --- a/static/messages.js +++ b/static/messages.js @@ -3844,6 +3844,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ _renderPending=false; // Guard: a pending setTimeout+rAF can outlive stream finalization. if(_streamFinalized) return; + // Mobile scroll-jank guard: enable overflow-anchor before DOM writes so + // the browser preserves scroll position during streaming content growth. + if(typeof window._fixMobileScrollJank==='function') window._fixMobileScrollJank(); _lastRenderMs=performance.now(); const parsed=_cachedParsed&&_cachedParsedText===assistantText&&_cachedParsedReasoning===liveReasoningText ? _cachedParsed : _parseStreamState(); _cachedParsed=null; diff --git a/tests/test_issue1360_streaming_scroll_hardening.py b/tests/test_issue1360_streaming_scroll_hardening.py index b50f72fa8..4f91aeaab 100644 --- a/tests/test_issue1360_streaming_scroll_hardening.py +++ b/tests/test_issue1360_streaming_scroll_hardening.py @@ -4,6 +4,7 @@ from pathlib import Path REPO = Path(__file__).parent.parent UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") +MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8") STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8") @@ -43,6 +44,28 @@ def test_messages_scroller_uses_overflow_anchor_auto_on_mobile(): ) +def test_streaming_render_enables_mobile_scroll_jank_guard_before_dom_writes(): + """Streaming must re-enable mobile scroll anchoring before every DOM write. + + Regression: #MOBILESCROLL originally added _fixMobileScrollJank() in the + live-stream render tick, but a later streaming parse-cache change dropped + that call while leaving the helper/CSS in place. On iOS PWA this lets Safari + paint a transient scrollTop=0 frame during streamed DOM rebuilds. + """ + guard_idx = MESSAGES_JS.find("window._fixMobileScrollJank") + assert guard_idx != -1, ( + "attachLiveStream() must call window._fixMobileScrollJank() during the " + "streaming render tick, before DOM writes, so iOS PWA does not jump to " + "the first/oldest message while assistant output streams." + ) + + render_idx = MESSAGES_JS.find("_lastRenderMs=performance.now()") + assert render_idx != -1, "streaming render timestamp not found" + assert guard_idx < render_idx, ( + "The mobile scroll-jank guard must run before streaming DOM work begins." + ) + + def test_scroll_repin_dead_zone_is_wider_for_mac_app_windows(): assert "clientHeight<250" in UI_JS or "bottomDistance<250" in UI_JS, ( "The near-bottom re-pin threshold should be at least 250px so small "