mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
205d1591e8
* feat(terminal): add disconnect/reconnect controls on session status bar Workspace panes could not reach tab-level reconnect. Expose disconnect (without closing the pane) and reconnect beside the host info actions so split sessions stay operable. Closes #2769. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): abort in-flight boot on status-bar disconnect Disconnect keeps the pane mounted, so cancel's unmount cleanup never runs. Flip isBootActiveRef (and isCancelling) so a late startSSH/startMosh attach cannot resurrect the session after the user disconnects mid-connect. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): ignore stale start UI after status-bar disconnect When disconnect aborts an in-flight connect, starter catch paths must not reopen auth or repopulate errors. Gate SSH/telnet/mosh/ET/local/serial failure UI on isTerminalBootActive and harden the boot effect awaits. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): hide disconnect/reconnect on compact popup terminals Owned command popups use compactToolbar with a one-shot startup command. Exposing reconnect there can re-run that command; keep the controls on normal tabs/workspace panes only. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): use boot epoch so reconnect cannot revive aborted starts Disconnect alone flipping isBootActiveRef is not enough: an immediate reconnect re-arms boot, and a late startSSH from the aborted attempt can still attach. Capture a boot epoch per attempt and ignore stale attaches/UI. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): do not let stale attach abort a newer reconnect tryAttach failure for an expired boot epoch must close the orphan session only. Calling abortSessionStartAfterUnmount() would force disconnected on a reconnect that already re-armed boot. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): reject late host-key prompts after disconnect Disconnect can land before pendingHostKeyRequestId is set. Auto-deny onHostKeyVerification when boot is inactive or status is disconnected so approval UI cannot reopen on an aborted pane. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): cancel wake and MFA when status-bar disconnects Invalidate hibernation reconnect-wake tokens on disconnect so a late wake cannot re-arm boot. Reject/cancel keyboard-interactive prompts for disconnected panes and clear any already-queued MFA for that session. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): ignore stale SSH catch UI and password fallback Check boot attempt currency before resetting reconnect UI state on SSH start failure, and before launching key→password fallback after disconnect so a replacement reconnect is not interrupted. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): guard stale SSH progress and host-key prompts Ignore chain-progress callbacks from superseded boot attempts, and thread bootEpoch through SSH host-key verification so a late prompt from an aborted start cannot overwrite a replacement reconnect. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): correlate MFA and jump host-keys with boot epoch Forward bootEpoch through jump-host verification and keyboard-interactive prompts, and reject superseded MFA when the retained tab has already started a replacement reconnect. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): keep replacement SSH session alive across stale starts Skip closeSession for superseded starts while a newer boot still owns the shared sessionId, and avoid clearing MFA wait state before the attempt-freshness guard on SSH success. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): own shared sessionIds by boot epoch Claim SSH/local registry slots with bootEpoch so a stale start cannot overwrite a replacement, and make closeSession no-op when the payload epoch does not match the live session owner. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): claim bootEpoch for all reconnectable backends Pass bootEpoch through Telnet/Mosh/ET/serial starts, claim registry slots the same way as SSH/local, and dispose displaced owners when a newer boot replaces the shared sessionId. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): protect preload listeners and ET artifacts on displace Defer epoch-scoped closeSession listener teardown until main confirms ownership, and remove ET external-auth temp artifacts when a newer boot displaces the previous registry owner. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): clean rejected Mosh starts and keep replacement listeners Kill superseded Mosh/ET PTYs and auth temp files when claimSessionSlot rejects, and never clear shared preload listeners on epoch-scoped closes. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): settle superseded boots and stop displaced log streams Reject Telnet/SSH claim failures through Promise reject/failReuse instead of uncaught EventEmitter/.then throws, and stop a displaced session's log stream so a logging-disabled reconnect cannot append into the old file. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): keep woken runtime when disconnect cancels hibernate wake Disconnect invalidates in-flight hibernated reconnects with keep mode so a successful wake that already cleared hibernatedRef is not disposed into a dead pane that can never reconnect. Unmount still disposes orphan runtimes. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): bind reused SSH shellPid only to the claimed session Late PID discovery after Copy Tab reuse must not write onto a newer bootEpoch that already replaced the shared sessionId registry slot. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): await plugin cancel cleanup and clear boot epochs on teardown Track plugin start/cancel promises so Disconnect→Reconnect waits until the external session finishes unregistering, and stop unmount teardown from republishing boot epochs into the process-wide map. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): cancel SSH passphrase prompts on disconnect Register a pending-boot AbortSignal for SSH starts, abort it (and session- scoped passphrase requests) from closeSession even before a registry slot exists, and filter/drop late passphrase modals by boot epoch / disconnect. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): close owned boot epoch and scope passphrase cancels Capture the pre-bump epoch for disconnect/teardown closeSession so live transports are not skipped by epoch mismatch, cancel only passphrase prompts at or below that epoch, and revalidate boot currency after async passphrase lookups before queuing a modal. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): tag Telnet auto-login and Mosh ready with bootEpoch Emit bootEpoch on telnet auto-login and mosh:ready events so overlapping Disconnect→Reconnect boots ignore cross-delivered callbacks meant for an older shared sessionId. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): update disconnect assertion and cancel pending serial opens Align the disconnect source regression with invalidateBootEpochForClose, and abort in-flight serialPort.open attempts via pending boot AbortSignal so Disconnect→Reconnect cannot leave a stale open racing the replacement. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(terminal): keep flow-pause leases on skipped epoch closes Worker close:await previously cleared pause ownership in finally even when the worker returned skipped for an epoch mismatch, dropping the live replacement's lease. Only clear on non-skipped closes; epoch-scoped fire-and-forget close similarly defers to onSessionClosed. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
removeKeyboardInteractiveRequest,
|
|
shouldQueueKeyboardInteractiveRequest,
|
|
} from "./useAppStartupEffects.ts";
|
|
import {
|
|
clearTerminalBootEpoch,
|
|
setTerminalBootEpoch,
|
|
} from "../../domain/terminalBootEpoch.ts";
|
|
|
|
const sessions = [{ id: "terminal-1" }, { id: "terminal-2" }];
|
|
|
|
test("terminal-scoped keyboard-interactive requests are limited to owned sessions", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "terminal-1" }, sessions),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "foreign-terminal" }, sessions),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("disconnected terminal sessions do not queue keyboard-interactive prompts", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest(
|
|
{ scope: "terminal", sessionId: "terminal-1" },
|
|
[{ id: "terminal-1", status: "disconnected" }],
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest(
|
|
{ scope: "terminal", sessionId: "terminal-1" },
|
|
[{ id: "terminal-1", status: "connecting" }],
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("superseded terminal boot epochs do not queue keyboard-interactive prompts", () => {
|
|
setTerminalBootEpoch("terminal-1", 3);
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest(
|
|
{ scope: "terminal", sessionId: "terminal-1", bootEpoch: 1 },
|
|
[{ id: "terminal-1", status: "connecting" }],
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest(
|
|
{ scope: "terminal", sessionId: "terminal-1", bootEpoch: 3 },
|
|
[{ id: "terminal-1", status: "connecting" }],
|
|
),
|
|
true,
|
|
);
|
|
clearTerminalBootEpoch("terminal-1");
|
|
});
|
|
|
|
test("external keyboard-interactive requests are not filtered by terminal session ids", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "sftp-conn-1" }, sessions),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "tunnel-1" }, sessions),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("disabled peer windows still queue sender-targeted external keyboard-interactive requests", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "sftp-conn-1" }, sessions),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("disabled peer windows can still queue owned terminal keyboard-interactive requests", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "terminal-1" }, sessions),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("legacy unscoped keyboard-interactive requests remain visible", () => {
|
|
assert.equal(
|
|
shouldQueueKeyboardInteractiveRequest({ sessionId: "legacy-conn" }, sessions),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("cancelled keyboard-interactive requests are removed from the renderer queue", () => {
|
|
const queue = [{ requestId: "keep" }, { requestId: "cancel" }];
|
|
assert.deepEqual(removeKeyboardInteractiveRequest(queue, "cancel"), [{ requestId: "keep" }]);
|
|
});
|