返回 DeepSeek-Reasonix
remoteForkTurn.ts
根目录 / desktop / frontend / src / lib / remoteForkTurn.ts
1 // The remote turn fork: the serve owns the child, while Desktop owns the
2 // durable operation id used to recover an unknown result. The renderer carries
3 // only the anchored target and acknowledges the operation after navigation.
4
5 import { useCallback, useEffect, useRef, type RefObject } from "react";
6 import { forkCreateFailureText, type ForkTargetSetView, type ForkTargetView } from "./forkTargets";
7 import { t } from "./i18n";
8 import { reducer, type State } from "./useController";
9 import type { ForkAnchorView, ForkCreationView } from "../generated/desktopContract.generated";
10
11 /** The bridge commands a remote turn fork needs, so a caller can supply its own. */
12 export interface RemoteForkBindings {
13 ForkTargetsRemoteTab(tabID: string): Promise<ForkTargetSetView>;
14 CreateForkRemoteTab(tabID: string, anchor: ForkAnchorView): Promise<ForkCreationView>;
15 AcknowledgeForkOperation(tabID: string, operationID: string): Promise<void>;
16 }
17
18 /**
19 * Reads a remote tab's fork boundaries, or undefined when this shell cannot ask.
20 * The serve derives them from its own committed turn records, so the read is
21 * valid while a turn runs, and an empty set means "no completed turn here" —
22 * never "this serve cannot fork", which its advertised capability decides.
23 */
24 export async function readRemoteForkTargets(bindings: RemoteForkBindings, tabId: string): Promise<ForkTargetSetView | undefined> {
25 // A shell that predates the command reads no targets instead of failing the
26 // transcript around it.
27 if (typeof bindings.ForkTargetsRemoteTab !== "function") return undefined;
28 const set = await bindings.ForkTargetsRemoteTab(tabId).catch(() => undefined);
29 if (!set) return undefined;
30 return { ...set, targets: Array.isArray(set.targets) ? set.targets : [], verifiable: Boolean(set.verifiable) };
31 }
32
33 /** What one remote fork request produced: the child's id, or the refusal to show the user. */
34 export type RemoteForkOutcome = { kind: "created"; sessionId: string; operationId: string } | { kind: "refused"; failure: string };
35
36 /** Creates or recovers one remote child through Desktop's durable operation. */
37 export async function requestRemoteForkTurn(
38 bindings: RemoteForkBindings,
39 tabId: string,
40 target: ForkTargetView,
41 ): Promise<RemoteForkOutcome> {
42 try {
43 const created = await bindings.CreateForkRemoteTab(tabId, target);
44 if (created?.sessionId && created.operationId) return { kind: "created", sessionId: created.sessionId, operationId: created.operationId };
45 // Structured reasons are localized here; ordinary diagnostics retain the
46 // host's message.
47 const detail = created?.error?.trim() ?? "";
48 return { kind: "refused", failure: detail ? forkCreateFailureText(detail, created?.reason) : t("chat.branchFailed") };
49 } catch (error) {
50 return { kind: "refused", failure: forkCreateFailureText(error) };
51 }
52 }
53
54 /** The anchored fork operations one remote session surface exposes. */
55 export interface RemoteForkTurnApi {
56 /** Creates or recovers the child for one anchored operation. */
57 forkTurn: (target: ForkTargetView) => Promise<{ sessionId: string; operationId: string } | undefined>;
58 acknowledgeFork: (operationId: string) => Promise<void>;
59 /**
60 * The read the connection effect runs once hydration lands and once a turn
61 * finishes. That effect owns its own install and uninstall per connection
62 * generation, so it reaches the read through this ref rather than closing
63 * over a callback identity it cannot depend on.
64 */
65 forkTargetsRefreshRef: RefObject<(() => Promise<void>) | null>;
66 }
67
68 /**
69 * Owns one remote session's fork reads and creates. Each read is fenced by the
70 * current session identity and a local sequence so a response from a replaced
71 * connection cannot enter the transcript.
72 */
73 export function useRemoteForkTurn(
74 bindings: RemoteForkBindings,
75 tabId: string | undefined,
76 sessionPath: string | undefined,
77 setTranscript: (update: (state: State) => State) => void,
78 setPromptError: (error: string) => void,
79 ): RemoteForkTurnApi {
80 const session = sessionPath ?? "";
81 const readSeqRef = useRef(0);
82 const readIdentityRef = useRef("");
83 readIdentityRef.current = `${tabId ?? ""}\0${session}`;
84 const forkTargetsRefreshRef = useRef<(() => Promise<void>) | null>(null);
85 const refreshForkTargets = useCallback(async (): Promise<void> => {
86 if (!tabId) return;
87 const readSeq = ++readSeqRef.current;
88 const readIdentity = readIdentityRef.current;
89 const targets = await readRemoteForkTargets(bindings, tabId);
90 if (!targets || readSeqRef.current !== readSeq || readIdentityRef.current !== readIdentity) return;
91 setTranscript((current) => reducer(current, { type: "fork_targets", targets }));
92 }, [bindings, session, setTranscript, tabId]);
93 useEffect(() => { forkTargetsRefreshRef.current = refreshForkTargets; }, [refreshForkTargets]);
94
95 const forkTurn = useCallback(async (target: ForkTargetView): Promise<{ sessionId: string; operationId: string } | undefined> => {
96 if (!tabId || !target?.turnId) return undefined;
97 setPromptError("");
98 const outcome = await requestRemoteForkTurn(bindings, tabId, target);
99 if (outcome.kind === "created") return { sessionId: outcome.sessionId, operationId: outcome.operationId };
100 setPromptError(outcome.failure);
101 return undefined;
102 }, [bindings, setPromptError, tabId]);
103
104 const acknowledgeFork = useCallback(async (operationId: string) => {
105 if (tabId && operationId) await bindings.AcknowledgeForkOperation(tabId, operationId);
106 }, [bindings, tabId]);
107
108 return { forkTurn, acknowledgeFork, forkTargetsRefreshRef };
109 }
110
110 lines TYPESCRIPT