mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-14 03:30:51 +00:00
feat(#4006): default auto-follow ON (Codex/Claude-Code parity) + fix default-hydration mismatch + CHANGELOG v0.51.378
Per maintainer decision, auto_scroll_follow defaults TRUE (sticky-bottom: follow new output while streaming, scroll-up unpins and is respected) — matching Codex CLI / Claude Code. The PR shipped it default-False. Default-mismatch bug fixed (would have shown the True default as OFF for existing users with no saved value): - boot.js settings path: !!s.auto_scroll_follow -> s.auto_scroll_follow!==false - boot.js no-settings fallback: false -> true - panels.js checkbox render: !!settings.auto_scroll_follow -> !==false Resolved the config.py + panels.js merge conflicts (kept both the Transparent Stream setting and auto_scroll_follow). Added test_issue4006_auto_scroll_follow_default.py pinning the True default across config/boot/panels + the follow-gate references.
This commit is contained in:
@@ -3,6 +3,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.378] — 2026-06-13 — Release MQ (don't yank the viewport while reading; auto-follow toggle, #4006)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Streaming no longer snaps you to the bottom when you've scrolled up to read (#4006).** Two pin-hysteresis bugs are fixed: `_scrollAfterMessageRender` now honors `_messageUserUnpinned` (so a forced follow can't override a deliberate scroll-up after the 250px near-bottom counter re-pins), and `_finishDone`'s completion scroll is gated on an actual near-bottom check instead of firing unconditionally. (#4006)
|
||||
|
||||
### Added
|
||||
|
||||
- **Auto-follow toggle (Settings → Appearance).** A new "Auto-follow new content" setting controls whether the transcript follows new output to the bottom while streaming. It defaults **on** — a Codex/Claude-Code-style sticky bottom: you stay pinned to the latest output until you scroll up, at which point you're left where you are. Turn it off to always manage the scroll position yourself. (#4006)
|
||||
|
||||
## [v0.51.377] — 2026-06-13 — Release MP (Firefox post-stream scroll jitter, #3920)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Pin the auto_scroll_follow setting's default + hydration consistency (#4006).
|
||||
|
||||
The viewport-follow default is `True` (Codex/Claude-Code-style sticky bottom:
|
||||
follow new output to the bottom while streaming, but a deliberate scroll-up
|
||||
unpins and is respected). This pins the default in every place it is read so a
|
||||
future edit can't silently flip it or, worse, default it ON in config.py while
|
||||
hydrating it OFF in the browser (the classic default-mismatch bug, where an
|
||||
existing user with no saved value sees the feature as disabled).
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_auto_scroll_follow_default_is_true_in_config():
|
||||
src = _read("api/config.py")
|
||||
assert re.search(r'["\']auto_scroll_follow["\']\s*:\s*True', src), (
|
||||
"auto_scroll_follow must default to True in _SETTINGS_DEFAULTS "
|
||||
"(sticky-bottom follow; scroll-up unpins)"
|
||||
)
|
||||
|
||||
|
||||
def test_auto_scroll_follow_in_bool_keys():
|
||||
src = _read("api/config.py")
|
||||
m = re.search(r"_SETTINGS_BOOL_KEYS\s*=\s*\{([^}]+)\}", src, re.DOTALL)
|
||||
assert m, "_SETTINGS_BOOL_KEYS not found"
|
||||
assert "auto_scroll_follow" in m.group(1), (
|
||||
"auto_scroll_follow must be in _SETTINGS_BOOL_KEYS so it round-trips as a bool"
|
||||
)
|
||||
|
||||
|
||||
def test_boot_hydration_defaults_true_when_setting_absent():
|
||||
"""boot.js must hydrate _autoScrollFollow as True when the saved settings
|
||||
omit the key — `!!s.auto_scroll_follow` would wrongly default it OFF for
|
||||
every existing user, contradicting the config.py default."""
|
||||
src = _read("static/boot.js")
|
||||
# Settings path: default-true read (=== false), not the truthy-coerce form.
|
||||
assert "window._autoScrollFollow=s.auto_scroll_follow!==false" in src, (
|
||||
"boot.js settings path must default _autoScrollFollow True when absent"
|
||||
)
|
||||
assert "window._autoScrollFollow=!!s.auto_scroll_follow" not in src, (
|
||||
"boot.js must not use !!s.auto_scroll_follow — that defaults the True "
|
||||
"setting OFF for users with no saved value"
|
||||
)
|
||||
# Fallback (no-settings) path must also default true.
|
||||
assert "window._autoScrollFollow=true" in src, (
|
||||
"boot.js fallback path must default _autoScrollFollow to true"
|
||||
)
|
||||
|
||||
|
||||
def test_settings_checkbox_renders_checked_by_default():
|
||||
"""The Appearance checkbox must render checked when the setting is absent,
|
||||
matching the True default (panels.js settings-load)."""
|
||||
src = _read("static/panels.js")
|
||||
assert "autoScrollFollowCb.checked=settings.auto_scroll_follow!==false" in src, (
|
||||
"the auto-follow checkbox must default checked (=== false), not "
|
||||
"!!settings.auto_scroll_follow which would render it unchecked by default"
|
||||
)
|
||||
|
||||
|
||||
def test_follow_gate_references_auto_scroll_follow_and_unpin():
|
||||
"""The DOM-replace follow gate must consult both _autoScrollFollow (the
|
||||
setting) and _messageUserUnpinned (the user's scroll-up), so the opt-out and
|
||||
the read-while-streaming behaviors both hold."""
|
||||
src = _read("static/ui.js")
|
||||
assert "_shouldFollowMessagesOnDomReplace" in src
|
||||
assert "_autoScrollFollow" in src and "_messageUserUnpinned" in src, (
|
||||
"the follow gate must reference both _autoScrollFollow and _messageUserUnpinned"
|
||||
)
|
||||
Reference in New Issue
Block a user