From 77c7240e4edc28f133dd83778e4e8d5fcce0192c Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Thu, 30 Apr 2026 10:26:27 +0800 Subject: [PATCH] fix: auto-close tab on graceful SSH disconnect (exit/Ctrl+D) (#723) Distinguish between graceful shell exit and unexpected disconnection using the stream close event's exit code. When the shell exits normally (code != null), send "session_ended" instead of "disconnected". The frontend auto-closes the tab on session_ended, and shows the reconnect overlay only on unexpected disconnections. Closes Termix-SSH/Support#643 --- src/backend/ssh/terminal.ts | 23 +++++++++++++------ src/locales/en.json | 1 + .../apps/features/terminal/Terminal.tsx | 8 +++++++ src/ui/mobile/apps/terminal/Terminal.tsx | 7 ++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 22d8e024..e34beff8 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -1503,15 +1503,24 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - stream.on("close", () => { + stream.on("close", (code: number | null) => { const session = sessionManager.getSession(boundSessionId); if (session?.attachedWs?.readyState === WebSocket.OPEN) { - session.attachedWs.send( - JSON.stringify({ - type: "disconnected", - message: "Connection lost", - }), - ); + if (code != null) { + session.attachedWs.send( + JSON.stringify({ + type: "session_ended", + code, + }), + ); + } else { + session.attachedWs.send( + JSON.stringify({ + type: "disconnected", + message: "Connection lost", + }), + ); + } } if (boundSessionId) { sessionManager.destroySession(boundSessionId); diff --git a/src/locales/en.json b/src/locales/en.json index 79564c6a..340bd021 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1676,6 +1676,7 @@ "automaticFallback": "Automatically trying {{method}} authentication...", "totpTimeout": "TOTP verification timeout. Please reconnect.", "passwordTimeout": "Password verification timeout. Please reconnect.", + "sessionEnded": "Session ended.", "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", "hostKeyRejected": "SSH host key verification rejected. Connection cancelled.", "sessionTakenOver": "Session was opened in another tab. Reconnecting...", diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index 1f32e98f..e6469bd0 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -1187,6 +1187,14 @@ const TerminalInner = forwardRef( ); } }, 100); + } else if (msg.type === "session_ended") { + wasDisconnectedBySSH.current = true; + setIsConnected(false); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + if (onClose) { + onClose(); + } } else if (msg.type === "disconnected") { wasDisconnectedBySSH.current = true; shouldNotReconnectRef.current = true; diff --git a/src/ui/mobile/apps/terminal/Terminal.tsx b/src/ui/mobile/apps/terminal/Terminal.tsx index 9ce4fa83..73a573c3 100644 --- a/src/ui/mobile/apps/terminal/Terminal.tsx +++ b/src/ui/mobile/apps/terminal/Terminal.tsx @@ -729,6 +729,13 @@ const TerminalInner = forwardRef( ); } }, 100); + } else if (msg.type === "session_ended") { + wasDisconnectedBySSH.current = true; + shouldNotReconnectRef.current = true; + isConnectingRef.current = false; + setIsConnected(false); + setIsConnecting(false); + updateConnectionError(t("terminal.sessionEnded")); } else if (msg.type === "disconnected") { wasDisconnectedBySSH.current = true; isConnectingRef.current = false;