| ... | ... | @@ -718,34 +718,59 @@ const ConversationTimeline = memo(function ConversationTimeline({ |
| 718 | 718 | }; |
| 719 | 719 | }, [settlingListIdentity]); |
| 720 | 720 | |
| 721 | // A toggle made at the end keeps the end in view while rows re-measure. |
| 722 | // Anywhere else, the toggled row holds its place. |
| 723 | const disclosurePinsEndRef = useRef(false); |
| 724 | const disclosureCollapsedRef = useRef(false); |
| 725 | const pinDisclosureToEnd = useCallback(() => { |
| 726 | if (!disclosurePinsEndRef.current) return; |
| 727 | const element = listRef.current?.getScrollableNode(); |
| 728 | if (element) element.scrollTop = element.scrollHeight; |
| 729 | }, [listRef]); |
| 730 | |
| 731 | // Settles two frames after the last toggle, or after the last row resize |
| 732 | // while pinned to the end, since tool output can render a few frames late. |
| 733 | const scheduleDisclosureSettle = useCallback(() => { |
| 734 | if (disclosureSettleFrameRef.current !== null) { |
| 735 | cancelAnimationFrame(disclosureSettleFrameRef.current); |
| 736 | } |
| 737 | if (disclosureSettleSecondFrameRef.current !== null) { |
| 738 | cancelAnimationFrame(disclosureSettleSecondFrameRef.current); |
| 739 | } |
| 740 | disclosureSettleFrameRef.current = requestAnimationFrame(() => { |
| 741 | pinDisclosureToEnd(); |
| 742 | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { |
| 743 | pinDisclosureToEnd(); |
| 744 | disclosurePinsEndRef.current = false; |
| 745 | disclosureAnchorKeyRef.current = null; |
| 746 | setDisclosureToggleSettling(false); |
| 747 | disclosureSettleFrameRef.current = null; |
| 748 | disclosureSettleSecondFrameRef.current = null; |
| 749 | // Wait for row measurement and the disclosure click's blur check. |
| 750 | // Closing output can reveal the end without a scroll event. |
| 751 | if ( |
| 752 | disclosureCollapsedRef.current && |
| 753 | resolveTimelineIsAtEnd(listRef.current?.getState()) === true |
| 754 | ) { |
| 755 | onToolOutputCollapsedAtEnd?.(); |
| 756 | } |
| 757 | }); |
| 758 | }); |
| 759 | }, [listRef, onToolOutputCollapsedAtEnd, pinDisclosureToEnd]); |
| 760 | |
| 721 | 761 | const suspendEndScrollMaintenanceForDisclosure = useCallback( |
| 722 | 762 | (anchorKey: string, collapsed = false) => { |
| 723 | 763 | disclosureAnchorKeyRef.current = anchorKey; |
| 764 | disclosureCollapsedRef.current = collapsed; |
| 765 | disclosurePinsEndRef.current = resolveTimelineIsAtEnd(listRef.current?.getState()) === true; |
| 724 | 766 | setDisclosureToggleSettling(true); |
| 725 | | if (disclosureSettleFrameRef.current !== null) { |
| 726 | | cancelAnimationFrame(disclosureSettleFrameRef.current); |
| 727 | | } |
| 728 | | if (disclosureSettleSecondFrameRef.current !== null) { |
| 729 | | cancelAnimationFrame(disclosureSettleSecondFrameRef.current); |
| 730 | | } |
| 731 | | disclosureSettleFrameRef.current = requestAnimationFrame(() => { |
| 732 | | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { |
| 733 | | disclosureAnchorKeyRef.current = null; |
| 734 | | setDisclosureToggleSettling(false); |
| 735 | | disclosureSettleFrameRef.current = null; |
| 736 | | disclosureSettleSecondFrameRef.current = null; |
| 737 | | // Wait for row measurement and the disclosure click's blur check. |
| 738 | | // Closing output can reveal the end without a scroll event. |
| 739 | | if (collapsed && resolveTimelineIsAtEnd(listRef.current?.getState()) === true) { |
| 740 | | onToolOutputCollapsedAtEnd?.(); |
| 741 | | } |
| 742 | | }); |
| 743 | | }); |
| 767 | scheduleDisclosureSettle(); |
| 744 | 768 | }, |
| 745 | | [listRef, onToolOutputCollapsedAtEnd], |
| 769 | [listRef, scheduleDisclosureSettle], |
| 746 | 770 | ); |
| 747 | 771 | |
| 748 | 772 | const shouldRestoreVisibleContentPosition = useCallback((row: MessagesTimelineRow) => { |
| 773 | if (disclosurePinsEndRef.current) return false; |
| 749 | 774 | const disclosureAnchorKey = disclosureAnchorKeyRef.current; |
| 750 | 775 | return disclosureAnchorKey === null || row.id === disclosureAnchorKey; |
| 751 | 776 | }, []); |
| ... | ... | @@ -1165,6 +1190,13 @@ const ConversationTimeline = memo(function ConversationTimeline({ |
| 1165 | 1190 | onContentOverflowChange(measureContentOverflow()); |
| 1166 | 1191 | }); |
| 1167 | 1192 | }, [measureContentOverflow, onContentOverflowChange]); |
| 1193 | const handleItemSizeChanged = useCallback(() => { |
| 1194 | if (disclosurePinsEndRef.current) { |
| 1195 | queueMicrotask(pinDisclosureToEnd); |
| 1196 | scheduleDisclosureSettle(); |
| 1197 | } |
| 1198 | reportContentOverflow(); |
| 1199 | }, [pinDisclosureToEnd, reportContentOverflow, scheduleDisclosureSettle]); |
| 1168 | 1200 | useEffect(() => cancelContentOverflowFrame, [cancelContentOverflowFrame]); |
| 1169 | 1201 | // The list's own layout effects have already run here, so estimated row |
| 1170 | 1202 | // positions are in place. Reporting before the first paint lets a thread |
| ... | ... | @@ -1615,7 +1647,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ |
| 1615 | 1647 | } |
| 1616 | 1648 | maintainScrollAtEndThreshold={1} |
| 1617 | 1649 | onScroll={handleScroll} |
| 1618 | | onItemSizeChanged={reportContentOverflow} |
| 1650 | onItemSizeChanged={handleItemSizeChanged} |
| 1619 | 1651 | className={cn( |
| 1620 | 1652 | "messages-timeline-scroll scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain [overflow-anchor:none]", |
| 1621 | 1653 | topFadeEnabled && "topbar-scroll-fade", |