返回 DeepSeek-Reasonix
ManualSessionRecovery.tsx
根目录 / desktop / frontend / src / components / ManualSessionRecovery.tsx
1 import { useEffect, useRef, useState } from "react";
2 import { app } from "../lib/bridge";
3 import { asArray } from "../lib/array";
4 import { useT } from "../lib/i18n";
5 import type { ManualSessionCreationView } from "../generated/desktopContract.generated";
6 import { manualCreationPresentation } from "../lib/manualCreationPresentation";
7 import type { ManualCreationObservation } from "../lib/manualCreationRequests";
8
9 type Props = {
10 sessionId?: string;
11 attempt?: ManualCreationObservation | null;
12 onRetry?: () => Promise<void>;
13 onNew?: () => Promise<void>;
14 onChooseProject?: () => unknown;
15 };
16
17 /** One notice for the selected surface. Mount with a navigation-scoped key. */
18 export function ManualSessionRecovery({ sessionId, attempt, onRetry, onNew, onChooseProject }: Props) {
19 const t = useT();
20 const [restored, setRestored] = useState<ManualSessionCreationView>();
21 const [error, setError] = useState(false);
22 const [busy, setBusy] = useState(false);
23 const [dismissed, setDismissed] = useState(false);
24 const inFlight = useRef(false);
25 const observation = useRef(0);
26 useEffect(() => {
27 // Explicit creation already has an observer; never poll it a second time.
28 if (attempt || !sessionId || dismissed) return;
29 let disposed = false;
30 let timer: ReturnType<typeof setTimeout>;
31 let id: string | undefined;
32 const refresh = async () => {
33 const sequence = ++observation.current;
34 let again = true;
35 try {
36 const item = id ? await app.GetManualSessionCreation(id)
37 : asArray(await app.ListManualSessionCreations()).find(item => item.ref?.sessionId === sessionId);
38 if (disposed || sequence !== observation.current) return;
39 setRestored(item); setError(false);
40 if (!item || item.phase === "ready") { again = false; return; }
41 id = item.operationId;
42 } catch {
43 if (disposed || sequence !== observation.current) return;
44 // A catalog failure is not evidence this particular session failed.
45 if (id) setError(true);
46 } finally {
47 if (!disposed && again) timer = setTimeout(() => void refresh(), 1500);
48 }
49 };
50 void refresh();
51 return () => { disposed = true; ++observation.current; clearTimeout(timer); };
52 }, [attempt, sessionId, dismissed]);
53
54 const item = attempt?.operation ?? restored;
55 const failed = error || attempt?.failed;
56 if (dismissed || (!attempt && !item) || (!failed && item?.phase === "ready")) return null;
57 const state = failed ? { key: "creation.requestError" as const, action: "retry" as const }
58 : item ? manualCreationPresentation(item) : { key: "creation.opening" as const, action: undefined };
59 if (state.key === "creation.preparing" && attempt && !item?.surfaceReady) state.key = "creation.opening";
60 const run = async (action: () => unknown) => {
61 if (inFlight.current) return;
62 inFlight.current = true; setBusy(true);
63 try { await action(); setError(false); }
64 catch { setError(true); }
65 finally { inFlight.current = false; setBusy(false); }
66 };
67 const retry = async () => {
68 if (attempt) { await onRetry?.(); return; }
69 if (item) {
70 const updated = await app.RetryManualSessionCreation(item.operationId);
71 ++observation.current;
72 setRestored(updated);
73 }
74 };
75 const action = state.action === "retry" ? retry : state.action === "new" ? onNew : state.action === "project" ? onChooseProject : undefined;
76 return <div className="creation-notice" data-testid="creation-notice">
77 <div className="creation-notice__row">
78 <span role="status">{t(state.key)}</span>
79 {action && <button className="btn btn--small" disabled={busy || attempt?.pending}
80 onClick={() => void run(action)}>{t(state.action === "new" ? "topbar.newSession" : state.action === "project" ? "creation.chooseProject" : "creation.retry")}</button>}
81 {state.action && <button className="btn btn--small" disabled={busy}
82 onClick={() => setDismissed(true)}>{t("common.close")}</button>}
83 </div>
84 {state.action && <details>
85 <summary>{t("sessionRecovery.details")}</summary>
86 <button className="btn btn--small" disabled={busy}
87 onClick={() => void run(() => app.ExportManualCreationDiagnostics())}>{t("creation.export")}</button>
88 </details>}
89 </div>;
90 }
91
91 lines Plain Text