The fallback effect that catches missed 'running' phase transitions was incorrectly firing on follow-up messages. When beginSendPhase was called, latestTurnSettled was still true from the previous completed turn, causing an immediate resetSendPhase before the server could create the new turn. Fix: record the turnId at the time beginSendPhase is called (via a ref), and only allow the fallback reset when the latest turn differs from the one recorded — meaning a new turn was actually created and has settled. Applied via @cursor push command
Unified · Split
| @@ -2161,14 +2161,21 @@ export default function ChatView({ threadId }: ChatViewProps) { · 2160 unmodified lines | |||
| 2161 | }; | 2161 | }; |
| 2162 | }, [phase, sendPhase]); | 2162 | }, [phase, sendPhase]); |
| 2163 | 2163 | ||
| 2164 | const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => { | 2164 | const turnIdAtSendRef = useRef<string | null>(null); |
| 2165 | setSendStartedAt((current) => current ?? new Date().toISOString()); | 2165 | |
| 2166 | setSendPhase(nextPhase); | 2166 | const beginSendPhase = useCallback( |
| 2167 | }, []); | 2167 | (nextPhase: Exclude<SendPhase, "idle">) => { |
| 2168 | setSendStartedAt((current) => current ?? new Date().toISOString()); | ||
| 2169 | turnIdAtSendRef.current = activeLatestTurn?.turnId ?? null; | ||
| 2170 | setSendPhase(nextPhase); | ||
| 2171 | }, | ||
| 2172 | [activeLatestTurn?.turnId], | ||
| 2173 | ); | ||
| 2168 | 2174 | ||
| 2169 | const resetSendPhase = useCallback(() => { | 2175 | const resetSendPhase = useCallback(() => { |
| 2170 | setSendPhase("idle"); | 2176 | setSendPhase("idle"); |
| 2171 | setSendStartedAt(null); | 2177 | setSendStartedAt(null); |
| 2178 | turnIdAtSendRef.current = null; | ||
| 2172 | }, []); | 2179 | }, []); |
| 2173 | 2180 | ||
| 2174 | useEffect(() => { | 2181 | useEffect(() => { |
| @@ -2198,12 +2205,18 @@ export default function ChatView({ threadId }: ChatViewProps) { · 23 unmodified lines | |||
| 2198 | // this by checking whether the turn has settled while sendPhase is still | 2205 | // 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 | 2206 | // non-idle — the turn completing is a definitive signal that "sending" is |
| 2200 | // over. | 2207 | // over. |
| 2208 | // | ||
| 2209 | // Guard: only reset when the latest turn differs from the one recorded at | ||
| 2210 | // send time. Without this, a follow-up message on a thread whose previous | ||
| 2211 | // turn already settled would be immediately reset before the server creates | ||
| 2212 | // the new turn. | ||
| 2201 | useEffect(() => { | 2213 | useEffect(() => { |
| 2202 | if (sendPhase === "idle") return; | 2214 | if (sendPhase === "idle") return; |
| 2203 | if (phase !== "running" && phase !== "connecting" && latestTurnSettled) { | 2215 | const turnChanged = activeLatestTurn?.turnId !== turnIdAtSendRef.current; |
| 2216 | if (phase !== "running" && phase !== "connecting" && latestTurnSettled && turnChanged) { | ||
| 2204 | resetSendPhase(); | 2217 | resetSendPhase(); |
| 2205 | } | 2218 | } |
| 2206 | }, [latestTurnSettled, phase, resetSendPhase, sendPhase]); | 2219 | }, [activeLatestTurn?.turnId, latestTurnSettled, phase, resetSendPhase, sendPhase]); |
| 2207 | 2220 | ||
| 2208 | useEffect(() => { | 2221 | useEffect(() => { |
| 2209 | if (!activeThreadId) return; | 2222 | if (!activeThreadId) return; |