Renders a scrollable, auto-anchoring chat message list with streaming indicators, infinite scroll upward pagination, and stick-to-bottom physics for AI chat interfaces. ## Key Components - **`ChatMessageList`** — Primary `forwardRef` component wrapping `useStickToBottom` for spring-physics scroll anchoring during token streaming - **`STREAMING_WORDS`** — Flamingo-themed cycling phrases displayed alongside the typing/streaming indicator - **`BOTTOM_THRESHOLD_PX`** / **`FOLLOW_RETAIN_MS`** — The at-bottom band (mirrors the library's `STICK_TO_BOTTOM_OFFSET_PX`) and the chase window each follow re-assert opens (mirrors its `RETAIN_ANIMATION_DURATION_MS`) - **`hasNonEmptyContent`** — Utility predicate that checks whether a `MessageContent` value (string or `MessageSegment[]`) contains visible text - **`AnchorWatcher`** / **`disposeAnchorWatcher`** — Interface and idempotent teardown helper for the top-anchor `ResizeObserver` settle window - **`ChatMessageEnhanced`** — Per-message renderer (avatars, approval variants, entity cards, context icons, mentions) - **`ChatMessageListSkeleton`** — Placeholder skeleton shown during initial load - **`CyclingPhrase`** / **`DotsLoaderIcon`** — The footer-pinned streaming indicator, rendered OUTSIDE the scroller so it can't jitter the thread ## Scroll Architecture | Concern | Owner | |---|---| | Streaming follow (spring) | `useStickToBottom` | | Follow INTENT (arm / disarm / re-assert) | `followBottomRef` + growth watch | | Dialog change / first load / new user message | Force-scroll `useEffect` | | Load-older prepend anchoring | `loadOlderAnchorRef` + geometry delta | | Top-sentinel infinite scroll | `sentinelRef` + `IntersectionObserver` | | Jump-to-bottom affordance | `atBottom` (measured geometry, NOT the library flag) | | Passive demo hard-pin | `pinBottom` prop path | ## Usage Example ```typescript import { ChatMessageList } from "@openframe/ui" import type { ChatMessage } from "@openframe/ui/types" const messages: ChatMessage[] = [ { id: "1", role: "user", content: "Hello!" }, { id: "2", role: "assistant", content: "Hi there!" }, ] export function MyChatView() { return ( fetchOlderMessages()} hasNextPage={true} isFetchingNextPage={false} /> ) } ``` ## Notes - Requires `use-stick-to-bottom` (StackBlitz Labs) — the same library used by Vercel AI SDK's `` and bolt.new - `initial: false` disables mount-time auto-scroll; the dialog-change `useEffect` owns first-paint positioning - The follow lock exists because this list's layout silently loses the library's own `isAtBottom` (siblings below the scroller change its box; a card settling out of its skeleton produces a resize-driven scroll the library reads as a gesture). Re-asserts pass `ignoreEscapes` and the library's COALESCING options (`wait: true` + `duration`) so per-frame growth folds into ONE spring instead of spawning competing chains - Growth is detected by an rAF poll on the scroller's `scrollHeight`, not only by `ResizeObserver`: the observed content node is a snapshot that React can swap out, after which the observer watches a detached element and streamed tokens grow the thread silently - `isLoading` is in the deps of every effect that touches the scroller (follow, load-more sentinel, imperative handle): the loading branch returns the skeleton, so those effects bail on null refs and would otherwise never re-run for hosts whose `isLoading` outlives `hasNextPage` turning true - `pinBottom` bypasses smart-follow for scripted demos (Fae/Mingo) where no human controls scroll