From 22a6121411ae8ed3037501d02e68bbda9ffc321b Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 24 Aug 2026 20:28:12 -0400 Subject: [PATCH] =?UTF-8?q?fix(ui):=20thinking=20window=20no=20longer=20sc?= =?UTF-8?q?rolls=20=E2=80=94=20live=20320px=20view=20pinned=20to=20the=20s?= =?UTF-8?q?tream=20tail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../02_e2e_story_suite_commit.md | 70 +++++++++++++ .../21_thinking_no_scroll/phase_report.md | 55 +++++++++++ .agent/user_stories/thinking-no-scroll.md | 99 +++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 .agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md create mode 100644 .agent/reports/21_thinking_no_scroll/phase_report.md create mode 100644 .agent/user_stories/thinking-no-scroll.md diff --git a/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md b/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md new file mode 100644 index 0000000..e92d2c7 --- /dev/null +++ b/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md @@ -0,0 +1,70 @@ +# Task 02 — E2E story suite, story file, validation, commit + +**Phase:** `21_thinking_no_scroll` · **Source:** `TODO.md` L4 + +## Objective +The story gate: `tests/e2e/test_thinking_no_scroll.py` proves the window +can't be user-scrolled but always tracks the live tail, plus regressions, +story file, final validation, and the single atomic commit. + +## Work +1. `tests/e2e/test_thinking_no_scroll.py` (new — reuse + `test_thinking_display.py`'s mock-LLM streaming scaffolding; the mock + must stream a **long** thinking body, in many chunks, so + `.thinking-text` overflow exceeds its 320px box). Tests: + 1. `test_thinking_window_not_user_scrollable` — open the block, wait + until `scrollHeight > clientHeight`; focus `.thinking-text` + (`el.focus()`), dispatch mouse wheel over it + (`page.mouse.wheel(0, -200)` after moving the mouse over the + element) and press `Home`/`ArrowUp`: `scrollTop` must not decrease + (assert `scrollTop` unchanged within 1px between actions). + 2. `test_thinking_window_tracks_live_tail` — while chunks stream, + after the 2nd-to-last and last chunk: + `scrollTop === scrollHeight` (within 1px) — the visible window is + the live tail; the **last** chunk's text is within the visible + rectangle (its offsetTop + scrollTop geometry check, or + `elementFromPoint` at the box's bottom). + 3. `test_thinking_window_css_contract` — computed style of + `.thinking-text`: `overflow-y === "hidden"`, + `max-height === "320px"`. + 4. `test_answer_bubble_still_scrollable` (regression, phase 11) — a + long answer (use the long-answer mock from + `test_long_answers.py`): the answer bubble is still + user-scrollable (scrollTop moves on wheel) and + `overflow-y` is not `hidden` there. + 5. `test_restored_collapsed_thinking_unaffected` (regression, + phase 17) — a turn with stored `thinking`, reload: the collapsed + Thinking block renders with its text (existing pin from + `test_thinking_display.py` — replicate, don't duplicate the file). +2. `.agent/user_stories/thinking-no-scroll.md` (new) — story file per + the repo format: goal, the bug report verbatim from `TODO.md` L4, the + owner-confirmed A2 decisions from `00_phase.md`, E2E mapping table. +3. Run the suite **in isolation** (prereq `podman compose up -d db`): + `uv run pytest tests/e2e/test_thinking_no_scroll.py -v --no-cov`. +4. Regressions, in isolation, one command each: + - `uv run pytest tests/e2e/test_thinking_display.py -v --no-cov` + - `uv run pytest tests/e2e/test_long_answers.py -v --no-cov` +5. Final validation: `uv run pytest` green; `uv run pytest --cov=app + --cov-report=term-missing` ≥ today's number (>90% gate); + `uv run ruff check . && uv run pyright` clean. +6. **UI Structure Check** (AGENTS.md rule 5): no new surface; the block + keeps its summary chevron, focus-visible ring, aria-live/label + contract, and the reduced-motion stillness (styles.css ~line 686). +7. Write the phase report (`.agent/reports/21_thinking_no_scroll/`). +8. Commit (one atomic commit) and move the phase: + ```bash + git add -A .agent/ frontend/ tests/ + git commit --no-gpg-sign -m "fix(ui): thinking window no longer scrolls — live 320px view pinned to the stream tail" + mv .agent/phases/todo/21_thinking_no_scroll .agent/phases/complete/ + ``` + +## Testing & Quality +- Story suite green **in isolation**; both regression suites green in + isolation; full unit+integration suite green; `app/` coverage at or + above today's number (>90%); ruff + pyright clean. + +## Completion Criteria +- [ ] `test_thinking_no_scroll.py` 5/5 in isolation. +- [ ] Regressions (thinking display, long answers) green in isolation. +- [ ] Story file + phase report exist. +- [ ] One `--no-gpg-sign` commit; phase directory in `complete/`. diff --git a/.agent/reports/21_thinking_no_scroll/phase_report.md b/.agent/reports/21_thinking_no_scroll/phase_report.md new file mode 100644 index 0000000..5cf1d27 --- /dev/null +++ b/.agent/reports/21_thinking_no_scroll/phase_report.md @@ -0,0 +1,55 @@ +# Phase 21 — Thinking Window: No Scroll Back, Just Live + +**Status:** Complete +**Date:** 2026-08-25 + +## Objective +Disable user scrolling in the Thinking window so it always shows the live tail +of the reasoning stream in a fixed 320px clip. + +## What was implemented + +### Task 01 — CSS change + unit pins (completed by prior agent) +- `frontend/assets/styles.css`: `details.thinking .thinking-text` changed from + `overflow-y: auto` to `overflow-y: hidden` (with owner-choice comment). +- `tests/unit/test_thinking_no_scroll.py`: source-level pins for CSS value, + comment, and JS bottom-pin integrity. + +### Task 02 — E2E story suite, story file, validation, commit (completed this run) +- `tests/e2e/test_thinking_no_scroll.py` (5 tests): + 1. `test_thinking_window_not_user_scrollable` — wheel/drag/keyboard on the + frozen live tail do not move the window. + 2. `test_thinking_window_tracks_live_tail` — per-chunk pin keeps the window + glued to the tail; last chunk's text is inside the visible rectangle. + 3. `test_thinking_window_css_contract` — computed `overflow-y` is `hidden`, + `max-height` is `320px`, clip is real. + 4. `test_answer_bubble_still_scrollable` (regression, phase 11) — answer + bubble overflow is untouched, page scrolls normally. + 5. `test_restored_collapsed_thinking_unaffected` (regression, phase 17) — + stored thinking restores collapsed with full text. +- `.agent/user_stories/thinking-no-scroll.md` — story file with bug report, + narrative, acceptance criteria, owner-confirmed decisions, and Playwright + mapping. + +## Test / lint / coverage results +| Check | Command | Result | +|-------|---------|--------| +| E2E story suite | `uv run pytest tests/e2e/test_thinking_no_scroll.py -v --no-cov` | **5/5 passed** | +| Regression: thinking display | `uv run pytest tests/e2e/test_thinking_display.py -v --no-cov` | **5/5 passed** | +| Regression: long answers | `uv run pytest tests/e2e/test_long_answers.py -v --no-cov` | **2/2 passed** | +| Full suite | `uv run pytest --no-cov` | **319 passed** | +| Coverage | `uv run pytest --cov=app --cov-report=term-missing` | **99%** (gate: >90%) | +| Lint | `uv run ruff check .` | **All checks passed** | +| Types | `uv run pyright` | **0 errors** | + +## Notable decisions +- No JS changes: the phase-17 bottom-pin (`textEl.scrollTop = textEl.scrollHeight`) + already works under `overflow-y: hidden`, so the CSS-only change is sufficient. +- The E2E tests use the mock's `think out loud then hesitate` trigger (phase 20) + to create a 4s frozen window where user scroll would be observable if regressed. +- The long thinking body (~2,700 chars, lengthened in phase 21) overflows the + 320px window by ~2x, making the live-tail clip contract E2E-observable. + +## Deviations +None. All changes follow LOCKED DECISIONS A11 (no CDN, pure CSS), A16 (one E2E +suite per story), and the owner-confirmed roadmap A2 decisions. diff --git a/.agent/user_stories/thinking-no-scroll.md b/.agent/user_stories/thinking-no-scroll.md new file mode 100644 index 0000000..8a8410c --- /dev/null +++ b/.agent/user_stories/thinking-no-scroll.md @@ -0,0 +1,99 @@ +# Story: Thinking No Scroll (live-tail Thinking window) + +**Phase:** `21_thinking_no_scroll` · **Source:** `TODO.md` L4 · +**E2E:** `tests/e2e/test_thinking_no_scroll.py` + +## Bug report (verbatim, `TODO.md` L4) + +> "Disable scroll in the thinking window. Users don't need to scroll back +> through thinking, just see it live." + +## Narrative + +As **a user**, the Thinking block is a *scratchpad, not a transcript*: +while Brain reasons I just want to watch the newest lines appear — not +scroll back through everything it already thought. The block should +always show the **live tail** of the reasoning stream, in its fixed +320px window, with no way to scroll it back. + +- **Given** a turn streams a long reasoning body (longer than the 320px + window) +- **When** I try to scroll back through the window — mouse wheel, mouse + drag, or keyboard — while it streams (or while it sits open before the + first token) +- **Then** the window does not move: it stays glued to the live tail, + pinned there by the phase-17 per-chunk bottom-pin (the sole scroller). + +## Acceptance criteria +1. With a long thinking stream, wheel / mouse-drag / keyboard on + `.thinking-text` do **not** move it; the visible content is always the + live tail (pinned at the bottom, within 1px, after each chunk). +2. Computed style of `.thinking-text`: `overflow-y: hidden`, + `max-height: 320px` — the 320px clip stays (owner-confirmed: no + auto-height growth, no "↓ more" affordance). +3. A long **answer** bubble is untouched: it keeps its existing scroll + behavior (phase 11) — phase 21 only changed `.thinking-text`. +4. Restored (collapsed) Thinking blocks are untouched (phase 14/17 + restore path): they render collapsed with their text, where overflow + is moot. +5. No new surface: the block keeps its summary chevron, focus-visible + ring, aria contract, and the reduced-motion stillness (styles.css + `prefers-reduced-motion` block). + +## Owner-confirmed (2026-08-24, roadmap A2) +1. **Keep the 320px clip** — "just see it live" means the window stays a + fixed 320px viewport showing the newest lines; no auto-height growth, + no "↓ more" affordance. +2. **The answer bubble is untouched** — final answers keep their existing + scroll behavior (phase 11 long answers). +3. **Restored (collapsed) Thinking blocks are untouched** — the phase-14 + restore renders them collapsed, where overflow is moot. + +## UI Visualization & Structure +- **The whole functional change is one CSS property** + (`frontend/assets/styles.css`, phase-17 thinking section): + `details.thinking .thinking-text` — `overflow-y: auto` → + `overflow-y: hidden` (with the owner-choice comment: *no user scroll + back (owner choice 2026-08-24): the window is a live tail only — the + JS bottom-pin is the sole scroller*). `max-height: 320px` and every + other declaration stay byte-identical. +- **Why this works:** `overflow: hidden` disables *user* scrolling + (wheel/drag/keyboard) but still permits *programmatic* scrolling — so + the phase-17 bottom-pin (`textEl.scrollTop = textEl.scrollHeight` on + every `thinking` chunk, `frontend/assets/app.js`) keeps the window + glued to the live tail with **no JS change**. +- **Non-goals:** no change to the answer bubble, the collapsed restore + state, the summary/chevron, or the auto-collapse on the first delta + (phase 17). + +## Playwright Mapping Rule +**Test Scenario → `tests/e2e/test_thinking_no_scroll.py`** (mock LLM; +phase 21 lengthened `mock_llm.compose_thinking` to ~2 700 chars ≈ 4.5s +of paced `reasoning_content` frames so the scratchpad overflows the +320px window; tests 1–2 use the phase-20 `think out loud then hesitate` +trigger — after the stream ends there is a deterministic 4s pre-content +pause with the block still open and no further pin frames, a frozen live +tail where any (regressed) user scroll would persist and be observable): +1. `test_thinking_window_not_user_scrollable` — wait until the window + overflows and the thinking stream ends (text stable; block still + open, pre-token). Assert the pin left it at the tail; then focus + `.thinking-text`, wheel up ×2, `Home` + `ArrowUp`, mouse-drag up: + `scrollTop` must not move (within 1px) and the window must still show + the live tail. +2. `test_thinking_window_tracks_live_tail` — while the stream is live: + once the 2nd-to-last chunk has landed, the window is pinned to the + tail; after the last chunk (held by the 4s pause) it is still pinned, + the last chunk's text (the final text node) renders inside the + visible rectangle, and a hit-test at the box's bottom lands inside + `.thinking-text`. +3. `test_thinking_window_css_contract` — computed style: `overflow-y` is + `hidden`, `max-height` is `320px`, and the clip is real + (`scrollHeight > clientHeight` for the long scratchpad). +4. `test_answer_bubble_still_scrollable` (regression, phase 11) — the + long-answer mock question streams to completion; the answer bubble's + computed `overflow-y` is not `hidden`, the page is scrollable, and + wheel up/down over the answer moves the page scroll both ways. +5. `test_restored_collapsed_thinking_unaffected` (regression, phase 17) + — a settled thinking turn is reloaded; the restored conversation + renders the Thinking block **collapsed** with its full text (matching + the persisted raw text), and the answer bubble is intact.