返回 DeepSeek-Reasonix
useExtensionSurface.ts
根目录 / desktop / frontend / src / app-runtime / useExtensionSurface.ts
1 import { useEffect, useRef, useState } from "react";
2 import { app } from "../lib/bridge";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { hasSessionGeneration } from "../lib/sessionIdentity";
5
6 export type ExtensionSurfaceView = {
7 pluginId: string;
8 surfaceId: string;
9 sessionId?: string;
10 generation?: number;
11 formInstanceId: string;
12 formInstanceExact: boolean;
13 };
14 export type ExtensionNotificationView = { severity?: string; title: string; body?: string };
15
16 /**
17 * Owns the extension form surface: submitting delivers the structured values
18 * to the owning sidecar, cancel reports values{"cancelled": true} over the
19 * same channel (a failed cancel still dismisses), and queued notifications
20 * drain into toasts from per-tab reducer state the toast context cannot read.
21 */
22 export function useExtensionSurface(input: {
23 activeTabId: string | undefined;
24 hostId?: string;
25 sessionId?: string;
26 sessionGeneration?: number;
27 form: ExtensionSurfaceView | undefined;
28 notifications: readonly ExtensionNotificationView[] | undefined;
29 dismissForm(tabId?: string, identity?: Pick<ExtensionSurfaceView, "pluginId" | "surfaceId" | "formInstanceId">): void;
30 drainNotifications(): void;
31 showToast(message: string, level: "info" | "warn" | "error"): void;
32 }) {
33 const { activeTabId, form, notifications, dismissForm, drainNotifications, showToast } = input;
34 const formKey = form ? JSON.stringify([activeTabId ?? "", form.pluginId, form.surfaceId, form.formInstanceId]) : "";
35 const visibleFormKeyRef = useRef(formKey);
36 visibleFormKeyRef.current = formKey;
37 const [busyFormKey, setBusyFormKey] = useState("");
38 const extensionFormBusy = Boolean(formKey && busyFormKey === formKey);
39
40 useEffect(() => {
41 const pending = notifications;
42 if (!pending || pending.length === 0) return;
43 for (const notification of pending) {
44 const level = notification.severity === "error" ? "error" : notification.severity === "warn" ? "warn" : "info";
45 showToast(notification.body ? `${notification.title} — ${notification.body}` : notification.title, level);
46 }
47 drainNotifications();
48 }, [drainNotifications, notifications, showToast]);
49
50 const submitExtensionForm = useCommittedCommand(async (values: Record<string, unknown>) => {
51 const pending = form;
52 if (!pending || !activeTabId || busyFormKey === formKey) return;
53 const target = {
54 tabId: activeTabId,
55 hostId: input.hostId ?? "local",
56 sessionId: pending.sessionId ?? input.sessionId ?? "",
57 sessionGeneration: input.sessionGeneration ?? 0,
58 pluginId: pending.pluginId,
59 surfaceId: pending.surfaceId,
60 pluginGeneration: pending.generation ?? 0,
61 formInstanceId: pending.formInstanceId,
62 };
63 const identity = { pluginId: pending.pluginId, surfaceId: pending.surfaceId, formInstanceId: pending.formInstanceId };
64 const requestKey = formKey;
65 setBusyFormKey(requestKey);
66 try {
67 if (!app.SubmitExtensionFormExact || !pending.formInstanceExact || !target.sessionId || !hasSessionGeneration(input.sessionGeneration) || !target.pluginGeneration || !target.formInstanceId) {
68 throw new Error("Exact extension form submission is unavailable; refresh or upgrade Reasonix.");
69 }
70 await app.SubmitExtensionFormExact(target, values);
71 dismissForm(target.tabId, identity);
72 } catch (err) {
73 if (visibleFormKeyRef.current === requestKey) showToast(err instanceof Error ? err.message : String(err), "error");
74 } finally {
75 setBusyFormKey((current) => current === requestKey ? "" : current);
76 }
77 });
78
79 const cancelExtensionForm = useCommittedCommand(async () => {
80 const pending = form;
81 if (!pending || busyFormKey === formKey) return;
82 const target = {
83 tabId: activeTabId ?? "",
84 hostId: input.hostId ?? "local",
85 sessionId: pending.sessionId ?? input.sessionId ?? "",
86 sessionGeneration: input.sessionGeneration ?? 0,
87 pluginId: pending.pluginId,
88 surfaceId: pending.surfaceId,
89 pluginGeneration: pending.generation ?? 0,
90 formInstanceId: pending.formInstanceId,
91 };
92 const identity = { pluginId: pending.pluginId, surfaceId: pending.surfaceId, formInstanceId: pending.formInstanceId };
93 const requestKey = formKey;
94 setBusyFormKey(requestKey);
95 try {
96 if (app.SubmitExtensionFormExact && pending.formInstanceExact && target.tabId && target.sessionId && hasSessionGeneration(input.sessionGeneration) && target.pluginGeneration && target.formInstanceId) {
97 await app.SubmitExtensionFormExact(target, { cancelled: true }).catch(() => {});
98 }
99 dismissForm(target.tabId, identity);
100 } finally {
101 setBusyFormKey((current) => current === requestKey ? "" : current);
102 }
103 });
104
105 return { extensionFormBusy, submitExtensionForm, cancelExtensionForm };
106 }
107
107 lines TYPESCRIPT