#4584 (fixes#4295): scroll re-pin requires deliberate downward intent (deltaY<-30 unpin
threshold, re-pin only if not user-unpinned OR bottomDistance<=80, reset counter) so reading
mid-stream isn't hijacked back to bottom. ui.js only + 5 scroll-regression tests (4 existing
assertions adjusted, all green; new test_issue4295_scroll_pin_reentry).
#4583 (fixes#4553): mobile transcript overflow-x:hidden + max-width:100% + word-break so wide
content can't pan sideways. style.css only + own test. Disjoint files. 32 scroll-family tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Supersedes the v0.51.199 proximity-re-pin (#3330) and the #3250 upward-intent
timeout with a sticky-unpin model (ChatGPT/Claude/Codex behavior): scroll up =
stay put until you return to the bottom or click the scroll-to-bottom control.
Reconciled against the shipped #3330 code: removed the now-dead
_recentMessageUpwardIntent reference from the #3319 rAF retry, kept the
load-time -Infinity intent-init fix, kept the pinned-only >500 catch-up.
Co-authored-by: pamnard <pamnard@users.noreply.github.com>
Keep explicit bottom pins stable across late layout growth and make clicking the already-active sidebar session a no-op before loadSession mutates state. Update scroll regression tests for the delayed settle path.
The streaming scroll listener applied hysteresis symmetrically: an
upward scroll that landed inside the 250px near-bottom dead zone still
reported the user as near the bottom, so _nearBottomCount kept
incrementing and _scrollPinned stayed true. The next streaming token
snapped the user back to the bottom. The user effectively had to escape
the 250px zone in one fling to read earlier output.
The 250px dead zone itself is required by #1360 / #677 (macOS small
window + trackpad momentum re-pin protection) so the fix is direction
detection, not threshold relaxation: track _lastScrollTop and unpin
immediately on an explicit upward movement (>2px decrease), while
downward / stationary movement keeps the original hysteresis re-pin
path so the macOS momentum protection is preserved.
Programmatic scrolls are still masked by the existing _programmaticScroll
guard, so scrollToBottom() never updates _lastScrollTop and never
spuriously unpins.
Adds tests/test_issue1731_upward_scroll_unpins.py covering: direction
tracker exists, upward branch sets _scrollPinned=false and resets the
counter without hysteresis, downward branch preserves the >=2
hysteresis re-pin requirement, the 250px threshold remains, and the
_programmaticScroll bail still runs before the rAF schedule.
Closes#1731.
Co-Authored-By: Potato (OpenClaw assistant) <noreply@openclaw.ai>