[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:
457
components/terminal/TerminalConnectionDialog.tsx
Normal file
457
components/terminal/TerminalConnectionDialog.tsx
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Terminal Connection Dialog
|
||||
* Full connection overlay with host info, progress indicator, and auth/progress content
|
||||
*/
|
||||
import { Fingerprint, Loader2, Plug, TerminalSquare, X } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Host, SSHKey } from '../../types';
|
||||
import { formatHostPort, resolveTelnetPort } from '../../domain/host';
|
||||
import { isPluginHostProtocol } from '../../domain/pluginConnection';
|
||||
import { DistroAvatar } from '../DistroAvatar';
|
||||
import { Button } from '../ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { TerminalAuthDialog, TerminalAuthDialogProps } from './TerminalAuthDialog';
|
||||
import { TerminalConnectionProgress, TerminalConnectionProgressProps } from './TerminalConnectionProgress';
|
||||
import { HostKeyInfo, TerminalHostKeyVerification } from './TerminalHostKeyVerification';
|
||||
import {
|
||||
resolveDisconnectedDialogTerminalRoot,
|
||||
restoreTerminalFocusFromDisconnectedDialog,
|
||||
shouldClaimDisconnectedDialogFocus,
|
||||
shouldReconnectDisconnectedDialogOnEnterKey,
|
||||
shouldRestoreDisconnectedDialogTerminalFocus,
|
||||
} from './terminalHelpers';
|
||||
|
||||
export interface ChainProgress {
|
||||
currentHop: number;
|
||||
totalHops: number;
|
||||
currentHostLabel: string;
|
||||
}
|
||||
|
||||
export interface TerminalConnectionDialogProps {
|
||||
host: Host;
|
||||
status: 'connecting' | 'connected' | 'disconnected';
|
||||
restoreState?: 'restored-disconnected';
|
||||
error: string | null;
|
||||
progressValue: number;
|
||||
chainProgress: ChainProgress | null;
|
||||
needsAuth: boolean;
|
||||
showLogs: boolean;
|
||||
_setShowLogs: (show: boolean) => void;
|
||||
// Auth dialog props
|
||||
authProps: Omit<TerminalAuthDialogProps, 'keys'>;
|
||||
keys: SSHKey[];
|
||||
onDismissDisconnected?: () => void;
|
||||
showEnterReconnectHint?: boolean;
|
||||
/** False for unfocused split siblings — do not claim body/document focus. */
|
||||
isFocusedPane?: boolean;
|
||||
hostKeyVerification?: {
|
||||
hostKeyInfo: HostKeyInfo;
|
||||
onClose: () => void;
|
||||
onContinue: () => void;
|
||||
onAddAndContinue: () => void;
|
||||
};
|
||||
// Progress props
|
||||
progressProps: Omit<TerminalConnectionProgressProps, 'status' | 'error' | 'showLogs' | 'showEnterReconnectHint'>;
|
||||
}
|
||||
|
||||
// Helper to get protocol display info
|
||||
const getProtocolInfo = (host: Host): { i18nKey: string; showPort: boolean; port: number } => {
|
||||
// Check moshEnabled first since mosh uses protocol: "ssh" with moshEnabled: true
|
||||
if (host.moshEnabled) {
|
||||
return { i18nKey: 'terminal.connection.protocol.mosh', showPort: true, port: host.port || 22 };
|
||||
}
|
||||
// ET likewise uses protocol: "ssh" with etEnabled: true. Show the ET
|
||||
// server port (default 2022) rather than the SSH port: ET connectivity
|
||||
// hinges on the etserver port, so surfacing the SSH port (22) here is
|
||||
// misleading when troubleshooting a connection that is actually stuck on
|
||||
// the ET port.
|
||||
if (host.etEnabled) {
|
||||
return { i18nKey: 'terminal.connection.protocol.et', showPort: true, port: host.etPort || 2022 };
|
||||
}
|
||||
const protocol = host.protocol || 'ssh';
|
||||
if (isPluginHostProtocol(protocol)) {
|
||||
return { i18nKey: 'terminal.connection.protocol.plugin', showPort: false, port: 0 };
|
||||
}
|
||||
switch (protocol) {
|
||||
case 'local':
|
||||
return { i18nKey: 'terminal.connection.protocol.local', showPort: false, port: 0 };
|
||||
case 'telnet':
|
||||
// Telnet uses telnetPort, not port (which is SSH port)
|
||||
return { i18nKey: 'terminal.connection.protocol.telnet', showPort: true, port: resolveTelnetPort(host) };
|
||||
case 'mosh':
|
||||
return { i18nKey: 'terminal.connection.protocol.mosh', showPort: true, port: host.port || 22 };
|
||||
case 'serial':
|
||||
return { i18nKey: 'terminal.connection.protocol.serial', showPort: false, port: 0 };
|
||||
case 'ssh':
|
||||
default:
|
||||
return { i18nKey: 'terminal.connection.protocol.ssh', showPort: true, port: host.port || 22 };
|
||||
}
|
||||
};
|
||||
|
||||
export const TerminalConnectionDialog: React.FC<TerminalConnectionDialogProps> = ({
|
||||
host,
|
||||
status,
|
||||
restoreState,
|
||||
error,
|
||||
progressValue,
|
||||
chainProgress,
|
||||
needsAuth,
|
||||
showLogs,
|
||||
_setShowLogs: setShowLogs, // Rename back to setShowLogs for internal use
|
||||
authProps,
|
||||
keys,
|
||||
onDismissDisconnected,
|
||||
showEnterReconnectHint,
|
||||
isFocusedPane,
|
||||
hostKeyVerification,
|
||||
progressProps,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const hasError = Boolean(error);
|
||||
const isRestoredDisconnected = status === 'disconnected' && restoreState === 'restored-disconnected';
|
||||
const isConnecting = status === 'connecting';
|
||||
const canDismissDisconnected = status === 'disconnected' && !needsAuth && !!onDismissDisconnected;
|
||||
const protocolInfo = getProtocolInfo(host);
|
||||
const isVerifyingHostKey = Boolean(hostKeyVerification);
|
||||
const isHostKeyChanged = hostKeyVerification?.hostKeyInfo.status === 'changed';
|
||||
const shouldCompleteProgress = hasError || (!isConnecting && !needsAuth);
|
||||
// When the disconnected overlay is up and Enter-reconnect is advertised,
|
||||
// keep a focus sink on the overlay itself so body/document focus loss cannot
|
||||
// make the hint lie (#2544). Auth/host-key flows keep their own inputs.
|
||||
const onRetry = progressProps.onRetry;
|
||||
const canEnterReconnectFromDialog = Boolean(
|
||||
showEnterReconnectHint
|
||||
&& status === 'disconnected'
|
||||
&& !needsAuth
|
||||
&& !isVerifyingHostKey
|
||||
&& onRetry,
|
||||
);
|
||||
const dialogFocusRef = useRef<HTMLDivElement | null>(null);
|
||||
// Unmount cleanup keeps [] deps; read the latest pane ownership then.
|
||||
const isFocusedPaneRef = useRef(isFocusedPane);
|
||||
isFocusedPaneRef.current = isFocusedPane;
|
||||
|
||||
// Claim focus only when Enter-reconnect mode turns on — not on every
|
||||
// showLogs/error rerender (those would steal keyboard focus off buttons).
|
||||
useEffect(() => {
|
||||
if (!canEnterReconnectFromDialog) return;
|
||||
const node = dialogFocusRef.current;
|
||||
if (!node) return;
|
||||
const sessionRoot = node.closest("[data-session-id]");
|
||||
const focusOverlay = () => {
|
||||
if (typeof document === "undefined") return;
|
||||
if (!shouldClaimDisconnectedDialogFocus({
|
||||
activeElement: document.activeElement,
|
||||
dialogNode: node,
|
||||
sessionRoot,
|
||||
documentBody: document.body,
|
||||
documentElement: document.documentElement,
|
||||
isFocusedPane,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
node.focus({ preventScroll: true });
|
||||
};
|
||||
focusOverlay();
|
||||
// Re-assert after paint/microtasks so late blur from xterm teardown
|
||||
// cannot leave focus on document.body — still never steals other panes.
|
||||
const timer = window.setTimeout(focusOverlay, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [canEnterReconnectFromDialog, isFocusedPane]);
|
||||
|
||||
// Restore xterm focus only when this overlay unmounts (connected / dismiss).
|
||||
// Do not key on Enter-reconnect mode: reconnect may keep the dialog mounted
|
||||
// for connecting / auth / host-key, and restoring then routes keyboard behind
|
||||
// the blocking overlay.
|
||||
useEffect(() => {
|
||||
const node = dialogFocusRef.current;
|
||||
if (!node) return;
|
||||
// Capture the terminal root while the dialog is still mounted — after
|
||||
// unmount, parentElement is null and popup trees have no data-session-id.
|
||||
const sessionRoot = resolveDisconnectedDialogTerminalRoot(
|
||||
node,
|
||||
node.closest("[data-session-id]"),
|
||||
);
|
||||
return () => {
|
||||
if (typeof document === "undefined") return;
|
||||
if (!shouldRestoreDisconnectedDialogTerminalFocus(node)) return;
|
||||
restoreTerminalFocusFromDisconnectedDialog({
|
||||
activeElement: document.activeElement,
|
||||
dialogNode: node,
|
||||
sessionRoot,
|
||||
documentBody: document.body,
|
||||
documentElement: document.documentElement,
|
||||
isFocusedPane: isFocusedPaneRef.current,
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDialogKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!shouldReconnectDisconnectedDialogOnEnterKey({
|
||||
key: event.key,
|
||||
enabled: canEnterReconnectFromDialog,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
isComposing: event.nativeEvent.isComposing,
|
||||
target: event.target,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onRetry?.();
|
||||
}, [canEnterReconnectFromDialog, onRetry]);
|
||||
const targetFirstSegmentWidth = isVerifyingHostKey || shouldCompleteProgress
|
||||
? 100
|
||||
: Math.min(100, progressValue * 2);
|
||||
const targetSecondSegmentWidth = isVerifyingHostKey
|
||||
? 0
|
||||
: shouldCompleteProgress
|
||||
? 100
|
||||
: Math.max(0, Math.min(100, (progressValue - 50) * 2));
|
||||
const [secondSegmentUnlocked, setSecondSegmentUnlocked] = React.useState(
|
||||
() => shouldCompleteProgress || targetSecondSegmentWidth <= 0
|
||||
);
|
||||
const secondSegmentUnlockTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (secondSegmentUnlockTimerRef.current) {
|
||||
clearTimeout(secondSegmentUnlockTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (needsAuth || isVerifyingHostKey || targetSecondSegmentWidth <= 0 || shouldCompleteProgress) {
|
||||
if (secondSegmentUnlockTimerRef.current) {
|
||||
clearTimeout(secondSegmentUnlockTimerRef.current);
|
||||
secondSegmentUnlockTimerRef.current = null;
|
||||
}
|
||||
setSecondSegmentUnlocked(shouldCompleteProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondSegmentUnlocked || secondSegmentUnlockTimerRef.current) return;
|
||||
|
||||
secondSegmentUnlockTimerRef.current = setTimeout(() => {
|
||||
secondSegmentUnlockTimerRef.current = null;
|
||||
setSecondSegmentUnlocked(true);
|
||||
}, 320);
|
||||
}, [isVerifyingHostKey, needsAuth, secondSegmentUnlocked, shouldCompleteProgress, targetSecondSegmentWidth]);
|
||||
|
||||
const firstSegmentWidth = targetFirstSegmentWidth;
|
||||
const secondSegmentWidth = shouldCompleteProgress || secondSegmentUnlocked ? targetSecondSegmentWidth : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-20 flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: needsAuth
|
||||
? 'var(--terminal-ui-bg, var(--background))'
|
||||
: 'color-mix(in srgb, var(--terminal-ui-bg, var(--background)) 35%, transparent)',
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
// Clicking the dimmed backdrop (not a control) should park focus
|
||||
// on the overlay so the next Enter still reconnects.
|
||||
if (!canEnterReconnectFromDialog) return;
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
if (target.closest("button, a, input, textarea, select, [contenteditable='true'], [role='button'], [role='menuitem'], [role='textbox']")) {
|
||||
return;
|
||||
}
|
||||
dialogFocusRef.current?.focus({ preventScroll: true });
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={dialogFocusRef}
|
||||
tabIndex={canEnterReconnectFromDialog ? -1 : undefined}
|
||||
data-terminal-disconnected-dialog={canEnterReconnectFromDialog ? 'true' : undefined}
|
||||
onKeyDown={handleDialogKeyDown}
|
||||
className="w-[540px] max-w-[88vw] rounded-xl shadow-xl p-4 space-y-3 transition-all duration-200 outline-none"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--terminal-ui-bg, var(--background)) 95%, transparent)',
|
||||
border: '1px solid color-mix(in srgb, var(--terminal-ui-fg, var(--foreground)) 12%, var(--terminal-ui-bg, var(--background)) 88%)',
|
||||
color: 'var(--terminal-ui-fg, var(--foreground))',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<DistroAvatar host={host} fallback={host.label.slice(0, 2).toUpperCase()} size="md" className="shrink-0" />
|
||||
<div className="min-w-0">
|
||||
{chainProgress ? (
|
||||
<>
|
||||
<div className="text-xs font-semibold truncate">
|
||||
<span className="text-muted-foreground">
|
||||
{t('terminal.connection.chainOf', {
|
||||
current: chainProgress.currentHop,
|
||||
total: chainProgress.totalHops,
|
||||
})}
|
||||
{': '}
|
||||
</span>
|
||||
<span>{chainProgress.currentHostLabel}</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-[10px] font-mono truncate"
|
||||
style={{ color: 'color-mix(in srgb, var(--terminal-ui-fg, var(--foreground)) 58%, transparent)' }}
|
||||
>
|
||||
{t(protocolInfo.i18nKey)} {protocolInfo.showPort ? formatHostPort(host.hostname, protocolInfo.port) : host.hostname}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-base font-semibold truncate">{host.label}</div>
|
||||
<div
|
||||
className="text-[10px] font-mono truncate"
|
||||
style={{ color: 'color-mix(in srgb, var(--terminal-ui-fg, var(--foreground)) 58%, transparent)' }}
|
||||
>
|
||||
{t(protocolInfo.i18nKey)} {protocolInfo.showPort ? formatHostPort(host.hostname, protocolInfo.port) : host.hostname}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 ml-3">
|
||||
{!needsAuth && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-3 text-[11px]"
|
||||
onClick={() => setShowLogs(!showLogs)}
|
||||
>
|
||||
{showLogs ? t('terminal.connection.hideLogs') : t('terminal.connection.showLogs')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'connecting' && !needsAuth && !isVerifyingHostKey && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-3 text-[11px]"
|
||||
onClick={progressProps.onCancelConnect}
|
||||
disabled={progressProps.isCancelling}
|
||||
>
|
||||
{progressProps.isCancelling ? t('terminal.progress.cancelling') : t('common.close')}
|
||||
</Button>
|
||||
)}
|
||||
{canDismissDisconnected && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
aria-label={t('terminal.connection.dismissDisconnectedDialog')}
|
||||
onClick={onDismissDisconnected}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('terminal.connection.dismissDisconnectedDialog')}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"h-7 w-7 rounded-md flex items-center justify-center flex-shrink-0",
|
||||
needsAuth || isVerifyingHostKey
|
||||
? "bg-primary text-primary-foreground"
|
||||
: hasError
|
||||
? "bg-destructive/20 text-destructive"
|
||||
: isConnecting
|
||||
? "bg-primary/15 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
<Plug size={13} />
|
||||
</div>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-border/60 overflow-hidden relative">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 left-0 rounded-full transition-all duration-300",
|
||||
error ? "bg-destructive" : "bg-primary"
|
||||
)}
|
||||
style={{ width: needsAuth ? '0%' : `${firstSegmentWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(
|
||||
"h-7 w-7 rounded-md flex items-center justify-center flex-shrink-0 transition-all duration-200",
|
||||
isHostKeyChanged
|
||||
? "bg-destructive/15 text-destructive ring-2 ring-destructive/25 animate-pulse"
|
||||
: isVerifyingHostKey
|
||||
? "bg-amber-500/15 text-amber-400 ring-2 ring-amber-400/25 animate-pulse"
|
||||
: progressValue > 50 && !hasError
|
||||
? "bg-primary/15 text-primary"
|
||||
: hasError
|
||||
? "bg-destructive/20 text-destructive"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
<Fingerprint size={13} />
|
||||
</div>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-border/60 overflow-hidden relative">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 left-0 rounded-full transition-all duration-300",
|
||||
error ? "bg-destructive" : "bg-primary"
|
||||
)}
|
||||
style={{ width: needsAuth || isVerifyingHostKey ? '0%' : `${secondSegmentWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(
|
||||
"h-7 w-7 rounded-md flex items-center justify-center flex-shrink-0",
|
||||
hasError ? "bg-destructive/20 text-destructive" : "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{isConnecting ? (
|
||||
<Loader2 size={13} className="animate-spin" />
|
||||
) : (
|
||||
<TerminalSquare size={13} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{needsAuth ? (
|
||||
<TerminalAuthDialog {...authProps} keys={keys} />
|
||||
) : hostKeyVerification ? (
|
||||
<TerminalHostKeyVerification
|
||||
hostKeyInfo={hostKeyVerification.hostKeyInfo}
|
||||
showLogs={showLogs}
|
||||
progressLogs={progressProps.progressLogs}
|
||||
onClose={hostKeyVerification.onClose}
|
||||
onContinue={hostKeyVerification.onContinue}
|
||||
onAddAndContinue={hostKeyVerification.onAddAndContinue}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isRestoredDisconnected && (
|
||||
<div className="rounded-md border border-border/35 bg-background/35 p-3 text-xs leading-5">
|
||||
<div className="font-semibold">{t('terminal.restore.placeholder.title')}</div>
|
||||
<div
|
||||
className="mt-1"
|
||||
style={{ color: 'color-mix(in srgb, var(--terminal-ui-fg, var(--foreground)) 68%, transparent)' }}
|
||||
>
|
||||
{t('terminal.restore.placeholder.desc')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TerminalConnectionProgress
|
||||
status={status}
|
||||
error={error}
|
||||
showLogs={showLogs}
|
||||
showEnterReconnectHint={showEnterReconnectHint}
|
||||
reconnectLabel={isRestoredDisconnected ? t('terminal.restore.placeholder.reconnect') : undefined}
|
||||
{...progressProps}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TerminalConnectionDialog;
|
||||
Reference in New Issue
Block a user