返回 DeepSeek-Reasonix
useRemoteWorkspaceCommands.ts
根目录 / desktop / frontend / src / app-runtime / useRemoteWorkspaceCommands.ts
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
84 lines TYPESCRIPT