ThreadViewport autoScroll behavior is broken
I have an issue with autoScroll prop.
I think it's wrong to prevent user to scroll up when response is generated. There is no way now to preserve sroll to bottom button functionality and disable autoscroll when response is generated.
useThreadViewportAutoScroll.ts
const scrollToBottom = (behavior: ScrollBehavior) => {
const div = divRef.current;
if (!div || !autoScroll) return;
isScrollingToBottomRef.current = true;
div.scrollTo({ top: div.scrollHeight, behavior });
};
if autoScroll is true, there is no way to scroll up when response is generated. if autoScroll is false, scroll to bottom never happened, even when scroll to bottom button clicked.
I've overridden default behavior by flipping autoScroll prop based on isRunning and container scroll state. It allows me to scroll UP while response is generated, but ThreadScrollToBottom control still not working due to code above. I added custom onClick handler to set my own autoScroll to true and it seems to work properly now. Probably this should be fixed on library level.
const isRunning = useThread(thread => thread.isRunning);
const [autoScroll, setAutoScroll] = useState(true);
const lastScrollTop = useRef<number>(0);
useEffect(() => {
setAutoScroll(true);
}, [isRunning]);
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.target as HTMLDivElement;
if (isRunning && lastScrollTop.current > target.scrollTop) {
setAutoScroll(false);
}
lastScrollTop.current = target.scrollTop;
};
return (
<ThreadPrimitive.Root
className={cn("bg-background box-border flex h-full flex-col", className)}
style={{
["--thread-max-width" as string]: "42rem"
}}
>
<ThreadPrimitive.Viewport
className="flex h-full flex-col items-center overflow-y-auto scroll-smooth bg-inherit px-4 pt-4 lg:pt-8"
autoScroll={autoScroll}
onScroll={handleScroll}
>
...
<div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
<ThreadScrollToBottom onClick={() => setAutoScroll(true)} />
<TrialAlert />
<Composer onClickInput={onClickInput} />
</div>
Hi, I can't reproduce this, auto scroll is working as expected for me. Please check again, and if you're still experiencing this provide a minimal reproduction
Hi, I can't reproduce this, auto scroll is working as expected for me. Please check again, and if you're still experiencing this provide a minimal reproduction
To clarify, I am able to scroll up when a message is streaming to stop the auto scrolling, scrolling back down re-engages auto scrolling.
Closing as stale for now, please report if anyone is experiencing this
@AVGVSTVS96 just checked on a fresh installation. It's working fine with OpenAI runtime, but with ExternalStoreRuntime, it's not. Now sure how to properly demonstrate you, but just switching the runtime to ExternalStoreRuntime makes chat not scrollable up while response is streaming...
Do you have an idea why is the difference in behavior based on the runtime provider?
@AVGVSTVS96 just checked on a fresh installation. It's working fine with OpenAI runtime, but with ExternalStoreRuntime, it's not. Now sure how to properly demonstrate you, but just switching the runtime to ExternalStoreRuntime makes chat not scrollable up while response is streaming...
Do you have an idea why is the difference in behavior based on the runtime provider?
Will look into this today, thanks for reporting
@AVGVSTVS96 thanks! Hope it'll reproduce on your end. I checked source code for any potential problems and didn't found anything.
In case anyone need temporary solution
import { useThread, useThreadViewport } from "@assistant-ui/react";
import { useEffect, useRef, useState } from "react";
/**
* Custom hook to manage autoscroll logic for chat thread.
* Returns [autoScroll, setAutoScroll, handleScroll].
*/
export function useThreadAutoScrollFix() {
const isRunning = useThread(thread => thread.isRunning);
const isAtBottom = useThreadViewport(thread => thread.isAtBottom);
const [autoScroll, setAutoScroll] = useState(true);
const lastScrollTop = useRef<number>(0);
useEffect(() => {
setAutoScroll(true);
}, [isRunning]);
useEffect(() => {
if (isRunning && isAtBottom) {
setAutoScroll(true);
}
}, [isRunning, isAtBottom]);
const handleClickAutoScroll = () => {
setAutoScroll(true);
};
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.target as HTMLDivElement;
if (isRunning && lastScrollTop.current > target.scrollTop) {
setAutoScroll(false);
}
lastScrollTop.current = target.scrollTop;
};
return { autoScroll, handleClickAutoScroll, handleScroll };
}
export const Thread: FC<{
agent?: Agent | null;
className?: string;
onClickInput?: (event: React.MouseEvent<HTMLElement>) => void;
}> = ({ agent, onClickInput, className }) => {
const { autoScroll, handleClickAutoScroll, handleScroll } =
useThreadAutoScrollFix();
return (
<ThreadPrimitive.Root
className={cn("bg-background box-border flex h-full flex-col", className)}
style={{
["--thread-max-width" as string]: "48rem"
}}
>
<ThreadPrimitive.Viewport
className="flex h-full flex-col items-center overflow-y-auto scroll-smooth bg-inherit px-4 pt-4 lg:pt-8"
autoScroll={autoScroll} // changed line here
onScroll={handleScroll} // changed line here
>
<ThreadWelcome />
<ThreadPrimitive.Messages
components={{
UserMessage: UserMessage,
EditComposer: EditComposer,
AssistantMessage: AssistantMessage
}}
/>
<ThreadPrimitive.If empty={false}>
<div className="min-h-8 flex-grow" />
</ThreadPrimitive.If>
<div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
<ThreadScrollToBottom onClick={handleClickAutoScroll} /> // changed line here
<Composer onClickInput={onClickInput} />
</div>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
);
};
Not sure if it helps, but uploaded video with proof that just switching runtime fixes issue.
https://github.com/user-attachments/assets/5445c73c-c793-4d2c-849f-4f13ce05b8b8
This is a tough bug to track down! We're not sure why this is happening. For anyone experiencing this issue, please use the workaround in https://github.com/assistant-ui/assistant-ui/issues/1916#issuecomment-2959330183 for the time being.