mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-05 14:07:40 +00:00
fix: prevent React 18 batching from losing messages during session sync (#461)
* fix: prevent React 18 batching from losing messages during session sync - Add lastLoadedSessionIdRef to skip redundant session reloads - Add length-based guards to sessionMessages -> chatMessages sync effect - Track whether chat is actively processing to prevent stale overwrites - Re-enable sessionMessages sync with proper guards that only fire when server data actually grew (new messages from server), not during re-renders * fix: reset message sync refs on session switch Reset prevSessionMessagesLengthRef and isInitialLoadRef when selectedSession changes to ensure correct message sync behavior when switching between sessions. Move ref declarations before their first usage for clarity. * fix: address CodeRabbit review — composite cache key and empty sync - Use composite key (sessionId:projectName:provider) for session load deduplication instead of sessionId alone, preventing stale cache hits when switching projects or providers with the same session ID - Allow zero-length sessionMessages to sync to chatMessages, so server clearing a session's messages is correctly reflected in the UI
This commit is contained in:
@@ -93,6 +93,7 @@ export function useChatSessionState({
|
||||
const scrollPositionRef = useRef({ height: 0, top: 0 });
|
||||
const loadAllFinishedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const loadAllOverlayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastLoadedSessionKeyRef = useRef<string | null>(null);
|
||||
|
||||
const createDiff = useMemo<DiffCalculator>(() => createCachedDiffCalculator(), []);
|
||||
|
||||
@@ -297,10 +298,15 @@ export function useChatSessionState({
|
||||
pendingScrollRestoreRef.current = null;
|
||||
}, [chatMessages.length]);
|
||||
|
||||
const prevSessionMessagesLengthRef = useRef(0);
|
||||
const isInitialLoadRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
pendingInitialScrollRef.current = true;
|
||||
topLoadLockRef.current = false;
|
||||
pendingScrollRestoreRef.current = null;
|
||||
prevSessionMessagesLengthRef.current = 0;
|
||||
isInitialLoadRef.current = true;
|
||||
setVisibleMessageCount(INITIAL_VISIBLE_MESSAGES);
|
||||
setIsUserScrolledUp(false);
|
||||
}, [selectedProject?.name, selectedSession?.id]);
|
||||
@@ -373,6 +379,15 @@ export function useChatSessionState({
|
||||
}
|
||||
}
|
||||
|
||||
// Skip loading if session+project+provider hasn't changed
|
||||
const sessionKey = `${selectedSession.id}:${selectedProject.name}:${provider}`;
|
||||
if (lastLoadedSessionKeyRef.current === sessionKey) {
|
||||
setTimeout(() => {
|
||||
isLoadingSessionRef.current = false;
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'cursor') {
|
||||
setCurrentSessionId(selectedSession.id);
|
||||
sessionStorage.setItem('cursorSessionId', selectedSession.id);
|
||||
@@ -400,6 +415,9 @@ export function useChatSessionState({
|
||||
setIsSystemSessionChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last loaded session key
|
||||
lastLoadedSessionKeyRef.current = sessionKey;
|
||||
} else {
|
||||
if (!isSystemSessionChange) {
|
||||
resetStreamingState();
|
||||
@@ -417,6 +435,7 @@ export function useChatSessionState({
|
||||
setHasMoreMessages(false);
|
||||
setTotalMessages(0);
|
||||
setTokenBudget(null);
|
||||
lastLoadedSessionKeyRef.current = null;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -433,7 +452,7 @@ export function useChatSessionState({
|
||||
pendingViewSessionRef,
|
||||
resetStreamingState,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
selectedSession?.id, // Only depend on session ID, not the entire object
|
||||
sendMessage,
|
||||
ws,
|
||||
]);
|
||||
@@ -490,11 +509,24 @@ export function useChatSessionState({
|
||||
}
|
||||
}, [pendingViewSessionRef, selectedSession?.id]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionMessages.length > 0) {
|
||||
setChatMessages(convertedMessages);
|
||||
// Only sync sessionMessages to chatMessages when:
|
||||
// 1. Not currently loading (to avoid overwriting user's just-sent message)
|
||||
// 2. SessionMessages actually changed (including from non-empty to empty)
|
||||
// 3. Either it's initial load OR sessionMessages increased (new messages from server)
|
||||
if (
|
||||
sessionMessages.length !== prevSessionMessagesLengthRef.current &&
|
||||
!isLoading
|
||||
) {
|
||||
// Only update if this is initial load, sessionMessages grew, or was cleared to empty
|
||||
if (isInitialLoadRef.current || sessionMessages.length === 0 || sessionMessages.length > prevSessionMessagesLengthRef.current) {
|
||||
setChatMessages(convertedMessages);
|
||||
isInitialLoadRef.current = false;
|
||||
}
|
||||
prevSessionMessagesLengthRef.current = sessionMessages.length;
|
||||
}
|
||||
}, [convertedMessages, sessionMessages.length]);
|
||||
}, [convertedMessages, sessionMessages.length, isLoading, setChatMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject && chatMessages.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user