feat(chat): pin the composer to the viewport bottom — Stop is always reachable while reading

This commit is contained in:
2026-08-30 14:59:40 -04:00
parent 619bf2187a
commit 820753948e
4 changed files with 897 additions and 0 deletions
@@ -0,0 +1,23 @@
# Task 01 — Sticky Bottom Composer
**Phase:** `52_pinned_composer` · **Source:** `TODO.md:3` — "The message input text box needs to be pinned to the bottom of the screen so it doesn't \"run away\" from the user as they try to click \"stop\""
**Story:** n/a (TODO-derived)
## Objective
The composer stays pinned to the bottom of the viewport at every scroll position — a CSS-only change inside the existing chat column.
## Work
1. `frontend/assets/styles.css` — in the `/* ---------- Composer ---------- */` block (`.composer`, ~L1133): add `position: sticky;` and `bottom: env(safe-area-inset-bottom);` to `.composer`. The page scrolls at the document level and `.chat-shell` is the composer's containing column, so the box sticks to the viewport's bottom edge (offset by the mobile safe-area inset) while `.messages` scrolls behind it; at the document bottom it settles back into its normal flow position above the footer. Keep the existing solid `background: var(--surface)`, border, radius and `box-shadow: var(--shadow)` — messages must never show through the pinned box.
2. `frontend/index.html` — verify NO change needed: the composer is already the LAST child of `.chat-shell` (the sticky context), and the `#message-input` / `#send-btn` / `#send-status` markup is untouched.
3. Do NOT touch `frontend/assets/app.js` — the pin must not add any scroll call site (phase-42 invariant; the one page scroll in the file stays `scrollReveal`).
4. `tests/unit/test_pinned_composer.py` (new, house pin style — see `tests/unit/test_frontend_scroll.py`): assert `styles.css` declares `position: sticky` AND a `bottom:` offset on `.composer` (the sticky-bottom pair, matched inside the `.composer` rule); assert `app.js` is unchanged in its scroll surface (the phase-42 single-`scrollReveal` pin still holds — reuse the same assertion approach `test_no_reply_autoscroll.py`'s companion pins use).
- ASSUMPTION: `bottom: env(safe-area-inset-bottom)` (not `bottom: 0` + extra padding) — the standard notch-aware inset; on desktop `env()` resolves to 0, so the box sits flush with the viewport bottom.
- ASSUMPTION: no `z-index` added — DOM order already stacks the composer above `.messages`; the sticky header (z 20) and doc modal (z 1000) are unaffected.
## Testing & Quality
- Unit: `tests/unit/test_pinned_composer.py` (the pins above) — `uv run pytest tests/unit/test_pinned_composer.py -v` green.
- Coverage: **>90%** — no `app/` change; the gate stays green.
## Completion Criteria
- [ ] `.composer` in `styles.css` carries `position: sticky` + the `bottom` safe-area offset; the `frontend/` diff contains no JS change.
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.