返回 DeepSeek-Reasonix
useInboxQueueEditor.ts
根目录 / desktop / frontend / src / lib / useInboxQueueEditor.ts
1 import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
2 import { app } from "./bridge";
3 import { inboxQueueDrafts, type QueueEditDraft } from "./inboxQueueDrafts";
4 import type { InboxQueueRequest, InboxQueueResult } from "./inboxQueueCommands";
5 import type { InboxSnapshotLike } from "./composerInboxQueue";
6 import type { InboxTarget } from "./pendingFollowup";
7
8 export function useInboxQueueEditor(scope: string, tabId: string, sessionPath: string, apply: (snapshot: InboxSnapshotLike) => unknown, refresh: () => void) {
9 const drafts = useSyncExternalStore(inboxQueueDrafts.subscribe, () => inboxQueueDrafts.get(scope));
10 const [open, setOpen] = useState(Boolean(drafts.edit));
11 const [busy, setBusy] = useState(false);
12 const [notice, setNotice] = useState("");
13 const owner = useRef<object | null>(null);
14 const gate = useRef<{ owner: object } | null>(null);
15 useLayoutEffect(() => {
16 owner.current = {};
17 gate.current = null;
18 setBusy(false);
19 return () => { owner.current = null; gate.current = null; };
20 }, [scope, tabId, sessionPath]);
21 // Component lifetime, not just session path: leaving and returning to the
22 // same session creates a new editor that an old reply must never clear.
23 const operation = async (run: (check: () => void) => Promise<void>) => {
24 if (gate.current) return;
25 if (!owner.current) return;
26 const ticket = { owner: owner.current };
27 gate.current = ticket; setBusy(true);
28 const current = () => owner.current === ticket.owner && gate.current === ticket;
29 const check = () => { if (!current()) throw new Error("queue editor operation expired"); };
30 try { await run(check); } catch { if (current()) setNotice("error"); }
31 finally { if (current()) { gate.current = null; setBusy(false); } }
32 };
33 const capture = async (check: () => void) => {
34 check();
35 if (!app.CaptureInboxTarget || !app.InboxQueueForTarget) throw new Error("unsupported");
36 const target = await app.CaptureInboxTarget(tabId, sessionPath);
37 check();
38 return target;
39 };
40 const call = async (target: InboxTarget, request: InboxQueueRequest, check: () => void): Promise<InboxQueueResult> => {
41 check();
42 if (!app.InboxQueueForTarget) throw new Error("unsupported");
43 const result = await app.InboxQueueForTarget(target, request);
44 check();
45 if (result.snapshot?.sessionPath === sessionPath) apply(result.snapshot);
46 return result;
47 };
48 const edit = (id: string) => operation(async check => {
49 const retained = inboxQueueDrafts.get(scope).edit;
50 if (retained?.id === id) { setOpen(true); return; }
51 const target = await capture(check);
52 const result = await call(target, { kind: "read", itemId: id }, check);
53 if (!result.edit) { setNotice(result.reason || "error"); return; }
54 const state = inboxQueueDrafts.get(scope);
55 if (state.edit !== retained) return;
56 inboxQueueDrafts.set(scope, { edit: { ...result.edit, target, value: result.edit.text }, recovered: state.edit && (state.edit.value !== state.edit.text || state.edit.reason) ? [...state.recovered, state.edit] : state.recovered });
57 setNotice(""); setOpen(true);
58 });
59 const update = (value: string) => {
60 const state = inboxQueueDrafts.get(scope);
61 if (state.edit) inboxQueueDrafts.set(scope, { ...state, edit: { ...state.edit, value } });
62 };
63 const save = () => operation(async check => {
64 const state = inboxQueueDrafts.get(scope), draft = state.edit;
65 if (!draft || !draft.value.trim()) return;
66 let result: InboxQueueResult;
67 try {
68 result = await call(draft.target, { kind: "edit", itemId: draft.id, text: draft.value, contentVersion: draft.contentVersion }, check);
69 } catch {
70 check();
71 // Reconcile without resubmitting. A missing/executing item is not proof
72 // that the attempted save succeeded.
73 try { await call(draft.target, { kind: "snapshot" }, check); } catch { /* Keep draft. */ }
74 check();
75 if (inboxQueueDrafts.get(scope).edit !== draft) return;
76 inboxQueueDrafts.set(scope, { ...inboxQueueDrafts.get(scope), edit: { ...draft, reason: "unconfirmed" } });
77 setNotice("unconfirmed"); return;
78 }
79 if (inboxQueueDrafts.get(scope).edit !== draft) return;
80 if (result.outcome === "applied" || result.outcome === "unchanged") {
81 inboxQueueDrafts.set(scope, { ...inboxQueueDrafts.get(scope), edit: undefined });
82 setOpen(false); setNotice("saved");
83 } else {
84 inboxQueueDrafts.set(scope, { ...inboxQueueDrafts.get(scope), edit: { ...draft, reason: result.reason || "error" } });
85 setNotice(result.reason || "error");
86 }
87 refresh();
88 });
89 const mutate = (request: InboxQueueRequest) => operation(async check => {
90 const result = await call(await capture(check), request, check);
91 setNotice(result.reason || ""); refresh();
92 });
93 const recover = (): string | undefined => {
94 if (gate.current) return;
95 const state = inboxQueueDrafts.get(scope);
96 if (!state.edit) return;
97 inboxQueueDrafts.set(scope, { edit: undefined, recovered: [...state.recovered, state.edit] });
98 setOpen(false); setNotice("retained");
99 return state.edit.value;
100 };
101 const reopen = (draft: QueueEditDraft) => {
102 if (gate.current) return;
103 const state = inboxQueueDrafts.get(scope);
104 inboxQueueDrafts.set(scope, { edit: draft, recovered: [...state.recovered.filter(d => d !== draft), ...(state.edit ? [state.edit] : [])] });
105 setOpen(true);
106 };
107 const reload = () => operation(async check => {
108 const state = inboxQueueDrafts.get(scope);
109 if (!state.edit) return;
110 // Reload is an explicit rebase onto the current session selection. Saving
111 // still uses the originally captured target and content version.
112 const target = await capture(check);
113 const result = await call(target, { kind: "read", itemId: state.edit.id }, check);
114 if (!result.edit) { setNotice(result.reason || "error"); return; }
115 const latest = inboxQueueDrafts.get(scope);
116 if (latest.edit !== state.edit) return;
117 inboxQueueDrafts.set(scope, { recovered: [...latest.recovered, state.edit], edit: { ...result.edit, target, value: result.edit.text } });
118 setNotice("");
119 });
120 const close = () => {
121 if (gate.current) return;
122 const state = inboxQueueDrafts.get(scope);
123 if (state.edit && state.edit.value === state.edit.text && !state.edit.reason) {
124 inboxQueueDrafts.set(scope, { ...state, edit: undefined });
125 }
126 setOpen(false); setNotice("");
127 };
128 return { drafts, open, busy, notice, edit, update, save, mutate, recover, reopen, reload, close, show: () => { if (!gate.current) setOpen(true); } };
129 }
130
130 lines TYPESCRIPT