/** * Remote open-in-editor: when this client is not on the environment's * machine, "Open" must hand the OS a `vscode://vscode-remote/ssh-remote+…` * deep link (local editor connects over SSH) instead of exec'ing an editor * on the environment host. * * Host precedence: a desktop-SSH environment's real `~/.ssh/config` alias * beats server-advertised names; among advertised names the tailnet MagicDNS * name beats mDNS `.local` (server sends them in that order). */ import type { ConnectionTarget } from "@t3tools/client-runtime/connection"; import { REMOTE_CAPABLE_EDITOR_IDS, type EditorId, type EnvironmentId, type RemoteOpenTarget, } from "@t3tools/contracts"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { useEffect, useMemo, useState } from "react"; import { isDesktopLocalConnectionTarget } from "~/connection/desktopLocal"; import { isLoopbackHostname } from "~/environments/primary/target"; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { useEnvironmentPresentation } from "~/state/presentation"; export interface RemoteOpenHost { readonly kind: "ssh-alias" | RemoteOpenTarget["kind"]; readonly host: string; } export type RemoteOpenState = | { readonly mode: "local-exec" } | { readonly mode: "remote-links"; readonly host: RemoteOpenHost } | { readonly mode: "remote-unavailable" }; export type RemoteOpenMode = RemoteOpenState["mode"]; export interface RemoteOpenResolution { readonly state: RemoteOpenState; readonly isResolved: boolean; } const LOCAL_EXEC: RemoteOpenState = { mode: "local-exec" }; const REMOTE_UNAVAILABLE: RemoteOpenState = { mode: "remote-unavailable" }; const UNRESOLVED_REMOTE_OPEN: RemoteOpenResolution = { state: LOCAL_EXEC, isResolved: false, }; function parseHostname(url: string): string | null { try { return new URL(url).hostname; } catch { return null; } } export function resolveRemoteOpenState(input: { readonly target: ConnectionTarget | null; /** Real ssh alias for desktop-SSH environments; null elsewhere. */ readonly sshAlias: string | null; /** Server-advertised hosts; undefined on servers that predate the feature. */ readonly remoteOpenTargets: ReadonlyArray | undefined; /** True when running inside the desktop app's renderer. */ readonly isDesktopRenderer: boolean; }): RemoteOpenState { const { target } = input; // No catalog entry: keep today's exec behavior rather than guessing. if (target === null) { return LOCAL_EXEC; } if (target._tag === "PrimaryConnectionTarget") { // The desktop app manages its own primary backend, so it is always on // this machine even when its URL is not loopback (wsl-only mode binds // the WSL2 NAT address). In a browser, a loopback primary means the // browser runs on the serving machine; a tailnet/LAN URL means remote. if (input.isDesktopRenderer) { return LOCAL_EXEC; } const hostname = parseHostname(target.httpBaseUrl); if (hostname !== null && isLoopbackHostname(hostname)) { return LOCAL_EXEC; } } else if (isDesktopLocalConnectionTarget(target)) { return LOCAL_EXEC; } if (input.sshAlias !== null && input.sshAlias.length > 0) { return { mode: "remote-links", host: { kind: "ssh-alias", host: input.sshAlias } }; } const advertised = input.remoteOpenTargets?.[0]; if (advertised !== undefined) { return { mode: "remote-links", host: advertised }; } return REMOTE_UNAVAILABLE; } export function useRemoteOpenResolution(environmentId: EnvironmentId | null): RemoteOpenResolution { const { presentation } = useEnvironmentPresentation(environmentId); return useMemo(() => { if (presentation === null) { return UNRESOLVED_REMOTE_OPEN; } const profile = Option.getOrNull(presentation.entry.profile); const sshAlias = profile !== null && profile._tag === "SshConnectionProfile" ? profile.target.alias : null; return { state: resolveRemoteOpenState({ target: presentation.entry.target, sshAlias, remoteOpenTargets: presentation.serverConfig?.remoteOpenTargets, isDesktopRenderer: window.desktopBridge !== undefined, }), isResolved: true, }; }, [presentation]); } export function useRemoteOpenState(environmentId: EnvironmentId | null): RemoteOpenState { return useRemoteOpenResolution(environmentId).state; } /** * Editors offered in remote-link mode. The desktop app probes the machine the * renderer runs on; a browser cannot, so it offers VS Code only. */ const REMOTE_FALLBACK_EDITORS: ReadonlyArray = ["vscode"]; let cachedProbedEditors: ReadonlyArray | null = null; export function useRemoteCapableEditors(): ReadonlyArray { const [editors, setEditors] = useState>( () => cachedProbedEditors ?? REMOTE_FALLBACK_EDITORS, ); useEffect(() => { if (cachedProbedEditors !== null) { return; } const probe = window.desktopBridge?.probeRemoteEditors; if (probe === undefined) { cachedProbedEditors = REMOTE_FALLBACK_EDITORS; return; } let cancelled = false; probe().then( (ids) => { const remoteCapable = ids.filter((id) => REMOTE_CAPABLE_EDITOR_IDS.includes(id)); cachedProbedEditors = remoteCapable.length > 0 ? remoteCapable : REMOTE_FALLBACK_EDITORS; if (!cancelled) { setEditors(cachedProbedEditors); } }, () => { cachedProbedEditors = REMOTE_FALLBACK_EDITORS; }, ); return () => { cancelled = true; }; }, []); return editors; } /** * Fire a remote editor deep link. In desktop, route through the Electron * shell so the OS handler opens without navigating the renderer; in a * browser, assign the location — unlike window.open this does not leave a * blank tab behind. * * Resolves false when the desktop shell refused the URL (e.g. an older * build whose protocol allowlist predates editor schemes) so callers do not * record a successful open that never happened. */ export async function openRemoteEditorUrl(url: string): Promise { const bridge = window.desktopBridge; if (bridge !== undefined) { try { return await bridge.openExternal(url); } catch { return false; } } window.location.assign(url); return true; } /** * One-time "you need SSH keys on that machine" hint, shown in the picker menu * until the first remote open fires (we cannot observe SSH success from here, * so first click is the dismiss signal). */ const REMOTE_OPEN_HINT_KEY = "t3code:remote-open-hint-seen"; export function useRemoteOpenHint(): readonly [seen: boolean, markSeen: () => void] { const [seen, setSeen] = useLocalStorage(REMOTE_OPEN_HINT_KEY, false, Schema.Boolean); return [seen, () => setSeen(true)] as const; }