From 10fd367962f7dad161af64f67a2687859ef751e1 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Tue, 8 Sep 2026 10:03:10 -0400 Subject: [PATCH] fix(chat): render typing-indicator elapsed hint as horizontal text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The phase-87 ".typing-elapsed" dot-geometry reset (specificity 0,1,0) lost every shared declaration to the ".typing span" dot rule (0,1,1): the hint rendered as an 8x8px bouncing dot and the "Ns" text wrapped one character per line below the bubble (overflow-wrap: anywhere on .bubble). Phase 87's e2e checked text values only, so the squish shipped unseen. - retarget the reset at ".typing span.typing-elapsed" (0,2,1) so it actually wins; center the dots while the hint line is taller - unit: pin the reset's selector context (specificity regression guard) - e2e: layout pin on the live hint — no dot animation, not an 8px box, horizontal single-line bounding box - before/after verification screenshots in .agents/reports/87_big_read_progress/ Verified: unit 41 passed, phase-87 e2e 4 passed (isolated), ruff + pyright clean. --- .../fix_typing_elapsed_after.png | Bin 0 -> 5638 bytes .../fix_typing_elapsed_before.png | Bin 0 -> 5686 bytes frontend/assets/styles.css | 15 ++++++-- tests/e2e/test_big_read_progress.py | 33 +++++++++++++++++- tests/unit/test_big_read_progress.py | 17 +++++++-- 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 .agents/reports/87_big_read_progress/fix_typing_elapsed_after.png create mode 100644 .agents/reports/87_big_read_progress/fix_typing_elapsed_before.png 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 0000000000000000000000000000000000000000..0ec5459f7ab35bff644a289f48a9093a89efd4cb GIT binary patch literal 5638 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 literal 0 HcmV?d00001 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)",