import { ArrowLeft, ChevronDown, Eye, EyeOff, Key, Lock, Plug, Plus, User, } from "lucide-react"; import React, { useMemo, useState } from "react"; import { useI18n } from "../application/i18n/I18nProvider"; import type { QuickConnectTarget } from "../domain/quickConnect"; import { formatHostPort } from "../domain/host"; import { buildQuickConnectHost, getQuickConnectDefaultPort, isQuickConnectIdentityUsable, type QuickConnectAuthMethod, type QuickConnectProtocol, } from "../domain/quickConnectHost"; import { cn } from "../lib/utils"; import { Host, Identity, SSHKey } from "../types"; import { Button } from "./ui/button"; import { Combobox } from "./ui/combobox"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { Dropdown, DropdownContent, DropdownTrigger } from "./ui/dropdown"; import { ScrollArea } from "./ui/scroll-area"; import { PROTOCOL_VISUAL_STYLES } from "./protocolVisuals"; // Wizard steps type WizardStep = "protocol" | "username" | "knownhost" | "auth"; interface QuickConnectWizardProps { open: boolean; target: QuickConnectTarget; keys: SSHKey[]; identities: Identity[]; warnings?: string[]; onConnect: (host: Host) => void; onSaveHost?: (host: Host) => void; onAddKey?: () => void; onClose: () => void; } const QuickConnectWizard: React.FC = ({ open, target, keys, identities, warnings, onConnect, onSaveHost, onAddKey, onClose, }) => { const { t } = useI18n(); // Wizard state const [step, setStep] = useState("protocol"); const [protocol, setProtocol] = useState("ssh"); const [username, setUsername] = useState(target.username || ""); const [port, setPort] = useState(target.port || 22); // Known host verification state const [knownHostInfo, setKnownHostInfo] = useState<{ keyType: string; fingerprint: string; } | null>(null); // Auth state const [authMethod, setAuthMethod] = useState("password"); const [password, setPassword] = useState(""); const [selectedKeyId, setSelectedKeyId] = useState(null); const [selectedIdentityId, setSelectedIdentityId] = useState(null); const [saveOptionsOpen, setSaveOptionsOpen] = useState(false); const [showPassword, setShowPassword] = useState(false); const selectedIdentity = useMemo( () => identities.find((identity) => identity.id === selectedIdentityId), [identities, selectedIdentityId], ); // Reset state when target changes React.useEffect(() => { if (open) { setStep("protocol"); setProtocol("ssh"); setUsername(target.username || ""); setPort(target.port || 22); setPassword(""); setSelectedKeyId(null); setSelectedIdentityId(null); setSaveOptionsOpen(false); setShowPassword(false); setKnownHostInfo(null); } }, [open, target]); const canUseSelectedIdentity = useMemo( () => isQuickConnectIdentityUsable(selectedIdentity, keys, protocol), [keys, protocol, selectedIdentity], ); const identityOptions = useMemo( () => identities.map((identity) => ({ value: identity.id, label: identity.label, sublabel: identity.username, icon: , })), [identities], ); const detachSelectedIdentity = () => { setSelectedIdentityId(null); setAuthMethod("password"); setPassword(""); setSelectedKeyId(null); }; const clearSelectedIdentity = () => { detachSelectedIdentity(); setUsername(target.username || ""); }; const handleIdentitySelect = (identityId: string) => { if (!identityId) { clearSelectedIdentity(); return; } const identity = identities.find((candidate) => candidate.id === identityId); if (!identity) return; setSelectedIdentityId(identity.id); setUsername(identity.username); setAuthMethod(identity.authMethod); setPassword(""); setSelectedKeyId(identity.keyId || null); }; // Handle protocol selection const handleProtocolSelect = (proto: QuickConnectProtocol) => { setProtocol(proto); if (proto === "telnet") { if (selectedIdentityId) clearSelectedIdentity(); setAuthMethod("password"); } if (port === getQuickConnectDefaultPort(protocol)) { setPort(getQuickConnectDefaultPort(proto)); } }; const handleProtocolPortChange = ( proto: QuickConnectProtocol, rawValue: string, fallbackPort: number, ) => { handleProtocolSelect(proto); setPort(parseInt(rawValue) || fallbackPort); }; // Navigate to next step const handleContinue = () => { switch (step) { case "protocol": if (canUseSelectedIdentity) { handleConnect(); break; } // Always go to username step to let user confirm/edit username setStep("username"); break; case "username": setStep("auth"); break; case "knownhost": setStep("auth"); break; case "auth": handleConnect(); break; } }; // Navigate back const handleBack = () => { switch (step) { case "username": setStep("protocol"); break; case "knownhost": setStep("username"); break; case "auth": // Always go back to username step setStep("username"); break; } }; // Create host and connect const handleConnect = (save = false) => { const effectiveUsername = username || target.username || "root"; const effectivePort = port || getQuickConnectDefaultPort(protocol); const now = Date.now(); const tempHost = buildQuickConnectHost({ id: `quick-${now}-${Math.random().toString(36).slice(2, 11)}`, createdAt: now, target, protocol, port: effectivePort, username: effectiveUsername, authMethod, password, selectedKeyId, selectedIdentityId: selectedIdentity?.id, save, }); if (save && onSaveHost) { onSaveHost(tempHost); } onConnect(tempHost); onClose(); }; // Check if can proceed const canProceed = useMemo(() => { switch (step) { case "protocol": return true; case "username": return username.trim().length > 0; case "knownhost": return true; case "auth": if (selectedIdentity && protocol !== "telnet") return canUseSelectedIdentity; if (authMethod === "password") { // Whitespace-only passwords are valid SSH secrets (issue #2036). return password.length > 0; } return Boolean(selectedKeyId && keys.some((key) => key.id === selectedKeyId)); } }, [ step, username, authMethod, password, selectedKeyId, selectedIdentity, canUseSelectedIdentity, keys, protocol, ]); // Render protocol selection step const renderProtocolStep = () => (

