11import { forwardRef , useCallback , useEffect , useImperativeHandle , useMemo , useRef , useState } from "react"
22import { useDeepCompareEffect , useEvent , useMount } from "react-use"
33import debounce from "debounce"
4- import { Virtuoso , type VirtuosoHandle } from "react-virtuoso"
4+ import { Virtuoso } from "react-virtuoso"
55import removeMd from "remove-markdown"
66import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
77import useSound from "use-sound"
@@ -58,6 +58,9 @@ import QueuedMessages from "./QueuedMessages"
5858import { getLatestTodo } from "@roo/todo"
5959import { QueuedMessage } from "@roo-code/types"
6060
61+ // Import the new virtualization utilities
62+ import { useOptimizedVirtualization } from "./virtualization"
63+
6164export interface ChatViewProps {
6265 isHidden : boolean
6366 showAnnouncement : boolean
@@ -167,13 +170,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
167170 const [ primaryButtonText , setPrimaryButtonText ] = useState < string | undefined > ( undefined )
168171 const [ secondaryButtonText , setSecondaryButtonText ] = useState < string | undefined > ( undefined )
169172 const [ didClickCancel , setDidClickCancel ] = useState ( false )
170- const virtuosoRef = useRef < VirtuosoHandle > ( null )
171- const [ expandedRows , setExpandedRows ] = useState < Record < number , boolean > > ( { } )
172- const prevExpandedRowsRef = useRef < Record < number , boolean > > ( )
173173 const scrollContainerRef = useRef < HTMLDivElement > ( null )
174- const disableAutoScrollRef = useRef ( false )
175- const [ showScrollToBottom , setShowScrollToBottom ] = useState ( false )
176- const [ isAtBottom , setIsAtBottom ] = useState ( false )
177174 const lastTtsRef = useRef < string > ( "" )
178175 const [ wasStreaming , setWasStreaming ] = useState < boolean > ( false )
179176 const [ showCheckpointWarning , setShowCheckpointWarning ] = useState < boolean > ( false )
@@ -189,6 +186,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
189186 const userRespondedRef = useRef < boolean > ( false )
190187 const [ currentFollowUpTs , setCurrentFollowUpTs ] = useState < number | null > ( null )
191188
189+ // Map the optimized state to the existing state variables for backward compatibility
190+ const [ expandedRows , setExpandedRows ] = useState < Record < number , boolean > > ( { } )
191+ const prevExpandedRowsRef = useRef < Record < number , boolean > > ( )
192+ const disableAutoScrollRef = useRef ( false )
193+ const [ showScrollToBottom , setShowScrollToBottom ] = useState ( false )
194+
192195 const clineAskRef = useRef ( clineAsk )
193196 useEffect ( ( ) => {
194197 clineAskRef . current = clineAsk
@@ -434,6 +437,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
434437 } , [ messages . length ] )
435438
436439 useEffect ( ( ) => {
440+ // Clear expanded rows for new task
437441 setExpandedRows ( { } )
438442 everVisibleMessagesTsRef . current . clear ( ) // Clear for new task
439443 setCurrentFollowUpTs ( null ) // Clear follow-up answered state for new task
@@ -480,6 +484,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
480484 prevExpandedRowsRef . current = expandedRows // Store current state for next comparison
481485 } , [ expandedRows ] )
482486
487+ // Define isStreaming before using it in the virtualization hook
483488 const isStreaming = useMemo ( ( ) => {
484489 // Checking clineAsk isn't enough since messages effect may be called
485490 // again for a tool for example, set clineAsk to its value, and if the
@@ -521,6 +526,88 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
521526 return false
522527 } , [ modifiedMessages , clineAsk , enableButtons , primaryButtonText ] )
523528
529+ // Use the optimized virtualization hook (after isStreaming is defined)
530+ const {
531+ virtuosoRef,
532+ viewportConfig,
533+ stateManager,
534+ scrollManager,
535+ performanceMonitor,
536+ handleScroll : handleVirtuosoScroll ,
537+ handleRangeChange,
538+ handleScrollStateChange,
539+ scrollToBottom : optimizedScrollToBottom ,
540+ isAtBottom,
541+ showScrollToBottom : shouldShowScrollButton ,
542+ visibleRange,
543+ } = useOptimizedVirtualization ( {
544+ messages : modifiedMessages ,
545+ isStreaming,
546+ isHidden,
547+ onPerformanceIssue : ( metric , value ) => {
548+ console . warn ( `ChatView performance issue: ${ metric } = ${ value } ` )
549+ } ,
550+ } )
551+
552+ // Sync expanded rows with state manager
553+ useEffect ( ( ) => {
554+ if ( stateManager ) {
555+ const newExpandedRows : Record < number , boolean > = { }
556+ modifiedMessages . forEach ( ( msg ) => {
557+ if ( stateManager . isExpanded ( msg . ts ) ) {
558+ newExpandedRows [ msg . ts ] = true
559+ }
560+ } )
561+ setExpandedRows ( newExpandedRows )
562+ }
563+ } , [ modifiedMessages , stateManager , visibleRange ] )
564+
565+ // Sync scroll button visibility
566+ useEffect ( ( ) => {
567+ setShowScrollToBottom ( shouldShowScrollButton || ( disableAutoScrollRef . current && ! isAtBottom ) )
568+ } , [ shouldShowScrollButton , isAtBottom ] )
569+
570+ // Clear state manager for new task
571+ useEffect ( ( ) => {
572+ if ( stateManager ) {
573+ stateManager . clear ( )
574+ }
575+ if ( scrollManager ) {
576+ scrollManager . reset ( )
577+ }
578+ } , [ task ?. ts , stateManager , scrollManager ] )
579+
580+ // Handle performance monitoring
581+ useEffect ( ( ) => {
582+ if ( performanceMonitor ) {
583+ if ( isHidden ) {
584+ performanceMonitor . stopMonitoring ( )
585+ } else {
586+ performanceMonitor . startMonitoring ( )
587+ }
588+ }
589+ } , [ isHidden , performanceMonitor ] )
590+
591+ // Update scroll manager when user expands rows
592+ useEffect ( ( ) => {
593+ const prev = prevExpandedRowsRef . current
594+ let wasAnyRowExpandedByUser = false
595+ if ( prev ) {
596+ // Check if any row transitioned from false/undefined to true
597+ for ( const [ tsKey , isExpanded ] of Object . entries ( expandedRows ) ) {
598+ const ts = Number ( tsKey )
599+ if ( isExpanded && ! ( prev [ ts ] ?? false ) ) {
600+ wasAnyRowExpandedByUser = true
601+ break
602+ }
603+ }
604+ }
605+
606+ if ( wasAnyRowExpandedByUser && scrollManager ) {
607+ scrollManager . forceUserScrolling ( )
608+ }
609+ } , [ expandedRows , scrollManager ] )
610+
524611 const markFollowUpAsAnswered = useCallback ( ( ) => {
525612 const lastFollowUpMessage = messagesRef . current . findLast ( ( msg ) => msg . ask === "followup" )
526613 if ( lastFollowUpMessage ) {
@@ -549,6 +636,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
549636 disableAutoScrollRef . current = false
550637 } , [ ] )
551638
639+ // Reset scroll manager state on chat reset
640+ const handleChatResetWithManagers = useCallback ( ( ) => {
641+ handleChatReset ( )
642+ if ( scrollManager ) {
643+ scrollManager . resetUserScrolling ( )
644+ }
645+ } , [ handleChatReset , scrollManager ] )
646+
552647 /**
553648 * Handles sending messages to the extension
554649 * @param text - The message text to send
@@ -607,7 +702,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
607702 vscode . postMessage ( { type : "askResponse" , askResponse : "messageResponse" , text, images } )
608703 }
609704
610- handleChatReset ( )
705+ handleChatResetWithManagers ( )
611706 }
612707 } catch ( error ) {
613708 console . error ( "Error in handleSendMessage:" , error )
@@ -619,7 +714,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
619714 // but for now we'll just log it
620715 }
621716 } ,
622- [ handleChatReset , markFollowUpAsAnswered , sendingDisabled ] , // messagesRef and clineAskRef are stable
717+ [ handleChatResetWithManagers , markFollowUpAsAnswered , sendingDisabled ] , // messagesRef and clineAskRef are stable
623718 )
624719
625720 useEffect ( ( ) => {
@@ -842,7 +937,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
842937 case "invoke" :
843938 switch ( message . invoke ! ) {
844939 case "newChat" :
845- handleChatReset ( )
940+ handleChatResetWithManagers ( )
846941 break
847942 case "sendMessage" :
848943 handleSendMessage ( message . text ?? "" , message . images ?? [ ] )
@@ -877,7 +972,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
877972 sendingDisabled ,
878973 enableButtons ,
879974 currentTaskItem ,
880- handleChatReset ,
975+ handleChatResetWithManagers ,
881976 handleSendMessage ,
882977 handleSetChatBoxMessage ,
883978 handlePrimaryButtonClick ,
@@ -1306,14 +1401,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13061401 return result
13071402 } , [ isCondensing , visibleMessages ] )
13081403
1309- // scrolling
1310-
1404+ // scrolling - use the optimized versions
13111405 const scrollToBottomSmooth = useMemo (
13121406 ( ) =>
1313- debounce ( ( ) => virtuosoRef . current ?. scrollTo ( { top : Number . MAX_SAFE_INTEGER , behavior : "smooth" } ) , 10 , {
1407+ debounce ( ( ) => optimizedScrollToBottom ( "smooth" ) , 10 , {
13141408 immediate : true ,
13151409 } ) ,
1316- [ ] ,
1410+ [ optimizedScrollToBottom ] ,
13171411 )
13181412
13191413 useEffect ( ( ) => {
@@ -1325,17 +1419,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13251419 } , [ scrollToBottomSmooth ] )
13261420
13271421 const scrollToBottomAuto = useCallback ( ( ) => {
1328- virtuosoRef . current ?. scrollTo ( {
1329- top : Number . MAX_SAFE_INTEGER ,
1330- behavior : "auto" , // Instant causes crash.
1331- } )
1332- } , [ ] )
1422+ optimizedScrollToBottom ( "auto" )
1423+ } , [ optimizedScrollToBottom ] )
13331424
13341425 const handleSetExpandedRow = useCallback (
13351426 ( ts : number , expand ?: boolean ) => {
1336- setExpandedRows ( ( prev ) => ( { ...prev , [ ts ] : expand === undefined ? ! prev [ ts ] : expand } ) )
1427+ if ( stateManager ) {
1428+ const newExpanded = expand === undefined ? ! stateManager . isExpanded ( ts ) : expand
1429+ stateManager . setState ( ts , { isExpanded : newExpanded } )
1430+ setExpandedRows ( ( prev ) => ( { ...prev , [ ts ] : newExpanded } ) )
1431+ } else {
1432+ // Fallback to local state if stateManager not ready
1433+ setExpandedRows ( ( prev ) => ( { ...prev , [ ts ] : expand === undefined ? ! prev [ ts ] : expand } ) )
1434+ }
13371435 } ,
1338- [ setExpandedRows ] , // setExpandedRows is stable
1436+ [ stateManager ] ,
13391437 )
13401438
13411439 // Scroll when user toggles certain rows.
@@ -1363,26 +1461,37 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13631461
13641462 useEffect ( ( ) => {
13651463 let timer : NodeJS . Timeout | undefined
1366- if ( ! disableAutoScrollRef . current ) {
1464+ if (
1465+ ! disableAutoScrollRef . current &&
1466+ scrollManager &&
1467+ stateManager &&
1468+ scrollManager . shouldAutoScroll ( stateManager . hasExpandedMessages ( ) )
1469+ ) {
13671470 timer = setTimeout ( ( ) => scrollToBottomSmooth ( ) , 50 )
13681471 }
13691472 return ( ) => {
13701473 if ( timer ) {
13711474 clearTimeout ( timer )
13721475 }
13731476 }
1374- } , [ groupedMessages . length , scrollToBottomSmooth ] )
1375-
1376- const handleWheel = useCallback ( ( event : Event ) => {
1377- const wheelEvent = event as WheelEvent
1378-
1379- if ( wheelEvent . deltaY && wheelEvent . deltaY < 0 ) {
1380- if ( scrollContainerRef . current ?. contains ( wheelEvent . target as Node ) ) {
1381- // User scrolled up
1382- disableAutoScrollRef . current = true
1477+ } , [ groupedMessages . length , scrollToBottomSmooth , scrollManager , stateManager ] )
1478+
1479+ const handleWheel = useCallback (
1480+ ( event : Event ) => {
1481+ const wheelEvent = event as WheelEvent
1482+
1483+ if ( wheelEvent . deltaY && wheelEvent . deltaY < 0 ) {
1484+ if ( scrollContainerRef . current ?. contains ( wheelEvent . target as Node ) ) {
1485+ // User scrolled up
1486+ disableAutoScrollRef . current = true
1487+ if ( scrollManager ) {
1488+ scrollManager . forceUserScrolling ( )
1489+ }
1490+ }
13831491 }
1384- }
1385- } , [ ] )
1492+ } ,
1493+ [ scrollManager ] ,
1494+ )
13861495
13871496 useEvent ( "wheel" , handleWheel , window , { passive : true } ) // passive improves scrolling performance
13881497
@@ -1844,16 +1953,27 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18441953 ref = { virtuosoRef }
18451954 key = { task . ts } // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
18461955 className = "scrollable grow overflow-y-scroll mb-1"
1847- // increasing top by 3_000 to prevent jumping around when user collapses a row
1848- increaseViewportBy = { { top : 3_000 , bottom : Number . MAX_SAFE_INTEGER } } // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
1956+ // Use dynamic viewport configuration based on device and state
1957+ increaseViewportBy = { viewportConfig }
18491958 data = { groupedMessages } // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
18501959 itemContent = { itemContent }
1851- atBottomStateChange = { ( isAtBottom ) => {
1852- setIsAtBottom ( isAtBottom )
1853- if ( isAtBottom ) {
1960+ onScroll = { ( e ) => {
1961+ const target = e . currentTarget as HTMLElement
1962+ handleVirtuosoScroll ( target . scrollTop )
1963+ handleScrollStateChange ( {
1964+ scrollTop : target . scrollTop ,
1965+ scrollHeight : target . scrollHeight ,
1966+ viewportHeight : target . clientHeight ,
1967+ } )
1968+ } }
1969+ rangeChanged = { handleRangeChange }
1970+ atBottomStateChange = { ( atBottom ) => {
1971+ if ( atBottom ) {
18541972 disableAutoScrollRef . current = false
1973+ if ( scrollManager ) {
1974+ scrollManager . resetUserScrolling ( )
1975+ }
18551976 }
1856- setShowScrollToBottom ( disableAutoScrollRef . current && ! isAtBottom )
18571977 } }
18581978 atBottomThreshold = { 10 } // anything lower causes issues with followOutput
18591979 initialTopMostItemIndex = { groupedMessages . length - 1 }
0 commit comments