From 17b0cc08c7306b2a6c9478a3ba9214333aa5d6ec Mon Sep 17 00:00:00 2001 From: xyOz Date: Tue, 5 Aug 2025 08:07:18 +0100 Subject: [PATCH 1/4] 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. --- webview-ui/src/components/chat/ChatView.tsx | 82 +++++++++++++-------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 1fe93eb4700..aacd69b727d 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -181,8 +181,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction>( new LRUCache({ - max: 250, - ttl: 1000 * 60 * 15, // 15 minutes TTL for long-running tasks + max: 100, + ttl: 1000 * 60 * 5, }), ) const autoApproveTimeoutRef = useRef(null) @@ -458,7 +458,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction () => everVisibleMessagesTsRef.current.clear(), []) + useEffect(() => { + return () => { + everVisibleMessagesTsRef.current.clear() + } + }, []) + + useEffect(() => { + const cleanupInterval = setInterval(() => { + const cache = everVisibleMessagesTsRef.current + const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts)) + + cache.forEach((value: boolean, key: number) => { + if (!currentMessageIds.has(key)) { + cache.delete(key) + } + }) + }, 60000) + + return () => clearInterval(cleanupInterval) + }, [modifiedMessages]) useEffect(() => { const prev = prevExpandedRowsRef.current @@ -502,7 +521,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction message.say === "api_req_started") + const lastApiReqStarted = findLast( + modifiedMessages, + (message: ClineMessage) => message.say === "api_req_started", + ) if ( lastApiReqStarted && @@ -522,7 +544,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const lastFollowUpMessage = messagesRef.current.findLast((msg) => msg.ask === "followup") + const lastFollowUpMessage = messagesRef.current.findLast((msg: ClineMessage) => msg.ask === "followup") if (lastFollowUpMessage) { setCurrentFollowUpTs(lastFollowUpMessage.ts) } @@ -564,7 +586,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction [...prev, { id: messageId, text, images }]) + setMessageQueue((prev: QueuedMessage[]) => [...prev, { id: messageId, text, images }]) setInputValue("") setSelectedImages([]) return @@ -660,7 +682,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction [...current, nextMessage]) + setMessageQueue((current: QueuedMessage[]) => [...current, nextMessage]) } else { console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`) retryCountRef.current.delete(nextMessage.id) @@ -834,7 +856,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + setSelectedImages((prevImages: string[]) => appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE), ) } @@ -901,10 +923,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const newVisibleMessages = modifiedMessages.filter((message) => { + const currentMessageCount = modifiedMessages.length + const startIndex = Math.max(0, currentMessageCount - 500) + const recentMessages = modifiedMessages.slice(startIndex) + + const newVisibleMessages = recentMessages.filter((message: ClineMessage) => { if (everVisibleMessagesTsRef.current.has(message.ts)) { - // If it was ever visible, and it's not one of the types that should always be hidden once processed, keep it. - // This helps prevent flickering for messages like 'api_req_retry_delayed' if they are no longer the absolute last. const alwaysHiddenOnceProcessedAsk: ClineAsk[] = [ "api_req_failed", "resume_task", @@ -918,14 +942,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction everVisibleMessagesTsRef.current.set(msg.ts, true)) + const viewportStart = Math.max(0, newVisibleMessages.length - 100) + newVisibleMessages + .slice(viewportStart) + .forEach((msg: ClineMessage) => everVisibleMessagesTsRef.current.set(msg.ts, true)) return newVisibleMessages }, [modifiedMessages]) @@ -1240,7 +1262,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + visibleMessages.forEach((message: ClineMessage) => { if (message.ask === "browser_action_launch") { // Complete existing browser session if any. endBrowserSession() @@ -1333,7 +1355,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - setExpandedRows((prev) => ({ ...prev, [ts]: expand === undefined ? !prev[ts] : expand })) + setExpandedRows((prev: Record) => ({ + ...prev, + [ts]: expand === undefined ? !prev[ts] : expand, + })) }, [setExpandedRows], // setExpandedRows is stable ) @@ -1362,7 +1387,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let timer: NodeJS.Timeout | undefined + let timer: ReturnType | undefined if (!disableAutoScrollRef.current) { timer = setTimeout(() => scrollToBottomSmooth(), 50) } @@ -1425,7 +1450,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + (suggestion: SuggestionItem, event?: MouseEvent) => { // Mark that user has responded if this is a manual click (not auto-approval) if (event) { userRespondedRef.current = true @@ -1448,7 +1473,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + setInputValue((currentValue: string) => { return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer }) } else { @@ -1482,7 +1507,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction expandedRows[messageTs] ?? false} onToggleExpand={(messageTs: number) => { - setExpandedRows((prev) => ({ + setExpandedRows((prev: Record) => ({ ...prev, [messageTs]: !prev[messageTs], })) @@ -1842,20 +1867,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + atBottomStateChange={(isAtBottom: boolean) => { setIsAtBottom(isAtBottom) if (isAtBottom) { disableAutoScrollRef.current = false } setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom) }} - atBottomThreshold={10} // anything lower causes issues with followOutput + atBottomThreshold={10} initialTopMostItemIndex={groupedMessages.length - 1} /> From 2d94fd2da21aec444d60531b99372fe16184acf9 Mon Sep 17 00:00:00 2001 From: xyOz Date: Tue, 5 Aug 2025 09:10:32 +0100 Subject: [PATCH 2/4] Optimize message cache cleanup in ChatView Improves the cleanup logic for the everVisibleMessagesTsRef cache by ensuring that only messages not present in current or recent viewport messages are deleted. Also updates event typing for suggestion clicks and minor refactoring for clarity. --- webview-ui/src/components/chat/ChatView.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index aacd69b727d..49dea8f8b0f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" import { useDeepCompareEffect, useEvent, useMount } from "react-use" import debounce from "debounce" import { Virtuoso, type VirtuosoHandle } from "react-virtuoso" @@ -459,8 +459,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const cache = everVisibleMessagesTsRef.current return () => { - everVisibleMessagesTsRef.current.clear() + cache.clear() } }, []) @@ -468,16 +469,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const cache = everVisibleMessagesTsRef.current const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts)) + const viewportMessages = visibleMessages.slice(Math.max(0, visibleMessages.length - 100)) + const viewportMessageIds = new Set(viewportMessages.map((m: ClineMessage) => m.ts)) - cache.forEach((value: boolean, key: number) => { - if (!currentMessageIds.has(key)) { + cache.forEach((_value: boolean, key: number) => { + if (!currentMessageIds.has(key) && !viewportMessageIds.has(key)) { cache.delete(key) } }) }, 60000) return () => clearInterval(cleanupInterval) - }, [modifiedMessages]) + }, [modifiedMessages, visibleMessages]) useEffect(() => { const prev = prevExpandedRowsRef.current @@ -966,6 +969,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + (suggestion: SuggestionItem, event?: React.MouseEvent) => { // Mark that user has responded if this is a manual click (not auto-approval) if (event) { userRespondedRef.current = true From 8d25b2913b58d52bb61839734454a79307745ee8 Mon Sep 17 00:00:00 2001 From: xyOz Date: Tue, 5 Aug 2025 09:19:45 +0100 Subject: [PATCH 3/4] Refactor effect hooks for cleanup and focus logic Moved the cleanup interval and debounced focus effect hooks to after the visibleMessages memoization. This change improves code organization and ensures that hooks are declared after all dependent variables are defined. --- webview-ui/src/components/chat/ChatView.tsx | 53 +++++++++++---------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 49dea8f8b0f..bd30d31d666 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -465,22 +465,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const cleanupInterval = setInterval(() => { - const cache = everVisibleMessagesTsRef.current - const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts)) - const viewportMessages = visibleMessages.slice(Math.max(0, visibleMessages.length - 100)) - const viewportMessageIds = new Set(viewportMessages.map((m: ClineMessage) => m.ts)) - - cache.forEach((_value: boolean, key: number) => { - if (!currentMessageIds.has(key) && !viewportMessageIds.has(key)) { - cache.delete(key) - } - }) - }, 60000) - - return () => clearInterval(cleanupInterval) - }, [modifiedMessages, visibleMessages]) useEffect(() => { const prev = prevExpandedRowsRef.current @@ -915,16 +899,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction textAreaRef.current?.focus()) - useDebounceEffect( - () => { - if (!isHidden && !sendingDisabled && !enableButtons) { - textAreaRef.current?.focus() - } - }, - 50, - [isHidden, sendingDisabled, enableButtons], - ) - const visibleMessages = useMemo(() => { const currentMessageCount = modifiedMessages.length const startIndex = Math.max(0, currentMessageCount - 500) @@ -990,6 +964,33 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const cleanupInterval = setInterval(() => { + const cache = everVisibleMessagesTsRef.current + const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts)) + const viewportMessages = visibleMessages.slice(Math.max(0, visibleMessages.length - 100)) + const viewportMessageIds = new Set(viewportMessages.map((m: ClineMessage) => m.ts)) + + cache.forEach((_value: boolean, key: number) => { + if (!currentMessageIds.has(key) && !viewportMessageIds.has(key)) { + cache.delete(key) + } + }) + }, 60000) + + return () => clearInterval(cleanupInterval) + }, [modifiedMessages, visibleMessages]) + + useDebounceEffect( + () => { + if (!isHidden && !sendingDisabled && !enableButtons) { + textAreaRef.current?.focus() + } + }, + 50, + [isHidden, sendingDisabled, enableButtons], + ) const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => { if (message?.type === "ask") { From 0e6ba46d8f34ca66b739c6e7042847df70ad271e Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Tue, 5 Aug 2025 10:46:12 -0500 Subject: [PATCH 4/4] Fix: Update scroll-to-bottom to use scrollToIndex for compatibility with limited viewport - Replace scrollTo with MAX_SAFE_INTEGER with scrollToIndex - Ensures scroll-to-bottom works correctly with viewport limited to 1000px - Add groupedMessages.length to dependency arrays --- webview-ui/src/components/chat/ChatView.tsx | 44 ++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index bd30d31d666..1fd5b012461 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -465,7 +465,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const prev = prevExpandedRowsRef.current let wasAnyRowExpandedByUser = false @@ -964,24 +963,24 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const cleanupInterval = setInterval(() => { const cache = everVisibleMessagesTsRef.current const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts)) const viewportMessages = visibleMessages.slice(Math.max(0, visibleMessages.length - 100)) const viewportMessageIds = new Set(viewportMessages.map((m: ClineMessage) => m.ts)) - + cache.forEach((_value: boolean, key: number) => { if (!currentMessageIds.has(key) && !viewportMessageIds.has(key)) { cache.delete(key) } }) }, 60000) - + return () => clearInterval(cleanupInterval) }, [modifiedMessages, visibleMessages]) - + useDebounceEffect( () => { if (!isHidden && !sendingDisabled && !enableButtons) { @@ -1337,10 +1336,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction - debounce(() => virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" }), 10, { - immediate: true, - }), - [], + debounce( + () => { + const lastIndex = groupedMessages.length - 1 + if (lastIndex >= 0) { + virtuosoRef.current?.scrollToIndex({ + index: lastIndex, + behavior: "smooth", + align: "end", + }) + } + }, + 10, + { + immediate: true, + }, + ), + [groupedMessages.length], ) useEffect(() => { @@ -1352,11 +1364,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - virtuosoRef.current?.scrollTo({ - top: Number.MAX_SAFE_INTEGER, - behavior: "auto", // Instant causes crash. - }) - }, []) + const lastIndex = groupedMessages.length - 1 + if (lastIndex >= 0) { + virtuosoRef.current?.scrollToIndex({ + index: lastIndex, + behavior: "auto", // Instant causes crash. + align: "end", + }) + } + }, [groupedMessages.length]) const handleSetExpandedRow = useCallback( (ts: number, expand?: boolean) => {