From d659637930ca15636bb6774ca4a225459076173f Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:11:05 +0200 Subject: [PATCH] docs(message-list): note smooth-scroll exclusion; point sentinel comment at subsystem doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/systems/message-list.md | 2 +- packages/web/src/components/chat/MessageList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/systems/message-list.md b/docs/systems/message-list.md index 269e7691..5c3a2171 100644 --- a/docs/systems/message-list.md +++ b/docs/systems/message-list.md @@ -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. -**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. diff --git a/packages/web/src/components/chat/MessageList.tsx b/packages/web/src/components/chat/MessageList.tsx index 9a45a437..3bb4e140 100644 --- a/packages/web/src/components/chat/MessageList.tsx +++ b/packages/web/src/components/chat/MessageList.tsx @@ -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 // 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 - // 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) { isAtBottomRef.current = true; setIsAtBottom(true);