From 061d289d0c3f0b35b6253f92f7e795ebbddf80f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 19 May 2026 16:14:25 +0000 Subject: [PATCH 1/2] Avoid reconnect countdown rerenders Co-authored-by: Julius Marminge --- .../components/WebSocketConnectionSurface.tsx | 110 ++++++++++++------ 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/apps/web/src/components/WebSocketConnectionSurface.tsx b/apps/web/src/components/WebSocketConnectionSurface.tsx index b54bd865c8b..619c7d50080 100644 --- a/apps/web/src/components/WebSocketConnectionSurface.tsx +++ b/apps/web/src/components/WebSocketConnectionSurface.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from "react"; +import { type ReactNode, type RefObject, useEffect, useEffectEvent, useRef } from "react"; import { type SlowRpcAckRequest, useSlowRpcAckRequests } from "../rpc/requestLatencyState"; import { @@ -53,6 +53,8 @@ function describeExhaustedToast(): string { return "Retries exhausted trying to reconnect"; } +type ThreadToastId = ReturnType; + function getConnectionDisplayName(status: WsConnectionStatus): string { return status.connectionLabel?.trim() || "T3 Server"; } @@ -90,6 +92,70 @@ function describeSlowRpcAckToast(requests: ReadonlyArray): st return `${count} request${count === 1 ? "" : "s"} waiting longer than ${thresholdSeconds}s.`; } +function buildReconnectToast( + status: WsConnectionStatus, + nowMs: number, + triggerManualReconnect: () => void, +) { + return stackedThreadToast({ + actionProps: { + children: "Retry now", + onClick: triggerManualReconnect, + }, + data: { + hideCopyButton: true, + }, + description: + status.nextRetryAt === null + ? `Reconnecting... ${formatReconnectAttemptLabel(status)}` + : `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`, + timeout: 0, + title: buildReconnectTitle(status), + type: "loading", + }); +} + +function useReconnectToastCountdown( + status: WsConnectionStatus, + toastIdRef: RefObject, + triggerManualReconnect: () => void, +) { + const nowMsRef = useRef(Date.now()); + + useEffect(() => { + if (status.reconnectPhase !== "waiting" || status.nextRetryAt === null) { + return; + } + + const refreshReconnectToast = () => { + nowMsRef.current = Date.now(); + const toastId = toastIdRef.current; + if (!toastId) { + return; + } + + const currentStatus = getWsConnectionStatus(); + if (getWsConnectionUiState(currentStatus) !== "reconnecting") { + return; + } + + toastManager.update( + toastId, + buildReconnectToast(currentStatus, nowMsRef.current, triggerManualReconnect), + ); + }; + + refreshReconnectToast(); + const intervalId = window.setInterval(refreshReconnectToast, 1_000); + + return () => { + window.clearInterval(intervalId); + }; + }, [status.nextRetryAt, status.reconnectPhase, toastIdRef, triggerManualReconnect]); + + return nowMsRef; +} + function SlowRpcAckRequestDetails({ requests }: { requests: ReadonlyArray }) { return (