mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: reconnect all disconnected terminal tabs (#1523)
This commit is contained in:
1 parent
5860921cbe
commit
e161ad22e0
12 files changed
+265
-17
No files matched your search
@@ -52,3 +52,9 @@ npm run build # build into dist/
|
||||
npm run test # run this plugin's tests
|
||||
npm run typecheck # type-check this plugin
|
||||
```
|
||||
|
||||
## Reconnecting a workspace
|
||||
|
||||
Use **Reconnect all disconnected terminals** from a tab's context menu or the command palette after a network interruption. It retries mounted SSH terminal tabs, including hidden tabs and split panes, through their existing manual reconnect flow. Connected terminals and terminals already connecting or waiting for an automatic retry are skipped. Invoking the action again does not interrupt an in-flight connection; failures in one tab do not stop the others. The notification counts reconnect attempts started, not successful connections.
|
||||
|
||||
This is a manual workspace action. It does not add automatic SSH-error retries, resume/network listeners, tmux-session restoration or scrollback preservation. Existing authentication prompts, host-key validation and manual-reconnect behavior still apply. Other session plugins participate only if they expose the SDK's optional `reconnectIfDisconnected` handle method.
|
||||
@@ -1357,6 +1357,24 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function reconnectTerminal() {
|
||||
isUnmountingRef.current = false;
|
||||
shouldNotReconnectRef.current = false;
|
||||
isReconnectingRef.current = false;
|
||||
isConnectingRef.current = false;
|
||||
reconnectAttempts.current = 0;
|
||||
wasDisconnectedBySSH.current = false;
|
||||
wasConnectedRef.current = false;
|
||||
updateConnectionError(null);
|
||||
setShowDisconnectedOverlay(false);
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
connectToHost(cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
@@ -1396,22 +1414,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
},
|
||||
reconnect: () => {
|
||||
isUnmountingRef.current = false;
|
||||
shouldNotReconnectRef.current = false;
|
||||
isReconnectingRef.current = false;
|
||||
isConnectingRef.current = false;
|
||||
reconnectAttempts.current = 0;
|
||||
wasDisconnectedBySSH.current = false;
|
||||
wasConnectedRef.current = false;
|
||||
updateConnectionError(null);
|
||||
setShowDisconnectedOverlay(false);
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
connectToHost(cols, rows);
|
||||
}
|
||||
reconnect: reconnectTerminal,
|
||||
reconnectIfDisconnected: () => {
|
||||
if (
|
||||
!terminal ||
|
||||
isConnected ||
|
||||
isUnmountingRef.current ||
|
||||
isConnectingRef.current ||
|
||||
isReconnectingRef.current ||
|
||||
reconnectTimeoutRef.current !== null
|
||||
)
|
||||
return false;
|
||||
reconnectTerminal();
|
||||
return true;
|
||||
},
|
||||
isConnected: () => isConnected,
|
||||
fit: () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface TerminalHostConfig {
|
||||
export interface TerminalHandle {
|
||||
disconnect: () => void;
|
||||
reconnect: () => void;
|
||||
reconnectIfDisconnected: () => boolean;
|
||||
isConnected: () => boolean;
|
||||
fit: () => void;
|
||||
focus: () => void;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment node
|
||||
import fs from "node:fs";
|
||||
import vm from "node:vm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const source = fs.readFileSync(
|
||||
new URL("../../../src/frontend/terminal/Terminal.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const reconnect = source.slice(
|
||||
source.indexOf(" function reconnectTerminal()"),
|
||||
source.indexOf(" useImperativeHandle("),
|
||||
);
|
||||
const methods = source.slice(
|
||||
source.indexOf(" reconnect: reconnectTerminal,"),
|
||||
source.indexOf(" isConnected: () => isConnected,"),
|
||||
);
|
||||
|
||||
function createHandle(overrides: Record<string, unknown> = {}) {
|
||||
const context = {
|
||||
terminal: { cols: 80, rows: 24, clear: vi.fn() },
|
||||
isConnected: false,
|
||||
isUnmountingRef: { current: false },
|
||||
shouldNotReconnectRef: { current: true },
|
||||
isReconnectingRef: { current: false },
|
||||
isConnectingRef: { current: false },
|
||||
reconnectTimeoutRef: { current: null },
|
||||
reconnectAttempts: { current: 8 },
|
||||
wasDisconnectedBySSH: { current: true },
|
||||
wasConnectedRef: { current: false },
|
||||
updateConnectionError: vi.fn(),
|
||||
setShowDisconnectedOverlay: vi.fn(),
|
||||
connectToHost: vi.fn(() => {
|
||||
context.isConnectingRef.current = true;
|
||||
}),
|
||||
...overrides,
|
||||
};
|
||||
const handle = vm.runInNewContext(
|
||||
reconnect + `\n({${methods}})`,
|
||||
context,
|
||||
) as { reconnectIfDisconnected: () => boolean; reconnect: () => void };
|
||||
return { handle, context };
|
||||
}
|
||||
|
||||
describe("terminal bulk reconnect handle", () => {
|
||||
it("uses the manual reconnect flow after a dropped SSH session and rejects duplicate requests", () => {
|
||||
const { handle, context } = createHandle();
|
||||
expect(handle.reconnectIfDisconnected()).toBe(true);
|
||||
expect(context.connectToHost).toHaveBeenCalledWith(80, 24);
|
||||
expect(context.shouldNotReconnectRef.current).toBe(false);
|
||||
expect(context.reconnectAttempts.current).toBe(0);
|
||||
expect(context.wasDisconnectedBySSH.current).toBe(false);
|
||||
expect(handle.reconnectIfDisconnected()).toBe(false);
|
||||
expect(context.connectToHost).toHaveBeenCalledOnce();
|
||||
});
|
||||
it.each([
|
||||
{ isConnected: true },
|
||||
{ isConnectingRef: { current: true } },
|
||||
{ isReconnectingRef: { current: true } },
|
||||
{ reconnectTimeoutRef: { current: 1 } },
|
||||
{ isUnmountingRef: { current: true } },
|
||||
{ terminal: null },
|
||||
])(
|
||||
"does not interrupt a connected, busy or unavailable terminal: %j",
|
||||
(overrides) => {
|
||||
const { handle, context } = createHandle(overrides);
|
||||
expect(handle.reconnectIfDisconnected()).toBe(false);
|
||||
expect(context.connectToHost).not.toHaveBeenCalled();
|
||||
expect(context.shouldNotReconnectRef.current).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user