fix(chat): rest the composer at the viewport bottom — sticky alone left it mid-screen
Phase 52's first pass shipped `position: sticky; bottom` on `.composer` and
called the phase done, but the owner's requirement — "the chat message-input
textarea should be at the bottom of the screen" — still failed in the browser:
on an empty/short chat the input rested just under the empty state (~57% of
the viewport) with a dead band down to the footer.
`position: sticky` can only pull a box UP toward the scrollport's bottom edge;
it can never push a box DOWN to meet it, so on a page that does not overflow
it is a no-op. The old story suite only exercised an overflowing conversation
(one test even asserted the buggy resting position as expected), which is why
the half-fix passed.
- `.messages { flex: 1 1 auto }` — absorbs a short page's free space so the
composer's resting in-flow position is the bottom of the full-height column
(body min-height:100dvh -> .app-main flex:1 -> .chat-shell flex:1); basis
stays `auto`, no height cap, no overflow — the document stays the scroller
- `.composer { bottom: env(safe-area-inset-bottom, 0) }` — the explicit 0
fallback replaces the env()-only offset, which degraded to `auto` (no pin)
wherever env() is unsupported
- E2E: `test_empty_chat_composer_sits_in_normal_flow` ->
`..._at_the_screen_bottom` (chrome-only band below the resting composer);
the phone suite now checks the resting position as well as the pinned one
- Unit pins: the flex-grow half and the full-height column are pinned, so the
fix cannot silently regress to sticky-only
Still CSS-only — no DOM change, no JS, no new scroll call site (phase 42
never-auto-scroll contract intact), no z-index.
Verified: 1019 unit/integration tests pass (app/ coverage 99%), ruff and
pyright clean; tests/e2e/test_pinned_composer.py green in isolation (4), plus
the stop/autoscroll/persistence/mobile-nav suites and 14 layout/scroll
neighbours green in isolation.
This commit is contained in:
@@ -404,6 +404,13 @@ html::after {
|
||||
}
|
||||
|
||||
/* ---------- Main frame ---------- */
|
||||
/* Phase 52 (owner revision 2026-08-30): `flex: 1` is load-bearing, not
|
||||
cosmetic. Body is a `min-height: 100dvh` column flex box, so `main`
|
||||
grows to the full viewport height and `.chat-shell` grows to fill
|
||||
`main`. That full-height column is what makes the composer's sticky
|
||||
shift range tall enough to hold the box on screen — the pin works in
|
||||
BOTH directions only because of it (see `.composer` and `.messages`).
|
||||
No `overflow` here: the document must stay the scroll container. */
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -423,7 +430,20 @@ html::after {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Phase 52 (owner revision 2026-08-30): `flex-grow` is the other half of
|
||||
the pin. `position: sticky` only ever pulls a box UP toward the
|
||||
viewport bottom — it can never push a box DOWN to meet it — so on an
|
||||
empty/short chat the composer used to sit just under the empty state,
|
||||
mid-screen, with a dead band between it and the footer. Growing the
|
||||
message list absorbs that free space instead, so the composer's
|
||||
resting (in-flow) position already IS the bottom of the screen. Once
|
||||
the conversation is taller than the viewport there is no free space
|
||||
left to absorb, `flex-grow` does nothing, and the `.composer` sticky
|
||||
rule takes over. `flex-basis: auto` (not `1`) so the list keeps its
|
||||
content height when the page overflows; the phase-01 `min-height`
|
||||
floor stays; still no inner scroller (the document scrolls). */
|
||||
.messages {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
@@ -1140,12 +1160,21 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
fold while phase 42 forbids auto-scrolling a streaming reply — the
|
||||
Stop control could literally not be reached ("it runs away from the
|
||||
user as they try to click stop").
|
||||
The pin is TWO rules working together — sticky alone is not enough
|
||||
(owner revision 2026-08-30: "the chat message-input textarea should be
|
||||
at the bottom of the screen"). `position: sticky` can only pull the
|
||||
box UP to the viewport's bottom edge while the document overflows; it
|
||||
never pushes it DOWN, so on an empty/short chat the box still sat
|
||||
mid-screen. `.messages { flex: 1 1 auto }` supplies the missing half:
|
||||
it puts the box's resting position at the bottom of the full-height
|
||||
column, and sticky takes over the moment the conversation overflows.
|
||||
The offset is the notch-aware safe-area inset (`env()` resolves to 0,
|
||||
or falls back to 0 where unsupported, so the box sits flush with the
|
||||
viewport bottom; ≤640px keeps its own
|
||||
`main { padding-bottom: env(safe-area-inset-bottom) }`).
|
||||
CSS-only by design: no DOM change, no JS, and NO new scroll call
|
||||
site (the phase-42 never-auto-scroll contract stays intact —
|
||||
`scrollReveal` is still the one page scroll in app.js). The offset
|
||||
is the notch-aware safe-area inset (`env()` resolves to 0 on
|
||||
desktop, so the box sits flush with the viewport bottom; ≤640px keeps
|
||||
its own `main { padding-bottom: env(safe-area-inset-bottom) }`).
|
||||
`scrollReveal` is still the one page scroll in app.js).
|
||||
The solid `--surface` background, border, radius and shadow are kept
|
||||
so messages scrolling behind the pinned box never show through it,
|
||||
and no z-index is added: DOM order already paints the composer over
|
||||
@@ -1156,7 +1185,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
align-items: flex-end;
|
||||
gap: 0.6rem;
|
||||
position: sticky;
|
||||
bottom: env(safe-area-inset-bottom);
|
||||
bottom: env(safe-area-inset-bottom, 0);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
|
||||
Reference in New Issue
Block a user