- Keep the send timer alive while a send is active - Fallback-reset sendPhase when a turn settles after a missed transition
Unified · Split
| @@ -2152,14 +2152,14 @@ export default function ChatView({ threadId }: ChatViewProps) { · 2151 unmodified lines | |||
| 2152 | : "local"; | 2152 | : "local"; |
| 2153 | 2153 | ||
| 2154 | useEffect(() => { | 2154 | useEffect(() => { |
| 2155 | if (phase !== "running") return; | 2155 | if (phase !== "running" && sendPhase === "idle") return; |
| 2156 | const timer = window.setInterval(() => { | 2156 | const timer = window.setInterval(() => { |
| 2157 | setNowTick(Date.now()); | 2157 | setNowTick(Date.now()); |
| 2158 | }, 1000); | 2158 | }, 1000); |
| 2159 | return () => { | 2159 | return () => { |
| 2160 | window.clearInterval(timer); | 2160 | window.clearInterval(timer); |
| 2161 | }; | 2161 | }; |
| 2162 | }, [phase]); | 2162 | }, [phase, sendPhase]); |
| 2163 | 2163 | ||
| 2164 | const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => { | 2164 | const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => { |
| 2165 | setSendStartedAt((current) => current ?? new Date().toISOString()); | 2165 | setSendStartedAt((current) => current ?? new Date().toISOString()); |
| @@ -2192,6 +2192,19 @@ export default function ChatView({ threadId }: ChatViewProps) { · 26 unmodified lines | |||
| 2192 | sendPhase, | 2192 | sendPhase, |
| 2193 | ]); | 2193 | ]); |
| 2194 | 2194 | ||
| 2195 | // Fallback: if the "running" phase transition was missed (e.g. due to React | ||
| 2196 | // batching two snapshots, a WebSocket reconnect, or a very fast turn), the | ||
| 2197 | // primary auto-reset above never fires and sendPhase stays stuck. Detect | ||
| 2198 | // this by checking whether the turn has settled while sendPhase is still | ||
| 2199 | // non-idle — the turn completing is a definitive signal that "sending" is | ||
| 2200 | // over. | ||
| 2201 | useEffect(() => { | ||
| 2202 | if (sendPhase === "idle") return; | ||
| 2203 | if (phase !== "running" && phase !== "connecting" && latestTurnSettled) { | ||
| 2204 | resetSendPhase(); | ||
| 2205 | } | ||
| 2206 | }, [latestTurnSettled, phase, resetSendPhase, sendPhase]); | ||
| 2207 | |||
| 2195 | useEffect(() => { | 2208 | useEffect(() => { |
| 2196 | if (!activeThreadId) return; | 2209 | if (!activeThreadId) return; |
| 2197 | const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false; | 2210 | const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false; |