diff --git a/server/openai-codex.js b/server/openai-codex.js index 8e14fcdf..1a2a8f15 100644 --- a/server/openai-codex.js +++ b/server/openai-codex.js @@ -279,6 +279,16 @@ export async function queryCodex(command, options = {}, ws) { startedAt: new Date().toISOString() }); }; + const markSessionFinished = (id) => { + if (!id) { + return; + } + + const session = activeCodexSessions.get(id); + if (session && session.status !== 'aborted') { + session.status = 'completed'; + } + }; // Existing sessions can be tracked immediately; new sessions are tracked after thread.started. if (capturedSessionId) { @@ -324,6 +334,10 @@ export async function queryCodex(command, options = {}, ws) { continue; } + if (event.type === 'turn.completed' || event.type === 'turn.failed') { + markSessionFinished(capturedSessionId || sessionId); + } + const transformed = transformCodexEvent(event); // Normalize the transformed event into NormalizedMessage(s) via adapter @@ -354,6 +368,8 @@ export async function queryCodex(command, options = {}, ws) { // Send completion event if (!terminalFailure) { + markSessionFinished(capturedSessionId || sessionId); + sendMessage(ws, createNormalizedMessage({ kind: 'complete', actualSessionId: capturedSessionId || thread.id || sessionId || null, diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts index 6a5a1b81..3efb737e 100644 --- a/src/components/chat/hooks/useChatRealtimeHandlers.ts +++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts @@ -98,6 +98,7 @@ export function useChatRealtimeHandlers({ }: UseChatRealtimeHandlersArgs) { const paletteOps = usePaletteOps(); const lastProcessedMessageRef = useRef(null); + const terminalSessionIdsRef = useRef>(new Set()); useEffect(() => { if (!latestMessage) return; @@ -151,6 +152,17 @@ export function useChatRealtimeHandlers({ const isCurrentSession = statusSessionId === currentSessionId || (selectedSession && statusSessionId === selectedSession.id); + if (msg.isProcessing && terminalSessionIdsRef.current.has(statusSessionId)) { + onSessionInactive?.(statusSessionId); + onSessionNotProcessing?.(statusSessionId); + if (isCurrentSession) { + setIsLoading(false); + setCanAbortSession(false); + setClaudeStatus(null); + } + return; + } + if (msg.isProcessing) { onSessionActive?.(statusSessionId); onSessionProcessing?.(statusSessionId); @@ -180,6 +192,10 @@ export function useChatRealtimeHandlers({ const sid = msg.sessionId || activeViewSessionId; + if (sid && msg.kind === 'session_created') { + terminalSessionIdsRef.current.delete(sid); + } + // --- Streaming: buffer for performance --- if (msg.kind === 'stream_delta') { const text = msg.content || ''; @@ -258,6 +274,10 @@ export function useChatRealtimeHandlers({ } case 'complete': { + if (sid) { + terminalSessionIdsRef.current.add(sid); + } + // Flush any remaining streaming state if (streamTimerRef.current) { clearTimeout(streamTimerRef.current); @@ -313,6 +333,10 @@ export function useChatRealtimeHandlers({ } case 'error': { + if (sid) { + terminalSessionIdsRef.current.add(sid); + } + setIsLoading(false); setCanAbortSession(false); setClaudeStatus(null); diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index 20f42551..85e37f77 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -131,6 +131,8 @@ export function useChatSessionState({ const pendingInitialScrollRef = useRef(true); const messagesOffsetRef = useRef(0); const scrollPositionRef = useRef({ height: 0, top: 0 }); + const previousProcessingSessionsRef = useRef | null>(null); + const previousProcessingSessionViewIdRef = useRef(null); const loadAllFinishedTimerRef = useRef | null>(null); const loadAllOverlayTimerRef = useRef | null>(null); const lastLoadedSessionKeyRef = useRef(null); @@ -693,9 +695,17 @@ export function useChatSessionState({ useEffect(() => { const activeViewSessionId = selectedSession?.id || currentSessionId; + const previousProcessingSessions = previousProcessingSessionsRef.current; + const previousProcessingSessionViewId = previousProcessingSessionViewIdRef.current; + previousProcessingSessionsRef.current = processingSessions ?? null; + previousProcessingSessionViewIdRef.current = activeViewSessionId ?? null; + if (!activeViewSessionId || !processingSessions) return; + + const activeViewSessionChanged = previousProcessingSessionViewId !== activeViewSessionId; + const wasProcessing = previousProcessingSessions?.has(activeViewSessionId) ?? false; const shouldBeProcessing = processingSessions.has(activeViewSessionId); - if (shouldBeProcessing && !isLoading) { + if (shouldBeProcessing && (!wasProcessing || activeViewSessionChanged) && !isLoading) { setIsLoading(true); setCanAbortSession(true); }