fix(site): update sticky messages during streaming (#23577)

## Problem

The sticky user message visual state (`--clip-h`, fade gradient, push-up
positioning) is driven by an `update()` function that only ran on
`scroll` events. The chat scroll container uses `flex-col-reverse`,
where `scrollTop = 0` means "at bottom." When streaming content grows
the transcript while the user is auto-scrolled to the bottom,
`scrollTop` stays at `0` — no `scroll` event fires — so `update()` never
runs and the sticky messages become visually stale until the user
manually scrolls.

## Fix

Add a `ResizeObserver` on the scroller's content wrapper inside the
existing `useLayoutEffect` that sets up the scroll/resize listeners.
When the content wrapper resizes (streaming growth), it fires the
observer which calls `update()` through the same RAF-throttle pattern
used by the scroll handler.

Single observer per sticky message instance. Zero cost when nothing is
resizing. Cleanup handled in the same effect teardown.
This commit is contained in:
Kyle Carberry
2026-03-25 09:07:20 -04:00
committed by GitHub
parent 82f9a4c691
commit 4ba9986301
@@ -856,6 +856,24 @@ const StickyUserMessage: FC<{
});
};
// Re-run the visual update when the scrollable content height
// changes (e.g. streaming responses growing the transcript).
// In flex-col-reverse, scrollTop stays at 0 when pinned to
// bottom so no scroll event fires — but the content wrapper
// resizes and this observer catches that.
const contentEl = scroller.firstElementChild as HTMLElement | null;
let contentRafId: number | null = null;
const contentObserver = contentEl
? new ResizeObserver(() => {
if (contentRafId !== null) return;
contentRafId = requestAnimationFrame(() => {
contentRafId = null;
update();
});
})
: null;
contentObserver?.observe(contentEl!);
scroller.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onResize);
update();
@@ -866,8 +884,10 @@ const StickyUserMessage: FC<{
return () => {
scroller.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onResize);
contentObserver?.disconnect();
container.style.removeProperty("--overlay-ready");
if (rafId !== null) cancelAnimationFrame(rafId);
if (contentRafId !== null) cancelAnimationFrame(contentRafId);
};
}, []);