/** Picks an SSH configuration and a folder on that host, then opens it in this or a new window. */ import { useState } from "react"; import { executeAction } from "@/platform/actions"; import { useTx } from "@/platform/i18n"; import { call, commands, isCancelled, type RemoteEntry } from "@/platform/ipc"; import { useRemoteProgress } from "@/platform/remote"; import { sshLabel } from "@/platform/remoteSettings"; import { useWorkflowSettings } from "@/platform/workflowSettings"; import { Button, Dialog, Icon, Input, ProgressBar, Select, Spinner } from "@/ui"; import { openRemoteProject } from "@/workbench/projectWindow"; import { browsableFolders, closeConnectDialog, listedDirectory, remoteParent, stageLabel, useConnectDialog, } from "./model"; import styles from "./RemoteDev.module.css"; export function ConnectHost() { const open = useConnectDialog((s) => s.open); return open ? : null; } const SSH_SETTINGS_PAGE = "tools.sshConfigurations"; function ConnectDialog() { const tx = useTx(); const configs = useWorkflowSettings((s) => s.ssh.configs); const [ssh, setSsh] = useState(configs[0]?.id ?? ""); const [path, setPath] = useState("~"); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [browse, setBrowse] = useState<{ dir: string; folders: RemoteEntry[] } | null>(null); const list = async (dir: string) => { setError(null); setBusy(true); try { const entries = await call(commands.remoteBootstrapList(ssh, dir)); const shown = listedDirectory(dir, entries); setBrowse({ dir: shown, folders: browsableFolders(entries) }); setPath(shown); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const connect = async () => { setError(null); setBusy(true); try { const target = path.trim() || "~"; if (await openRemoteProject(ssh, target)) closeConnectDialog(); else setBusy(false); } catch (e) { if (!isCancelled(e)) setError(e instanceof Error ? e.message : String(e)); setBusy(false); } }; const options = configs.map((c) => ({ value: c.id, label: c.name.trim() || sshLabel(c), detail: sshLabel(c) })); const ready = !!ssh && !busy && !!path.trim(); const connecting = useRemoteProgress((s) => s.progress !== null); return ( void connect(), }, { label: "Cancel", role: "cancel" }, ]} > { e.preventDefault(); if (ready) void connect(); }} > {tx("The project runs on the remote host; the IDE backend is installed there on first use.")} {tx("SSH configuration:")} { setSsh(id); setBrowse(null); }} aria-label={tx("SSH configuration")} /> void executeAction("file.settings", SSH_SETTINGS_PAGE)}>{tx("Configure…")} {tx("Project directory:")} setPath(e.target.value)} /> void list(path.trim() || "~")}> {tx("Browse")} {browse && ( {browse.dir !== "/" && ( void list(remoteParent(browse.dir))}> .. )} {browse.folders.map((f) => ( void list(f.path)}> {f.name} ))} )} {error && ( {error} )} ); } /** The stage of the connection in progress (and how far a download / upload is). */ function ConnectProgress() { const tx = useTx(); const progress = useRemoteProgress((s) => s.progress); if (!progress) return null; const percent = progress.fraction == null ? null : Math.round(progress.fraction * 100); return ( {tx(stageLabel(progress.stage))} {percent != null && {percent}%} ); }
{tx("The project runs on the remote host; the IDE backend is installed there on first use.")}