From 7835df4c9924f28adeda5fd0a67f14d7e28aa814 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 9 Apr 2026 17:55:03 -0700 Subject: [PATCH] fix snapshot crash bug --- .../components/file-viewer/file-viewer.tsx | 6 + .../[workspaceId]/home/hooks/use-chat.ts | 103 +++++++++++------- apps/sim/hooks/use-streaming-text.ts | 1 + 3 files changed, 71 insertions(+), 39 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index f1cd23cf8c..2cbc64a415 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -256,6 +256,12 @@ function TextEditor({ fetchedContent.endsWith(`\n${streamingContent}`) ? fetchedContent : `${fetchedContent}\n${streamingContent}` + if (nextContent === contentRef.current) { + pendingStreamReconcileRef.current = true + lastStreamedContentRef.current = nextContent + initializedRef.current = true + return + } pendingStreamReconcileRef.current = true lastStreamedContentRef.current = nextContent setContent(nextContent) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 1811d24e82..8ac22e6c34 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -642,8 +642,9 @@ export function useChat( const [error, setError] = useState(null) const [resolvedChatId, setResolvedChatId] = useState(initialChatId) const [resources, setResources] = useState([]) - const [activeResourceId, setActiveResourceId] = useState(null) - const initialActiveResourceIdRef = useRef(options?.initialActiveResourceId) + const [activeResourceId, setActiveResourceId] = useState( + options?.initialActiveResourceId ?? null + ) const [genericResourceData, setGenericResourceData] = useState(null) const onResourceEventRef = useRef(options?.onResourceEvent) onResourceEventRef.current = options?.onResourceEvent @@ -677,9 +678,10 @@ export function useChat( const [streamingFile, setStreamingFile] = useState(null) const streamingFileRef = useRef(streamingFile) streamingFileRef.current = streamingFile + const pendingStreamingFileRef = useRef<{ value: StreamingFilePreview | null } | null>(null) + const streamingFileFrameRef = useRef(null) const filePreviewSessionsRef = useRef>(new Map()) const activeFilePreviewToolCallIdRef = useRef(null) - const editContentParentToolCallIdRef = useRef>(new Map()) const [messageQueue, setMessageQueue] = useState([]) const messageQueueRef = useRef([]) @@ -708,6 +710,48 @@ export function useChat( >(async () => false) const finalizeRef = useRef<(options?: { error?: boolean }) => void>(() => {}) + const cancelQueuedStreamingFileUpdate = useCallback(() => { + if (streamingFileFrameRef.current !== null) { + cancelAnimationFrame(streamingFileFrameRef.current) + streamingFileFrameRef.current = null + } + pendingStreamingFileRef.current = null + }, []) + + const setStreamingFileImmediate = useCallback( + (next: StreamingFilePreview | null) => { + cancelQueuedStreamingFileUpdate() + streamingFileRef.current = next + setStreamingFile(next) + }, + [cancelQueuedStreamingFileUpdate] + ) + + const resetEphemeralPreviewState = useCallback( + (options?: { removeStreamingResource?: boolean }) => { + setStreamingFileImmediate(null) + filePreviewSessionsRef.current.clear() + activeFilePreviewToolCallIdRef.current = null + if (options?.removeStreamingResource) { + setResources((current) => current.filter((resource) => resource.id !== 'streaming-file')) + } + }, + [setStreamingFileImmediate] + ) + + const queueStreamingFileUpdate = useCallback((next: StreamingFilePreview) => { + streamingFileRef.current = next + pendingStreamingFileRef.current = { value: next } + if (streamingFileFrameRef.current !== null) return + streamingFileFrameRef.current = requestAnimationFrame(() => { + streamingFileFrameRef.current = null + const pending = pendingStreamingFileRef.current + pendingStreamingFileRef.current = null + if (!pending) return + setStreamingFile(pending.value) + }) + }, []) + const abortControllerRef = useRef(null) const streamReaderRef = useRef | null>(null) const chatIdRef = useRef(initialChatId) @@ -849,13 +893,9 @@ export function useChat( setIsReconnecting(false) setResources([]) setActiveResourceId(null) - setStreamingFile(null) - streamingFileRef.current = null - filePreviewSessionsRef.current.clear() - activeFilePreviewToolCallIdRef.current = null - editContentParentToolCallIdRef.current.clear() + resetEphemeralPreviewState() setMessageQueue([]) - }, [initialChatId, queryClient]) + }, [initialChatId, queryClient, resetEphemeralPreviewState]) useEffect(() => { if (workflowIdRef.current) return @@ -873,13 +913,9 @@ export function useChat( setIsReconnecting(false) setResources([]) setActiveResourceId(null) - setStreamingFile(null) - streamingFileRef.current = null - filePreviewSessionsRef.current.clear() - activeFilePreviewToolCallIdRef.current = null - editContentParentToolCallIdRef.current.clear() + resetEphemeralPreviewState() setMessageQueue([]) - }, [isHomePage]) + }, [isHomePage, resetEphemeralPreviewState]) useEffect(() => { if (!chatHistory || appliedChatIdRef.current === chatHistory.id) return @@ -1271,7 +1307,7 @@ export function useChat( if (nextSession.fileId) { setActiveResourceId(nextSession.fileId) } - setStreamingFile(nextSession) + setStreamingFileImmediate(nextSession) break } @@ -1316,7 +1352,7 @@ export function useChat( } } - setStreamingFile(nextSession) + setStreamingFileImmediate(nextSession) break } @@ -1327,7 +1363,7 @@ export function useChat( } sessions.set(id, nextSession) activeFilePreviewToolCallIdRef.current = id - setStreamingFile(nextSession) + setStreamingFileImmediate(nextSession) break } @@ -1351,12 +1387,11 @@ export function useChat( } sessions.set(id, nextSession) activeFilePreviewToolCallIdRef.current = id - streamingFileRef.current = nextSession const previewToolIdx = toolMap.get(id) if (previewToolIdx !== undefined && blocks[previewToolIdx].toolCall) { blocks[previewToolIdx].toolCall!.status = 'executing' } - setStreamingFile(nextSession) + queueStreamingFileUpdate(nextSession) break } @@ -1382,8 +1417,7 @@ export function useChat( } if (!activeSubagent || activeSubagent !== FileTool.id) { - setStreamingFile(null) - streamingFileRef.current = null + setStreamingFileImmediate(null) } break } @@ -1407,7 +1441,6 @@ export function useChat( } if (phase === MothershipStreamV1ToolPhase.result) { - const resultToolName = typeof payload.toolName === 'string' ? payload.toolName : '' const idx = toolMap.get(id) if (idx === undefined || !blocks[idx].toolCall) { break @@ -1514,8 +1547,7 @@ export function useChat( if (activeFilePreviewToolCallIdRef.current === id) { activeFilePreviewToolCallIdRef.current = null if (!activeSubagent || activeSubagent !== FileTool.id) { - setStreamingFile(null) - streamingFileRef.current = null + setStreamingFileImmediate(null) } } const fileResource = extractedResources.find((r) => r.type === 'file') @@ -1533,7 +1565,6 @@ export function useChat( setResources((rs) => rs.filter((r) => r.id !== 'streaming-file')) } } - editContentParentToolCallIdRef.current.delete(id) break } @@ -1566,7 +1597,6 @@ export function useChat( : undefined if (parentIdx !== undefined && blocks[parentIdx].toolCall) { toolMap.set(id, parentIdx) - editContentParentToolCallIdRef.current.set(id, parentToolCallId!) const tc = blocks[parentIdx].toolCall! tc.status = 'executing' tc.result = undefined @@ -1741,8 +1771,7 @@ export function useChat( fileName: '', content: '', } - streamingFileRef.current = emptyFile - setStreamingFile(emptyFile) + setStreamingFileImmediate(emptyFile) } flush() } else if (spanEvent === MothershipStreamV1SpanLifecycleEvent.end) { @@ -1750,8 +1779,7 @@ export function useChat( break } if (streamingFileRef.current && !activeFilePreviewToolCallIdRef.current) { - setStreamingFile(null) - streamingFileRef.current = null + setStreamingFileImmediate(null) const lastFileResource = resourcesRef.current.find( (r) => r.type === 'file' && r.id !== 'streaming-file' ) @@ -2118,6 +2146,7 @@ export function useChat( (options?: { error?: boolean }) => { sendingRef.current = false setIsSending(false) + setIsReconnecting(false) abortControllerRef.current = null invalidateChatQueries() @@ -2389,12 +2418,7 @@ export function useChat( await persistPartialResponse() } invalidateChatQueries() - setStreamingFile(null) - streamingFileRef.current = null - filePreviewSessionsRef.current.clear() - activeFilePreviewToolCallIdRef.current = null - editContentParentToolCallIdRef.current.clear() - setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) + resetEphemeralPreviewState({ removeStreamingResource: true }) const execState = useExecutionStore.getState() const consoleStore = useTerminalConsoleStore.getState() @@ -2437,7 +2461,7 @@ export function useChat( reportManualRunToolStop(workflowId, toolCallId).catch(() => {}) } - }, [invalidateChatQueries, persistPartialResponse, executionStream]) + }, [invalidateChatQueries, persistPartialResponse, executionStream, resetEphemeralPreviewState]) const removeFromQueue = useCallback((id: string) => { messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id) @@ -2467,12 +2491,13 @@ export function useChat( useEffect(() => { return () => { + cancelQueuedStreamingFileUpdate() streamReaderRef.current = null abortControllerRef.current = null streamGenRef.current++ sendingRef.current = false } - }, []) + }, [cancelQueuedStreamingFileUpdate]) return { messages, diff --git a/apps/sim/hooks/use-streaming-text.ts b/apps/sim/hooks/use-streaming-text.ts index 66ed1e343b..369977a0f6 100644 --- a/apps/sim/hooks/use-streaming-text.ts +++ b/apps/sim/hooks/use-streaming-text.ts @@ -54,6 +54,7 @@ export function useStreamingText(target: string, isStreaming: boolean): string { useEffect(() => { if (isStreaming) return + if (revealedRef.current === target) return revealedRef.current = target lastTargetChangeAtRef.current = Date.now() lastTargetLengthRef.current = target.length