History

fix: prevent fallback effect from immediately resetting sendPhase on follow-up messages

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

Cursor Agent committed 7 months ago

Commit c91e8b21 parent 9a9be391 file · +19 −6Browse files

Unified · Split

apps/web/src/components/ChatView.tsx+19−6
@@ -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 still2205 // this by checking whether the turn has settled while sendPhase is still
2199 // non-idle — the turn completing is a definitive signal that "sending" is2206 // 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;