@@ -25,6 +25,7 @@ import { StreamMessage } from "./StreamMessage";
2525import { ExecutionControlBar } from "./ExecutionControlBar" ;
2626import { ErrorBoundary } from "./ErrorBoundary" ;
2727import { enhanceMessages , type EnhancedMessage } from "@/types/enhanced-messages" ;
28+ import { useVirtualizer } from "@tanstack/react-virtual" ;
2829
2930interface AgentExecutionProps {
3031 /**
@@ -94,6 +95,21 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
9495 const unlistenRefs = useRef < UnlistenFn [ ] > ( [ ] ) ;
9596 const elapsedTimeIntervalRef = useRef < NodeJS . Timeout | null > ( null ) ;
9697
98+ // Virtualizers for efficient, smooth scrolling of potentially very long outputs
99+ const rowVirtualizer = useVirtualizer ( {
100+ count : enhancedMessages . length ,
101+ getScrollElement : ( ) => scrollContainerRef . current ,
102+ estimateSize : ( ) => 150 , // fallback estimate; dynamically measured afterwards
103+ overscan : 5 ,
104+ } ) ;
105+
106+ const fullscreenRowVirtualizer = useVirtualizer ( {
107+ count : enhancedMessages . length ,
108+ getScrollElement : ( ) => fullscreenScrollRef . current ,
109+ estimateSize : ( ) => 150 ,
110+ overscan : 5 ,
111+ } ) ;
112+
97113 useEffect ( ( ) => {
98114 // Clean up listeners on unmount
99115 return ( ) => {
@@ -116,17 +132,19 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
116132 } ;
117133
118134 useEffect ( ( ) => {
119- // Only auto-scroll if user hasn't manually scrolled OR if they're at the bottom
135+ if ( enhancedMessages . length === 0 ) return ;
136+
137+ // Auto-scroll only if the user has not manually scrolled OR they are still at the bottom
120138 const shouldAutoScroll = ! hasUserScrolled || isAtBottom ( ) ;
121-
139+
122140 if ( shouldAutoScroll ) {
123- const endRef = isFullscreenModalOpen ? fullscreenMessagesEndRef . current : messagesEndRef . current ;
124- if ( endRef ) {
125- endRef . scrollIntoView ( { behavior : "smooth" } ) ;
141+ if ( isFullscreenModalOpen ) {
142+ fullscreenRowVirtualizer . scrollToIndex ( enhancedMessages . length - 1 , { align : "end" , behavior : "smooth" } ) ;
143+ } else {
144+ rowVirtualizer . scrollToIndex ( enhancedMessages . length - 1 , { align : "end" , behavior : "smooth" } ) ;
126145 }
127146 }
128- } , [ messages , hasUserScrolled , isFullscreenModalOpen ] ) ;
129-
147+ } , [ enhancedMessages . length , hasUserScrolled , isFullscreenModalOpen , rowVirtualizer , fullscreenRowVirtualizer ] ) ;
130148
131149 // Update elapsed time while running
132150 useEffect ( ( ) => {
@@ -621,21 +639,32 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
621639 </ div >
622640 ) }
623641
624- < AnimatePresence >
625- { enhancedMessages . map ( ( message , index ) => (
626- < motion . div
627- key = { index }
628- initial = { { opacity : 0 , y : 10 } }
629- animate = { { opacity : 1 , y : 0 } }
630- transition = { { duration : 0.2 } }
631- className = "mb-4"
632- >
633- < ErrorBoundary >
634- < StreamMessage message = { message } streamMessages = { enhancedMessages } />
635- </ ErrorBoundary >
636- </ motion . div >
637- ) ) }
638- </ AnimatePresence >
642+ < div
643+ className = "relative w-full max-w-5xl mx-auto"
644+ style = { { height : `${ rowVirtualizer . getTotalSize ( ) } px` } }
645+ >
646+ < AnimatePresence >
647+ { rowVirtualizer . getVirtualItems ( ) . map ( ( virtualItem ) => {
648+ const message = enhancedMessages [ virtualItem . index ] ;
649+ return (
650+ < motion . div
651+ key = { virtualItem . key }
652+ data-index = { virtualItem . index }
653+ ref = { ( el ) => el && rowVirtualizer . measureElement ( el ) }
654+ initial = { { opacity : 0 , y : 10 } }
655+ animate = { { opacity : 1 , y : 0 } }
656+ transition = { { duration : 0.2 } }
657+ className = "absolute inset-x-4 pb-4"
658+ style = { { top : virtualItem . start } }
659+ >
660+ < ErrorBoundary >
661+ < StreamMessage message = { message } streamMessages = { enhancedMessages } />
662+ </ ErrorBoundary >
663+ </ motion . div >
664+ ) ;
665+ } ) }
666+ </ AnimatePresence >
667+ </ div >
639668
640669 < div ref = { messagesEndRef } />
641670 </ div >
@@ -751,21 +780,32 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
751780 </ div >
752781 ) }
753782
754- < AnimatePresence >
755- { enhancedMessages . map ( ( message , index ) => (
756- < motion . div
757- key = { index }
758- initial = { { opacity : 0 , y : 10 } }
759- animate = { { opacity : 1 , y : 0 } }
760- transition = { { duration : 0.2 } }
761- className = "mb-4"
762- >
763- < ErrorBoundary >
764- < StreamMessage message = { message } streamMessages = { enhancedMessages } />
765- </ ErrorBoundary >
766- </ motion . div >
767- ) ) }
768- </ AnimatePresence >
783+ < div
784+ className = "relative w-full max-w-5xl mx-auto"
785+ style = { { height : `${ fullscreenRowVirtualizer . getTotalSize ( ) } px` } }
786+ >
787+ < AnimatePresence >
788+ { fullscreenRowVirtualizer . getVirtualItems ( ) . map ( ( virtualItem ) => {
789+ const message = enhancedMessages [ virtualItem . index ] ;
790+ return (
791+ < motion . div
792+ key = { virtualItem . key }
793+ data-index = { virtualItem . index }
794+ ref = { ( el ) => el && fullscreenRowVirtualizer . measureElement ( el ) }
795+ initial = { { opacity : 0 , y : 10 } }
796+ animate = { { opacity : 1 , y : 0 } }
797+ transition = { { duration : 0.2 } }
798+ className = "absolute inset-x-4 pb-4"
799+ style = { { top : virtualItem . start } }
800+ >
801+ < ErrorBoundary >
802+ < StreamMessage message = { message } streamMessages = { enhancedMessages } />
803+ </ ErrorBoundary >
804+ </ motion . div >
805+ ) ;
806+ } ) }
807+ </ AnimatePresence >
808+ </ div >
769809
770810 < div ref = { fullscreenMessagesEndRef } />
771811 </ div >
0 commit comments