Files
hermes-webui/tests/test_issue3250_upward_scroll_intent_window.py
T
nesquena-hermesandpamnard d09f2e4efb feat: sticky manual unpin for streaming chat scroll (#3343)
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>
2026-06-01 20:34:06 +00:00

62 lines
2.3 KiB
Python

"""Regression: sticky manual unpin during streaming (supersedes #3250 timeout tuning).
After the user scrolls up to read earlier content, streaming tokens, tool cards,
and layout growth must not re-pin the viewport until the user scrolls back to
the bottom or clicks the scroll-to-bottom control.
"""
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
def _scroll_listener_block() -> str:
anchor = "el.addEventListener('scroll'"
start = UI_JS.index(anchor)
raf_start = UI_JS.index("requestAnimationFrame", start)
brace = UI_JS.index("{", raf_start)
depth = 0
for i in range(brace, len(UI_JS)):
ch = UI_JS[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return UI_JS[brace : i + 1]
raise AssertionError("scroll listener rAF callback not found")
def test_scroll_if_pinned_respects_sticky_user_unpin():
fn = UI_JS[UI_JS.index("function scrollIfPinned"): UI_JS.index("function scrollToBottom")]
compact = fn.replace(" ", "")
assert "if(_messageUserUnpinned)return" in compact, (
"scrollIfPinned() must not fight a sticky manual unpin during streaming"
)
def test_sticky_unpin_blocks_near_bottom_repin_without_downward_scroll():
block = _scroll_listener_block()
assert "_recentMessageUpwardIntent()" not in block
compact = block.replace(" ", "")
assert "elseif(!_messageUserUnpinned)" in compact, (
"Near-bottom hysteresis re-pin must be gated off while the user is manually unpinned"
)
assert "elseif(movedDown&&nearBottom)" in compact, (
"Re-follow must require explicit downward scroll into the near-bottom zone"
)
def test_new_stream_resets_follow_state():
attach = MESSAGES_JS[MESSAGES_JS.index("function attachLiveStream"):]
assert "_resetStreamScrollFollow" in attach, (
"A fresh live stream should default to following the tail until the user scrolls up"
)
def test_scroll_to_bottom_clears_sticky_unpin():
fn = UI_JS[UI_JS.index("function scrollToBottom"): UI_JS.index("function _fmtOllamaLabel")]
assert "_messageUserUnpinned=false" in fn.replace(" ", "")