fix(dashboard): null WebSocket handlers before close to stop terminal tab doubling

Creating a new terminal tab caused every pty data chunk (including
keystroke echo) to render twice in xterm. The connect-effect's
`contextChanged` dep flips true→false in the same render cycle as
the new connection: the effect re-runs, React calls cleanup which
closed the still-CONNECTING WS without nulling its handlers, then
connect() opens a fresh WS. The ghost socket's onmessage continued
firing on the shared `onDataCallbacksRef` Set, delivering each chunk
twice (and producing the "WebSocket is closed before the connection
is established" warning). Null onopen/onmessage/onclose/onerror in
both cleanup() and connect()'s pre-close branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 23:29:07 -07:00
parent 9ff2af567d
commit b6ffb613b0
2 changed files with 22 additions and 1 deletions

View File

@@ -238,6 +238,15 @@ export function useTerminal(sessionId: string | null, projectId?: string): UseTe
if (wsRef.current) {
isManualCloseRef.current = true;
// Null handlers before close so any in-flight `onopen`/`onmessage`
// from a still-connecting socket can't fire on the shared callback
// Set after we've moved on. Without this, a new tab's reconnect
// cycle leaves a ghost socket whose onmessage doubles every output
// chunk (the echoed keystroke shows up twice → "aa" per 'a').
wsRef.current.onopen = null;
wsRef.current.onmessage = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}
@@ -258,10 +267,17 @@ export function useTerminal(sessionId: string | null, projectId?: string): UseTe
return;
}
// Clean up any existing connection
// Clean up any existing connection. Null handlers before close so a
// still-connecting socket can't fire onopen/onmessage on the shared
// callback Set after we've moved on (would double output).
if (wsRef.current) {
isManualCloseRef.current = true;
wsRef.current.onopen = null;
wsRef.current.onmessage = null;
wsRef.current.onclose = null;
wsRef.current.onerror = null;
wsRef.current.close();
wsRef.current = null;
}
isManualCloseRef.current = false;