- 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 | >(() => readLastInvokedScriptByProjectFromStorage()); | 402 | >(() => readLastInvokedScriptByProjectFromStorage()); |
| 403 | const messagesScrollRef = useRef<HTMLDivElement>(null); | 403 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 404 | const shouldAutoScrollRef = useRef(true); | 404 | const shouldAutoScrollRef = useRef(true); |
| 405 | const pendingAutoScrollFrameRef = useRef<number | null>(null); | ||
| 405 | const textareaRef = useRef<HTMLTextAreaElement>(null); | 406 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 406 | const composerCommandInputRef = useRef<HTMLInputElement>(null); | 407 | const composerCommandInputRef = useRef<HTMLInputElement>(null); |
| 407 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); | 408 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| @@ -1102,6 +1103,14 @@ export default function ChatView({ threadId }: ChatViewProps) { · 694 unmodified lines | |||
| 1102 | scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior }); | 1103 | scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior }); |
| 1103 | shouldAutoScrollRef.current = true; | 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 | const onMessagesScroll = useCallback(() => { | 1114 | const onMessagesScroll = useCallback(() => { |
| 1106 | const scrollContainer = messagesScrollRef.current; | 1115 | const scrollContainer = messagesScrollRef.current; |
| 1107 | if (!scrollContainer) return; | 1116 | if (!scrollContainer) return; |
| @@ -1113,8 +1122,16 @@ export default function ChatView({ threadId }: ChatViewProps) { · 5 unmodified lines | |||
| 1113 | }, [activeThread?.id, scrollMessagesToBottom]); | 1122 | }, [activeThread?.id, scrollMessagesToBottom]); |
| 1114 | useLayoutEffect(() => { | 1123 | useLayoutEffect(() => { |
| 1115 | if (!shouldAutoScrollRef.current) return; | 1124 | if (!shouldAutoScrollRef.current) return; |
| 1116 | scrollMessagesToBottom(); | 1125 | requestAutoTailToBottom(); |
| 1117 | }, [activeThread?.messages, workLogCount, scrollMessagesToBottom]); | 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 | useEffect(() => { | 1136 | useEffect(() => { |
| 1120 | setExpandedWorkGroups({}); | 1137 | setExpandedWorkGroups({}); |