- Queue tail-to-bottom scrolling with `requestAnimationFrame` to avoid redundant scroll calls - Preserve user scroll position by checking auto-scroll intent before running - Cancel any pending frame on unmount to prevent stale callbacks
Unified · Split
| @@ -402,6 +402,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 401 unmodified lines | ||
| 402 | 402 | >(() => readLastInvokedScriptByProjectFromStorage()); |
| 403 | 403 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 404 | 404 | const shouldAutoScrollRef = useRef(true); |
| 405 | + const pendingAutoScrollFrameRef = useRef<number | null>(null); | |
| 405 | 406 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 406 | 407 | const composerCommandInputRef = useRef<HTMLInputElement>(null); |
| 407 | 408 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| @@ -1102,6 +1103,14 @@ export default function ChatView({ threadId }: ChatViewProps) { · 694 unmodified lines | ||
| 1102 | 1103 | scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior }); |
| 1103 | 1104 | shouldAutoScrollRef.current = true; |
| 1104 | 1105 | }, []); |
| 1106 | + const requestAutoTailToBottom = useCallback(() => { | |
| 1107 | + if (pendingAutoScrollFrameRef.current !== null) return; | |
| 1108 | + pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => { | |
| 1109 | + pendingAutoScrollFrameRef.current = null; | |
| 1110 | + if (!shouldAutoScrollRef.current) return; | |
| 1111 | + scrollMessagesToBottom(); | |
| 1112 | + }); | |
| 1113 | + }, [scrollMessagesToBottom]); | |
| 1105 | 1114 | const onMessagesScroll = useCallback(() => { |
| 1106 | 1115 | const scrollContainer = messagesScrollRef.current; |
| 1107 | 1116 | if (!scrollContainer) return; |
| @@ -1113,8 +1122,16 @@ export default function ChatView({ threadId }: ChatViewProps) { · 5 unmodified lines | ||
| 1113 | 1122 | }, [activeThread?.id, scrollMessagesToBottom]); |
| 1114 | 1123 | useLayoutEffect(() => { |
| 1115 | 1124 | if (!shouldAutoScrollRef.current) return; |
| 1116 | − scrollMessagesToBottom(); | |
| 1117 | − }, [activeThread?.messages, workLogCount, scrollMessagesToBottom]); | |
| 1125 | + requestAutoTailToBottom(); | |
| 1126 | + }, [activeThread?.messages, workLogCount, requestAutoTailToBottom]); | |
| 1127 | + useEffect( | |
| 1128 | + () => () => { | |
| 1129 | + if (pendingAutoScrollFrameRef.current === null) return; | |
| 1130 | + window.cancelAnimationFrame(pendingAutoScrollFrameRef.current); | |
| 1131 | + pendingAutoScrollFrameRef.current = null; | |
| 1132 | + }, | |
| 1133 | + [], | |
| 1134 | + ); | |
| 1118 | 1135 | |
| 1119 | 1136 | useEffect(() => { |
| 1120 | 1137 | setExpandedWorkGroups({}); |