Skip to content

Commit 17b0cc0

Browse files
committed
Reduces the LRU cache size and TTL for everVisibleMessagesTsRef, adds periodic cleanup of cache entries for removed messages, and limits visible message processing to the most recent 500. Improves type safety by adding explicit types to various callbacks and state setters, and adjusts Virtuoso viewport and event handler types for better performance and maintainability.
1 parent d90bab7 commit 17b0cc0

File tree

1 file changed

+53
-29
lines changed

1 file changed

+53
-29
lines changed

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
458458
}
459459
}, [isHidden])
460460

461-
useEffect(() => () => everVisibleMessagesTsRef.current.clear(), [])
461+
useEffect(() => {
462+
return () => {
463+
everVisibleMessagesTsRef.current.clear()
464+
}
465+
}, [])
466+
467+
useEffect(() => {
468+
const cleanupInterval = setInterval(() => {
469+
const cache = everVisibleMessagesTsRef.current
470+
const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts))
471+
472+
cache.forEach((value: boolean, key: number) => {
473+
if (!currentMessageIds.has(key)) {
474+
cache.delete(key)
475+
}
476+
})
477+
}, 60000)
478+
479+
return () => clearInterval(cleanupInterval)
480+
}, [modifiedMessages])
462481

463482
useEffect(() => {
464483
const prev = prevExpandedRowsRef.current
@@ -502,7 +521,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
502521
if (isLastMessagePartial) {
503522
return true
504523
} else {
505-
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
524+
const lastApiReqStarted = findLast(
525+
modifiedMessages,
526+
(message: ClineMessage) => message.say === "api_req_started",
527+
)
506528

507529
if (
508530
lastApiReqStarted &&
@@ -522,7 +544,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
522544
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
523545

524546
const markFollowUpAsAnswered = useCallback(() => {
525-
const lastFollowUpMessage = messagesRef.current.findLast((msg) => msg.ask === "followup")
547+
const lastFollowUpMessage = messagesRef.current.findLast((msg: ClineMessage) => msg.ask === "followup")
526548
if (lastFollowUpMessage) {
527549
setCurrentFollowUpTs(lastFollowUpMessage.ts)
528550
}
@@ -564,7 +586,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
564586
if (sendingDisabled && !fromQueue) {
565587
// Generate a more unique ID using timestamp + random component
566588
const messageId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
567-
setMessageQueue((prev) => [...prev, { id: messageId, text, images }])
589+
setMessageQueue((prev: QueuedMessage[]) => [...prev, { id: messageId, text, images }])
568590
setInputValue("")
569591
setSelectedImages([])
570592
return
@@ -660,7 +682,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
660682
if (retryCount < MAX_RETRY_ATTEMPTS) {
661683
retryCountRef.current.set(nextMessage.id, retryCount + 1)
662684
// Re-add the message to the end of the queue
663-
setMessageQueue((current) => [...current, nextMessage])
685+
setMessageQueue((current: QueuedMessage[]) => [...current, nextMessage])
664686
} else {
665687
console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`)
666688
retryCountRef.current.delete(nextMessage.id)
@@ -834,7 +856,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
834856
// Only handle selectedImages if it's not for editing context
835857
// When context is "edit", ChatRow will handle the images
836858
if (message.context !== "edit") {
837-
setSelectedImages((prevImages) =>
859+
setSelectedImages((prevImages: string[]) =>
838860
appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE),
839861
)
840862
}
@@ -901,10 +923,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
901923
)
902924

903925
const visibleMessages = useMemo(() => {
904-
const newVisibleMessages = modifiedMessages.filter((message) => {
926+
const currentMessageCount = modifiedMessages.length
927+
const startIndex = Math.max(0, currentMessageCount - 500)
928+
const recentMessages = modifiedMessages.slice(startIndex)
929+
930+
const newVisibleMessages = recentMessages.filter((message: ClineMessage) => {
905931
if (everVisibleMessagesTsRef.current.has(message.ts)) {
906-
// If it was ever visible, and it's not one of the types that should always be hidden once processed, keep it.
907-
// This helps prevent flickering for messages like 'api_req_retry_delayed' if they are no longer the absolute last.
908932
const alwaysHiddenOnceProcessedAsk: ClineAsk[] = [
909933
"api_req_failed",
910934
"resume_task",
@@ -918,14 +942,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
918942
]
919943
if (message.ask && alwaysHiddenOnceProcessedAsk.includes(message.ask)) return false
920944
if (message.say && alwaysHiddenOnceProcessedSay.includes(message.say)) return false
921-
// Also, re-evaluate empty text messages if they were previously visible but now empty (e.g. partial stream ended)
922945
if (message.say === "text" && (message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
923946
return false
924947
}
925948
return true
926949
}
927950

928-
// Original filter logic
929951
switch (message.ask) {
930952
case "completion_result":
931953
if (message.text === "") return false
@@ -944,9 +966,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
944966
const last1 = modifiedMessages.at(-1)
945967
const last2 = modifiedMessages.at(-2)
946968
if (last1?.ask === "resume_task" && last2 === message) {
947-
// This specific sequence should be visible
948969
} else if (message !== last1) {
949-
// If not the specific sequence above, and not the last message, hide it.
950970
return false
951971
}
952972
break
@@ -959,8 +979,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
959979
return true
960980
})
961981

962-
// Update the set of ever-visible messages (LRUCache automatically handles cleanup)
963-
newVisibleMessages.forEach((msg) => everVisibleMessagesTsRef.current.set(msg.ts, true))
982+
const viewportStart = Math.max(0, newVisibleMessages.length - 100)
983+
newVisibleMessages
984+
.slice(viewportStart)
985+
.forEach((msg: ClineMessage) => everVisibleMessagesTsRef.current.set(msg.ts, true))
964986

965987
return newVisibleMessages
966988
}, [modifiedMessages])
@@ -1240,7 +1262,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12401262
}
12411263
}
12421264

1243-
visibleMessages.forEach((message) => {
1265+
visibleMessages.forEach((message: ClineMessage) => {
12441266
if (message.ask === "browser_action_launch") {
12451267
// Complete existing browser session if any.
12461268
endBrowserSession()
@@ -1333,7 +1355,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13331355

13341356
const handleSetExpandedRow = useCallback(
13351357
(ts: number, expand?: boolean) => {
1336-
setExpandedRows((prev) => ({ ...prev, [ts]: expand === undefined ? !prev[ts] : expand }))
1358+
setExpandedRows((prev: Record<number, boolean>) => ({
1359+
...prev,
1360+
[ts]: expand === undefined ? !prev[ts] : expand,
1361+
}))
13371362
},
13381363
[setExpandedRows], // setExpandedRows is stable
13391364
)
@@ -1362,7 +1387,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13621387
)
13631388

13641389
useEffect(() => {
1365-
let timer: NodeJS.Timeout | undefined
1390+
let timer: ReturnType<typeof setTimeout> | undefined
13661391
if (!disableAutoScrollRef.current) {
13671392
timer = setTimeout(() => scrollToBottomSmooth(), 50)
13681393
}
@@ -1425,7 +1450,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14251450
)
14261451

14271452
const handleSuggestionClickInRow = useCallback(
1428-
(suggestion: SuggestionItem, event?: React.MouseEvent) => {
1453+
(suggestion: SuggestionItem, event?: MouseEvent) => {
14291454
// Mark that user has responded if this is a manual click (not auto-approval)
14301455
if (event) {
14311456
userRespondedRef.current = true
@@ -1448,7 +1473,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14481473

14491474
if (event?.shiftKey) {
14501475
// Always append to existing text, don't overwrite
1451-
setInputValue((currentValue) => {
1476+
setInputValue((currentValue: string) => {
14521477
return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer
14531478
})
14541479
} else {
@@ -1482,7 +1507,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14821507
isStreaming={isStreaming}
14831508
isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
14841509
onToggleExpand={(messageTs: number) => {
1485-
setExpandedRows((prev) => ({
1510+
setExpandedRows((prev: Record<number, boolean>) => ({
14861511
...prev,
14871512
[messageTs]: !prev[messageTs],
14881513
}))
@@ -1842,20 +1867,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18421867
<div className="grow flex" ref={scrollContainerRef}>
18431868
<Virtuoso
18441869
ref={virtuosoRef}
1845-
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
1870+
key={task.ts}
18461871
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)
1849-
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
1872+
increaseViewportBy={{ top: 3_000, bottom: 1000 }}
1873+
data={groupedMessages}
18501874
itemContent={itemContent}
1851-
atBottomStateChange={(isAtBottom) => {
1875+
atBottomStateChange={(isAtBottom: boolean) => {
18521876
setIsAtBottom(isAtBottom)
18531877
if (isAtBottom) {
18541878
disableAutoScrollRef.current = false
18551879
}
18561880
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
18571881
}}
1858-
atBottomThreshold={10} // anything lower causes issues with followOutput
1882+
atBottomThreshold={10}
18591883
initialTopMostItemIndex={groupedMessages.length - 1}
18601884
/>
18611885
</div>

0 commit comments

Comments
 (0)