From ad4fd5f984705771161a9f3d37ea385702b49289 Mon Sep 17 00:00:00 2001 From: Prateek Date: Mon, 16 Feb 2026 06:32:38 +0530 Subject: [PATCH] fix: prevent race condition in concurrent "Ask Agent to Fix" clicks Changed state tracking from single values to Sets to properly handle multiple concurrent button clicks: - `sendingComments: Set` - tracks all in-flight requests - `sentComments: Set` - tracks successful completions - `errorComments: Set` - tracks failures - `timersRef: Map` - per-comment cleanup timers This ensures that: - Each button correctly shows its own loading/success/error state - Buttons remain disabled only while their specific request is pending - Callbacks from different requests don't overwrite each other's state Fixes the race condition identified by Cursor Bugbot. Co-Authored-By: Claude Sonnet 4.5 --- packages/web/src/components/SessionDetail.tsx | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx index 8c6f74afc..dd4e9e6aa 100644 --- a/packages/web/src/components/SessionDetail.tsx +++ b/packages/web/src/components/SessionDetail.tsx @@ -290,36 +290,79 @@ function ClientTimestamps({ // ── PR Card ────────────────────────────────────────────────────────── function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) { - const [sendingCommentUrl, setSendingCommentUrl] = useState(null); - const [sentCommentUrl, setSentCommentUrl] = useState(null); - const [errorCommentUrl, setErrorCommentUrl] = useState(null); - const timerRef = useRef | null>(null); + const [sendingComments, setSendingComments] = useState>(new Set()); + const [sentComments, setSentComments] = useState>(new Set()); + const [errorComments, setErrorComments] = useState>(new Set()); + const timersRef = useRef>>(new Map()); useEffect(() => { return () => { - if (timerRef.current) clearTimeout(timerRef.current); + timersRef.current.forEach((timer) => clearTimeout(timer)); }; }, []); const handleAskAgentToFix = async (comment: { url: string; path: string; body: string }) => { - setSendingCommentUrl(comment.url); - setErrorCommentUrl(null); - setSentCommentUrl(null); + // Clear any existing feedback state for this comment + setSentComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + setErrorComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + + // Mark as sending + setSendingComments((prev) => new Set(prev).add(comment.url)); await askAgentToFix( sessionId, comment, () => { - setSendingCommentUrl(null); - setSentCommentUrl(comment.url); - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setSentCommentUrl(null), 3000); + // Success: remove from sending, add to sent + setSendingComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + setSentComments((prev) => new Set(prev).add(comment.url)); + + // Clear sent state after 3 seconds + const existingTimer = timersRef.current.get(comment.url); + if (existingTimer) clearTimeout(existingTimer); + const timer = setTimeout(() => { + setSentComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + timersRef.current.delete(comment.url); + }, 3000); + timersRef.current.set(comment.url, timer); }, () => { - setSendingCommentUrl(null); - setErrorCommentUrl(comment.url); - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setErrorCommentUrl(null), 3000); + // Error: remove from sending, add to error + setSendingComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + setErrorComments((prev) => new Set(prev).add(comment.url)); + + // Clear error state after 3 seconds + const existingTimer = timersRef.current.get(comment.url); + if (existingTimer) clearTimeout(existingTimer); + const timer = setTimeout(() => { + setErrorComments((prev) => { + const next = new Set(prev); + next.delete(comment.url); + return next; + }); + timersRef.current.delete(comment.url); + }, 3000); + timersRef.current.set(comment.url, timer); }, ); }; @@ -433,21 +476,21 @@ function PRCard({ pr, sessionId }: { pr: DashboardPR; sessionId: string }) {