Build ET UI: host settings panel, protocol picker, and session starters
Add ET configuration section in HostDetailsAdvancedSections (etPort, etTerminalPath). Add ET option to ProtocolSelectDialog. Wire ET session creation in createTerminalSessionStarters with proxy and multi-jump validation. Add ET badges and labels in Terminal, TerminalToolbar, TerminalConnectionDialog, and TerminalLayerSupport. Propagate ET settings through GroupDetailsPanel, GroupSshSettingsSection, and VaultView.
This commit is contained in:
@@ -103,6 +103,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
||||
!!c.proxyProfileId || !!c.proxyConfig || !!c.hostChain || !!c.startupCommand || c.legacyAlgorithms !== undefined || c.skipEcdsaHostKey !== undefined || c.algorithms !== undefined || c.backspaceBehavior !== undefined ||
|
||||
(c.environmentVariables && c.environmentVariables.length > 0) ||
|
||||
c.moshEnabled !== undefined || !!c.moshServerPath ||
|
||||
c.etEnabled !== undefined || c.etPort !== undefined ||
|
||||
(c.identityFilePaths && c.identityFilePaths.length > 0);
|
||||
const hasTelnetFields = (c: Partial<GroupConfig>) =>
|
||||
c.telnetPort !== undefined || !!c.telnetUsername || !!c.telnetPassword || c.telnetEnabled === true;
|
||||
@@ -171,6 +172,8 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
||||
delete next.protocol;
|
||||
delete next.moshEnabled;
|
||||
delete next.moshServerPath;
|
||||
delete next.etEnabled;
|
||||
delete next.etPort;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -391,6 +394,8 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
||||
...(form.environmentVariables !== undefined && { environmentVariables: form.environmentVariables }),
|
||||
...(form.moshEnabled !== undefined && { moshEnabled: form.moshEnabled }),
|
||||
...(form.moshServerPath !== undefined && { moshServerPath: form.moshServerPath }),
|
||||
...(form.etEnabled !== undefined && { etEnabled: form.etEnabled }),
|
||||
...(form.etPort !== undefined && { etPort: form.etPort }),
|
||||
}),
|
||||
// Only include Telnet fields if Telnet section is enabled
|
||||
...(telnetEnabled && {
|
||||
|
||||
@@ -523,6 +523,25 @@ export const GroupSshSettingsSection: React.FC<GroupSshSettingsSectionProps> = (
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* EternalTerminal */}
|
||||
<ToggleRow
|
||||
label="EternalTerminal"
|
||||
enabled={!!form.etEnabled}
|
||||
onToggle={() => update("etEnabled", !form.etEnabled)}
|
||||
/>
|
||||
{form.etEnabled && (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t("hostDetails.et.port") || "ET server port (2022)"}
|
||||
value={form.etPort ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
update("etPort", v === "" ? undefined : Number(v));
|
||||
}}
|
||||
className="h-10"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Backspace behavior — terminal input mapping, lives at the
|
||||
bottom of the SSH section so it doesn't get visually
|
||||
grouped with the algorithm controls above. */}
|
||||
|
||||
@@ -175,6 +175,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
moshEnabled: true,
|
||||
etEnabled: false,
|
||||
deviceType: prev.deviceType === 'network' ? undefined : prev.deviceType,
|
||||
x11Forwarding: undefined,
|
||||
}));
|
||||
@@ -185,6 +186,46 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
/>
|
||||
</HostDetailsSection>
|
||||
|
||||
<HostDetailsSection
|
||||
icon={<Wifi size={14} className="text-muted-foreground" />}
|
||||
title={t("hostDetails.section.et")}
|
||||
>
|
||||
<ToggleRow
|
||||
label="EternalTerminal"
|
||||
enabled={!!form.etEnabled}
|
||||
onToggle={() => {
|
||||
const enabling = !form.etEnabled;
|
||||
if (enabling) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
etEnabled: true,
|
||||
moshEnabled: false,
|
||||
deviceType: prev.deviceType === 'network' ? undefined : prev.deviceType,
|
||||
x11Forwarding: undefined,
|
||||
}));
|
||||
} else {
|
||||
update("etEnabled", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{form.etEnabled && (
|
||||
<>
|
||||
<HostDetailsSettingRow label={t("hostDetails.et.port")} hint={t("hostDetails.et.port.desc")}>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
placeholder="2022"
|
||||
value={form.etPort ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
update("etPort", v === "" ? undefined : Number(v));
|
||||
}}
|
||||
/>
|
||||
</HostDetailsSettingRow>
|
||||
</>
|
||||
)}
|
||||
</HostDetailsSection>
|
||||
|
||||
{/* Agent Forwarding */}
|
||||
<HostDetailsSection
|
||||
icon={<Forward size={14} className="text-muted-foreground" />}
|
||||
@@ -212,7 +253,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
</HostDetailsSection>
|
||||
|
||||
{/* X11 Forwarding */}
|
||||
{(!form.protocol || form.protocol === "ssh") && !form.moshEnabled && (
|
||||
{(!form.protocol || form.protocol === "ssh") && !form.moshEnabled && !form.etEnabled && (
|
||||
<HostDetailsSection
|
||||
icon={<TerminalSquare size={14} className="text-muted-foreground" />}
|
||||
title={t("hostDetails.section.x11Forwarding")}
|
||||
@@ -226,8 +267,8 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
</HostDetailsSection>
|
||||
)}
|
||||
|
||||
{/* Network Device Mode — only for SSH hosts without Mosh (serial already uses raw mode) */}
|
||||
{(!form.protocol || form.protocol === 'ssh') && !form.moshEnabled && (
|
||||
{/* Network Device Mode — only for SSH hosts without Mosh / ET (serial already uses raw mode) */}
|
||||
{(!form.protocol || form.protocol === 'ssh') && !form.moshEnabled && !form.etEnabled && (
|
||||
<HostDetailsSection
|
||||
icon={<Router size={14} className="text-muted-foreground" />}
|
||||
title={t("hostDetails.section.deviceType")}
|
||||
|
||||
@@ -443,7 +443,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
||||
cleaned.fontSize = initialData?.fontSize;
|
||||
}
|
||||
|
||||
if ((cleaned.protocol && cleaned.protocol !== "ssh") || cleaned.moshEnabled) {
|
||||
if ((cleaned.protocol && cleaned.protocol !== "ssh") || cleaned.moshEnabled || cleaned.etEnabled) {
|
||||
delete cleaned.x11Forwarding;
|
||||
}
|
||||
onSave(cleaned);
|
||||
|
||||
@@ -63,6 +63,18 @@ const ProtocolSelectDialog: React.FC<ProtocolSelectDialogProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// EternalTerminal (if enabled)
|
||||
if (host.etEnabled || host.protocols?.some(p => p.protocol === 'et' && p.enabled)) {
|
||||
options.push({
|
||||
protocol: 'et',
|
||||
port: host.port || 22,
|
||||
label: 'EternalTerminal',
|
||||
icon: <Wifi size={18} />,
|
||||
description: `et ${host.hostname}`,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Telnet (if enabled)
|
||||
if (host.telnetEnabled || host.protocol === 'telnet' || host.protocols?.some(p => p.protocol === 'telnet' && p.enabled)) {
|
||||
const telnetConfig = host.protocols?.find(p => p.protocol === 'telnet');
|
||||
|
||||
@@ -425,6 +425,10 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
starters.startMosh(term);
|
||||
return;
|
||||
}
|
||||
if (host.etEnabled) {
|
||||
starters.startEt(term);
|
||||
return;
|
||||
}
|
||||
starters.startSSH(term);
|
||||
},
|
||||
setStatus: (next) => setStatus(next),
|
||||
@@ -605,7 +609,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
const isSerial = host.protocol === 'serial' || host.id?.startsWith('serial-');
|
||||
const isTelnet = host.protocol === 'telnet';
|
||||
const isMosh = host.protocol === 'mosh' || host.moshEnabled;
|
||||
const isSSH = !isLocal && !isSerial && !isTelnet && !isMosh;
|
||||
const isEt = host.protocol === 'et' || host.etEnabled;
|
||||
const isSSH = !isLocal && !isSerial && !isTelnet && !isMosh && !isEt;
|
||||
if (isSSH) {
|
||||
setSessionEncoding(id, terminalEncodingRef.current);
|
||||
return;
|
||||
@@ -893,6 +898,8 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
sessionStarters.startTelnet(term);
|
||||
} else if (host.moshEnabled) {
|
||||
sessionStarters.startMosh(term);
|
||||
} else if (host.etEnabled) {
|
||||
sessionStarters.startEt(term);
|
||||
} else {
|
||||
sessionStarters.startSSH(term);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
protocol: session.protocol ?? host.protocol,
|
||||
port: session.port ?? host.port,
|
||||
moshEnabled: session.moshEnabled ?? host.moshEnabled,
|
||||
etEnabled: session.etEnabled ?? host.etEnabled,
|
||||
}
|
||||
: {
|
||||
// Quick Connect / temporary session — build minimal host from session data
|
||||
@@ -518,11 +519,13 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
const protocol = session.protocol ?? existingHost.protocol;
|
||||
const port = session.port ?? existingHost.port;
|
||||
const moshEnabled = session.moshEnabled ?? existingHost.moshEnabled;
|
||||
const etEnabled = session.etEnabled ?? existingHost.etEnabled;
|
||||
|
||||
if (
|
||||
protocol === existingHost.protocol &&
|
||||
port === existingHost.port &&
|
||||
moshEnabled === existingHost.moshEnabled
|
||||
&& etEnabled === existingHost.etEnabled
|
||||
) {
|
||||
map.set(session.id, existingHost);
|
||||
} else {
|
||||
@@ -531,6 +534,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
protocol,
|
||||
port,
|
||||
moshEnabled,
|
||||
etEnabled,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -555,6 +559,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
tags: [],
|
||||
protocol: fallbackProtocol,
|
||||
moshEnabled: session.moshEnabled,
|
||||
etEnabled: session.etEnabled,
|
||||
charset: session.charset,
|
||||
localShell: session.localShell,
|
||||
localShellArgs: session.localShellArgs,
|
||||
|
||||
@@ -430,9 +430,10 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
||||
if (protocolSelectHost) {
|
||||
const hostWithProtocol: Host = {
|
||||
...protocolSelectHost,
|
||||
protocol: protocol === "mosh" ? "ssh" : protocol,
|
||||
protocol: (protocol === "mosh" || protocol === "et") ? "ssh" : protocol,
|
||||
port,
|
||||
moshEnabled: protocol === "mosh",
|
||||
etEnabled: protocol === "et",
|
||||
};
|
||||
onConnect(hostWithProtocol);
|
||||
setProtocolSelectHost(null);
|
||||
|
||||
@@ -50,6 +50,10 @@ const getProtocolInfo = (host: Host): { i18nKey: string; showPort: boolean; port
|
||||
if (host.moshEnabled) {
|
||||
return { i18nKey: 'terminal.connection.protocol.mosh', showPort: true, port: host.port || 22 };
|
||||
}
|
||||
// ET likewise uses protocol: "ssh" with etEnabled: true
|
||||
if (host.etEnabled) {
|
||||
return { i18nKey: 'terminal.connection.protocol.et', showPort: true, port: host.port || 22 };
|
||||
}
|
||||
const protocol = host.protocol || 'ssh';
|
||||
switch (protocol) {
|
||||
case 'local':
|
||||
|
||||
@@ -61,13 +61,14 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
const isLocalTerminal = host?.protocol === 'local' || host?.id?.startsWith('local-');
|
||||
const isSerialTerminal = host?.protocol === 'serial' || host?.id?.startsWith('serial-');
|
||||
const isMoshSession = host?.protocol === 'mosh' || host?.moshEnabled;
|
||||
// Local PTY inherits the OS locale and mosh always uses its own UTF-8
|
||||
// framing, so the quick-switch menu only makes sense for sessions whose
|
||||
const isEtSession = host?.protocol === 'et' || host?.etEnabled;
|
||||
// Local PTY inherits the OS locale and mosh/ET always use their own framing,
|
||||
// so the quick-switch menu only makes sense for sessions whose
|
||||
// backend decoder we actually control (SSH, telnet, serial). Hostname
|
||||
// isn't part of the gate — telnet/SSH targets pointed at localhost
|
||||
// (test daemons, forwarded endpoints) still have a real backend
|
||||
// decoder we can drive.
|
||||
const encodingSwitchSupported = !isLocalTerminal && !isMoshSession;
|
||||
const encodingSwitchSupported = !isLocalTerminal && !isMoshSession && !isEtSession;
|
||||
const hidesSftp = isLocalTerminal || isSerialTerminal;
|
||||
|
||||
const menuItemClass = "w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-sm hover:bg-secondary transition-colors";
|
||||
|
||||
@@ -732,6 +732,206 @@ export const createTerminalSessionStarters = (ctx: TerminalSessionStartersContex
|
||||
}
|
||||
};
|
||||
|
||||
const startEt = async (term: XTerm) => {
|
||||
if (!ctx.terminalBackend.etAvailable()) {
|
||||
ctx.setError("EternalTerminal bridge unavailable. Please run the desktop build.");
|
||||
writeTerminalLine(ctx, term, "\r\n[EternalTerminal bridge unavailable. Please run the desktop build.]");
|
||||
ctx.updateStatus("disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stopEt = (message: string) => {
|
||||
ctx.setError(message);
|
||||
writeTerminalLine(ctx, term, `\r\n[${message}]`);
|
||||
ctx.updateStatus("disconnected");
|
||||
};
|
||||
|
||||
if (ctx.host.proxyProfileId && !ctx.host.proxyConfig) {
|
||||
stopEt(`Saved proxy for host "${ctx.host.label || ctx.host.hostname}" is missing. Open host settings and select a valid proxy.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.host.proxyConfig?.host && ctx.host.proxyConfig?.port) {
|
||||
stopEt(tr(
|
||||
"terminal.et.proxyUnsupported",
|
||||
"EternalTerminal does not currently support Netcatty proxy settings. Use SSH or remove the proxy for this host.",
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.resolvedChainHosts.length > 1) {
|
||||
stopEt(tr(
|
||||
"terminal.et.multiJumpUnsupported",
|
||||
"EternalTerminal currently supports at most one jump host in Netcatty.",
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingAuth = ctx.pendingAuthRef.current;
|
||||
const resolvedAuth = resolveHostAuth({
|
||||
host: ctx.host,
|
||||
keys: ctx.keys,
|
||||
identities: ctx.identities,
|
||||
override: pendingAuth
|
||||
? {
|
||||
authMethod: pendingAuth.authMethod,
|
||||
username: pendingAuth.username,
|
||||
password: pendingAuth.password,
|
||||
keyId: pendingAuth.keyId,
|
||||
passphrase: pendingAuth.passphrase,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
const effectivePassword = sanitizeCredentialValue(resolvedAuth.password);
|
||||
const effectivePassphrase = sanitizeCredentialValue(resolvedAuth.passphrase);
|
||||
const authMethod = resolvedAuth.authMethod;
|
||||
const key = authMethod === "password" ? undefined : resolvedAuth.key;
|
||||
const hasEncryptedPrimaryPassword = isEncryptedCredentialPlaceholder(resolvedAuth.password);
|
||||
const hasEncryptedPrimaryKey = isEncryptedCredentialPlaceholder(resolvedAuth.key?.privateKey);
|
||||
const allowsLocalIdentityFallback = !resolvedAuth.keyId;
|
||||
const etReferenceKeyPath = key?.source === 'reference' ? key.filePath : undefined;
|
||||
const etIdentityFilePaths = authMethod === "password"
|
||||
? undefined
|
||||
: etReferenceKeyPath
|
||||
? [etReferenceKeyPath]
|
||||
: allowsLocalIdentityFallback
|
||||
? ctx.host.identityFilePaths
|
||||
: undefined;
|
||||
const hasKeyMaterial = (!!sanitizeCredentialValue(key?.privateKey) || !!etIdentityFilePaths?.length) && authMethod !== "password";
|
||||
const hasPassword = !!effectivePassword;
|
||||
const needsCredentialReentry =
|
||||
(authMethod === "password" && hasEncryptedPrimaryPassword && !hasPassword) ||
|
||||
(authMethod !== "password" && hasEncryptedPrimaryKey && !hasKeyMaterial && !hasPassword);
|
||||
|
||||
if (needsCredentialReentry) {
|
||||
ctx.setError(null);
|
||||
ctx.setNeedsAuth(true);
|
||||
ctx.setAuthRetryMessage(
|
||||
tr(
|
||||
"terminal.auth.credentialsUnavailable",
|
||||
"Saved credentials cannot be decrypted on this device. Please re-enter and save them again.",
|
||||
),
|
||||
);
|
||||
ctx.setAuthPassword("");
|
||||
ctx.setStatus("connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
const jumpHostsWithUnavailableCredentials: string[] = [];
|
||||
const unsupportedJumpProxies: string[] = [];
|
||||
const jumpHosts = ctx.resolvedChainHosts.map<NetcattyJumpHost>((jumpHost) => {
|
||||
const jumpAuth = resolveHostAuth({
|
||||
host: jumpHost,
|
||||
keys: ctx.keys,
|
||||
identities: ctx.identities,
|
||||
});
|
||||
const jumpKey = jumpAuth.key;
|
||||
const rawJumpPassword = jumpAuth.password;
|
||||
const rawJumpPrivateKey = jumpKey?.privateKey;
|
||||
const rawJumpPassphrase = jumpAuth.passphrase || jumpKey?.passphrase;
|
||||
const jumpPassword = sanitizeCredentialValue(rawJumpPassword);
|
||||
const jumpPrivateKey = sanitizeCredentialValue(rawJumpPrivateKey);
|
||||
const jumpPassphrase = sanitizeCredentialValue(rawJumpPassphrase);
|
||||
|
||||
if (jumpHost.proxyConfig?.host && jumpHost.proxyConfig?.port) {
|
||||
unsupportedJumpProxies.push(jumpHost.label || jumpHost.hostname);
|
||||
}
|
||||
|
||||
const hasEncryptedJumpCredential =
|
||||
isEncryptedCredentialPlaceholder(rawJumpPassword) ||
|
||||
isEncryptedCredentialPlaceholder(rawJumpPrivateKey) ||
|
||||
isEncryptedCredentialPlaceholder(rawJumpPassphrase);
|
||||
if (hasEncryptedJumpCredential && !jumpPassword && !jumpPrivateKey && !jumpPassphrase) {
|
||||
jumpHostsWithUnavailableCredentials.push(jumpHost.label || jumpHost.hostname);
|
||||
}
|
||||
|
||||
return {
|
||||
hostname: jumpHost.hostname,
|
||||
port: jumpHost.port || 22,
|
||||
username: jumpAuth.username || "root",
|
||||
password: jumpPassword,
|
||||
privateKey: jumpKey?.source === 'reference' ? undefined : jumpPrivateKey,
|
||||
certificate: jumpKey?.certificate,
|
||||
passphrase: jumpPassphrase,
|
||||
keyId: jumpAuth.keyId,
|
||||
keySource: jumpKey?.source,
|
||||
label: jumpHost.label,
|
||||
identityFilePaths: jumpHost.identityFilePaths,
|
||||
};
|
||||
});
|
||||
|
||||
if (unsupportedJumpProxies.length > 0) {
|
||||
stopEt(tr(
|
||||
"terminal.et.proxyUnsupported",
|
||||
"EternalTerminal does not currently support Netcatty proxy settings. Use SSH or remove the proxy for this host.",
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (jumpHostsWithUnavailableCredentials.length > 0) {
|
||||
const jumpList = jumpHostsWithUnavailableCredentials.slice(0, 2).join(", ");
|
||||
const suffix = jumpHostsWithUnavailableCredentials.length > 2
|
||||
? ` +${jumpHostsWithUnavailableCredentials.length - 2}`
|
||||
: "";
|
||||
const base = tr(
|
||||
"terminal.auth.jumpCredentialsUnavailable",
|
||||
"A jump host has saved credentials that cannot be decrypted on this device. Open host settings and re-enter them.",
|
||||
);
|
||||
stopEt(`${base} (${jumpList}${suffix})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const etEnv = buildTermEnv(ctx.host, ctx.terminalSettings);
|
||||
const id = await ctx.terminalBackend.startEtSession({
|
||||
sessionId: ctx.sessionId,
|
||||
hostname: ctx.host.hostname,
|
||||
username: resolvedAuth.username || "root",
|
||||
password: effectivePassword,
|
||||
privateKey: key?.source === 'reference' ? undefined : sanitizeCredentialValue(key?.privateKey),
|
||||
certificate: key?.certificate,
|
||||
keyId: key?.id,
|
||||
passphrase: key
|
||||
? (effectivePassphrase || sanitizeCredentialValue(key.passphrase))
|
||||
: undefined,
|
||||
authMethod,
|
||||
identityFilePaths: etIdentityFilePaths,
|
||||
port: ctx.host.port || 22,
|
||||
etPort: ctx.host.etPort,
|
||||
legacyAlgorithms: ctx.host.legacyAlgorithms,
|
||||
jumpHosts: jumpHosts.length > 0 ? jumpHosts : undefined,
|
||||
agentForwarding: ctx.host.agentForwarding,
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
charset: ctx.host.charset,
|
||||
env: etEnv,
|
||||
sessionLog: ctx.sessionLog?.enabled ? ctx.sessionLog : undefined,
|
||||
});
|
||||
|
||||
attachSessionToTerminal(ctx, term, id, {
|
||||
onExitMessage: (evt) =>
|
||||
`\r\n[EternalTerminal session closed${evt?.exitCode !== undefined ? ` (code ${evt.exitCode})` : ""}]`,
|
||||
});
|
||||
|
||||
scheduleStartupCommand(ctx, term, id);
|
||||
|
||||
// ET sessions are full remote shells, so run OS detection like SSH for
|
||||
// server stats / distro icons.
|
||||
{
|
||||
const connectionToken = registerConnectionToken(id);
|
||||
setTimeout(() => {
|
||||
if (!isConnectionTokenCurrent(id, connectionToken)) return;
|
||||
void runDistroDetection(ctx, id, connectionToken);
|
||||
}, 600);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
ctx.setError(message);
|
||||
writeTerminalLine(ctx, term, `\r\n[Failed to start EternalTerminal: ${message}]`);
|
||||
ctx.updateStatus("disconnected");
|
||||
}
|
||||
};
|
||||
|
||||
const startLocal = async (term: XTerm) => {
|
||||
if (!ctx.terminalBackend.localAvailable()) {
|
||||
ctx.setError("Local shell bridge unavailable. Please run the desktop build.");
|
||||
@@ -868,5 +1068,5 @@ export const createTerminalSessionStarters = (ctx: TerminalSessionStartersContex
|
||||
}
|
||||
};
|
||||
|
||||
return { startSSH, startTelnet, startMosh, startLocal, startSerial };
|
||||
return { startSSH, startTelnet, startMosh, startEt, startLocal, startSerial };
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TerminalBackendApi = {
|
||||
backendAvailable: () => boolean;
|
||||
telnetAvailable: () => boolean;
|
||||
moshAvailable: () => boolean;
|
||||
etAvailable: () => boolean;
|
||||
localAvailable: () => boolean;
|
||||
serialAvailable: () => boolean;
|
||||
execAvailable: () => boolean;
|
||||
@@ -18,6 +19,9 @@ export type TerminalBackendApi = {
|
||||
startMoshSession: (
|
||||
options: Parameters<NonNullable<NetcattyBridge["startMoshSession"]>>[0],
|
||||
) => Promise<string>;
|
||||
startEtSession: (
|
||||
options: Parameters<NonNullable<NetcattyBridge["startEtSession"]>>[0],
|
||||
) => Promise<string>;
|
||||
startLocalSession: (
|
||||
options: Parameters<NonNullable<NetcattyBridge["startLocalSession"]>>[0],
|
||||
) => Promise<string>;
|
||||
|
||||
@@ -254,6 +254,10 @@ export function useTerminalEffects(ctx: TerminalEffectsContext) {
|
||||
setStatus("connecting");
|
||||
setProgressLogs(["Initializing Mosh connection..."]);
|
||||
await sessionStarters.startMosh(term);
|
||||
} else if (host.etEnabled) {
|
||||
setStatus("connecting");
|
||||
setProgressLogs(["Initializing EternalTerminal connection..."]);
|
||||
await sessionStarters.startEt(term);
|
||||
} else {
|
||||
const resolvedAuth = resolveHostAuth({ host, keys, identities });
|
||||
const hasPassword = !!resolvedAuth.password;
|
||||
|
||||
@@ -218,9 +218,9 @@ export const buildAITerminalSessionInfo = (
|
||||
username: host?.username || session?.username,
|
||||
protocol,
|
||||
shellType: session?.shellType && session.shellType !== 'unknown' ? session.shellType : undefined,
|
||||
// Suppress deviceType for Mosh sessions — Mosh requires a shell-backed PTY
|
||||
// and cannot connect to vendor CLIs, so network device mode doesn't apply.
|
||||
deviceType: (session?.moshEnabled || host?.moshEnabled) ? undefined : host?.deviceType,
|
||||
// Suppress deviceType for Mosh / ET sessions — both require a shell-backed
|
||||
// PTY and cannot connect to vendor CLIs, so network device mode doesn't apply.
|
||||
deviceType: (session?.moshEnabled || host?.moshEnabled || session?.etEnabled || host?.etEnabled) ? undefined : host?.deviceType,
|
||||
connected: session?.status === 'connected',
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user