201 lines · 6.8 KB · TypeScript
RawDownload
/** * 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 `<hostname>.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<RemoteOpenTarget> | 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<EditorId> = ["vscode"]; let cachedProbedEditors: ReadonlyArray<EditorId> | null = null; export function useRemoteCapableEditors(): ReadonlyArray<EditorId> {  const [editors, setEditors] = useState<ReadonlyArray<EditorId>>(    () => 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<boolean> {  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;}