diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index d4563e242..b4f9281b4 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; +import React, { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Copy, @@ -167,7 +167,7 @@ export const ClaudeCodeSession: React.FC = ({ // Filter out messages that shouldn't be displayed const displayableMessages = useMemo(() => { - return messages.filter((message, index) => { + const filtered = messages.filter((message, index) => { // Skip meta messages that don't have meaningful content if (message.isMeta && !message.leafUuid && !message.summary) { return false; @@ -196,13 +196,13 @@ export const ClaudeCodeSession: React.FC = ({ for (let i = index - 1; i >= 0; i--) { const prevMsg = messages[i]; if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { - const toolUse = prevMsg.message.content.find((c: any) => + const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id ); if (toolUse) { const toolName = toolUse.name?.toLowerCase(); const toolsWithWidgets = [ - 'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', + 'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', 'glob', 'bash', 'write', 'grep' ]; if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { @@ -226,13 +226,30 @@ export const ClaudeCodeSession: React.FC = ({ } return true; }); - }, [messages]); + + // Log for debugging the empty chat issue on macOS + if (process.env.NODE_ENV === 'development') { + console.log('[ClaudeCodeSession] displayableMessages update:', { + total: messages.length, + displayable: filtered.length, + sessionId: session?.id + }); + } + + return filtered; + }, [messages, session?.id]); + + // Use stable virtualizer instance with proper key for remounting + const virtualizerKey = useMemo(() => { + return `virtualizer-${session?.id || 'new'}-${displayableMessages.length}`; + }, [session?.id, displayableMessages.length]); const rowVirtualizer = useVirtualizer({ count: displayableMessages.length, getScrollElement: () => parentRef.current, estimateSize: () => 150, // Estimate, will be dynamically measured overscan: 5, + debug: false, }); // Debug logging @@ -247,12 +264,22 @@ export const ClaudeCodeSession: React.FC = ({ }); }, [projectPath, session, extractedSessionInfo, effectiveSession, messages.length, isLoading]); + // Reset component state when session changes (critical for macOS tab switching) + useLayoutEffect(() => { + // Clear messages immediately when session changes to prevent race conditions + if (session) { + setMessages([]); + setError(null); + setIsLoading(true); + } + }, [session?.id]); + // Load session history if resuming useEffect(() => { if (session) { // Set the claudeSessionId immediately when we have a session setClaudeSessionId(session.id); - + // Load session history first, then check for active session const initializeSession = async () => { await loadSessionHistory(); @@ -261,7 +288,7 @@ export const ClaudeCodeSession: React.FC = ({ await checkForActiveSession(); } }; - + initializeSession(); } }, [session]); // Remove hasLoadedSession dependency to ensure it runs on mount @@ -271,13 +298,31 @@ export const ClaudeCodeSession: React.FC = ({ onStreamingChange?.(isLoading, claudeSessionId); }, [isLoading, claudeSessionId, onStreamingChange]); - // Auto-scroll to bottom when new messages arrive - useEffect(() => { - if (displayableMessages.length > 0) { - rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' }); + // Initialize virtual scroller properly after DOM updates + const scrollToBottom = useCallback(() => { + if (displayableMessages.length > 0 && rowVirtualizer) { + try { + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' }); + } catch (error) { + console.warn('Failed to scroll to index:', error); + // Fallback scroll to bottom + if (parentRef.current) { + parentRef.current.scrollTop = parentRef.current.scrollHeight; + } + } } }, [displayableMessages.length, rowVirtualizer]); + // Use layoutEffect for immediate DOM operations - better for macOS + useLayoutEffect(() => { + if (displayableMessages.length > 0) { + // Use requestAnimationFrame for smooth macOS rendering + requestAnimationFrame(() => { + scrollToBottom(); + }); + } + }, [displayableMessages.length, scrollToBottom]); + // Calculate total tokens from messages useEffect(() => { const tokens = messages.reduce((total, msg) => { @@ -323,12 +368,23 @@ export const ClaudeCodeSession: React.FC = ({ // After loading history, we're continuing a conversation setIsFirstPrompt(false); - // Scroll to bottom after loading history - setTimeout(() => { - if (loadedMessages.length > 0) { - rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); - } - }, 100); + // Scroll to bottom after loading history with proper timing for macOS + // Use requestAnimationFrame for better macOS compatibility + requestAnimationFrame(() => { + setTimeout(() => { + if (loadedMessages.length > 0) { + try { + rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + } catch (error) { + console.warn('Failed to scroll to index after loading history:', error); + // Fallback scroll + if (parentRef.current) { + parentRef.current.scrollTop = parentRef.current.scrollHeight; + } + } + } + }, 100); // Reduced delay since we're using RAF + }); } catch (err) { console.error("Failed to load session history:", err); setError("Failed to load session history"); @@ -1145,6 +1201,7 @@ export const ClaudeCodeSession: React.FC = ({ style={{ contain: 'strict', }} + key={virtualizerKey} // Force re-render when virtualizer key changes >
= ({ }} > - {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const message = displayableMessages[virtualItem.index]; - return ( - el && rowVirtualizer.measureElement(el)} - initial={{ opacity: 0, y: 8 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -8 }} - transition={{ duration: 0.3 }} - className="absolute inset-x-4 pb-4" - style={{ - top: virtualItem.start, - }} - > - - - ); - })} + {rowVirtualizer && displayableMessages.length > 0 ? ( + rowVirtualizer.getVirtualItems().map((virtualItem) => { + const message = displayableMessages[virtualItem.index]; + if (!message) return null; // Guard against undefined messages + + return ( + el && rowVirtualizer.measureElement(el)} + initial={{ opacity: 0, y: 8 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -8 }} + transition={{ duration: 0.3 }} + className="absolute inset-x-4 pb-4" + style={{ + top: virtualItem.start, + }} + > + + + ); + }) + ) : ( + // Show empty state when no messages to help debug + displayableMessages.length === 0 && messages.length > 0 && ( +
+

Messages are being processed...

+

Raw: {messages.length}, Filtered: {displayableMessages.length}

+
+ ) + )}