From 9de456fd82d29e7d750663a2f6692f27ad102e71 Mon Sep 17 00:00:00 2001 From: konsomejona Date: Wed, 17 Sep 2025 22:45:21 +0900 Subject: [PATCH] Fix session view rendering with stable remount and reliable scroll --- src/components/ClaudeCodeSession.tsx | 97 ++++++++++++++++------------ 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index d4563e242..8e477c54b 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 } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Copy, @@ -16,7 +16,6 @@ import { Popover } from "@/components/ui/popover"; import { api, type Session } from "@/lib/api"; import { cn } from "@/lib/utils"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { StreamMessage } from "./StreamMessage"; import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput"; import { ErrorBoundary } from "./ErrorBoundary"; import { TimelineNavigator } from "./TimelineNavigator"; @@ -28,6 +27,7 @@ import { SplitPane } from "@/components/ui/split-pane"; import { WebviewPreview } from "./WebviewPreview"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { useVirtualizer } from "@tanstack/react-virtual"; +import { StreamMessage } from "./StreamMessage"; import { useTrackEvent, useComponentMetrics, useWorkflowTracking } from "@/hooks"; import { SessionPersistenceService } from "@/services/sessionPersistence"; @@ -78,7 +78,7 @@ export const ClaudeCodeSession: React.FC = ({ const [projectPath] = useState(initialProjectPath || session?.project_path || ""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [, setError] = useState(null); const [rawJsonlOutput, setRawJsonlOutput] = useState([]); const [copyPopoverOpen, setCopyPopoverOpen] = useState(false); const [isFirstPrompt, setIsFirstPrompt] = useState(!session); @@ -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,15 +226,36 @@ export const ClaudeCodeSession: React.FC = ({ } return true; }); - }, [messages]); + return filtered; + }, [messages, session?.id]); + + // Stable key to remount list when the session changes + const virtualizerKey = useMemo(() => `virtualizer-${session?.id || 'new'}`, [session?.id]); const rowVirtualizer = useVirtualizer({ count: displayableMessages.length, getScrollElement: () => parentRef.current, - estimateSize: () => 150, // Estimate, will be dynamically measured + estimateSize: () => 150, overscan: 5, }); + // Ensure the latest item stays in view when messages update + useLayoutEffect(() => { + if (displayableMessages.length > 0) { + requestAnimationFrame(() => { + try { + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { + align: 'end', + behavior: 'smooth', + }); + } catch { + const el = parentRef.current; + if (el) el.scrollTop = el.scrollHeight; + } + }); + } + }, [displayableMessages.length, rowVirtualizer]); + // Debug logging useEffect(() => { console.log('[ClaudeCodeSession] State update:', { @@ -247,6 +268,15 @@ export const ClaudeCodeSession: React.FC = ({ }); }, [projectPath, session, extractedSessionInfo, effectiveSession, messages.length, isLoading]); + // Reset state quickly on session change to avoid race conditions on some platforms + useLayoutEffect(() => { + if (session) { + setMessages([]); + setError(null); + setIsLoading(true); + } + }, [session?.id]); + // Load session history if resuming useEffect(() => { if (session) { @@ -271,12 +301,7 @@ 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' }); - } - }, [displayableMessages.length, rowVirtualizer]); + // Smooth scroll helper handled directly where needed // Calculate total tokens from messages useEffect(() => { @@ -324,11 +349,16 @@ export const ClaudeCodeSession: React.FC = ({ setIsFirstPrompt(false); // Scroll to bottom after loading history - setTimeout(() => { - if (loadedMessages.length > 0) { - rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); - } - }, 100); + requestAnimationFrame(() => { + setTimeout(() => { + try { + rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + } catch { + const el = parentRef.current; + if (el) el.scrollTop = el.scrollHeight; + } + }, 100); + }); } catch (err) { console.error("Failed to load session history:", err); setError("Failed to load session history"); @@ -1140,11 +1170,10 @@ export const ClaudeCodeSession: React.FC = ({ const messagesList = (
= ({ {rowVirtualizer.getVirtualItems().map((virtualItem) => { const message = displayableMessages[virtualItem.index]; + if (!message) return null; return ( el && rowVirtualizer.measureElement(el)} initial={{ opacity: 0, y: 8 }} @@ -1166,12 +1196,10 @@ export const ClaudeCodeSession: React.FC = ({ exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.3 }} className="absolute inset-x-4 pb-4" - style={{ - top: virtualItem.start, - }} + style={{ top: virtualItem.start }} > - @@ -1181,7 +1209,6 @@ export const ClaudeCodeSession: React.FC = ({
- {/* Loading indicator under the latest message */} {isLoading && ( = ({
)} - - {/* Error indicator */} - {error && ( - - {error} - - )}
);