{t("protocolSelect.chooseProtocol")}

{/* SSH */} {/* Mosh */} {/* Eternal Terminal */} {/* Telnet */}
{protocol !== "telnet" && identities.length > 0 && (
} />

{selectedIdentity ? t("quickConnect.identity.selectedHint", { username: selectedIdentity.username }) : t("quickConnect.identity.hint")}

)}
); // Render username step const renderUsernameStep = () => (
{ if (selectedIdentityId) detachSelectedIdentity(); setUsername(e.target.value); }} placeholder={t("terminal.auth.username.placeholder")} autoFocus onKeyDown={(e) => { if (e.key === "Enter" && username.trim()) { handleContinue(); } }} />
); // Render known host verification step const renderKnownHostStep = () => (

{t("quickConnect.knownHost.title")}

{t("quickConnect.knownHost.authenticity", { hostname: target.hostname })}

{knownHostInfo && ( <>

{t("quickConnect.knownHost.fingerprintLabel", { keyType: knownHostInfo.keyType })}

{knownHostInfo.fingerprint}

)}

{t("quickConnect.knownHost.addQuestion")}

); // Render auth step const renderAuthStep = () => (
{selectedIdentity ? (
{selectedIdentity.label}
{selectedIdentity.username} ยท {t(`quickConnect.identity.method.${selectedIdentity.authMethod}`)}
{!canUseSelectedIdentity && (

{t("quickConnect.identity.unavailable")}

)}
) : ( <> {/* Auth method tabs */}
{/* Password field */} {authMethod === "password" && (
setPassword(e.target.value)} placeholder={t("terminal.auth.password.placeholder")} className="pr-10" autoFocus onKeyDown={(e) => { if (e.key === "Enter" && password.length > 0) { handleConnect(); } }} />
)} {/* Key selection */} {authMethod === "key" && (
{keys.filter((k) => k.category === "key").length === 0 ? (
{t("terminal.auth.noKeysHint")}
) : (
{keys .filter((k) => k.category === "key") .map((key) => ( ))}
)} {onAddKey && ( )}
)} )}
); const hostEndpoint = formatHostPort(target.hostname, port || getQuickConnectDefaultPort(protocol)); const connectTitle = t("quickConnect.connectTitle", { host: hostEndpoint }); const protocolLabel = protocol === "et" ? "ET" : protocol.toUpperCase(); const effectiveUsername = username || target.username || ""; const connectSubtitle = step === "protocol" ? null : ( effectiveUsername ? `${protocolLabel} ${effectiveUsername}@${hostEndpoint}` : `${protocolLabel} ${hostEndpoint}` ); return (
e.stopPropagation()} > {/* Header */}

{connectTitle}

{connectSubtitle ? (

{connectSubtitle}

) : null}
{warnings && warnings.length > 0 && (
{t("quickConnect.warning.unparsedOptions", { options: warnings.join(", "), })}
)} {/* Content */}
{step === "protocol" && renderProtocolStep()} {step === "username" && renderUsernameStep()} {step === "knownhost" && renderKnownHostStep()} {step === "auth" && renderAuthStep()}
{/* Footer */}
{step === "knownhost" ? (
) : step === "auth" ? (
{onAddKey && authMethod === "key" && keys.filter((k) => k.category === "key").length === 0 && ( )}
) : ( )}
); }; export default QuickConnectWizard;