diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index 7f4da960..950db891 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -551,7 +551,7 @@ export function useChatComposerState({ }; setChatMessages((previous) => [...previous, userMessage]); - setIsLoading(true); + setIsLoading(true); // Processing banner starts setCanAbortSession(true); setClaudeStatus({ text: 'Processing', diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts index 84984a33..73c305d2 100644 --- a/src/components/chat/hooks/useChatRealtimeHandlers.ts +++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts @@ -134,9 +134,10 @@ export function useChatRealtimeHandlers({ latestMessage.data && typeof latestMessage.data === 'object' ? (latestMessage.data as Record) : null; + const messageType = String(latestMessage.type); const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created']; - const isGlobalMessage = globalMessageTypes.includes(String(latestMessage.type)); + const isGlobalMessage = globalMessageTypes.includes(messageType); const lifecycleMessageTypes = new Set([ 'claude-complete', 'codex-complete', @@ -146,6 +147,7 @@ export function useChatRealtimeHandlers({ 'cursor-error', 'codex-error', 'gemini-error', + 'error', ]); const isClaudeSystemInit = @@ -168,9 +170,12 @@ export function useChatRealtimeHandlers({ const activeViewSessionId = selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null; + const hasPendingUnboundSession = + Boolean(pendingViewSessionRef.current) && !pendingViewSessionRef.current?.sessionId; const isSystemInitForView = systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId); const shouldBypassSessionFilter = isGlobalMessage || Boolean(isSystemInitForView); + const isLifecycleMessage = lifecycleMessageTypes.has(messageType); const isUnscopedError = !latestMessage.sessionId && pendingViewSessionRef.current && @@ -201,6 +206,30 @@ export function useChatRealtimeHandlers({ setClaudeStatus(null); }; + const clearPendingViewSession = (resolvedSessionId?: string | null) => { + const pendingSession = pendingViewSessionRef.current; + if (!pendingSession) { + return; + } + + // If the in-view request never received a concrete session ID (or this terminal event + // resolves the same pending session), clear it to avoid stale "in-flight" UI state. + if (!pendingSession.sessionId || !resolvedSessionId || pendingSession.sessionId === resolvedSessionId) { + pendingViewSessionRef.current = null; + } + }; + + const flushStreamingState = () => { + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; + } + const pendingChunk = streamBufferRef.current; + streamBufferRef.current = ''; + appendStreamingChunk(setChatMessages, pendingChunk, false); + finalizeStreamingMessage(setChatMessages); + }; + const markSessionsAsCompleted = (...sessionIds: Array) => { const normalizedSessionIds = collectSessionIds(...sessionIds); normalizedSessionIds.forEach((sessionId) => { @@ -209,25 +238,46 @@ export function useChatRealtimeHandlers({ }); }; + const finalizeLifecycleForCurrentView = (...sessionIds: Array) => { + const pendingSessionId = typeof window !== 'undefined' ? sessionStorage.getItem('pendingSessionId') : null; + const resolvedSessionIds = collectSessionIds(...sessionIds, pendingSessionId, pendingViewSessionRef.current?.sessionId); + const resolvedPrimarySessionId = resolvedSessionIds[0] || null; + + flushStreamingState(); + clearLoadingIndicators(); + markSessionsAsCompleted(...resolvedSessionIds); + setPendingPermissionRequests([]); + clearPendingViewSession(resolvedPrimarySessionId); + }; + if (!shouldBypassSessionFilter) { if (!activeViewSessionId) { - if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) { + if (latestMessage.sessionId && isLifecycleMessage && !hasPendingUnboundSession) { handleBackgroundLifecycle(latestMessage.sessionId); + return; } - if (!isUnscopedError) { + if (!isUnscopedError && !hasPendingUnboundSession) { return; } } - if (!latestMessage.sessionId && !isUnscopedError) { + if (!latestMessage.sessionId && !isUnscopedError && !hasPendingUnboundSession) { return; } if (latestMessage.sessionId !== activeViewSessionId) { - if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) { - handleBackgroundLifecycle(latestMessage.sessionId); + const shouldTreatAsPendingViewLifecycle = + !activeViewSessionId && + hasPendingUnboundSession && + latestMessage.sessionId && + isLifecycleMessage; + + if (!shouldTreatAsPendingViewLifecycle) { + if (latestMessage.sessionId && isLifecycleMessage) { + handleBackgroundLifecycle(latestMessage.sessionId); + } + return; } - return; } } @@ -545,6 +595,7 @@ export function useChatRealtimeHandlers({ break; case 'claude-error': + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); setChatMessages((previous) => [ ...previous, { @@ -604,6 +655,7 @@ export function useChatRealtimeHandlers({ break; case 'cursor-error': + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); setChatMessages((previous) => [ ...previous, { @@ -618,8 +670,7 @@ export function useChatRealtimeHandlers({ const cursorCompletedSessionId = latestMessage.sessionId || currentSessionId; const pendingCursorSessionId = sessionStorage.getItem('pendingSessionId'); - clearLoadingIndicators(); - markSessionsAsCompleted( + finalizeLifecycleForCurrentView( cursorCompletedSessionId, currentSessionId, selectedSession?.id, @@ -701,8 +752,7 @@ export function useChatRealtimeHandlers({ const completedSessionId = latestMessage.sessionId || currentSessionId || pendingSessionId; - clearLoadingIndicators(); - markSessionsAsCompleted( + finalizeLifecycleForCurrentView( completedSessionId, currentSessionId, selectedSession?.id, @@ -718,7 +768,6 @@ export function useChatRealtimeHandlers({ if (selectedProject && latestMessage.exitCode === 0) { safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`); } - setPendingPermissionRequests([]); break; } @@ -836,13 +885,11 @@ export function useChatRealtimeHandlers({ } if (codexData.type === 'turn_complete') { - clearLoadingIndicators(); - markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id); + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); } if (codexData.type === 'turn_failed') { - clearLoadingIndicators(); - markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id); + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); setChatMessages((previous) => [ ...previous, { @@ -861,8 +908,7 @@ export function useChatRealtimeHandlers({ const codexCompletedSessionId = latestMessage.sessionId || currentSessionId || codexPendingSessionId; - clearLoadingIndicators(); - markSessionsAsCompleted( + finalizeLifecycleForCurrentView( codexCompletedSessionId, codexActualSessionId, currentSessionId, @@ -886,8 +932,7 @@ export function useChatRealtimeHandlers({ } case 'codex-error': - setIsLoading(false); - setCanAbortSession(false); + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); setChatMessages((previous) => [ ...previous, { @@ -937,8 +982,7 @@ export function useChatRealtimeHandlers({ } case 'gemini-error': - setIsLoading(false); - setCanAbortSession(false); + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); setChatMessages((previous) => [ ...previous, { @@ -990,13 +1034,11 @@ export function useChatRealtimeHandlers({ const abortSucceeded = latestMessage.success !== false; if (abortSucceeded) { - clearLoadingIndicators(); - markSessionsAsCompleted(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId); + finalizeLifecycleForCurrentView(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId); if (pendingSessionId && (!abortedSessionId || pendingSessionId === abortedSessionId)) { sessionStorage.removeItem('pendingSessionId'); } - setPendingPermissionRequests([]); setChatMessages((previous) => [ ...previous, { @@ -1093,6 +1135,12 @@ export function useChatRealtimeHandlers({ break; } + case 'error': + // Generic backend failure (e.g., provider process failed before a provider-specific + // completion event was emitted). Treat it as terminal for current view lifecycle. + finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id); + break; + default: break; } diff --git a/src/contexts/WebSocketContext.tsx b/src/contexts/WebSocketContext.tsx index 2f2677ff..ff680963 100644 --- a/src/contexts/WebSocketContext.tsx +++ b/src/contexts/WebSocketContext.tsx @@ -67,6 +67,7 @@ const useWebSocketProviderState = (): WebSocketContextType => { try { const data = JSON.parse(event.data); setLatestMessage(data); + console.log('--->Received WebSocket message:', data); } catch (error) { console.error('Error parsing WebSocket message:', error); }