fix(ui): thinking window no longer scrolls — live 320px view pinned to the stream tail
This commit is contained in:
@@ -234,8 +234,9 @@ def test_restore_renders_collapsed_thinking_block() -> None:
|
||||
|
||||
def test_thinking_block_css_uses_phase08_tokens() -> None:
|
||||
"""Phase 17 styling (Phase-08 tokens, WCAG AA): the block frame, the
|
||||
≥44px summary control (brand-ink ≈8.7:1 on surface) and the scrollable
|
||||
scratchpad (ink-soft ≈6.9:1 on surface, 320px cap)."""
|
||||
≥44px summary control (brand-ink ≈8.7:1 on surface) and the live-tail
|
||||
scratchpad (ink-soft ≈6.9:1 on surface, 320px cap; phase 21 removed the
|
||||
user scroll — owner choice 2026-08-24)."""
|
||||
css = _css()
|
||||
block = re.search(r"details\.thinking \{([\s\S]*?)\n\}", css)
|
||||
assert block, "styles.css must style details.thinking"
|
||||
@@ -251,8 +252,10 @@ def test_thinking_block_css_uses_phase08_tokens() -> None:
|
||||
assert "var(--brand-ink)" in sbody
|
||||
assert "cursor: pointer" in sbody
|
||||
text = re.search(r"details\.thinking \.thinking-text \{([\s\S]*?)\n\}", css)
|
||||
assert text, "the .thinking-text scroll area must be styled"
|
||||
assert text, "the .thinking-text live-tail area must be styled"
|
||||
tbody = text.group(1)
|
||||
assert "var(--ink-soft)" in tbody
|
||||
assert "max-height: 320px" in tbody
|
||||
assert "overflow-y: auto" in tbody
|
||||
# Phase 21: no user scroll back — the window is a live tail only.
|
||||
assert "overflow-y: hidden" in tbody
|
||||
assert "overflow-y: auto" not in tbody
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Unit: the "no scroll back" contract for the Thinking window (phase 21,
|
||||
owner choice 2026-08-24, roadmap A2).
|
||||
|
||||
The live Thinking block is a scratchpad, not a transcript: the 320px window
|
||||
always shows the *live tail* of the reasoning stream. The whole functional
|
||||
change is one CSS property — ``details.thinking .thinking-text`` goes from
|
||||
``overflow-y: auto`` (a user-scrollable window) to ``overflow-y: hidden``
|
||||
(a live-tail clip). ``overflow: hidden`` still permits *programmatic*
|
||||
scrolling, so the phase-17 JS bottom-pin
|
||||
(``textEl.scrollTop = textEl.scrollHeight`` on every thinking chunk) is the
|
||||
sole scroller — wheel, drag, and keyboard scrolling stop working.
|
||||
|
||||
The browser behavior itself is E2E-covered (tests/e2e/test_thinking_no_scroll.py);
|
||||
here we pin the CSS value + the owner-choice comment and the intact
|
||||
bottom-pin so a silent regression (``overflow-y`` back to ``auto``, pin
|
||||
removed) is caught without a browser.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||||
APP_JS = FRONTEND / "assets" / "app.js"
|
||||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||||
|
||||
|
||||
def _js() -> str:
|
||||
return APP_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _css() -> str:
|
||||
return STYLES_CSS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _thinking_text_rule(css: str) -> str:
|
||||
"""Body of the `details.thinking .thinking-text { ... }` rule."""
|
||||
rule = re.search(
|
||||
r"details\.thinking \.thinking-text \{([\s\S]*?)\n\}", css
|
||||
)
|
||||
assert rule, "styles.css must style details.thinking .thinking-text"
|
||||
return rule.group(1)
|
||||
|
||||
|
||||
def test_thinking_text_is_live_tail_clip() -> None:
|
||||
"""The window stays the fixed 320px clip (owner-confirmed: no
|
||||
auto-height growth) but is NO LONGER user-scrollable."""
|
||||
body = _thinking_text_rule(_css())
|
||||
assert "max-height: 320px" in body, "the 320px clip must stay"
|
||||
assert "overflow-y: hidden" in body, "the window must not scroll"
|
||||
assert "overflow-y: auto" not in body, "no user-scrollable window remains"
|
||||
assert "overflow-y: scroll" not in body
|
||||
|
||||
|
||||
def test_thinking_text_carries_owner_choice_comment() -> None:
|
||||
"""The owner-choice comment explains WHY the window is a live tail —
|
||||
the phase-17 JS bottom-pin is the sole scroller."""
|
||||
body = _thinking_text_rule(_css())
|
||||
assert "owner choice 2026-08-24" in body
|
||||
assert "live tail" in body
|
||||
assert "sole scroller" in body
|
||||
|
||||
|
||||
def test_js_bottom_pin_intact_and_sole_scroller() -> None:
|
||||
"""The live-tail mechanism (phase 17) must survive phase 21 untouched:
|
||||
the streaming `thinking` branch pins `textEl.scrollTop =
|
||||
textEl.scrollHeight` per chunk, and it is the ONLY scrollTop
|
||||
assignment in app.js (no new user-facing scroll code was added to the
|
||||
window)."""
|
||||
js = _js()
|
||||
pin = "textEl.scrollTop = textEl.scrollHeight"
|
||||
assert js.count(pin) == 1, "the bottom-pin must exist exactly once"
|
||||
# It lives in the streaming thinking branch (before the delta branch),
|
||||
# inside the `block.open` guard so closed blocks are not scrolled.
|
||||
thinking_idx = js.find('ev.type === "thinking"')
|
||||
delta_idx = js.find('ev.type === "delta"')
|
||||
assert -1 < thinking_idx < delta_idx
|
||||
thinking_branch = js[thinking_idx:delta_idx]
|
||||
assert pin in thinking_branch
|
||||
assert "if (block.open)" in thinking_branch
|
||||
|
||||
|
||||
def test_no_js_change_to_thinking_scroll_behavior() -> None:
|
||||
"""Phase 21 is CSS-only: nothing else in app.js touches the
|
||||
.thinking-text scroll (no scroll-behavior, no wheel/touch handlers, no
|
||||
scrollIntoView on the block — the page-level reveal stays the
|
||||
phase-18 scrollReveal, which is not a .thinking-text scroller)."""
|
||||
js = _js()
|
||||
block_template = js.find('<div class="thinking-text">')
|
||||
assert block_template != -1, "the thinking block template must exist"
|
||||
# No inline scroll styling on the element itself.
|
||||
assert 'style="scroll' not in js
|
||||
assert "scroll-behavior" not in js
|
||||
assert "addEventListener(\"wheel\"" not in js
|
||||
assert "addEventListener('wheel'" not in js
|
||||
Reference in New Issue
Block a user