diff --git a/CHANGELOG.md b/CHANGELOG.md index 48314f2b..ac53bbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.617] — 2026-06-24 — Release VX (fix mobile scroll-jank to top during streaming) + +### Fixed + +- **On mobile (iOS Safari / Android Chrome), the transcript no longer randomly jumps to the top while the assistant is streaming.** The message container carried a global `overflow-anchor: none` (needed for desktop virtual-scroll position stability), but on touch browsers that made the browser paint a frame at `scrollTop=0` during the render's DOM wipe-and-rebuild, scroll-janking the reader to the oldest message mid-stream. `overflow-anchor: none` is now scoped to desktop (hover + fine-pointer) via a media query — mobile keeps the browser's native scroll anchoring — and the render briefly re-enables anchoring on touch devices across the DOM-rebuild gap. Desktop behavior is unchanged. Thanks @akrhin. (#4815, #4818) + ## [v0.51.616] — 2026-06-24 — Release VW (harden recycled assistant turns + scrollbar-drag detection) ### Fixed diff --git a/static/style.css b/static/style.css index 365bd512..b9ec288e 100644 --- a/static/style.css +++ b/static/style.css @@ -1708,7 +1708,8 @@ .workspace-toggle-btn:disabled{opacity:.38;cursor:not-allowed;} .chip.model{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);} .messages-shell{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;} - .messages{flex:1;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;overflow-anchor:none;} + .messages{flex:1;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;overflow-anchor:auto;} +@media (hover:hover) and (pointer:fine){.messages{overflow-anchor:none;}} /* Overlay scroll controls so they do not affect the transcript's native scroll geometry. */ .scroll-to-bottom-btn{position:absolute;right:20px;bottom:16px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:10;transition:color .12s,border-color .12s,background .12s,transform .12s;} .scroll-to-bottom-btn:hover{color:var(--text);border-color:var(--border);background:var(--hover-bg);} diff --git a/static/ui.js b/static/ui.js index 10e3b631..965d41e2 100644 --- a/static/ui.js +++ b/static/ui.js @@ -11010,6 +11010,26 @@ function _restoreMessageScrollSnapshot(snapshot){ else requestAnimationFrame(()=>{ setTimeout(()=>{ _programmaticScroll=false; },0); }); } } +/** + * Mobile scroll-jank guard: temporarily re-enable overflow-anchor so the + * browser preserves scroll position across the innerHTML='' + DOM rebuild gap. + * Safari/Chrome on iOS/Android can paint a frame with scrollTop=0 between + * innerHTML='' and snapshot restore, scroll-janking the user to the top. + * Called from renderMessages() before the DOM wipe — not from streaming ticks, + * since CSS already gives overflow-anchor:auto on mobile via media query. + */ +window._fixMobileScrollJank=function _fixMobileScrollJank(){ + const el=document.getElementById('messages'); + if(!el) return; + // Desktop with a mouse: keep overflow-anchor:none (explicitly set by CSS). + // Mobile touch devices only: temporarily enable it. + if(window.matchMedia('(hover:hover) and (pointer:fine)').matches) return; + el.style.overflowAnchor='auto'; + requestAnimationFrame(()=>{ + if(el.style.overflowAnchor==='auto') el.style.overflowAnchor=''; + }); +}; + function _restoreMessageScrollSnapshotSameFrame(snapshot){ const el=$('messages'); if(!el||!snapshot) return; @@ -11244,6 +11264,11 @@ function renderMessages(options){ _recycleStash.set(Number(key), child); } } + // Mobile scroll-jank fix: temporarily re-enable overflow-anchor so browser + // preserves scroll position across the DOM wipe-and-rebuild gap. + // Safari/Chrome on iOS/Android can paint a frame with scrollTop=0 between + // innerHTML='' and snapshot restore, scroll-janking the user to the top. + if(window._fixMobileScrollJank) window._fixMobileScrollJank(); inner.innerHTML=''; const compressionNode=compressionState?_compressionCardsNode(compressionState):null; const {message:referenceMessage, rawIdx:referenceMessageRawIdx}=_latestCompressionReferenceMessage( diff --git a/tests/test_issue1360_streaming_scroll_hardening.py b/tests/test_issue1360_streaming_scroll_hardening.py index 3a59db8c..b50f72fa 100644 --- a/tests/test_issue1360_streaming_scroll_hardening.py +++ b/tests/test_issue1360_streaming_scroll_hardening.py @@ -22,10 +22,24 @@ def _extract_function(src: str, name: str) -> str: raise AssertionError(f"Could not extract {name}") -def test_messages_scroller_disables_browser_scroll_anchoring(): - assert "overflow-anchor:none" in STYLE_CSS, ( - "#messages must disable browser scroll anchoring so tool/card inserts " - "cannot yank the transcript while the user reads earlier content." +def test_messages_scroller_disables_browser_scroll_anchoring_on_desktop(): + # Desktop (mouse): overflow-anchor:none — tool/card inserts cannot yank + # the transcript while the user reads earlier content. + assert "@media (hover:hover) and (pointer:fine){.messages{overflow-anchor:none;}}" in STYLE_CSS, ( + "On desktop (mouse-driven devices) #messages must disable browser scroll " + "anchoring so tool/card inserts cannot yank the transcript. " + "On mobile (touch devices) overflow-anchor:auto is used instead to prevent " + "scrollTop=0 jank during innerHTML rebuild (#MOBILESCROLL)." + ) + + +def test_messages_scroller_uses_overflow_anchor_auto_on_mobile(): + # Mobile (touch): overflow-anchor:auto — prevents scrollTop=0 jank during + # innerHTML rebuild and streaming DOM updates (#MOBILESCROLL). + assert "overflow-anchor:auto;" in STYLE_CSS, ( + "On mobile/touch devices #messages must default to overflow-anchor:auto " + "to prevent the browser painting a frame with scrollTop=0 between " + "innerHTML='' and snapshot restore." ) diff --git a/tests/test_mobile_scroll_jank.html b/tests/test_mobile_scroll_jank.html new file mode 100644 index 00000000..35e6ef24 --- /dev/null +++ b/tests/test_mobile_scroll_jank.html @@ -0,0 +1,390 @@ + + +
+ + +