[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
708
application/state/useTerminalBackend.ts
Normal file
708
application/state/useTerminalBackend.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { netcattyBridge } from "../../infrastructure/services/netcattyBridge";
|
||||
import type { TerminalSessionExitEvent } from "./resolveTerminalSessionExitIntent";
|
||||
|
||||
type PluginConnectionStartOptions = NetcattyPluginConnectionStartRequest & {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
const throwIfPluginConnectionStartAborted = (signal?: AbortSignal): void => {
|
||||
if (!signal?.aborted) return;
|
||||
const reason = signal.reason;
|
||||
if (reason instanceof Error) throw reason;
|
||||
throw new DOMException("Plugin connection request was cancelled", "AbortError");
|
||||
};
|
||||
|
||||
const raceWithPluginConnectionStartAbort = async <T,>(
|
||||
operation: Promise<T>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> => {
|
||||
if (!signal) return operation;
|
||||
throwIfPluginConnectionStartAborted(signal);
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
reject(signal.reason instanceof Error
|
||||
? signal.reason
|
||||
: new DOMException("Plugin connection request was cancelled", "AbortError"));
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
operation.then(
|
||||
(value) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
if (signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
resolve(value);
|
||||
},
|
||||
(error) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export async function startPluginConnectionWithBridge(
|
||||
bridge: Pick<NetcattyBridge, "invokePluginExtensionProvider" | "startPluginConnection">,
|
||||
options: PluginConnectionStartOptions,
|
||||
) {
|
||||
if (!bridge?.startPluginConnection) throw new Error("startPluginConnection unavailable");
|
||||
if (!bridge.invokePluginExtensionProvider) throw new Error("Plugin connection validation unavailable");
|
||||
const { signal, ...bridgeOptions } = options;
|
||||
throwIfPluginConnectionStartAborted(signal);
|
||||
const validation = await raceWithPluginConnectionStartAbort(bridge.invokePluginExtensionProvider({
|
||||
requestId: bridgeOptions.requestId,
|
||||
providerId: bridgeOptions.providerId,
|
||||
kind: "connection",
|
||||
operation: "validateConfiguration",
|
||||
payload: { configuration: bridgeOptions.configuration },
|
||||
deadlineMs: bridgeOptions.deadlineMs,
|
||||
}), signal);
|
||||
throwIfPluginConnectionStartAborted(signal);
|
||||
if (!validation || typeof validation !== "object" || Array.isArray(validation)
|
||||
|| (validation as { valid?: unknown }).valid !== true) {
|
||||
const issues = (validation as { issues?: Array<{ message?: string }> } | null)?.issues;
|
||||
throw new Error(issues?.[0]?.message || "Plugin connection configuration is invalid");
|
||||
}
|
||||
throwIfPluginConnectionStartAborted(signal);
|
||||
const probe = await raceWithPluginConnectionStartAbort(bridge.invokePluginExtensionProvider({
|
||||
requestId: bridgeOptions.requestId,
|
||||
providerId: bridgeOptions.providerId,
|
||||
kind: "connection",
|
||||
operation: "probe",
|
||||
payload: { configuration: bridgeOptions.configuration },
|
||||
deadlineMs: bridgeOptions.deadlineMs,
|
||||
}), signal);
|
||||
throwIfPluginConnectionStartAborted(signal);
|
||||
if (!probe || typeof probe !== "object" || Array.isArray(probe)
|
||||
|| (probe as { available?: unknown }).available !== true) {
|
||||
throw new Error((probe as { message?: string } | null)?.message || "Plugin connection provider is unavailable");
|
||||
}
|
||||
return bridge.startPluginConnection(bridgeOptions);
|
||||
}
|
||||
|
||||
export async function signalPluginConnectionWithBridge(
|
||||
bridge: Pick<NetcattyBridge, "controlPluginConnection">,
|
||||
sessionId: string,
|
||||
signal: "interrupt" | "terminate" | "kill" | "eof" | "break" = "interrupt",
|
||||
) {
|
||||
if (!bridge?.controlPluginConnection) throw new Error("Plugin connection signaling unavailable");
|
||||
return bridge.controlPluginConnection(sessionId, "signal", { signal });
|
||||
}
|
||||
|
||||
export const useTerminalBackend = () => {
|
||||
const telnetAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startTelnetSession;
|
||||
}, []);
|
||||
|
||||
const moshAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startMoshSession;
|
||||
}, []);
|
||||
|
||||
const etAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startEtSession;
|
||||
}, []);
|
||||
|
||||
const localAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startLocalSession;
|
||||
}, []);
|
||||
|
||||
const serialAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startSerialSession;
|
||||
}, []);
|
||||
|
||||
const pluginConnectionAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startPluginConnection;
|
||||
}, []);
|
||||
|
||||
const execAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.execCommand;
|
||||
}, []);
|
||||
|
||||
const startSSHSession = useCallback(async (options: NetcattySSHOptions) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startSSHSession) throw new Error("startSSHSession unavailable");
|
||||
return bridge.startSSHSession(options);
|
||||
}, []);
|
||||
|
||||
const startTelnetSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startTelnetSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startTelnetSession) throw new Error("startTelnetSession unavailable");
|
||||
return bridge.startTelnetSession(options);
|
||||
}, []);
|
||||
|
||||
const startMoshSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startMoshSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startMoshSession) throw new Error("startMoshSession unavailable");
|
||||
return bridge.startMoshSession(options);
|
||||
}, []);
|
||||
|
||||
const startEtSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startEtSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startEtSession) throw new Error("startEtSession unavailable");
|
||||
return bridge.startEtSession(options);
|
||||
}, []);
|
||||
|
||||
const startLocalSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startLocalSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startLocalSession) throw new Error("startLocalSession unavailable");
|
||||
return bridge.startLocalSession(options);
|
||||
}, []);
|
||||
|
||||
const startSerialSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startSerialSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startSerialSession) throw new Error("startSerialSession unavailable");
|
||||
return bridge.startSerialSession(options);
|
||||
}, []);
|
||||
|
||||
const startPluginConnection = useCallback(async (options: PluginConnectionStartOptions) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge) throw new Error("startPluginConnection unavailable");
|
||||
return startPluginConnectionWithBridge(bridge, options);
|
||||
}, []);
|
||||
|
||||
const cancelPluginExtensionRequest = useCallback(async (requestId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.cancelPluginExtensionRequest?.(requestId) ?? false;
|
||||
}, []);
|
||||
|
||||
const signalPluginConnection = useCallback(async (
|
||||
sessionId: string,
|
||||
signal: "interrupt" | "terminate" | "kill" | "eof" | "break" = "interrupt",
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge) throw new Error("Plugin connection signaling unavailable");
|
||||
return signalPluginConnectionWithBridge(bridge, sessionId, signal);
|
||||
}, []);
|
||||
|
||||
const execCommand = useCallback(async (options: Parameters<NetcattyBridge["execCommand"]>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.execCommand) throw new Error("execCommand unavailable");
|
||||
return bridge.execCommand(options);
|
||||
}, []);
|
||||
|
||||
const setupOsc7Tracking = useCallback(async (sessionId: string, command: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.setupOsc7Tracking) {
|
||||
return { success: false, error: "setupOsc7Tracking unavailable" };
|
||||
}
|
||||
return bridge.setupOsc7Tracking(sessionId, command);
|
||||
}, []);
|
||||
|
||||
const writeToSession = useCallback((sessionId: string, data: string, options?: Parameters<NonNullable<NetcattyBridge["writeToSession"]>>[2]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.writeToSession?.(sessionId, data, options);
|
||||
}, []);
|
||||
|
||||
const interruptSession = useCallback((sessionId: string, trace?: NetcattyTerminalInterruptTrace) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.interruptSession) {
|
||||
bridge.interruptSession(sessionId, trace);
|
||||
return;
|
||||
}
|
||||
bridge?.writeToSession?.(sessionId, "\x03");
|
||||
}, []);
|
||||
|
||||
const resizeSession = useCallback((sessionId: string, cols: number, rows: number) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.resizeSession?.(sessionId, cols, rows);
|
||||
}, []);
|
||||
|
||||
const clearSessionPtyBuffer = useCallback((sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.clearSessionPtyBuffer?.(sessionId);
|
||||
}, []);
|
||||
|
||||
const setSessionFlowPaused = useCallback((sessionId: string, paused: boolean) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.setSessionFlowPaused?.(sessionId, paused);
|
||||
}, []);
|
||||
|
||||
const setSessionFlowPausedAndWait = useCallback(async (sessionId: string, paused: boolean) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.setSessionFlowPausedAndWait) {
|
||||
bridge?.setSessionFlowPaused?.(sessionId, paused);
|
||||
return paused
|
||||
? { success: false, error: "Output drain unavailable" }
|
||||
: { success: true };
|
||||
}
|
||||
return bridge.setSessionFlowPausedAndWait(sessionId, paused);
|
||||
}, []);
|
||||
|
||||
const acquireSessionFlowPauseLease = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.acquireSessionFlowPauseLease) {
|
||||
throw new Error("Terminal flow pause leases unavailable");
|
||||
}
|
||||
const acquired = await bridge.acquireSessionFlowPauseLease(sessionId);
|
||||
if (!acquired?.success || !acquired.leaseId) {
|
||||
throw new Error(acquired?.error || "Failed to pause terminal output");
|
||||
}
|
||||
const leaseId = acquired.leaseId;
|
||||
let released = false;
|
||||
return {
|
||||
release: (options?: { keepPaused?: boolean }) => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
void bridge.releaseSessionFlowPauseLease?.(
|
||||
sessionId,
|
||||
leaseId,
|
||||
options,
|
||||
);
|
||||
},
|
||||
waitForPause: async () => {
|
||||
if (!bridge.waitSessionFlowPauseLease) {
|
||||
return { success: false, error: "Output drain unavailable" };
|
||||
}
|
||||
return bridge.waitSessionFlowPauseLease(sessionId, leaseId);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ackSessionFlow = useCallback((sessionId: string, bytes: number) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.ackSessionFlow?.(sessionId, bytes);
|
||||
}, []);
|
||||
|
||||
const notifyTerminalSessionDisplayReady = useCallback((sessionId: string) => {
|
||||
netcattyBridge.get()?.notifyTerminalSessionDisplayReady?.(sessionId);
|
||||
}, []);
|
||||
|
||||
const closeSession = useCallback(async (sessionId: string, options?: { bootEpoch?: number }) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
await bridge?.closeSession?.(sessionId, options);
|
||||
}, []);
|
||||
|
||||
const rebindSessionOutput = useCallback(async (sessionId: string, authorization: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.rebindTerminalSessionOutput) {
|
||||
return { success: false as const, error: "rebindTerminalSessionOutput unavailable" };
|
||||
}
|
||||
return bridge.rebindTerminalSessionOutput(sessionId, authorization);
|
||||
}, []);
|
||||
|
||||
const restoreSessionOutput = useCallback(async (
|
||||
sessionId: string,
|
||||
webContentsId?: number | null,
|
||||
authorization?: string,
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.restoreTerminalSessionOutput) {
|
||||
return { success: false as const, error: "restoreTerminalSessionOutput unavailable" };
|
||||
}
|
||||
return bridge.restoreTerminalSessionOutput(sessionId, webContentsId, authorization);
|
||||
}, []);
|
||||
|
||||
const requestSessionSnapshot = useCallback(async (sessionId: string, authorization: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.requestTerminalSessionSnapshot) {
|
||||
return { success: false as const, snapshot: "", error: "requestTerminalSessionSnapshot unavailable" };
|
||||
}
|
||||
return bridge.requestTerminalSessionSnapshot(sessionId, authorization);
|
||||
}, []);
|
||||
|
||||
const applySessionSnapshot = useCallback(async (
|
||||
sessionId: string,
|
||||
snapshot: string,
|
||||
context: {
|
||||
contextSnapshot: string;
|
||||
contextViewportSnapshot: string;
|
||||
contextScrollbackSnapshot: string;
|
||||
alternateScreen: boolean;
|
||||
kittyKeyboardModeState?: NetcattyKittyKeyboardModeState;
|
||||
kittyKeyboardProtocolEnabled?: boolean;
|
||||
},
|
||||
authorization: string,
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.applyTerminalSessionSnapshot) {
|
||||
return { success: false as const, error: "applyTerminalSessionSnapshot unavailable" };
|
||||
}
|
||||
return bridge.applyTerminalSessionSnapshot(sessionId, snapshot, context, authorization);
|
||||
}, []);
|
||||
|
||||
const setSessionEncoding = useCallback(async (sessionId: string, encoding: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.setSessionEncoding) return { ok: false, encoding };
|
||||
return bridge.setSessionEncoding(sessionId, encoding);
|
||||
}, []);
|
||||
|
||||
const onSessionData = useCallback((
|
||||
sessionId: string,
|
||||
cb: Parameters<NetcattyBridge["onSessionData"]>[1],
|
||||
options?: Parameters<NetcattyBridge["onSessionData"]>[2],
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onSessionData) throw new Error("onSessionData unavailable");
|
||||
return bridge.onSessionData(sessionId, cb, options);
|
||||
}, []);
|
||||
|
||||
const onSessionExit = useCallback((sessionId: string, cb: (evt: TerminalSessionExitEvent) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onSessionExit) throw new Error("onSessionExit unavailable");
|
||||
return bridge.onSessionExit(sessionId, cb);
|
||||
}, []);
|
||||
|
||||
const onTelnetAutoLoginComplete = useCallback((sessionId: string, cb: (evt: { sessionId: string; bootEpoch?: number }) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onTelnetAutoLoginComplete?.(sessionId, cb);
|
||||
}, []);
|
||||
|
||||
const onTelnetAutoLoginCancelled = useCallback((sessionId: string, cb: (evt: { sessionId: string; bootEpoch?: number }) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onTelnetAutoLoginCancelled?.(sessionId, cb);
|
||||
}, []);
|
||||
|
||||
const onMoshSessionReady = useCallback((sessionId: string, cb: (evt: { sessionId: string; bootEpoch?: number }) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onMoshSessionReady?.(sessionId, cb);
|
||||
}, []);
|
||||
|
||||
const onTelnetEchoMode = useCallback((sessionId: string, cb: (evt: { sessionId: string; remoteEcho: boolean; localEcho: boolean }) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onTelnetEchoMode?.(sessionId, cb);
|
||||
}, []);
|
||||
|
||||
const getTelnetEchoMode = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.getTelnetEchoMode) return { success: false as const, error: "getTelnetEchoMode unavailable" };
|
||||
return bridge.getTelnetEchoMode(sessionId);
|
||||
}, []);
|
||||
|
||||
const onChainProgress = useCallback((cb: (sessionId: string, hop: number, total: number, label: string, status: string, error?: string) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onChainProgress?.(cb);
|
||||
}, []);
|
||||
|
||||
const onConnectionReuseFallback = useCallback((cb: (sessionId: string, sourceSessionId?: string) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onConnectionReuseFallback?.(cb);
|
||||
}, []);
|
||||
|
||||
const onWindowFullScreenChanged = useCallback((cb: (isFullscreen: boolean) => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onWindowFullScreenChanged?.(cb);
|
||||
}, []);
|
||||
|
||||
const onWindowShown = useCallback((cb: () => void) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onWindowShown?.(cb);
|
||||
}, []);
|
||||
|
||||
const onHostKeyVerification = useCallback((cb: Parameters<NonNullable<NetcattyBridge["onHostKeyVerification"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onHostKeyVerification?.(cb);
|
||||
}, []);
|
||||
|
||||
const respondHostKeyVerification = useCallback(async (
|
||||
requestId: string,
|
||||
accept: boolean,
|
||||
addToKnownHosts?: boolean,
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.respondHostKeyVerification) {
|
||||
return { success: false, error: "respondHostKeyVerification unavailable" };
|
||||
}
|
||||
return bridge.respondHostKeyVerification(requestId, accept, addToKnownHosts);
|
||||
}, []);
|
||||
|
||||
const openExternal = useCallback(async (url: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
await bridge?.openExternal?.(url);
|
||||
}, []);
|
||||
|
||||
const openExternalAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.openExternal;
|
||||
}, []);
|
||||
|
||||
const backendAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.startSSHSession;
|
||||
}, []);
|
||||
|
||||
const listSerialPorts = useCallback(async () => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.listSerialPorts) return [];
|
||||
return bridge.listSerialPorts();
|
||||
}, []);
|
||||
|
||||
const serialYmodemAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.sendSerialYmodem;
|
||||
}, []);
|
||||
|
||||
const serialYmodemReceiveAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.receiveSerialYmodem;
|
||||
}, []);
|
||||
|
||||
const selectFileAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.selectFile;
|
||||
}, []);
|
||||
|
||||
const selectDirectoryAvailable = useCallback(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return !!bridge?.selectDirectory;
|
||||
}, []);
|
||||
|
||||
const sendSerialYmodem = useCallback(async (sessionId: string, filePath: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.sendSerialYmodem) return { success: false, error: 'sendSerialYmodem unavailable' };
|
||||
return bridge.sendSerialYmodem(sessionId, filePath);
|
||||
}, []);
|
||||
|
||||
const receiveSerialYmodem = useCallback(async (sessionId: string, destinationDir: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.receiveSerialYmodem) return { success: false, error: 'receiveSerialYmodem unavailable' };
|
||||
return bridge.receiveSerialYmodem(sessionId, destinationDir);
|
||||
}, []);
|
||||
|
||||
const selectFile = useCallback(async (
|
||||
title?: string,
|
||||
defaultPath?: string,
|
||||
filters?: Array<{ name: string; extensions: string[] }>,
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.selectFile) return null;
|
||||
return bridge.selectFile(title, defaultPath, filters);
|
||||
}, []);
|
||||
|
||||
const selectDirectory = useCallback(async (title?: string, defaultPath?: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.selectDirectory) return null;
|
||||
return bridge.selectDirectory(title, defaultPath);
|
||||
}, []);
|
||||
|
||||
const startZmodemDragDropUpload = useCallback(async (
|
||||
sessionId: string,
|
||||
files: Array<{
|
||||
path?: string;
|
||||
name: string;
|
||||
remoteName: string;
|
||||
data?: ArrayBuffer;
|
||||
}>,
|
||||
uploadCommand?: string,
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startZmodemDragDropUpload) {
|
||||
return { success: false, error: "startZmodemDragDropUpload unavailable" };
|
||||
}
|
||||
return bridge.startZmodemDragDropUpload(sessionId, files, uploadCommand);
|
||||
}, []);
|
||||
|
||||
const cancelZmodem = useCallback((sessionId: string, options?: { interrupt?: boolean }) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
bridge?.cancelZmodem?.(sessionId, options);
|
||||
}, []);
|
||||
|
||||
const onZmodemEvent = useCallback((
|
||||
sessionId: string,
|
||||
cb: Parameters<NonNullable<NetcattyBridge["onZmodemEvent"]>>[1],
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
return bridge?.onZmodemEvent?.(sessionId, cb) ?? (() => {});
|
||||
}, []);
|
||||
|
||||
const getSessionPwd = useCallback(async (
|
||||
sessionId: string,
|
||||
options?: { allowHomeFallback?: boolean; allowLoginShellFallback?: boolean },
|
||||
) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.getSessionPwd) return { success: false, error: 'getSessionPwd unavailable' };
|
||||
return bridge.getSessionPwd(sessionId, options);
|
||||
}, []);
|
||||
|
||||
const getSessionRemoteInfo = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.getSessionRemoteInfo) {
|
||||
return { success: false, error: 'getSessionRemoteInfo unavailable' };
|
||||
}
|
||||
return bridge.getSessionRemoteInfo(sessionId);
|
||||
}, []);
|
||||
|
||||
const getSessionDistroInfo = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.getSessionDistroInfo) {
|
||||
return { success: false, error: 'getSessionDistroInfo unavailable' };
|
||||
}
|
||||
return bridge.getSessionDistroInfo(sessionId);
|
||||
}, []);
|
||||
|
||||
const getServerStats = useCallback(async (sessionId: string) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.getServerStats) return { success: false, error: 'getServerStats unavailable' };
|
||||
return bridge.getServerStats(sessionId);
|
||||
}, []);
|
||||
|
||||
// Memoize the returned object so its identity is stable across the
|
||||
// hook's lifetime. Each method above is already useCallback([])-stable,
|
||||
// so listing them as deps means useMemo recomputes once and then
|
||||
// caches forever. Without this, every render produced a fresh object
|
||||
// literal — making `terminalBackend` an unstable reference that
|
||||
// forced consumers' useEffects (`}, [..., terminalBackend])`) to
|
||||
// rerun on every parent render and forced lint to flag any deeper
|
||||
// property dep (`}, [terminalBackend.onHostKeyVerification])`) it
|
||||
// couldn't statically prove safe.
|
||||
return useMemo(
|
||||
() => {
|
||||
const api = {
|
||||
backendAvailable,
|
||||
telnetAvailable,
|
||||
moshAvailable,
|
||||
etAvailable,
|
||||
localAvailable,
|
||||
serialAvailable,
|
||||
pluginConnectionAvailable,
|
||||
execAvailable,
|
||||
openExternalAvailable,
|
||||
startSSHSession,
|
||||
startTelnetSession,
|
||||
startMoshSession,
|
||||
startEtSession,
|
||||
startLocalSession,
|
||||
startSerialSession,
|
||||
startPluginConnection,
|
||||
cancelPluginExtensionRequest,
|
||||
signalPluginConnection,
|
||||
listSerialPorts,
|
||||
serialYmodemAvailable,
|
||||
serialYmodemReceiveAvailable,
|
||||
selectFileAvailable,
|
||||
selectDirectoryAvailable,
|
||||
sendSerialYmodem,
|
||||
receiveSerialYmodem,
|
||||
selectFile,
|
||||
selectDirectory,
|
||||
startZmodemDragDropUpload,
|
||||
cancelZmodem,
|
||||
onZmodemEvent,
|
||||
execCommand,
|
||||
setupOsc7Tracking,
|
||||
getSessionPwd,
|
||||
getSessionRemoteInfo,
|
||||
getSessionDistroInfo,
|
||||
getServerStats,
|
||||
writeToSession,
|
||||
interruptSession,
|
||||
resizeSession,
|
||||
clearSessionPtyBuffer,
|
||||
setSessionFlowPaused,
|
||||
setSessionFlowPausedAndWait,
|
||||
acquireSessionFlowPauseLease,
|
||||
ackSessionFlow,
|
||||
notifyTerminalSessionDisplayReady,
|
||||
closeSession,
|
||||
rebindSessionOutput,
|
||||
restoreSessionOutput,
|
||||
requestSessionSnapshot,
|
||||
applySessionSnapshot,
|
||||
setSessionEncoding,
|
||||
onSessionData,
|
||||
onSessionExit,
|
||||
onTelnetAutoLoginComplete,
|
||||
onTelnetAutoLoginCancelled,
|
||||
onTelnetEchoMode,
|
||||
getTelnetEchoMode,
|
||||
onChainProgress,
|
||||
onConnectionReuseFallback,
|
||||
onWindowFullScreenChanged,
|
||||
onWindowShown,
|
||||
onHostKeyVerification,
|
||||
respondHostKeyVerification,
|
||||
openExternal,
|
||||
};
|
||||
// Only surface onMoshSessionReady when the bridge actually implements it.
|
||||
// A always-truthy wrapper would skip the documented no-event fallback and
|
||||
// leave mosh startup/scripts waiting forever on older builds.
|
||||
Object.defineProperty(api, "onMoshSessionReady", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get() {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (typeof bridge?.onMoshSessionReady !== "function") {
|
||||
return undefined;
|
||||
}
|
||||
return onMoshSessionReady;
|
||||
},
|
||||
});
|
||||
return api;
|
||||
},
|
||||
[
|
||||
backendAvailable,
|
||||
telnetAvailable,
|
||||
moshAvailable,
|
||||
etAvailable,
|
||||
localAvailable,
|
||||
serialAvailable,
|
||||
pluginConnectionAvailable,
|
||||
execAvailable,
|
||||
openExternalAvailable,
|
||||
startSSHSession,
|
||||
startTelnetSession,
|
||||
startMoshSession,
|
||||
startEtSession,
|
||||
startLocalSession,
|
||||
startSerialSession,
|
||||
startPluginConnection,
|
||||
cancelPluginExtensionRequest,
|
||||
signalPluginConnection,
|
||||
listSerialPorts,
|
||||
serialYmodemAvailable,
|
||||
serialYmodemReceiveAvailable,
|
||||
selectFileAvailable,
|
||||
selectDirectoryAvailable,
|
||||
sendSerialYmodem,
|
||||
receiveSerialYmodem,
|
||||
selectFile,
|
||||
selectDirectory,
|
||||
startZmodemDragDropUpload,
|
||||
cancelZmodem,
|
||||
onZmodemEvent,
|
||||
execCommand,
|
||||
setupOsc7Tracking,
|
||||
getSessionPwd,
|
||||
getSessionRemoteInfo,
|
||||
getSessionDistroInfo,
|
||||
getServerStats,
|
||||
writeToSession,
|
||||
interruptSession,
|
||||
resizeSession,
|
||||
clearSessionPtyBuffer,
|
||||
setSessionFlowPaused,
|
||||
setSessionFlowPausedAndWait,
|
||||
acquireSessionFlowPauseLease,
|
||||
ackSessionFlow,
|
||||
notifyTerminalSessionDisplayReady,
|
||||
closeSession,
|
||||
rebindSessionOutput,
|
||||
restoreSessionOutput,
|
||||
requestSessionSnapshot,
|
||||
applySessionSnapshot,
|
||||
setSessionEncoding,
|
||||
onSessionData,
|
||||
onSessionExit,
|
||||
onTelnetAutoLoginComplete,
|
||||
onTelnetAutoLoginCancelled,
|
||||
onMoshSessionReady,
|
||||
onTelnetEchoMode,
|
||||
getTelnetEchoMode,
|
||||
onChainProgress,
|
||||
onConnectionReuseFallback,
|
||||
onWindowFullScreenChanged,
|
||||
onWindowShown,
|
||||
onHostKeyVerification,
|
||||
respondHostKeyVerification,
|
||||
openExternal,
|
||||
],
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user