diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index af2682c34..8d0ae128c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -974,11 +974,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { [lockedProvider, providerModelsByProvider, selectableProviders], ); const phase = derivePhase(activeThread?.session ?? null); + const isTurnActive = + activeThread?.session?.activeTurnId !== undefined && + activeThread?.session?.activeTurnId !== null; const isSendBusy = sendPhase !== "idle"; const isPreparingWorktree = sendPhase === "preparing-worktree"; const isTransportReady = transportState === "open"; const isRemoteActionBlocked = !isTransportReady; - const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; + const isWorking = isTurnActive || isSendBusy || isConnecting || isRevertingCheckpoint; const nowIso = new Date(nowTick).toISOString(); const activeWorkStartedAt = deriveActiveWorkStartedAt( activeLatestTurn, @@ -2530,10 +2533,10 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { scheduleStickToBottom(); }, [messageCount, scheduleStickToBottom]); useEffect(() => { - if (phase !== "running") return; + if (!isTurnActive) return; if (!shouldAutoScrollRef.current) return; scheduleStickToBottom(); - }, [phase, scheduleStickToBottom, timelineEntries]); + }, [isTurnActive, scheduleStickToBottom, timelineEntries]); // Aggressively scroll to bottom after the user submits a new message. // The virtualizer may not have settled by the time the first scroll fires, @@ -2778,14 +2781,14 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { : "local"; useEffect(() => { - if (phase !== "running") return; + if (!isTurnActive) return; const timer = window.setInterval(() => { setNowTick(Date.now()); }, 1000); return () => { window.clearInterval(timer); }; - }, [phase]); + }, [isTurnActive]); const beginSendPhase = useCallback((nextPhase: Exclude) => { setSendStartedAt((current) => current ?? new Date().toISOString()); @@ -2802,7 +2805,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { return; } if ( - phase === "running" || + isTurnActive || activePendingApproval !== null || activePendingUserInput !== null || activeThread?.error @@ -2813,7 +2816,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { activePendingApproval, activePendingUserInput, activeThread?.error, - phase, + isTurnActive, resetSendPhase, sendPhase, ]); @@ -3171,7 +3174,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { const api = readNativeApi(); if (!api || !activeThread || isRevertingCheckpoint) return; - if (phase === "running" || isSendBusy || isConnecting) { + if (isTurnActive || isSendBusy || isConnecting) { setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints."); return; } @@ -3204,7 +3207,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { } setIsRevertingCheckpoint(false); }, - [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError], + [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, isTurnActive, setThreadError], ); const readLiveComposerDraftSnapshot = useCallback(() => { @@ -3448,7 +3451,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { } // ── Queue message if a turn is already running ──────────────────── - if (phase === "running") { + if (isTurnActive) { const composerAttachmentsSnapshot = [...composerAttachmentsForSend]; const hiddenProviderInput = buildHiddenProviderInput({ prompt: promptForSend, @@ -5548,7 +5551,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) { Preparing worktree... ) : null} - {queuedMessages.length > 0 && phase === "running" ? ( + {queuedMessages.length > 0 && isTurnActive ? (