Add auto-reconnect with exponential backoff
Sessions configured with auto-reconnect will automatically attempt to re-establish the connection when unexpectedly disconnected. Uses exponential backoff (1s, 2s, 4s, 8s... up to 30s) with a configurable maximum number of attempts (default 5). - New SessionReconnecting state with pulsing indicator in tab bar - SSH and Telnet profiles gain autoReconnect and maxReconnectTries fields - RAW protocol already had autoReconnect; now uses the shared mechanism - CancelReconnect API to abort in-progress reconnection - Frontend shows reconnect attempt notifications - Profile editor exposes toggle and max attempts in Advanced/Reconnection
This commit is contained in:
@@ -125,9 +125,17 @@
|
||||
|
||||
.tab-status.is-connected { background: var(--success); }
|
||||
.tab-status.is-connecting { background: var(--warning); animation: pulse-dot 1.2s ease infinite; }
|
||||
.tab-status.is-reconnecting { background: var(--warning); animation: pulse-dot 0.8s ease infinite; }
|
||||
.tab-status.is-error { background: var(--error); }
|
||||
.tab-status.is-disconnected { background: var(--text-tertiary); }
|
||||
|
||||
/* Also match the status-* pattern used by updateSessionState */
|
||||
.tab-status.status-connected { background: var(--success); }
|
||||
.tab-status.status-connecting { background: var(--warning); animation: pulse-dot 1.2s ease infinite; }
|
||||
.tab-status.status-reconnecting { background: var(--warning); animation: pulse-dot 0.8s ease infinite; }
|
||||
.tab-status.status-error { background: var(--error); }
|
||||
.tab-status.status-disconnected { background: var(--text-tertiary); }
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
SFTP toggle button — revealed on hover (SSH tabs only)
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
+12
-1
@@ -134,10 +134,21 @@ class App {
|
||||
this.terminals.handleData(payload.sessionID, payload.data);
|
||||
});
|
||||
|
||||
// Session state transitions (connecting → connected → disconnected)
|
||||
// Session state transitions (connecting → connected → reconnecting → disconnected)
|
||||
window.runtime.EventsOn('session:state', (payload) => {
|
||||
this.tabs.updateSessionState(payload.sessionID, payload.state, payload.error);
|
||||
this.updateStatusBar();
|
||||
|
||||
// Show reconnecting notification
|
||||
if (payload.state === 'reconnecting') {
|
||||
const attempt = payload.attempt || 1;
|
||||
const maxRetries = payload.maxRetries || 5;
|
||||
const delaySec = payload.delaySec || 1;
|
||||
this.notifications.info(
|
||||
`Reconnecting (attempt ${attempt}/${maxRetries}) in ${delaySec}s…`
|
||||
);
|
||||
}
|
||||
|
||||
// Once connected, ensure the backend PTY has the correct dimensions.
|
||||
// The initial fit happens before the SSH handshake completes, so the
|
||||
// onResize callback fires when the backend isn't ready. By the time
|
||||
|
||||
@@ -406,6 +406,10 @@ class ProfileEditor {
|
||||
this._field('number', 'ssh.keepaliveInterval', 'Keepalive Interval (s)', ssh.keepaliveInterval, { min: 0 }),
|
||||
this._field('number', 'ssh.keepaliveCountMax', 'Max Missed Keepalives', ssh.keepaliveCountMax, { min: 0 }),
|
||||
this._field('toggle', 'ssh.compression', 'Compression', ssh.compression),
|
||||
this._field('toggle', 'ssh.autoReconnect', 'Auto-Reconnect', ssh.autoReconnect, {
|
||||
help: 'Automatically reconnect with exponential backoff when the connection drops',
|
||||
}),
|
||||
this._field('number', 'ssh.maxReconnectTries', 'Max Reconnect Attempts', ssh.maxReconnectTries || 5, { min: 1, max: 20 }),
|
||||
], true));
|
||||
|
||||
return sections;
|
||||
@@ -447,6 +451,13 @@ class ProfileEditor {
|
||||
this._field('toggle', 'telnet.negotiationDebug', 'Debug Negotiation', tel.negotiationDebug),
|
||||
], true));
|
||||
|
||||
sections.push(this._renderSection('Reconnection', [
|
||||
this._field('toggle', 'telnet.autoReconnect', 'Auto-Reconnect', tel.autoReconnect, {
|
||||
help: 'Automatically reconnect with exponential backoff when the connection drops',
|
||||
}),
|
||||
this._field('number', 'telnet.maxReconnectTries', 'Max Reconnect Attempts', tel.maxReconnectTries || 5, { min: 1, max: 20 }),
|
||||
], true));
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
@@ -908,6 +919,8 @@ class ProfileEditor {
|
||||
p.ssh.compression = val('ssh.compression') || false;
|
||||
p.ssh.agentAuth = val('ssh.agentAuth') || false;
|
||||
p.ssh.agentForwarding = val('ssh.agentForwarding') || false;
|
||||
p.ssh.autoReconnect = val('ssh.autoReconnect') || false;
|
||||
p.ssh.maxReconnectTries = val('ssh.maxReconnectTries') ?? 5;
|
||||
p.ssh.startupCommand = val('ssh.startupCommand') || '';
|
||||
|
||||
const authStr = val('ssh.authMethods');
|
||||
@@ -970,6 +983,8 @@ class ProfileEditor {
|
||||
p.telnet.encoding = val('telnet.encoding') || 'utf-8';
|
||||
p.telnet.lineEnding = val('telnet.lineEnding') || 'crlf';
|
||||
p.telnet.negotiationDebug = val('telnet.negotiationDebug') || false;
|
||||
p.telnet.autoReconnect = val('telnet.autoReconnect') || false;
|
||||
p.telnet.maxReconnectTries = val('telnet.maxReconnectTries') ?? 5;
|
||||
}
|
||||
|
||||
// RAW-specific
|
||||
|
||||
@@ -251,6 +251,7 @@ function stateLabel(state) {
|
||||
case 'connecting': return 'Connecting';
|
||||
case 'connected': return 'Connected';
|
||||
case 'disconnected': return 'Disconnected';
|
||||
case 'reconnecting': return 'Reconnecting';
|
||||
case 'error': return 'Error';
|
||||
default: return state || 'Unknown';
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -3,6 +3,8 @@
|
||||
import {models} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
export function CancelReconnect(arg1:string):Promise<void>;
|
||||
|
||||
export function CheckForUpdates():Promise<Record<string, any>>;
|
||||
|
||||
export function ClearSessionHistory(arg1:string):Promise<void>;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CancelReconnect(arg1) {
|
||||
return window['go']['api']['Service']['CancelReconnect'](arg1);
|
||||
}
|
||||
|
||||
export function CheckForUpdates() {
|
||||
return window['go']['api']['Service']['CheckForUpdates']();
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ export namespace models {
|
||||
encoding: string;
|
||||
negotiationDebug: boolean;
|
||||
lineMode: boolean;
|
||||
autoReconnect: boolean;
|
||||
maxReconnectTries: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TelnetConfig(source);
|
||||
@@ -297,6 +299,8 @@ export namespace models {
|
||||
this.encoding = source["encoding"];
|
||||
this.negotiationDebug = source["negotiationDebug"];
|
||||
this.lineMode = source["lineMode"];
|
||||
this.autoReconnect = source["autoReconnect"];
|
||||
this.maxReconnectTries = source["maxReconnectTries"];
|
||||
}
|
||||
}
|
||||
export class SSHConfig {
|
||||
@@ -311,6 +315,8 @@ export namespace models {
|
||||
compression: boolean;
|
||||
agentAuth: boolean;
|
||||
agentForwarding: boolean;
|
||||
autoReconnect: boolean;
|
||||
maxReconnectTries: number;
|
||||
jumpHosts: JumpHost[];
|
||||
portForwards: PortForward[];
|
||||
startupCommand: string;
|
||||
@@ -333,6 +339,8 @@ export namespace models {
|
||||
this.compression = source["compression"];
|
||||
this.agentAuth = source["agentAuth"];
|
||||
this.agentForwarding = source["agentForwarding"];
|
||||
this.autoReconnect = source["autoReconnect"];
|
||||
this.maxReconnectTries = source["maxReconnectTries"];
|
||||
this.jumpHosts = this.convertValues(source["jumpHosts"], JumpHost);
|
||||
this.portForwards = this.convertValues(source["portForwards"], PortForward);
|
||||
this.startupCommand = source["startupCommand"];
|
||||
|
||||
Reference in New Issue
Block a user