diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 3004a7e578..d09b9e9e86 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -545,10 +545,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - console.warn(`ChatView performance issue: ${metric} = ${value}`) + console.warn(`[VIRTUALIZATION] ChatView performance issue: ${metric} = ${value}`) }, }) + console.log("[VIRTUALIZATION] Virtualization hook initialized:", { + messagesCount: modifiedMessages.length, + isStreaming, + isHidden, + viewportConfig, + timestamp: new Date().toISOString(), + }) + // Sync expanded rows with state manager useEffect(() => { if (stateManager) { @@ -559,6 +567,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction - debounce(() => optimizedScrollToBottom("smooth"), 10, { - immediate: true, - }), + debounce( + () => { + console.log("[VIRTUALIZATION] Smooth scroll to bottom triggered") + optimizedScrollToBottom("smooth") + }, + 10, + { + immediate: true, + }, + ), [optimizedScrollToBottom], ) @@ -1419,6 +1440,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + console.log("[VIRTUALIZATION] Auto scroll to bottom triggered") optimizedScrollToBottom("auto") }, [optimizedScrollToBottom]) @@ -1580,6 +1602,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + console.log("[VIRTUALIZATION] Rendering item at index:", { + index, + isGroup: Array.isArray(messageOrGroup), + messageTs: Array.isArray(messageOrGroup) ? messageOrGroup[0]?.ts : messageOrGroup.ts, + timestamp: new Date().toISOString(), + }) + // browser session group if (Array.isArray(messageOrGroup)) { return ( @@ -1959,12 +1988,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const target = e.currentTarget as HTMLElement - handleVirtuosoScroll(target.scrollTop) - handleScrollStateChange({ + const scrollState = { scrollTop: target.scrollTop, scrollHeight: target.scrollHeight, viewportHeight: target.clientHeight, + } + + console.log("[VIRTUALIZATION] Virtuoso scroll event:", { + ...scrollState, + distanceFromBottom: + scrollState.scrollHeight - scrollState.scrollTop - scrollState.viewportHeight, + timestamp: new Date().toISOString(), }) + + handleVirtuosoScroll(target.scrollTop) + handleScrollStateChange(scrollState) }} rangeChanged={handleRangeChange} atBottomStateChange={(atBottom) => { diff --git a/webview-ui/src/components/chat/hooks/useOptimizedVirtualization.ts b/webview-ui/src/components/chat/hooks/useOptimizedVirtualization.ts index ca86e28ff1..b25b94e757 100644 --- a/webview-ui/src/components/chat/hooks/useOptimizedVirtualization.ts +++ b/webview-ui/src/components/chat/hooks/useOptimizedVirtualization.ts @@ -104,18 +104,29 @@ export function useOptimizedVirtualization({ const devicePerf = detectDevicePerformance() const hasExpanded = stateManager.hasExpandedMessages() + console.log("[VIRTUALIZATION] Viewport config calculation:", { + devicePerf, + hasExpanded, + isStreaming, + timestamp: new Date().toISOString(), + }) + // Streaming takes priority if (isStreaming) { + console.log("[VIRTUALIZATION] Using streaming viewport config:", config.viewport.streaming) return config.viewport.streaming } // Expanded messages need more buffer if (hasExpanded) { + console.log("[VIRTUALIZATION] Using expanded viewport config:", config.viewport.expanded) return config.viewport.expanded } // Use device-specific config - return getViewportConfigForDevice(devicePerf) + const deviceConfig = getViewportConfigForDevice(devicePerf) + console.log("[VIRTUALIZATION] Using device-specific viewport config:", deviceConfig) + return deviceConfig }, [isStreaming, stateManager, config]) // Create optimized message groups @@ -132,6 +143,14 @@ export function useOptimizedVirtualization({ // Use stored scroll state const { scrollHeight, viewportHeight } = scrollState + console.log("[VIRTUALIZATION] Scroll event:", { + scrollTop, + scrollHeight, + viewportHeight, + distanceFromBottom: scrollHeight - scrollTop - viewportHeight, + timestamp: new Date().toISOString(), + }) + scrollManager.handleScroll(scrollTop, scrollHeight, viewportHeight) // Update performance metrics @@ -141,6 +160,12 @@ export function useOptimizedVirtualization({ const atBottom = scrollManager.isAtBottom(scrollTop, scrollHeight, viewportHeight) setIsAtBottom(atBottom) setShowScrollToBottom(!atBottom && scrollManager.getState().isUserScrolling) + + console.log("[VIRTUALIZATION] Scroll state updated:", { + isAtBottom: atBottom, + showScrollToBottom: !atBottom && scrollManager.getState().isUserScrolling, + userScrolling: scrollManager.getState().isUserScrolling, + }) }, [scrollState, scrollManager, performanceMonitor], ) @@ -148,29 +173,47 @@ export function useOptimizedVirtualization({ // Handle visible range changes const handleRangeChange = useCallback( (range: { startIndex: number; endIndex: number }) => { + console.log("[VIRTUALIZATION] Visible range changed:", { + startIndex: range.startIndex, + endIndex: range.endIndex, + totalGroups: messageGroups.length, + timestamp: new Date().toISOString(), + }) + setVisibleRange(range) // Update performance metrics const messageIndices = getVisibleMessageIndices(messageGroups, range) - performanceMonitor.updateMessageCounts( - messages.length, - messageIndices.endIndex - messageIndices.startIndex + 1, - ) + const visibleMessageCount = messageIndices.endIndex - messageIndices.startIndex + 1 + performanceMonitor.updateMessageCounts(messages.length, visibleMessageCount) + + console.log("[VIRTUALIZATION] Performance metrics updated:", { + totalMessages: messages.length, + visibleMessages: visibleMessageCount, + messageIndices, + }) // Pin important messages in visible range const visibleGroups = messageGroups.slice(range.startIndex, range.endIndex + 1) + let pinnedCount = 0 visibleGroups.forEach((group) => { group.messages.forEach((msg) => { // Pin error messages and active tools if (msg.ask === "api_req_failed" || msg.say === "error" || (msg.ask === "tool" && !msg.partial)) { stateManager.pinMessage(msg.ts) + pinnedCount++ } }) }) + if (pinnedCount > 0) { + console.log("[VIRTUALIZATION] Pinned messages in visible range:", pinnedCount) + } + // Cleanup old states periodically if (Math.random() < 0.1) { // 10% chance on each range change + console.log("[VIRTUALIZATION] Running state cleanup") stateManager.cleanup() } }, @@ -218,11 +261,15 @@ export function useOptimizedVirtualization({ performanceMonitor.updateDOMNodeCount() // Log metrics in development - if (process.env.NODE_ENV === "development") { - const report = performanceMonitor.getReport() - if (report.issues.length > 0) { - console.warn("Performance issues detected:", report.issues) - } + const report = performanceMonitor.getReport() + console.log("[VIRTUALIZATION] Performance report:", { + metrics: report.metrics, + issues: report.issues, + timestamp: new Date().toISOString(), + }) + + if (report.issues.length > 0) { + console.warn("[VIRTUALIZATION] Performance issues detected:", report.issues) } }, 5000) diff --git a/webview-ui/src/components/chat/utils/AutoScrollManager.ts b/webview-ui/src/components/chat/utils/AutoScrollManager.ts index f428ab9ca4..c69ef77200 100644 --- a/webview-ui/src/components/chat/utils/AutoScrollManager.ts +++ b/webview-ui/src/components/chat/utils/AutoScrollManager.ts @@ -36,16 +36,34 @@ export class AutoScrollManager { const isScrollingUp = deltaScroll < -5 // Small threshold to avoid noise const significantScroll = Math.abs(deltaScroll) > 10 + console.log("[VIRTUALIZATION] AutoScrollManager.handleScroll:", { + scrollTop, + scrollHeight, + clientHeight, + deltaScroll, + deltaTime, + distanceFromBottom, + scrollVelocity: this.scrollVelocity, + isScrollingUp, + significantScroll, + timestamp: new Date().toISOString(), + }) + if (isScrollingUp && distanceFromBottom > this.atBottomThreshold) { + console.log("[VIRTUALIZATION] User scrolling detected: scrolling up") this.isUserScrolling = true this.isScrolling = true } else if (significantScroll && !this.isNearBottom(scrollTop, scrollHeight, clientHeight)) { + console.log("[VIRTUALIZATION] User scrolling detected: significant scroll") this.isUserScrolling = true this.isScrolling = true } // Reset user scrolling flag if they scroll to bottom if (distanceFromBottom <= this.atBottomThreshold) { + if (this.isUserScrolling) { + console.log("[VIRTUALIZATION] User scrolling reset: reached bottom") + } this.isUserScrolling = false } @@ -61,6 +79,7 @@ export class AutoScrollManager { // Set timeout to detect end of scrolling this.scrollTimeout = setTimeout(() => { + console.log("[VIRTUALIZATION] Scrolling ended") this.isScrolling = false this.scrollVelocity = 0 this.scrollTimeout = null @@ -75,7 +94,17 @@ export class AutoScrollManager { // 1. User is manually scrolling // 2. There are expanded messages (user might be reading) // 3. Currently in a scroll animation - return !this.isUserScrolling && !hasExpandedMessages && !this.isScrolling + const shouldScroll = !this.isUserScrolling && !hasExpandedMessages && !this.isScrolling + + console.log("[VIRTUALIZATION] AutoScrollManager.shouldAutoScroll:", { + shouldScroll, + isUserScrolling: this.isUserScrolling, + hasExpandedMessages, + isScrolling: this.isScrolling, + timestamp: new Date().toISOString(), + }) + + return shouldScroll } /** @@ -98,6 +127,7 @@ export class AutoScrollManager { * Reset user scrolling flag */ resetUserScrolling(): void { + console.log("[VIRTUALIZATION] User scrolling reset manually") this.isUserScrolling = false } @@ -105,6 +135,7 @@ export class AutoScrollManager { * Force user scrolling state (e.g., when user expands a message) */ forceUserScrolling(): void { + console.log("[VIRTUALIZATION] User scrolling forced") this.isUserScrolling = true } @@ -127,6 +158,16 @@ export class AutoScrollManager { */ getScrollBehavior(currentTop: number, targetTop: number, maxSmoothDistance: number = 5000): ScrollBehavior { const distance = Math.abs(targetTop - currentTop) + const behavior = distance > maxSmoothDistance ? "auto" : "smooth" + + console.log("[VIRTUALIZATION] Scroll behavior calculated:", { + currentTop, + targetTop, + distance, + maxSmoothDistance, + behavior, + timestamp: new Date().toISOString(), + }) // Use instant scroll for large jumps to avoid janky animation if (distance > maxSmoothDistance) { diff --git a/webview-ui/src/components/chat/utils/MessageStateManager.ts b/webview-ui/src/components/chat/utils/MessageStateManager.ts index e1685d99be..239652c9cd 100644 --- a/webview-ui/src/components/chat/utils/MessageStateManager.ts +++ b/webview-ui/src/components/chat/utils/MessageStateManager.ts @@ -25,11 +25,17 @@ export class MessageStateManager { ttl: ttl, updateAgeOnGet: true, updateAgeOnHas: true, - dispose: (value, _key) => { + dispose: (value, key) => { // Update expanded count when items are evicted if (value.isExpanded) { this.expandedCount = Math.max(0, this.expandedCount - 1) } + console.log("[VIRTUALIZATION] MessageStateManager state evicted:", { + messageTs: key, + wasExpanded: value.isExpanded, + expandedCount: this.expandedCount, + timestamp: new Date().toISOString(), + }) }, }) this.pinnedMessages = new Set() @@ -39,7 +45,20 @@ export class MessageStateManager { * Get the state of a message */ getState(messageTs: number): MessageState | undefined { - return this.states.get(messageTs) + const state = this.states.get(messageTs) + if (state) { + console.log("[VIRTUALIZATION] MessageStateManager cache hit:", { + messageTs, + state, + timestamp: new Date().toISOString(), + }) + } else { + console.log("[VIRTUALIZATION] MessageStateManager cache miss:", { + messageTs, + timestamp: new Date().toISOString(), + }) + } + return state } /** @@ -70,6 +89,15 @@ export class MessageStateManager { this.expandedCount = Math.max(0, this.expandedCount - 1) } + console.log("[VIRTUALIZATION] MessageStateManager.setState:", { + messageTs, + previousState: existing, + newState, + expandedCount: this.expandedCount, + cacheSize: this.states.size, + timestamp: new Date().toISOString(), + }) + this.states.set(messageTs, newState) } @@ -172,6 +200,9 @@ export class MessageStateManager { } }) + const previousSize = this.states.size + const previousExpandedCount = this.expandedCount + // Clear all states this.states.clear() this.expandedCount = 0 @@ -183,13 +214,33 @@ export class MessageStateManager { this.expandedCount++ } }) + + console.log("[VIRTUALIZATION] MessageStateManager cleared:", { + previousSize, + previousExpandedCount, + newSize: this.states.size, + newExpandedCount: this.expandedCount, + pinnedCount: this.pinnedMessages.size, + timestamp: new Date().toISOString(), + }) } /** * Cleanup old states (called automatically by LRU cache) */ cleanup(): void { + const beforeSize = this.states.size this.states.purgeStale() + const afterSize = this.states.size + + if (beforeSize !== afterSize) { + console.log("[VIRTUALIZATION] MessageStateManager cleanup:", { + beforeSize, + afterSize, + removed: beforeSize - afterSize, + timestamp: new Date().toISOString(), + }) + } } /**