From ef6901c947cbbb90245d9502a4017788fa28f5f5 Mon Sep 17 00:00:00 2001 From: allenliang2022 Date: Fri, 26 Jun 2026 01:35:07 +0800 Subject: [PATCH 1/2] fix(chat): ignore DOM-wipe scroll clamp during render --- static/ui.js | 8 +++++++ ...est_issue4856_android_scroll_regression.py | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/static/ui.js b/static/ui.js index 58635ada..e3fb3c4c 100644 --- a/static/ui.js +++ b/static/ui.js @@ -11316,6 +11316,13 @@ function renderMessages(options){ // Mobile scroll-jank fix: temporarily disable overflow-anchor so Chromium // cannot re-anchor to the topmost row during the DOM wipe-and-rebuild gap. if(window._fixMobileScrollJank) window._fixMobileScrollJank(); + // The DOM wipe can briefly collapse #msgInner to zero height, causing the + // browser to clamp #messages.scrollTop to 0 and emit a scroll event. That + // event is a render artifact, not user intent; if the scroll listener sees it + // with _programmaticScroll=false, it marks the reader manually unpinned and + // the live reply stops following / appears to jump backward. + _programmaticScroll=true; + _programmaticScrollSetAt=performance.now(); inner.innerHTML=''; const compressionNode=compressionState?_compressionCardsNode(compressionState):null; const {message:referenceMessage, rawIdx:referenceMessageRawIdx}=_latestCompressionReferenceMessage( @@ -12524,6 +12531,7 @@ function renderMessages(options){ } _updateMessageVirtualMeasurements(renderVisWithIdx, renderVisibleIdxs, virtualWindow); _recycleStash.clear(); + if(typeof _deferClearProgrammaticScroll==='function') _deferClearProgrammaticScroll(160); } function _toolDisplayName(tc){ diff --git a/tests/test_issue4856_android_scroll_regression.py b/tests/test_issue4856_android_scroll_regression.py index f765dbb2..a1c3f60a 100644 --- a/tests/test_issue4856_android_scroll_regression.py +++ b/tests/test_issue4856_android_scroll_regression.py @@ -83,6 +83,27 @@ def test_rebuild_path_calls_fix_before_wipe(): ) +def test_rebuild_path_marks_dom_wipe_scroll_as_programmatic(): + # During innerHTML='' the scroller can transiently collapse to clientHeight + # and clamp scrollTop to 0. That browser event must be suppressed as + # programmatic; otherwise the scroll listener treats it as user upward + # intent and disables live auto-follow. + fix_idx = UI_JS.find("window._fixMobileScrollJank()") + assert fix_idx != -1, "renderMessages() guard call not found" + wipe_idx = UI_JS.find("innerHTML=''", fix_idx) + assert wipe_idx != -1, "innerHTML='' not found after _fixMobileScrollJank()" + window = UI_JS[fix_idx:wipe_idx] + assert "_programmaticScroll=true" in window, ( + "renderMessages() must mark the DOM wipe/rebuild scroll event as " + "programmatic before innerHTML='' can clamp scrollTop." + ) + assert "_programmaticScrollSetAt=performance.now()" in window + assert UI_JS.find("_deferClearProgrammaticScroll(160)", wipe_idx) != -1, ( + "renderMessages() must clear the programmatic-scroll suppression after " + "the rebuild/post-render paint window." + ) + + def test_streaming_tick_calls_fix_before_dom_writes(): # The streaming render tick in messages.js must call _fixMobileScrollJank() # before _lastRenderMs=performance.now() so anchor suppression covers every From 2c1b0f00fd7eb3c94dd88a3ecc7f7a2e1f042d0b Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 25 Jun 2026 20:35:20 +0000 Subject: [PATCH 2/2] Release XR (v0.51.662): ignore DOM-wipe scroll clamp during render (#4934) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12955813..f65d67a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.662] — 2026-06-25 — Release XR (live replies keep following during transcript rebuilds) + +### Fixed + +- **A streaming reply no longer stops following (or appears to jump backward) when the transcript rebuilds mid-stream.** `renderMessages` rebuilds the message pane by clearing it (`innerHTML=''`), and during that brief gap the scroll container can collapse and the browser clamps `scrollTop` to 0, emitting a scroll event. The scroll listener was interpreting that render artifact as a deliberate user scroll-up and unpinning the reader, so the live reply stopped auto-following. The DOM-wipe scroll is now marked programmatic and ignored; genuine user scroll-up (wheel/touch) during a render is still detected and still unpins. Thanks @allenliang2022. (#4934) + ## [v0.51.661] — 2026-06-25 — Release XQ (no more fabricated multi-hour processing time) ### Fixed