| 1 | import { useRef } from "react"; |
| 2 | import { app } from "../lib/bridge"; |
| 3 | import { useCommittedCommand } from "../lib/useCommittedCommand"; |
| 4 | import { RemoteConnectionTimeoutError, useRemoteStore, waitForRemoteConnection } from "../store/remote"; |
| 5 | import { RemoteWorkspaceLaunchGate, resolveRemoteWorkspace } from "../lib/remoteWorkspace"; |
| 6 | import { publishNavigationIntent } from "../lib/useNavigationIntentFence"; |
| 7 | import type { RemoteHostView } from "../lib/types"; |
| 8 | import type { Translator } from "../lib/i18n"; |
| 9 | |
| 10 | export type RemoteWorkspaceCommandsInput = { |
| 11 | t: Translator; |
| 12 | showToast(message: string, level: "error", options?: { durationMs?: number; actionLabel?: string; onAction?: () => void }): void; |
| 13 | }; |
| 14 | |
| 15 | /** |
| 16 | * Owns remote workspace launches and host connections. Each host gets one |
| 17 | * launch generation; a status popover entry may open the workspace, and a |
| 18 | * connect request first drives the host to connected (clearing stale failure |
| 19 | * state, then waiting on the connection waiter) before launching. A timeout |
| 20 | * offers stop-and-retry; other failures stay host-scoped on the status entry. |
| 21 | */ |
| 22 | export function useRemoteWorkspaceCommands(input: RemoteWorkspaceCommandsInput) { |
| 23 | const { t, showToast } = input; |
| 24 | const remoteWorkspaceLaunchGate = useRef(new RemoteWorkspaceLaunchGate()); |
| 25 | |
| 26 | const launchRemoteWorkspace = useCommittedCommand(async (host: RemoteHostView, requestSeq: number) => { |
| 27 | const lastWorkspace = await app.RemoteLastWorkspace(host.id).catch(() => ""); |
| 28 | const workspace = resolveRemoteWorkspace(lastWorkspace, host.defaultWorkspace); |
| 29 | if (!remoteWorkspaceLaunchGate.current.isCurrent(host.id, requestSeq)) return; |
| 30 | await publishNavigationIntent("remote-workspace"); |
| 31 | await app.OpenRemoteWorkspace(host.id, workspace); |
| 32 | }); |
| 33 | |
| 34 | const openRemoteWorkspaceFromStatus = useCommittedCommand((host: RemoteHostView) => { |
| 35 | const requestSeq = remoteWorkspaceLaunchGate.current.begin(host.id); |
| 36 | void launchRemoteWorkspace(host, requestSeq).catch((err) => { |
| 37 | showToast(err instanceof Error ? err.message : String(err), "error", { durationMs: 6000 }); |
| 38 | }); |
| 39 | }); |
| 40 | |
| 41 | const connectAndOpenRemoteWorkspace = useCommittedCommand(function connectRemoteWorkspace(host: RemoteHostView) { |
| 42 | const requestSeq = remoteWorkspaceLaunchGate.current.begin(host.id); |
| 43 | void (async () => { |
| 44 | try { |
| 45 | const status = useRemoteStore.getState().statuses[host.id]?.state; |
| 46 | if (status !== "connected" && status !== "degraded") { |
| 47 | // Clear any stale failure before the new generation starts; otherwise a |
| 48 | // previous stopped+error snapshot could make the waiter reject before |
| 49 | // the kernel's fresh connecting event reaches the frontend. |
| 50 | useRemoteStore.getState().applyStatus({ hostId: host.id, state: "connecting" }); |
| 51 | await app.ConnectRemoteHost(host.id); |
| 52 | await waitForRemoteConnection(host.id); |
| 53 | } |
| 54 | } catch (err) { |
| 55 | if (err instanceof RemoteConnectionTimeoutError) { |
| 56 | showToast(t("remote.error.timeout", { host: host.label }), "error", { |
| 57 | actionLabel: t("remote.error.stopAndRetry"), |
| 58 | durationMs: 10_000, |
| 59 | onAction: () => { |
| 60 | void app.DisconnectRemoteHost(host.id) |
| 61 | .catch(() => undefined) |
| 62 | .then(() => connectRemoteWorkspace(host)); |
| 63 | }, |
| 64 | }); |
| 65 | return; |
| 66 | } |
| 67 | // Connection failures are host-scoped. Keep the persistent error and its |
| 68 | // recovery actions beside the Remote SSH status entry instead of |
| 69 | // stretching a raw backend error across the native titlebar. |
| 70 | useRemoteStore.getState().requestStatusPopover(host.id); |
| 71 | return; |
| 72 | } |
| 73 | |
| 74 | try { |
| 75 | await launchRemoteWorkspace(host, requestSeq); |
| 76 | } catch (err) { |
| 77 | showToast(err instanceof Error ? err.message : String(err), "error", { durationMs: 6000 }); |
| 78 | } |
| 79 | })(); |
| 80 | }); |
| 81 | |
| 82 | return { openRemoteWorkspaceFromStatus, connectAndOpenRemoteWorkspace }; |
| 83 | } |
| 84 |