1
- import { forwardRef , useCallback , useEffect , useImperativeHandle , useMemo , useRef , useState } from "react"
1
+ import React , { forwardRef , useCallback , useEffect , useImperativeHandle , useMemo , useRef , useState } from "react"
2
2
import { useDeepCompareEffect , useEvent , useMount } from "react-use"
3
3
import debounce from "debounce"
4
4
import { Virtuoso , type VirtuosoHandle } from "react-virtuoso"
@@ -181,8 +181,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
181
181
const [ showAnnouncementModal , setShowAnnouncementModal ] = useState ( false )
182
182
const everVisibleMessagesTsRef = useRef < LRUCache < number , boolean > > (
183
183
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 ,
186
186
} ) ,
187
187
)
188
188
const autoApproveTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
@@ -458,7 +458,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
458
458
}
459
459
} , [ isHidden ] )
460
460
461
- useEffect ( ( ) => ( ) => everVisibleMessagesTsRef . current . clear ( ) , [ ] )
461
+ useEffect ( ( ) => {
462
+ const cache = everVisibleMessagesTsRef . current
463
+ return ( ) => {
464
+ cache . clear ( )
465
+ }
466
+ } , [ ] )
462
467
463
468
useEffect ( ( ) => {
464
469
const prev = prevExpandedRowsRef . current
@@ -502,7 +507,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
502
507
if ( isLastMessagePartial ) {
503
508
return true
504
509
} 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
+ )
506
514
507
515
if (
508
516
lastApiReqStarted &&
@@ -522,7 +530,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
522
530
} , [ modifiedMessages , clineAsk , enableButtons , primaryButtonText ] )
523
531
524
532
const markFollowUpAsAnswered = useCallback ( ( ) => {
525
- const lastFollowUpMessage = messagesRef . current . findLast ( ( msg ) => msg . ask === "followup" )
533
+ const lastFollowUpMessage = messagesRef . current . findLast ( ( msg : ClineMessage ) => msg . ask === "followup" )
526
534
if ( lastFollowUpMessage ) {
527
535
setCurrentFollowUpTs ( lastFollowUpMessage . ts )
528
536
}
@@ -564,7 +572,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
564
572
if ( sendingDisabled && ! fromQueue ) {
565
573
// Generate a more unique ID using timestamp + random component
566
574
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 } ] )
568
576
setInputValue ( "" )
569
577
setSelectedImages ( [ ] )
570
578
return
@@ -660,7 +668,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
660
668
if ( retryCount < MAX_RETRY_ATTEMPTS ) {
661
669
retryCountRef . current . set ( nextMessage . id , retryCount + 1 )
662
670
// Re-add the message to the end of the queue
663
- setMessageQueue ( ( current ) => [ ...current , nextMessage ] )
671
+ setMessageQueue ( ( current : QueuedMessage [ ] ) => [ ...current , nextMessage ] )
664
672
} else {
665
673
console . error ( `Message ${ nextMessage . id } failed after ${ MAX_RETRY_ATTEMPTS } attempts, discarding` )
666
674
retryCountRef . current . delete ( nextMessage . id )
@@ -832,7 +840,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
832
840
// Only handle selectedImages if it's not for editing context
833
841
// When context is "edit", ChatRow will handle the images
834
842
if ( message . context !== "edit" ) {
835
- setSelectedImages ( ( prevImages ) =>
843
+ setSelectedImages ( ( prevImages : string [ ] ) =>
836
844
appendImages ( prevImages , message . images , MAX_IMAGES_PER_MESSAGE ) ,
837
845
)
838
846
}
@@ -888,21 +896,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
888
896
// NOTE: the VSCode window needs to be focused for this to work.
889
897
useMount ( ( ) => textAreaRef . current ?. focus ( ) )
890
898
891
- useDebounceEffect (
892
- ( ) => {
893
- if ( ! isHidden && ! sendingDisabled && ! enableButtons ) {
894
- textAreaRef . current ?. focus ( )
895
- }
896
- } ,
897
- 50 ,
898
- [ isHidden , sendingDisabled , enableButtons ] ,
899
- )
900
-
901
899
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 ) => {
903
905
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.
906
906
const alwaysHiddenOnceProcessedAsk : ClineAsk [ ] = [
907
907
"api_req_failed" ,
908
908
"resume_task" ,
@@ -916,14 +916,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
916
916
]
917
917
if ( message . ask && alwaysHiddenOnceProcessedAsk . includes ( message . ask ) ) return false
918
918
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)
920
919
if ( message . say === "text" && ( message . text ?? "" ) === "" && ( message . images ?. length ?? 0 ) === 0 ) {
921
920
return false
922
921
}
923
922
return true
924
923
}
925
924
926
- // Original filter logic
927
925
switch ( message . ask ) {
928
926
case "completion_result" :
929
927
if ( message . text === "" ) return false
@@ -942,9 +940,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
942
940
const last1 = modifiedMessages . at ( - 1 )
943
941
const last2 = modifiedMessages . at ( - 2 )
944
942
if ( last1 ?. ask === "resume_task" && last2 === message ) {
945
- // This specific sequence should be visible
943
+ return true
946
944
} else if ( message !== last1 ) {
947
- // If not the specific sequence above, and not the last message, hide it.
948
945
return false
949
946
}
950
947
break
@@ -957,12 +954,41 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
957
954
return true
958
955
} )
959
956
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 ) )
962
961
963
962
return newVisibleMessages
964
963
} , [ modifiedMessages ] )
965
964
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
+
966
992
const isReadOnlyToolAction = useCallback ( ( message : ClineMessage | undefined ) => {
967
993
if ( message ?. type === "ask" ) {
968
994
if ( ! message . text ) {
@@ -1238,7 +1264,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1238
1264
}
1239
1265
}
1240
1266
1241
- visibleMessages . forEach ( ( message ) => {
1267
+ visibleMessages . forEach ( ( message : ClineMessage ) => {
1242
1268
if ( message . ask === "browser_action_launch" ) {
1243
1269
// Complete existing browser session if any.
1244
1270
endBrowserSession ( )
@@ -1308,10 +1334,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1308
1334
1309
1335
const scrollToBottomSmooth = useMemo (
1310
1336
( ) =>
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 ] ,
1315
1354
)
1316
1355
1317
1356
useEffect ( ( ) => {
@@ -1323,15 +1362,22 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1323
1362
} , [ scrollToBottomSmooth ] )
1324
1363
1325
1364
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 ] )
1331
1374
1332
1375
const handleSetExpandedRow = useCallback (
1333
1376
( 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
+ } ) )
1335
1381
} ,
1336
1382
[ setExpandedRows ] , // setExpandedRows is stable
1337
1383
)
@@ -1360,7 +1406,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1360
1406
)
1361
1407
1362
1408
useEffect ( ( ) => {
1363
- let timer : NodeJS . Timeout | undefined
1409
+ let timer : ReturnType < typeof setTimeout > | undefined
1364
1410
if ( ! disableAutoScrollRef . current ) {
1365
1411
timer = setTimeout ( ( ) => scrollToBottomSmooth ( ) , 50 )
1366
1412
}
@@ -1446,7 +1492,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1446
1492
1447
1493
if ( event ?. shiftKey ) {
1448
1494
// Always append to existing text, don't overwrite
1449
- setInputValue ( ( currentValue ) => {
1495
+ setInputValue ( ( currentValue : string ) => {
1450
1496
return currentValue !== "" ? `${ currentValue } \n${ suggestion . answer } ` : suggestion . answer
1451
1497
} )
1452
1498
} else {
@@ -1480,7 +1526,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1480
1526
isStreaming = { isStreaming }
1481
1527
isExpanded = { ( messageTs : number ) => expandedRows [ messageTs ] ?? false }
1482
1528
onToggleExpand = { ( messageTs : number ) => {
1483
- setExpandedRows ( ( prev ) => ( {
1529
+ setExpandedRows ( ( prev : Record < number , boolean > ) => ( {
1484
1530
...prev ,
1485
1531
[ messageTs ] : ! prev [ messageTs ] ,
1486
1532
} ) )
@@ -1839,20 +1885,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
1839
1885
< div className = "grow flex" ref = { scrollContainerRef } >
1840
1886
< Virtuoso
1841
1887
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 }
1843
1889
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 }
1847
1892
itemContent = { itemContent }
1848
- atBottomStateChange = { ( isAtBottom ) => {
1893
+ atBottomStateChange = { ( isAtBottom : boolean ) => {
1849
1894
setIsAtBottom ( isAtBottom )
1850
1895
if ( isAtBottom ) {
1851
1896
disableAutoScrollRef . current = false
1852
1897
}
1853
1898
setShowScrollToBottom ( disableAutoScrollRef . current && ! isAtBottom )
1854
1899
} }
1855
- atBottomThreshold = { 10 } // anything lower causes issues with followOutput
1900
+ atBottomThreshold = { 10 }
1856
1901
initialTopMostItemIndex = { groupedMessages . length - 1 }
1857
1902
/>
1858
1903
</ div >
0 commit comments