- 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 | 2152 | : "local"; |
| 2153 | 2153 | |
| 2154 | 2154 | useEffect(() => { |
| 2155 | − if (phase !== "running") return; | |
| 2155 | + if (phase !== "running" && sendPhase === "idle") return; | |
| 2156 | 2156 | const timer = window.setInterval(() => { |
| 2157 | 2157 | setNowTick(Date.now()); |
| 2158 | 2158 | }, 1000); |
| 2159 | 2159 | return () => { |
| 2160 | 2160 | window.clearInterval(timer); |
| 2161 | 2161 | }; |
| 2162 | − }, [phase]); | |
| 2162 | + }, [phase, sendPhase]); | |
| 2163 | 2163 | |
| 2164 | 2164 | const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => { |
| 2165 | 2165 | setSendStartedAt((current) => current ?? new Date().toISOString()); |
| @@ -2192,6 +2192,19 @@ export default function ChatView({ threadId }: ChatViewProps) { · 26 unmodified lines | ||
| 2192 | 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 | 2208 | useEffect(() => { |
| 2196 | 2209 | if (!activeThreadId) return; |
| 2197 | 2210 | const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false; |