fix(site/src/pages/AgentsPage): fire chat-ready after store sync so scroll-to-bottom hits rendered DOM (#23693)

`onChatReady` now waits for the store to have all fetched messages (not
just query success), so the DOM has content when the parent frame acts
on the signal. Removed the `scroll-to-bottom` round-trip from
`onChatReady`, the embed page no longer needs the parent to echo a
scroll command back.

Also moved `autoScrollRef` check before the `widthChanged` guard in
`ScrollAnchoredContainer`'s ResizeObserver. The width guard is only
relevant for scroll compensation (user scrolled up); it should never
prevent pinning to bottom when auto-scroll is active. Also tightened the
store sync guard to `storeMessageCount < fetchedMessageCount`.
This commit is contained in:
Ehab Younes
2026-03-27 15:01:38 +00:00
committed by GitHub
parent 4ed9094305
commit 97a27d3c09
3 changed files with 22 additions and 18 deletions
+16 -3
View File
@@ -884,15 +884,28 @@ const AgentDetail: FC = () => {
requestUnarchiveAgent(agentId);
};
// Signal the parent layout that messages have loaded.
// Signal ready only after the store has synced fetched messages,
// so the DOM actually contains them when the parent scrolls.
const chatReadyFiredRef = useRef<string | null>(null);
const storeMessageCount = useChatSelector(store, (s) => s.messagesByID.size);
const fetchedMessageCount = chatMessagesList?.length ?? 0;
useEffect(() => {
if (chatReadyFiredRef.current === agentId || !chatMessagesQuery.isSuccess) {
if (
chatReadyFiredRef.current === agentId ||
!chatMessagesQuery.isSuccess ||
storeMessageCount < fetchedMessageCount
) {
return;
}
chatReadyFiredRef.current = agentId ?? null;
onChatReady();
}, [onChatReady, chatMessagesQuery.isSuccess, agentId]);
}, [
onChatReady,
storeMessageCount,
fetchedMessageCount,
chatMessagesQuery.isSuccess,
agentId,
]);
const handleRegenerateTitle = () => {
if (!agentId || isRegenerateTitleDisabled || !onRegenerateTitle) {
+1 -10
View File
@@ -194,8 +194,7 @@ const AgentEmbedPage: FC = () => {
// instead of creating its own.
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
// Listen for parent frame commands: theme changes and
// scroll-to-bottom requests.
// Listen for parent frame commands (e.g. theme changes).
useEffect(() => {
const parentWindow = window.parent;
const handler = (event: MessageEvent) => {
@@ -205,14 +204,6 @@ const AgentEmbedPage: FC = () => {
const theme = getThemeFromMessage(event.data);
if (theme) {
applyEmbedTheme(theme);
return;
}
if (event.data?.type === "coder:scroll-to-bottom") {
// Normal flow: scroll to the bottom of the transcript.
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop =
scrollContainerRef.current.scrollHeight;
}
}
};
@@ -842,6 +842,11 @@ const ScrollAnchoredContainer: FC<{
return;
}
if (autoScrollRef.current) {
scheduleBottomPin();
return;
}
// Skip compensation during reflow. Width changes indicate the
// height delta is distributed through the transcript rather than
// appended at the bottom, so applying the full delta would
@@ -850,11 +855,6 @@ const ScrollAnchoredContainer: FC<{
return;
}
if (autoScrollRef.current) {
scheduleBottomPin();
return;
}
// In normal flow, appends grow below the viewport, so users reading
// history do not need scroll compensation.
});