diff --git a/src/lib/components/chat/XTerminal.svelte b/src/lib/components/chat/XTerminal.svelte index eae497a500..e10ba02596 100644 --- a/src/lib/components/chat/XTerminal.svelte +++ b/src/lib/components/chat/XTerminal.svelte @@ -20,6 +20,7 @@ export let connected = false; export let connecting = false; let resizeObserver: ResizeObserver | null = null; + let pingInterval: ReturnType | null = null; // Resolve the active terminal server's info for the WebSocket URL const getTerminalInfo = (): { serverId: string; baseUrl: string } | null => { @@ -108,6 +109,13 @@ if (term && ws) { ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); } + // Keepalive ping to prevent idle timeout from proxies/LBs + if (pingInterval) clearInterval(pingInterval); + pingInterval = setInterval(() => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'ping' })); + } + }, 25000); }; ws.onmessage = (event) => { @@ -141,6 +149,10 @@ }; const disconnect = () => { + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } if (ws) { ws.close(); ws = null; @@ -229,8 +241,10 @@ }); resizeObserver.observe(terminalEl); - // Auto-connect - connect(); + // Connection is handled by the reactive block below (which fires + // when `term` is set here), so we intentionally do NOT call + // connect() to avoid creating a duplicate WebSocket whose onclose + // handler would write a spurious "[Connection closed]" message. }; // Reconnect when the selected terminal changes