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 | 2162 | }, [phase, sendPhase]); |
| 2163 | 2163 | |
| 2164 | − const beginSendPhase = useCallback((nextPhase: Exclude<SendPhase, "idle">) => { | |
| 2165 | − setSendStartedAt((current) => current ?? new Date().toISOString()); | |
| 2166 | − setSendPhase(nextPhase); | |
| 2167 | − }, []); | |
| 2164 | + const turnIdAtSendRef = useRef<string | null>(null); | |
| 2165 | + | |
| 2166 | + const beginSendPhase = useCallback( | |
| 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 | 2175 | const resetSendPhase = useCallback(() => { |
| 2170 | 2176 | setSendPhase("idle"); |
| 2171 | 2177 | setSendStartedAt(null); |
| 2178 | + turnIdAtSendRef.current = null; | |
| 2172 | 2179 | }, []); |
| 2173 | 2180 | |
| 2174 | 2181 | useEffect(() => { |
| @@ -2198,12 +2205,18 @@ export default function ChatView({ threadId }: ChatViewProps) { · 23 unmodified lines | ||
| 2198 | 2205 | // this by checking whether the turn has settled while sendPhase is still |
| 2199 | 2206 | // non-idle — the turn completing is a definitive signal that "sending" is |
| 2200 | 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 | 2213 | useEffect(() => { |
| 2202 | 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 | 2217 | resetSendPhase(); |
| 2205 | 2218 | } |
| 2206 | − }, [latestTurnSettled, phase, resetSendPhase, sendPhase]); | |
| 2219 | + }, [activeLatestTurn?.turnId, latestTurnSettled, phase, resetSendPhase, sendPhase]); | |
| 2207 | 2220 | |
| 2208 | 2221 | useEffect(() => { |
| 2209 | 2222 | if (!activeThreadId) return; |