From 73f49cae9f775fa2c4793226c5adb60b951a372d Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 6 Oct 2025 12:49:08 +0100 Subject: [PATCH] Merged tasks are endlessly re-rendering (vibe-kanban) (#938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ## ✅ Fix Complete **Problem:** Merged tasks caused endless WebSocket reconnections because `useJsonPatchWsStream` incorrectly treated `{finished: true}` messages as signals to reconnect. **Solution:** Modified [useJsonPatchWsStream.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/0f2d-merged-tasks-are/frontend/src/hooks/useJsonPatchWsStream.ts) to treat "finished" as terminal: 1. Added `finishedRef` to track when stream completes 2. On `{finished: true}`: set flag, close cleanly (code 1000), **no reconnect** 3. On socket close: skip reconnection if finished flag is set or clean close 4. Reset flag on cleanup and new connections **Result:** - Merged tasks connect once, receive final state, and stop cleanly - Active tasks still reconnect on network errors - Aligned behavior with existing `streamJsonPatchEntries` utility - All type checks pass ✅ * Cleanup script changes for task attempt 0f2d0086-1de2-4517-a023-1ee8cf133181 --- frontend/src/hooks/useJsonPatchWsStream.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/useJsonPatchWsStream.ts b/frontend/src/hooks/useJsonPatchWsStream.ts index dbc28082..97988f99 100644 --- a/frontend/src/hooks/useJsonPatchWsStream.ts +++ b/frontend/src/hooks/useJsonPatchWsStream.ts @@ -40,6 +40,7 @@ export const useJsonPatchWsStream = ( const retryTimerRef = useRef(null); const retryAttemptsRef = useRef(0); const [retryNonce, setRetryNonce] = useState(0); + const finishedRef = useRef(false); function scheduleReconnect() { if (retryTimerRef.current) return; // already scheduled @@ -64,6 +65,7 @@ export const useJsonPatchWsStream = ( retryTimerRef.current = null; } retryAttemptsRef.current = 0; + finishedRef.current = false; setData(undefined); setIsConnected(false); setError(null); @@ -85,6 +87,9 @@ export const useJsonPatchWsStream = ( // Create WebSocket if it doesn't exist if (!wsRef.current) { + // Reset finished flag for new connection + finishedRef.current = false; + // Convert HTTP endpoint to WebSocket endpoint const wsEndpoint = endpoint.replace(/^http/, 'ws'); const ws = new WebSocket(wsEndpoint); @@ -124,13 +129,12 @@ export const useJsonPatchWsStream = ( } // Handle finished messages ({finished: true}) + // Treat finished as terminal - do NOT reconnect if ('finished' in msg) { - ws.close(); + finishedRef.current = true; + ws.close(1000, 'finished'); wsRef.current = null; setIsConnected(false); - // Treat finished as terminal and schedule reconnect; servers may rotate - retryAttemptsRef.current += 1; - scheduleReconnect(); } } catch (err) { console.error('Failed to process WebSocket message:', err); @@ -142,9 +146,16 @@ export const useJsonPatchWsStream = ( setError('Connection failed'); }; - ws.onclose = () => { + ws.onclose = (evt) => { setIsConnected(false); wsRef.current = null; + + // Do not reconnect if we received a finished message or clean close + if (finishedRef.current || (evt?.code === 1000 && evt?.wasClean)) { + return; + } + + // Otherwise, reconnect on unexpected/error closures retryAttemptsRef.current += 1; scheduleReconnect(); }; @@ -170,6 +181,7 @@ export const useJsonPatchWsStream = ( window.clearTimeout(retryTimerRef.current); retryTimerRef.current = null; } + finishedRef.current = false; dataRef.current = undefined; setData(undefined); };