4.5 KiB
Phase 52 — Pinned Message Composer
Source: TODO.md L3 — "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 — owner instruction 2026-08-30: convert without confirmation)
Context: The chat page (frontend/index.html) scrolls at the document level: .chat-shell (the centered 46rem column, PLAN §7) is a flex column — kb-banner, steering panel, New chat, Save/Share, .messages, and finally the .composer form (#message-input + #send-btn). The composer is NOT sticky — in a long conversation it sits below the fold, and since the page never auto-scrolls while a turn streams (phase 42), the Stop button (phase 48: #send-btn morphs into the enabled Stop control in flight) can be off-screen exactly when the user wants to click it. The sticky app header is the only sticky chrome (z-index 20, 2px hairline below); the document modal is the topmost layer (z-index 1000). House frontend testing: source pins (tests/unit/test_frontend_feedback.py style — test_frontend_scroll.py / test_history_page.py are the closest precedents) plus one isolated Playwright suite per story (A16).
Objective
The composer (input + Send/Stop button) is pinned to the bottom of the viewport at every scroll position — the Stop control is always reachable mid-turn without scrolling, and no new auto-scroll behaviour is introduced (the phase-42 contract stays intact).
Dependencies
48_stop_generation(complete) — the Send↔Stop morph; Stop is clicked FROM the pinned composer (the original "run away" scenario).42_no_reply_autoscroll(complete) — the no-autoscroll-while-streaming contract the pin must not revise.07_story_responsive_polish(complete) — the 46rem column / responsive rules the pinned composer must sit within.
Tasks
01_sticky_composer.md— theposition: sticky; bottompin on.composer+ safe-area inset + the frontend source pins.02_e2e_pinned_composer.md— the story Playwright suite + regressions + commit.
Testing & Quality
- Unit:
tests/unit/test_pinned_composer.py— source pins:.composercarriesposition: stickywith abottomoffset (safe-area inset) instyles.css;app.jsgains NO new page-scroll call site (the phase-42 invariant — the one page scroll is stillscrollReveal). - Coverage: >90% on
app/(validate.sh gate — this phase makes noapp/changes; the gate must stay green). - E2E (mandatory, A16):
tests/e2e/test_pinned_composer.py, run in isolation.
Completion Criteria
- With an over-viewport conversation, scrolled to the top: the composer is fully visible (bounding box inside the viewport) at the bottom edge.
- In flight, scrolled up to read earlier content: the Stop button is visible and clickable; clicking it (no scrolling) stops the turn — partial kept + persisted with
stopped: true(the phase-48 contract, unchanged), no error banner, no window scroll (phase 42). - On an empty/short chat the composer renders in its normal flow position (the pin does not float it over the footer or shift the layout).
uv run pytestgreen; coverage TOTAL >90%.uv run pytest tests/e2e/test_pinned_composer.py -v --no-covgreen in isolation (DB up).- Regression E2E suites green in isolation:
test_stop_generation.py,test_no_reply_autoscroll.py,test_chat_persistence.py,test_mobile_hamburger_nav.py. uv run ruff check . && uv run pyrightclean.- One
--no-gpg-signcommit; phase dir moved to.agent/phases/complete/.
Locked decisions
- Recorded assumptions (TODO conversion, 2026-08-30 — owner asked for no confirmation): (1) the pin is CSS-only —
position: sticky; bottom: env(safe-area-inset-bottom)on the existing.composerinside the existing.chat-shellcolumn; noindex.htmlDOM change, no JS; (2) the composer keeps its current solid--surfacebackground + border + shadow (no glass/transparency), so scrolled messages never show through it; (3) NO z-index change — the composer already paints above.messagesby DOM order, never overlaps the sticky header, and stays under the z-1000 document modal; (4) the phase-42 never-auto-scroll contract is strictly upheld — the pin adds zero scroll call sites. - A16/A17 honoured — one story E2E suite, one atomic commit.
Commit
git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): pin the composer to the viewport bottom — Stop is always reachable while reading"