docs(message-list): note smooth-scroll exclusion; point sentinel comment at subsystem doc

Reviewer caught two small gaps after Task 3:
- Effect A's smooth-scroll path on new messages is intentionally NOT
  instrumented with the sentinel (the animation lands asynchronously
  across frames; no intermediate scrollTop is worth pinning to). The
  doc now records this so the reader's intuition matches the code.
- The sentinel-branch comment in MessageList.tsx pointed at "spec §2",
  which is the planning doc rather than the durable subsystem spec.
  Pointed at docs/systems/message-list.md instead.
This commit is contained in:
Jannis Braun
2026-04-25 13:11:05 +02:00
parent 4b040114ed
commit d659637930
2 changed files with 2 additions and 2 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ The chat message list (`packages/web/src/components/chat/MessageList.tsx`) is re
Three effects cooperate. Their ordering is established by the 2026-03-25 race-fix and the 2026-04-25 sentinel addendum. Three effects cooperate. Their ordering is established by the 2026-03-25 race-fix and the 2026-04-25 sentinel addendum.
**Effect A — initial snap / restore.** Runs once when `messages.length` transitions from 0 to N for a channel. Reads `chatStore.scrollPositions.get(channelId)`. If a saved anchor exists, scrolls that message into view and computes the resulting `isAtBottomRef` from actual distance. Otherwise, sets `container.scrollTop = container.scrollHeight`, captures the post-clamp value into `lastProgrammaticBottomScrollRef`, and sets `isAtBottomRef.current = true`. On subsequent message arrivals (`messages.length > prev`), if `isAtBottomRef.current`, smooth-scrolls via `bottomRef.scrollIntoView({ behavior: 'smooth' })`. **Effect A — initial snap / restore.** Runs once when `messages.length` transitions from 0 to N for a channel. Reads `chatStore.scrollPositions.get(channelId)`. If a saved anchor exists, scrolls that message into view and computes the resulting `isAtBottomRef` from actual distance. Otherwise, sets `container.scrollTop = container.scrollHeight`, captures the post-clamp value into `lastProgrammaticBottomScrollRef`, and sets `isAtBottomRef.current = true`. On subsequent message arrivals (`messages.length > prev`), if `isAtBottomRef.current`, smooth-scrolls via `bottomRef.scrollIntoView({ behavior: 'smooth' })` — this path deliberately does *not* update the sentinel, because the smooth animation lands asynchronously across many frames and no single intermediate `scrollTop` is worth pinning to. The path is already gated on `isAtBottomRef.current`, so it cannot fire while the user is scrolled away; the rare image-load growth during a smooth scroll is tolerated.
**Effect B — ResizeObserver.** Observes the message-list content container. When height grows and `isAtBottomRef.current === true`, re-pins to bottom and updates the sentinel. Gated on `isAtBottomRef.current` so it cannot interfere when the user has scrolled away. **Effect B — ResizeObserver.** Observes the message-list content container. When height grows and `isAtBottomRef.current === true`, re-pins to bottom and updates the sentinel. Gated on `isAtBottomRef.current` so it cannot interfere when the user has scrolled away.
@@ -240,7 +240,7 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
// was queued by our own command. Layout may have grown between the command and the // was queued by our own command. Layout may have grown between the command and the
// event firing, but our intent is "stay at bottom" — do not let a post-growth distance // event firing, but our intent is "stay at bottom" — do not let a post-growth distance
// measurement flip the at-bottom flags. Re-pin defensively (content may have grown // measurement flip the at-bottom flags. Re-pin defensively (content may have grown
// again) and update the sentinel; convergence is described in the spec §2. // again) and update the sentinel. See docs/systems/message-list.md (Auto-scroll model).
if (container.scrollTop === lastProgrammaticBottomScrollRef.current) { if (container.scrollTop === lastProgrammaticBottomScrollRef.current) {
isAtBottomRef.current = true; isAtBottomRef.current = true;
setIsAtBottom(true); setIsAtBottom(true);