diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 1fe93eb4700..1fd5b012461 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" @@ -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,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction () => everVisibleMessagesTsRef.current.clear(), []) + useEffect(() => { + const cache = everVisibleMessagesTsRef.current + return () => { + cache.clear() + } + }, []) useEffect(() => { const prev = prevExpandedRowsRef.current @@ -502,7 +507,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 +530,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 +572,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction [...prev, { id: messageId, text, images }]) + setMessageQueue((prev: QueuedMessage[]) => [...prev, { id: messageId, text, images }]) setInputValue("") setSelectedImages([]) return @@ -660,7 +668,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 +842,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + setSelectedImages((prevImages: string[]) => appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE), ) } @@ -890,21 +898,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction textAreaRef.current?.focus()) - useDebounceEffect( - () => { - if (!isHidden && !sendingDisabled && !enableButtons) { - textAreaRef.current?.focus() - } - }, - 50, - [isHidden, sendingDisabled, enableButtons], - ) - const visibleMessages = useMemo(() => { - 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 +918,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]) + useEffect(() => { + 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") { if (!message.text) { @@ -1240,7 +1266,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + visibleMessages.forEach((message: ClineMessage) => { if (message.ask === "browser_action_launch") { // Complete existing browser session if any. endBrowserSession() @@ -1310,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(() => { @@ -1325,15 +1364,22 @@ 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) => { - 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 +1408,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let timer: NodeJS.Timeout | undefined + let timer: ReturnType | undefined if (!disableAutoScrollRef.current) { timer = setTimeout(() => scrollToBottomSmooth(), 50) } @@ -1448,7 +1494,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + setInputValue((currentValue: string) => { return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer }) } else { @@ -1482,7 +1528,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction expandedRows[messageTs] ?? false} onToggleExpand={(messageTs: number) => { - setExpandedRows((prev) => ({ + setExpandedRows((prev: Record) => ({ ...prev, [messageTs]: !prev[messageTs], })) @@ -1842,20 +1888,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} />