* fix(chat): keep transcript pinned across composer auto-resize (#5514)
Reported by @JeffArcLogic.
autoResize() sets the textarea to height:'auto' before measuring scrollHeight
and restoring the height. The transient collapse grows the flex:1 #messages
viewport and the browser clamps a bottom-anchored scrollTop down by the collapse
delta, stranding the reader a few px above the bottom on every keystroke while
the composer is multi-row (delta proportional to composer height) — and the
clamp's async scroll event sticky-unpins the reader, dead-ending the #5516
grow-path re-pin and stream auto-follow. The prior #5516 fix only re-pinned on
NET growth, so a steady-state keystroke was missed.
Fix: snapshot #messages.scrollTop before the height round-trip and restore it
after, undoing the transient clamp within the same synchronous task. Protects
pinned AND near-bottom scrolled-up readers, takes no _programmaticScroll latch,
inert to iOS toolbar reflows. The grow-gated re-pin + ResizeObserver remain for
genuine net growth.
* test(#5514): make near-bottom-unpinned case non-vacuous (Codex gate nit)
The companion unpinned case used scrollTop=7900, below the transient clamp max
(7903), so it never actually clamped and passed on master too. Move it to 7990
(inside the clamp zone: 7903 < 7990 < settled-bottom 8023) so it genuinely
exercises the transient-clamp yank the fix prevents.
* Release exp-v0.52.52: keep transcript pinned across composer auto-resize (#5962, #5514)
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Addresses Greptile P2 nit: autoResize() now only calls the re-pin when the
composer actually grew (offsetHeight before/after compare), skipping the DOM
read on a no-height-change keystroke. The #composerWrap ResizeObserver remains
the safety net for growth paths that bypass autoResize. Test tightened to assert
the grow-guard. CHANGELOG entry added (self-built, release owns changelog).
Fable senior review (COMMENT, no blockers) + DRY nit:
- Widen the ResizeObserver from #msg to the whole #composerWrap so growth from the
attachment tray / selection chips (not just textarea rows) also re-pins a bottom-
pinned reader. Fable flagged tray/chip growth as a likely REMAINING #5515 trigger
— same viewport-shrink physics, now covered by the same guarded re-pin at one seam.
#composerWrap contains #msg, so the textarea path is still covered.
- DRY: _repinMessagesAfterComposerResize() now reuses _messageBottomDistance()
instead of re-implementing the bottom-distance expression inline.
Re-verified live (composer grow -> bottomDist stays 0) + 524 scroll/composer
regression tests green + 6 issue tests green.
#5514 (deterministic): with the chat pinned to the bottom, growing the composer
past one row (multi-line typing, Shift+Enter, a multi-line / WisprFlow paste)
shrinks the flex:1 .messages viewport by the same delta. scrollTop is unchanged
but max-scrollTop rose by delta, so the reader is stranded delta-px above the
bottom — the transcript 'scrolls up one row per composer row.' autoResize()
resized the textarea and called updateSendBtn() but never re-pinned the transcript.
Reproduced live: composer 176px -> viewport shrank 745->613 -> transcript stranded
132px above bottom while _scrollPinned stayed true.
#5515 (intermittent 'random scroll up'): the same viewport-shrink from composer
growth via paths that don't route through input->autoResize (paste, draft restore,
programmatic value set, reflow) reads to the user as a random upward jump. Covered
by a ResizeObserver seam (below).
Fix: new _repinMessagesAfterComposerResize() (static/ui.js) re-pins to the bottom
ONLY when the reader is genuinely still pinned (honors _messageUserUnpinned /
_scrollPinned — never yanks a reader who scrolled away; verified live). Called from:
(a) autoResize() after the height write, and
(b) a ResizeObserver on the #msg textarea that re-pins on GROW only, catching
every composer height-change path (not just the typed-input seam).
Verified live on a 40-message seeded session: pinned reader stays at bottomDist=0
after a 4-line composer grow; an unpinned (scrolled-up) reader is NOT moved.
Tests: tests/test_issue5514_composer_grow_scroll_pin.py (3 static wiring + 3 node-vm
behavioral). 524 scroll/composer regression tests across 39 files still green.
NOTE on #5515: this resolves the composer-growth cause (which the reporter's
WisprFlow-paste symptom points at). If a purely non-composer intermittent jump
persists, the remaining suspect is touch-device overflow-anchor:auto on content-
above height change — that path has no repro yet and is tracked as needinfo on
#5515.