返回 DeepSeek-Reasonix
transientComposerGuidance.ts
根目录 / desktop / frontend / src / lib / transientComposerGuidance.ts
1 import type { AppBindings } from "./bridge";
2 import type { StructuredInvocationSubmit } from "./invocationDisplay";
3 import { pendingFollowups, type PendingFollowup } from "./pendingFollowup";
4 import { formatInboxError } from "./inboxError";
5 import type { Translator } from "./i18n";
6 import { TRANSIENT_GUIDANCE_RETRY_DELAYS_MS, isTransientInboxTargetError } from "./transientInboxTarget";
7
8 type RetryContext = {
9 app: AppBindings;
10 transientRetriesRef: { current: Map<string, { cancelled: boolean }> };
11 pendingKeyRef: { current: string };
12 submitPendingKey: string;
13 submitDraftKey: string;
14 submitTabId?: string;
15 inboxSessionPath?: string;
16 submittedDraft: string;
17 queueOnly: boolean;
18 followupDraftFingerprint: (key: string) => string;
19 clearSubmittedDraft: (key: string) => void;
20 setGuidanceRetryNonce: (update: (value: number) => number) => void;
21 showToast: (message: string, kind: "info" | "warn") => void;
22 t: Translator;
23 locale: Parameters<typeof formatInboxError>[1];
24 };
25
26 export function createTransientGuidance(context: RetryContext) {
27 const { app, transientRetriesRef, pendingKeyRef, submitPendingKey, submitDraftKey, submitTabId,
28 inboxSessionPath, submittedDraft, queueOnly, followupDraftFingerprint, clearSubmittedDraft,
29 setGuidanceRetryNonce, showToast, t, locale } = context;
30 // Retry temporary route fences while preserving the exact pending request.
31 // Exhausted requests remain visible for manual reconciliation.
32 const retryTransientGuidanceEnqueue = (
33 request: PendingFollowup,
34 options: {
35 pendingKey: string;
36 submitDraftKey: string;
37 submitTabId: string;
38 sessionPath: string;
39 submittedDraft: string;
40 queueOnly: boolean;
41 turnId?: string;
42 },
43 ) => {
44 if (transientRetriesRef.current.has(request.key)) return;
45 const token = { cancelled: false };
46 transientRetriesRef.current.set(request.key, token);
47 void (async () => {
48 const { enqueueComposerGuidance } = await import("./inboxGuidanceSubmit");
49 for (const delayMs of TRANSIENT_GUIDANCE_RETRY_DELAYS_MS) {
50 await new Promise((resolve) => window.setTimeout(resolve, delayMs));
51 if (token.cancelled || transientRetriesRef.current.get(request.key) !== token) return;
52 // The user moved to another session: leave the pending request for
53 // that session's own reconciliation instead of retrying a gone route.
54 if (options.pendingKey !== pendingKeyRef.current) {
55 transientRetriesRef.current.delete(request.key);
56 return;
57 }
58 try {
59 const target = app.CaptureInboxTarget
60 ? await app.CaptureInboxTarget(options.submitTabId, options.sessionPath)
61 : undefined;
62 const receipt = await enqueueComposerGuidance(app, { ...request, target }, options.queueOnly, options.turnId);
63 if (receipt?.error) throw new Error(receipt.error);
64 if (!receipt?.itemId) throw new Error("Follow-up receipt unconfirmed");
65 pendingFollowups.clear(options.pendingKey, request);
66 if (followupDraftFingerprint(options.submitDraftKey) === options.submittedDraft) clearSubmittedDraft(options.submitDraftKey);
67 setGuidanceRetryNonce((value) => value + 1);
68 showToast(t("runtime.queued"), "info");
69 transientRetriesRef.current.delete(request.key);
70 return;
71 } catch (error) {
72 if (isTransientInboxTargetError(error)) continue;
73 // A permanent refusal is actionable: surface it and drop the hold.
74 pendingFollowups.clear(options.pendingKey, request);
75 showToast(formatInboxError(error, locale), "warn");
76 transientRetriesRef.current.delete(request.key);
77 return;
78 }
79 }
80 // The window outlasted the bound: keep the pending request so its
81 // banner stays visible and a later send reconciles the receipt.
82 if (transientRetriesRef.current.get(request.key) === token) transientRetriesRef.current.delete(request.key);
83 })();
84 };
85 const holdTransientGuidance = (
86 error: unknown,
87 input: { display: string; submit: string; structured?: StructuredInvocationSubmit; turnId?: string },
88 ): boolean => {
89 if (!isTransientInboxTargetError(error)) return false;
90 if (!submitPendingKey) return false;
91 const existing = pendingFollowups.get(submitPendingKey);
92 const request: PendingFollowup = existing ?? {
93 key: `followup-${crypto.randomUUID()}`,
94 tabId: submitTabId || "",
95 display: input.display,
96 submit: input.submit,
97 structured: input.structured,
98 draft: submittedDraft,
99 };
100 pendingFollowups.set(submitPendingKey, request);
101 retryTransientGuidanceEnqueue(request, {
102 pendingKey: submitPendingKey,
103 submitDraftKey,
104 submitTabId: submitTabId || "",
105 sessionPath: inboxSessionPath || "",
106 submittedDraft,
107 queueOnly,
108 turnId: input.turnId,
109 });
110 return true;
111 };
112
113 return holdTransientGuidance;
114 }
115
116 export async function captureStableInboxTarget(app: AppBindings, tabId: string, sessionPath: string) {
117 if (!app.CaptureInboxTarget) return undefined;
118 for (let attempt = 0; ; attempt++) {
119 try { return await app.CaptureInboxTarget(tabId, sessionPath); }
120 catch (error) {
121 if (!isTransientInboxTargetError(error) || attempt >= TRANSIENT_GUIDANCE_RETRY_DELAYS_MS.length) throw error;
122 await new Promise(resolve => window.setTimeout(resolve, TRANSIENT_GUIDANCE_RETRY_DELAYS_MS[attempt]));
123 }
124 }
125 }
126
126 lines TYPESCRIPT