diff --git a/.agents/reports/87_big_read_progress/fix_typing_elapsed_after.png b/.agents/reports/87_big_read_progress/fix_typing_elapsed_after.png new file mode 100644 index 0000000..0ec5459 Binary files /dev/null and b/.agents/reports/87_big_read_progress/fix_typing_elapsed_after.png differ diff --git a/.agents/reports/87_big_read_progress/fix_typing_elapsed_before.png b/.agents/reports/87_big_read_progress/fix_typing_elapsed_before.png new file mode 100644 index 0000000..237fb6b Binary files /dev/null and b/.agents/reports/87_big_read_progress/fix_typing_elapsed_before.png differ diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index b8a9482..c20cdd9 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -1110,7 +1110,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } } /* typing indicator */ -.typing { display: inline-flex; gap: 5px; padding: 0.9rem 1rem; } +/* align-items: center — the dots stay vertically centered when the + phase-87 "Ns" hint (a full text line) makes the row taller; with + only the three 8px dots the centering is a visual no-op. */ +.typing { display: inline-flex; align-items: center; gap: 5px; padding: 0.9rem 1rem; } .typing span { width: 8px; height: 8px; border-radius: 50%; @@ -1129,8 +1132,14 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } ink-soft on the bubble surface (the AA pairing), small mono like every status line. Plain text: no animation, no motion opt-out. The span is a sibling of the dots inside the same .typing bubble, so - it resets the dot geometry (.typing span) it would otherwise inherit. */ -.typing-elapsed { + it resets the dot geometry (.typing span) it would otherwise inherit. + The selector MUST stay ".typing span.typing-elapsed": a bare + ".typing-elapsed" (0,1,0) LOSES the specificity war to the dot rule + ".typing span" (0,1,1) — every reset below (width/height/border- + radius/background/opacity/animation) would be overridden and the + hint would render as an 8×8px bouncing dot with the "Ns" text + wrapping one character per line (phase-87 regression, 2026-09). */ +.typing span.typing-elapsed { width: auto; height: auto; border-radius: 0; diff --git a/tests/e2e/test_big_read_progress.py b/tests/e2e/test_big_read_progress.py index 4678d55..03a823e 100644 --- a/tests/e2e/test_big_read_progress.py +++ b/tests/e2e/test_big_read_progress.py @@ -567,7 +567,15 @@ def test_typing_indicator_shows_visible_elapsed( the screen-reader channel the unit pins protect) read together at the same moment; a second value sample ≥1.5 s later is strictly greater (the clock ticks while the gap holds — the next frame is - ≥6 s away).""" + ≥6 s away). + + GEOMETRY GUARD (2026-09, phase-87 fix): the hint must render as a + single horizontal text line inline with the dots — NOT as the 8×8px + bouncing dot it became when the bare ``.typing-elapsed`` selector + lost the specificity war to the ``.typing span`` dot rule (the + "Ns" text then wrapped one character per line below the bubble). + Phase 87's original run checked text values only, so the squish + shipped unseen; this assertion is the layout pin that was missing.""" page.set_default_timeout(30_000) _reset_db(_seed) _submit_tools_turn(page, app_url) @@ -580,6 +588,29 @@ def test_typing_indicator_shows_visible_elapsed( v1 = _wait_visible_hint_and_aria(page, 10, TYPING_HINT_TIMEOUT_MS) assert v1 >= 10, f"the hint appeared below the 10 s gate: {v1}s" + # Layout pin: the hint is a horizontal text line, not the 8×8px + # dot it degrades to when the dot-geometry reset loses the + # specificity war (width 8px + overflow-wrap: anywhere then wraps + # the "Ns" one character per line, spilling below the bubble). + geom = page.locator("#typing-indicator .typing-elapsed").first.evaluate( + """e => { + const s = getComputedStyle(e); + const r = e.getBoundingClientRect(); + return {anim: s.animationName, w: s.width, h: s.height, + bw: r.width, bh: r.height}; + }""" + ) + assert geom["anim"] == "none", ( + f"the hint must be plain text, not the dot animation: {geom}" + ) + assert geom["w"] != "8px" and geom["h"] != "8px", ( + f"the hint must not be an 8px dot: {geom}" + ) + assert geom["bw"] >= 16 and geom["bw"] > geom["bh"], ( + "the 'Ns' text must run HORIZONTALLY (one line ≥3ch wide), not " + f"wrap one character per line: {geom}" + ) + # The clock ticks while the gap holds: ≥1.5 s later the value is # strictly greater (the read frame is ≥6 s away; the first delta # — which removes the whole indicator — is ≈25 s from submit). diff --git a/tests/unit/test_big_read_progress.py b/tests/unit/test_big_read_progress.py index a8f2e91..cee977c 100644 --- a/tests/unit/test_big_read_progress.py +++ b/tests/unit/test_big_read_progress.py @@ -111,10 +111,21 @@ def test_typing_elapsed_css_rule_is_the_aa_pairing() -> None: language as every status line. Plain text — ``animation: none`` + no background, and the dot-geometry reset (the span is a sibling of the dots inside the .typing bubble, so without the reset it would - render as an 8px bouncing dot, not a hint).""" + render as an 8px bouncing dot, not a hint). + + The reset's SELECTOR is pinned too (phase-87 regression, 2026-09): + a bare ``.typing-elapsed`` (specificity 0,1,0) loses every shared + declaration to the dot rule ``.typing span`` (0,1,1) — the hint + then rendered as an 8×8px bouncing dot with the "Ns" text wrapping + one character per line below the bubble. The reset must carry the + span context (``.typing span.typing-elapsed``, 0,2,1) to win.""" css = _css() - block = re.search(r"\.typing-elapsed \{([\s\S]*?)\n\}", css) - assert block, "styles.css must style .typing-elapsed" + assert ".typing span.typing-elapsed {" in css, ( + "the dot-geometry reset must target the span in .typing context " + "(bare .typing-elapsed loses the specificity war to .typing span)" + ) + block = re.search(r"\.typing span\.typing-elapsed \{([\s\S]*?)\n\}", css) + assert block, "styles.css must style .typing span.typing-elapsed" body = block.group(1) for prop in ( "font-family: var(--mono)",