1- import { forwardRef , useCallback , useEffect , useImperativeHandle , useMemo , useRef , useState } from "react"
1+ import React , { forwardRef , useCallback , useEffect , useImperativeHandle , useMemo , useRef , useState } from "react"
22import { useDeepCompareEffect , useEvent , useMount } from "react-use"
33import debounce from "debounce"
44import { Virtuoso , type VirtuosoHandle } from "react-virtuoso"
@@ -181,8 +181,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
181181 const [ showAnnouncementModal , setShowAnnouncementModal ] = useState ( false )
182182 const everVisibleMessagesTsRef = useRef < LRUCache < number , boolean > > (
183183 new LRUCache ( {
184- max : 250 ,
185- ttl : 1000 * 60 * 15 , // 15 minutes TTL for long-running tasks
184+ max : 100 ,
185+ ttl : 1000 * 60 * 5 ,
186186 } ) ,
187187 )
188188 const autoApproveTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
@@ -458,7 +458,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
458458 }
459459 } , [ isHidden ] )
460460
461- useEffect ( ( ) => ( ) => everVisibleMessagesTsRef . current . clear ( ) , [ ] )
461+ useEffect ( ( ) => {
462+ const cache = everVisibleMessagesTsRef . current
463+ return ( ) => {
464+ cache . clear ( )
465+ }
466+ } , [ ] )
462467
463468 useEffect ( ( ) => {
464469 const prev = prevExpandedRowsRef . current
@@ -502,7 +507,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
502507 if ( isLastMessagePartial ) {
503508 return true
504509 } else {
505- const lastApiReqStarted = findLast ( modifiedMessages , ( message ) => message . say === "api_req_started" )
510+ const lastApiReqStarted = findLast (
511+ modifiedMessages ,
512+ ( message : ClineMessage ) => message . say === "api_req_started" ,
513+ )
506514
507515 if (
508516 lastApiReqStarted &&
@@ -522,7 +530,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
522530 } , [ modifiedMessages , clineAsk , enableButtons , primaryButtonText ] )
523531
524532 const markFollowUpAsAnswered = useCallback ( ( ) => {
525- const lastFollowUpMessage = messagesRef . current . findLast ( ( msg ) => msg . ask === "followup" )
533+ const lastFollowUpMessage = messagesRef . current . findLast ( ( msg : ClineMessage ) => msg . ask === "followup" )
526534 if ( lastFollowUpMessage ) {
527535 setCurrentFollowUpTs ( lastFollowUpMessage . ts )
528536 }
@@ -564,7 +572,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
564572 if ( sendingDisabled && ! fromQueue ) {
565573 // Generate a more unique ID using timestamp + random component
566574 const messageId = `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } `
567- setMessageQueue ( ( prev ) => [ ...prev , { id : messageId , text, images } ] )
575+ setMessageQueue ( ( prev : QueuedMessage [ ] ) => [ ...prev , { id : messageId , text, images } ] )
568576 setInputValue ( "" )
569577 setSelectedImages ( [ ] )
570578 return
@@ -660,7 +668,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
660668 if ( retryCount < MAX_RETRY_ATTEMPTS ) {
661669 retryCountRef . current . set ( nextMessage . id , retryCount + 1 )
662670 // Re-add the message to the end of the queue
663- setMessageQueue ( ( current ) => [ ...current , nextMessage ] )
671+ setMessageQueue ( ( current : QueuedMessage [ ] ) => [ ...current , nextMessage ] )
664672 } else {
665673 console . error ( `Message ${ nextMessage . id } failed after ${ MAX_RETRY_ATTEMPTS } attempts, discarding` )
666674 retryCountRef . current . delete ( nextMessage . id )
@@ -832,7 +840,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
832840 // Only handle selectedImages if it's not for editing context
833841 // When context is "edit", ChatRow will handle the images
834842 if ( message . context !== "edit" ) {
835- setSelectedImages ( ( prevImages ) =>
843+ setSelectedImages ( ( prevImages : string [ ] ) =>
836844 appendImages ( prevImages , message . images , MAX_IMAGES_PER_MESSAGE ) ,
837845 )
838846 }
@@ -888,21 +896,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
888896 // NOTE: the VSCode window needs to be focused for this to work.
889897 useMount ( ( ) => textAreaRef . current ?. focus ( ) )
890898
891- useDebounceEffect (
892- ( ) => {
893- if ( ! isHidden && ! sendingDisabled && ! enableButtons ) {
894- textAreaRef . current ?. focus ( )
895- }
896- } ,
897- 50 ,
898- [ isHidden , sendingDisabled , enableButtons ] ,
899- )
900-
901899 const visibleMessages = useMemo ( ( ) => {
902- const newVisibleMessages = modifiedMessages . filter ( ( message ) => {
900+ const currentMessageCount = modifiedMessages . length
901+ const startIndex = Math . max ( 0 , currentMessageCount - 500 )
902+ const recentMessages = modifiedMessages . slice ( startIndex )
903+
904+ const newVisibleMessages = recentMessages . filter ( ( message : ClineMessage ) => {
903905 if ( everVisibleMessagesTsRef . current . has ( message . ts ) ) {
904- // If it was ever visible, and it's not one of the types that should always be hidden once processed, keep it.
905- // This helps prevent flickering for messages like 'api_req_retry_delayed' if they are no longer the absolute last.
906906 const alwaysHiddenOnceProcessedAsk : ClineAsk [ ] = [
907907 "api_req_failed" ,
908908 "resume_task" ,
@@ -916,14 +916,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
916916 ]
917917 if ( message . ask && alwaysHiddenOnceProcessedAsk . includes ( message . ask ) ) return false
918918 if ( message . say && alwaysHiddenOnceProcessedSay . includes ( message . say ) ) return false
919- // Also, re-evaluate empty text messages if they were previously visible but now empty (e.g. partial stream ended)
920919 if ( message . say === "text" && ( message . text ?? "" ) === "" && ( message . images ?. length ?? 0 ) === 0 ) {
921920 return false
922921 }
923922 return true
924923 }
925924
926- // Original filter logic
927925 switch ( message . ask ) {
928926 case "completion_result" :
929927 if ( message . text === "" ) return false
@@ -942,9 +940,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
942940 const last1 = modifiedMessages . at ( - 1 )
943941 const last2 = modifiedMessages . at ( - 2 )
944942 if ( last1 ?. ask === "resume_task" && last2 === message ) {
945- // This specific sequence should be visible
943+ return true
946944 } else if ( message !== last1 ) {
947- // If not the specific sequence above, and not the last message, hide it.
948945 return false
949946 }
950947 break
@@ -957,12 +954,41 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
957954 return true
958955 } )
959956
960- // Update the set of ever-visible messages (LRUCache automatically handles cleanup)
961- newVisibleMessages . forEach ( ( msg ) => everVisibleMessagesTsRef . current . set ( msg . ts , true ) )
957+ const viewportStart = Math . max ( 0 , newVisibleMessages . length - 100 )
958+ newVisibleMessages
959+ . slice ( viewportStart )
960+ . forEach ( ( msg : ClineMessage ) => everVisibleMessagesTsRef . current . set ( msg . ts , true ) )
962961
963962 return newVisibleMessages
964963 } , [ modifiedMessages ] )
965964
965+ useEffect ( ( ) => {
966+ const cleanupInterval = setInterval ( ( ) => {
967+ const cache = everVisibleMessagesTsRef . current
968+ const currentMessageIds = new Set ( modifiedMessages . map ( ( m : ClineMessage ) => m . ts ) )
969+ const viewportMessages = visibleMessages . slice ( Math . max ( 0 , visibleMessages . length - 100 ) )
970+ const viewportMessageIds = new Set ( viewportMessages . map ( ( m : ClineMessage ) => m . ts ) )
971+
972+ cache . forEach ( ( _value : boolean , key : number ) => {
973+ if ( ! currentMessageIds . has ( key ) && ! viewportMessageIds . has ( key ) ) {
974+ cache . delete ( key )
975+ }
976+ } )
977+ } , 60000 )
978+
979+ return ( ) => clearInterval ( cleanupInterval )
980+ } , [ modifiedMessages , visibleMessages ] )
981+
982+ useDebounceEffect (
983+ ( ) => {
984+ if ( ! isHidden && ! sendingDisabled && ! enableButtons ) {
985+ textAreaRef . current ?. focus ( )
986+ }
987+ } ,
988+ 50 ,
989+ [ isHidden , sendingDisabled , enableButtons ] ,
990+ )
991+
966992 const isReadOnlyToolAction = useCallback ( ( message : ClineMessage | undefined ) => {
967993 if ( message ?. type === "ask" ) {
968994 if ( ! message . text ) {
@@ -1238,7 +1264,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12381264 }
12391265 }
12401266
1241- visibleMessages . forEach ( ( message ) => {
1267+ visibleMessages . forEach ( ( message : ClineMessage ) => {
12421268 if ( message . ask === "browser_action_launch" ) {
12431269 // Complete existing browser session if any.
12441270 endBrowserSession ( )
@@ -1308,10 +1334,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13081334
13091335 const scrollToBottomSmooth = useMemo (
13101336 ( ) =>
1311- debounce ( ( ) => virtuosoRef . current ?. scrollTo ( { top : Number . MAX_SAFE_INTEGER , behavior : "smooth" } ) , 10 , {
1312- immediate : true ,
1313- } ) ,
1314- [ ] ,
1337+ debounce (
1338+ ( ) => {
1339+ const lastIndex = groupedMessages . length - 1
1340+ if ( lastIndex >= 0 ) {
1341+ virtuosoRef . current ?. scrollToIndex ( {
1342+ index : lastIndex ,
1343+ behavior : "smooth" ,
1344+ align : "end" ,
1345+ } )
1346+ }
1347+ } ,
1348+ 10 ,
1349+ {
1350+ immediate : true ,
1351+ } ,
1352+ ) ,
1353+ [ groupedMessages . length ] ,
13151354 )
13161355
13171356 useEffect ( ( ) => {
@@ -1323,15 +1362,22 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13231362 } , [ scrollToBottomSmooth ] )
13241363
13251364 const scrollToBottomAuto = useCallback ( ( ) => {
1326- virtuosoRef . current ?. scrollTo ( {
1327- top : Number . MAX_SAFE_INTEGER ,
1328- behavior : "auto" , // Instant causes crash.
1329- } )
1330- } , [ ] )
1365+ const lastIndex = groupedMessages . length - 1
1366+ if ( lastIndex >= 0 ) {
1367+ virtuosoRef . current ?. scrollToIndex ( {
1368+ index : lastIndex ,
1369+ behavior : "auto" , // Instant causes crash.
1370+ align : "end" ,
1371+ } )
1372+ }
1373+ } , [ groupedMessages . length ] )
13311374
13321375 const handleSetExpandedRow = useCallback (
13331376 ( ts : number , expand ?: boolean ) => {
1334- setExpandedRows ( ( prev ) => ( { ...prev , [ ts ] : expand === undefined ? ! prev [ ts ] : expand } ) )
1377+ setExpandedRows ( ( prev : Record < number , boolean > ) => ( {
1378+ ...prev ,
1379+ [ ts ] : expand === undefined ? ! prev [ ts ] : expand ,
1380+ } ) )
13351381 } ,
13361382 [ setExpandedRows ] , // setExpandedRows is stable
13371383 )
@@ -1360,7 +1406,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13601406 )
13611407
13621408 useEffect ( ( ) => {
1363- let timer : NodeJS . Timeout | undefined
1409+ let timer : ReturnType < typeof setTimeout > | undefined
13641410 if ( ! disableAutoScrollRef . current ) {
13651411 timer = setTimeout ( ( ) => scrollToBottomSmooth ( ) , 50 )
13661412 }
@@ -1446,7 +1492,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14461492
14471493 if ( event ?. shiftKey ) {
14481494 // Always append to existing text, don't overwrite
1449- setInputValue ( ( currentValue ) => {
1495+ setInputValue ( ( currentValue : string ) => {
14501496 return currentValue !== "" ? `${ currentValue } \n${ suggestion . answer } ` : suggestion . answer
14511497 } )
14521498 } else {
@@ -1480,7 +1526,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14801526 isStreaming = { isStreaming }
14811527 isExpanded = { ( messageTs : number ) => expandedRows [ messageTs ] ?? false }
14821528 onToggleExpand = { ( messageTs : number ) => {
1483- setExpandedRows ( ( prev ) => ( {
1529+ setExpandedRows ( ( prev : Record < number , boolean > ) => ( {
14841530 ...prev ,
14851531 [ messageTs ] : ! prev [ messageTs ] ,
14861532 } ) )
@@ -1839,20 +1885,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18391885 < div className = "grow flex" ref = { scrollContainerRef } >
18401886 < Virtuoso
18411887 ref = { virtuosoRef }
1842- key = { task . ts } // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
1888+ key = { task . ts }
18431889 className = "scrollable grow overflow-y-scroll mb-1"
1844- // increasing top by 3_000 to prevent jumping around when user collapses a row
1845- 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)
1846- 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
1890+ increaseViewportBy = { { top : 3_000 , bottom : 1000 } }
1891+ data = { groupedMessages }
18471892 itemContent = { itemContent }
1848- atBottomStateChange = { ( isAtBottom ) => {
1893+ atBottomStateChange = { ( isAtBottom : boolean ) => {
18491894 setIsAtBottom ( isAtBottom )
18501895 if ( isAtBottom ) {
18511896 disableAutoScrollRef . current = false
18521897 }
18531898 setShowScrollToBottom ( disableAutoScrollRef . current && ! isAtBottom )
18541899 } }
1855- atBottomThreshold = { 10 } // anything lower causes issues with followOutput
1900+ atBottomThreshold = { 10 }
18561901 initialTopMostItemIndex = { groupedMessages . length - 1 }
18571902 />
18581903 </ div >
0 commit comments