feat: optional auto reconnect for dropped ssh sessions (#1321)

This commit is contained in:
LukeGus committed 2026-09-30 14:02:42 -05:00
1 parent 41d0c0ab85
commit 911fc3bc69
6 files changed
+129 -3

No files matched your search

@@ -720,6 +720,16 @@ export function HostTerminalSettings({
onChange={(v) => setField("autoMosh", v)}
/>
</SettingRow>
<SettingRow
label={t("hosts.autoReconnectLabel")}
defaultKey="autoReconnect"
description={t("hosts.autoReconnectDesc")}
>
<FakeSwitch
checked={form.autoReconnect}
onChange={(v) => setField("autoReconnect", v)}
/>
</SettingRow>
<SettingRow
label={t("hosts.passwordPromptAutoFillLabel")}
defaultKey="passwordPromptAutoFill"
@@ -282,6 +282,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
const terminalInputDisposableRef = useRef<{ dispose(): void } | null>(null);
const localEchoRef = useRef<TerminalLocalEcho | null>(null);
const sessionOriginRef = useRef<"local" | "remote">("local");
// Set while an auto reconnect runs, so the old output stays on screen.
const keepScrollbackRef = useRef(false);
const customKeybindingsRef = useRef<CustomKeybinding[]>([]);
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
const wasDisconnectedBySSH = useRef(false);
@@ -1514,6 +1516,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
if (reconnectAttempts.current >= maxReconnectAttempts) {
setIsConnecting(false);
keepScrollbackRef.current = false;
shouldNotReconnectRef.current = true;
setShowDisconnectedOverlay(true);
addLog({
@@ -1526,7 +1529,11 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
isReconnectingRef.current = true;
if (terminal && !isAttachingSessionRef.current) {
if (
terminal &&
!isAttachingSessionRef.current &&
!keepScrollbackRef.current
) {
terminal.clear();
}
@@ -1564,7 +1571,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
if (terminal && hostConfig) {
if (!isAttachingSessionRef.current) {
if (!isAttachingSessionRef.current && !keepScrollbackRef.current) {
terminal.clear();
}
const cols = terminal.cols;
@@ -1929,6 +1936,13 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
errorMessage.toLowerCase().includes("timeout") ||
errorMessage.toLowerCase().includes("network")
) {
if (keepScrollbackRef.current && !isUnmountingRef.current) {
// The host is still unreachable; try again after the backoff.
isConnectingRef.current = false;
wasDisconnectedBySSH.current = false;
attemptReconnection();
return;
}
updateConnectionError(errorMessage);
setIsConnected(false);
if (terminal) {
@@ -1948,6 +1962,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
errorMessage.toLowerCase().includes("key"))) ||
errorMessage.toLowerCase().includes("incorrect password")
) {
keepScrollbackRef.current = false;
updateConnectionError(errorMessage);
setIsConnecting(false);
shouldNotReconnectRef.current = true;
@@ -1960,6 +1975,10 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
updateConnectionError(errorMessage);
setIsConnecting(false);
} else if (msg.type === "connected") {
if (keepScrollbackRef.current) {
keepScrollbackRef.current = false;
reconnectAttempts.current = 0;
}
wasConnectedRef.current = true;
setIsConnected(true);
setIsConnecting(false);
@@ -2061,7 +2080,23 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
if (onClose) onClose();
} else if (wasConnectedRef.current) {
wasConnectedRef.current = false;
setShowDisconnectedOverlay(true);
if (
termSettingsRef.current.autoReconnect &&
!isUnmountingRef.current
) {
wasDisconnectedBySSH.current = false;
shouldNotReconnectRef.current = false;
reconnectAttempts.current = 0;
keepScrollbackRef.current = true;
terminal?.write(
`
${t("terminal.autoReconnecting")}
`,
);
attemptReconnection();
} else {
setShowDisconnectedOverlay(true);
}
} else if (!connectionErrorRef.current) {
updateConnectionError(
msg.message || t("terminal.connectionRejected"),
@@ -2386,6 +2421,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
if (
!wasConnectedRef.current &&
!isAttachingSessionRef.current &&
!keepScrollbackRef.current &&
event.wasClean &&
(event.code === 1005 || event.code === 1000)
) {
@@ -81,6 +81,7 @@ export interface TerminalBehavior {
localEcho: HostLocalEcho;
passwordPromptAutoFill: boolean;
sudoPasswordAutoFill: boolean;
autoReconnect: boolean;
}
/** Everything the host editor's Terminal tab edits. */
@@ -121,6 +122,7 @@ export const BEHAVIOR_KEYS = [
"localEcho",
"passwordPromptAutoFill",
"sudoPasswordAutoFill",
"autoReconnect",
] as const satisfies readonly (keyof TerminalBehavior)[];
export const INHERIT_APPEARANCE_KEY = "inheritAppearance";
@@ -169,6 +171,7 @@ export const DEFAULT_BEHAVIOR: TerminalBehavior = {
localEcho: "auto",
passwordPromptAutoFill: true,
sudoPasswordAutoFill: false,
autoReconnect: false,
};
export const DEFAULT_HOST_TERMINAL_SETTINGS: HostTerminalSettings = {