返回 DeepSeek-Reasonix
useRuntimeEventHandlers.ts
根目录 / desktop / frontend / src / app-runtime / useRuntimeEventHandlers.ts
1 import { useEffect, useRef, type Dispatch, type RefObject, type SetStateAction } from "react";
2 import { app, onProjectTreeChanged, onTabMeta } from "../lib/bridge";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { activeTabMirror } from "./activeTabMirror";
5 import { asArray } from "../lib/array";
6 import { createBoundedRefreshCoordinator, sameTabMetaLists, seedActiveTabMetaList, shouldRefreshTabMetaForEvent, TAB_META_MAX_IN_FLIGHT } from "../lib/tabMetaRefresh";
7 import { useRuntimeNotifications } from "./useRuntimeNotifications";
8 import { composerProfileFromTab, defaultComposerProfile, patchComposerProfile, resolvePlanRestoreTabId, shouldRestoreUserPlanModeForProfile, updateUserPlanModeIntent, type ComposerProfile, type UserPlanModeIntents } from "../lib/composerProfile";
9 import { useRemoteTabOpened } from "../lib/useRemoteTabOpened";
10 import { recordFrontendDiagnostic } from "../lib/frontendDiagnosticBridge";
11 import { useRemoteStore } from "../store/remote";
12 import type { TabMeta } from "../lib/types";
13 import type {
14 RemoteForwardsListener,
15 RemoteServerListener,
16 RemoteStatusListener,
17 RuntimeEventListener,
18 RuntimeReadyListener,
19 RuntimeRebuiltListener,
20 } from "./AppRuntimeEffects";
21
22 export type RuntimeEventHandlersInput = {
23 activeTabId: string | undefined;
24 workspaceScopeKey: string;
25 workspaceScopeActiveTabRef: RefObject<string | undefined>;
26 userPlanModeByTabRef: RefObject<UserPlanModeIntents>;
27 setTabMetas: Dispatch<SetStateAction<TabMeta[]>>;
28 setTabOrderIds: Dispatch<SetStateAction<string[]>>;
29 setComposerProfilesByTab: Dispatch<SetStateAction<Record<string, ComposerProfile>>>;
30 setDockRefreshKey: Dispatch<SetStateAction<number>>;
31 setProjectRevision: Dispatch<SetStateAction<number>>;
32 setWorkspaceControllerEpoch: Dispatch<SetStateAction<number>>;
33 setControllerCollaborationMode(mode: string): Promise<void>;
34 };
35
36 /**
37 * Owns the runtime event surface: tab-meta registry refresh/seed/remote
38 * registration with its single-flight coordinator, the runtime
39 * event/ready/rebuilt listeners (chimes, plan-mode restore, workspace-scope
40 * epochs), the remote status/forwards/server listeners, and the workspace
41 * focus reconciliation that refreshes tab metas when the project tree changes.
42 */
43 export function useRuntimeEventHandlers(input: RuntimeEventHandlersInput) {
44 const { activeTabId, workspaceScopeKey, setProjectRevision } = input;
45 const { handleNotification, resetLegacyAttention } = useRuntimeNotifications(activeTabId);
46 const tabMetaRefreshCoordinatorRef = useRef<ReturnType<typeof createBoundedRefreshCoordinator<TabMeta[]>> | null>(null);
47 if (!tabMetaRefreshCoordinatorRef.current) {
48 tabMetaRefreshCoordinatorRef.current = createBoundedRefreshCoordinator<TabMeta[]>(TAB_META_MAX_IN_FLIGHT);
49 }
50
51 const refreshTabMetas = useCommittedCommand(async (
52 apply?: () => boolean,
53 options?: { afterMutation?: boolean },
54 ): Promise<TabMeta[]> => {
55 const result = await tabMetaRefreshCoordinatorRef.current!.run(
56 async () => asArray(await app.ListTabs().catch(() => [] as TabMeta[])),
57 options?.afterMutation ? { invalidate: true } : undefined,
58 );
59 const tabs = result.value;
60 if (result.latest && (!apply || apply())) {
61 input.setTabMetas((current) => sameTabMetaLists(current, tabs) ? current : tabs);
62 }
63 return tabs;
64 });
65 const seedActiveTabMeta = useCommittedCommand((tab: TabMeta): void => {
66 input.setTabMetas((current) => seedActiveTabMetaList(current, tab));
67 input.setTabOrderIds((current) => current.includes(tab.id) ? current : [...current, tab.id]);
68 });
69 // Authentication changes belong to the current backend generation. Refresh
70 // the registry rather than granting a local, tab-id-only send bypass.
71 useEffect(() => onTabMeta(() => { void refreshTabMetas(undefined, { afterMutation: true }); }), [refreshTabMetas]);
72 const updateRemoteTabMeta = useCommittedCommand((tab: TabMeta): void => {
73 input.setTabMetas((current) => current.map((existing) => existing.id === tab.id
74 ? { ...existing, ...tab, active: existing.active }
75 : existing));
76 });
77
78 const registerRemoteTabMeta = useCommittedCommand((tab: TabMeta) => {
79 input.setTabMetas(current => current.some(existing => existing.id === tab.id) ? current : [...current, { ...tab, active: false }]);
80 });
81 useRemoteTabOpened(registerRemoteTabMeta, updateRemoteTabMeta);
82
83 const handleRuntimeEvent = useCommittedCommand<RuntimeEventListener>((event) => {
84 recordFrontendDiagnostic("runtime", "runtime.event", { action: event.kind, status: event.err ? "error" : "ok" });
85 if (event.kind === "turn_done") {
86 input.setDockRefreshKey((value) => value + 1);
87 input.setProjectRevision((value) => value + 1);
88 }
89 handleNotification(event);
90 if (shouldRefreshTabMetaForEvent(event.kind)) void refreshTabMetas(undefined, { afterMutation: true });
91 if (event.kind !== "turn_done") return;
92 const turnTabId = resolvePlanRestoreTabId(event.tabId, activeTabMirror().current);
93 void refreshTabMetas(undefined, { afterMutation: true }).then((tabs) => {
94 if (!turnTabId) return;
95 const tab = tabs.find((item) => item.id === turnTabId);
96 const baseProfile = tab ? composerProfileFromTab(tab) : defaultComposerProfile;
97 if (!shouldRestoreUserPlanModeForProfile(input.userPlanModeByTabRef.current, turnTabId, baseProfile)) {
98 if (baseProfile.goal.trim()) {
99 input.userPlanModeByTabRef.current = updateUserPlanModeIntent(input.userPlanModeByTabRef.current, turnTabId, false);
100 }
101 return;
102 }
103 input.setComposerProfilesByTab((current) => patchComposerProfile(
104 current, turnTabId, current[turnTabId] ?? baseProfile,
105 { collaborationMode: "plan", goalDraftMode: false, goal: "" },
106 ["collaborationMode", "goal"],
107 ));
108 if (activeTabMirror().current === turnTabId) void input.setControllerCollaborationMode("plan");
109 });
110 });
111
112 const handleRuntimeReady = useCommittedCommand<RuntimeReadyListener>((readyTabId) => {
113 recordFrontendDiagnostic("runtime", "runtime.ready", { ready: true, hasActiveTab: Boolean(readyTabId) });
114 resetLegacyAttention(readyTabId);
115 void refreshTabMetas();
116 if (!readyTabId || readyTabId === input.workspaceScopeActiveTabRef.current) {
117 input.setWorkspaceControllerEpoch((value) => value + 1);
118 }
119 });
120
121 const handleRuntimeRebuilt = useCommittedCommand<RuntimeRebuiltListener>((rebuiltTabId) => {
122 recordFrontendDiagnostic("runtime", "runtime.rebuilt", { ready: true, hasActiveTab: Boolean(rebuiltTabId) });
123 resetLegacyAttention(rebuiltTabId);
124 if (!rebuiltTabId || rebuiltTabId === input.workspaceScopeActiveTabRef.current) {
125 input.setWorkspaceControllerEpoch((value) => value + 1);
126 }
127 });
128
129 useEffect(() => {
130 let live = true;
131 const ready = import("../lib/workspaceRefreshStore")
132 .then(({ default: startWorkspaceFocusReconciliation }) => live ? startWorkspaceFocusReconciliation(activeTabId, workspaceScopeKey, refreshTabMetas) : undefined)
133 .catch(() => undefined);
134 const stopProjectTree = onProjectTreeChanged(() => {
135 setProjectRevision((value) => value + 1);
136 void refreshTabMetas(undefined, { afterMutation: true });
137 });
138 return () => {
139 live = false;
140 stopProjectTree();
141 void ready.then((stop) => stop?.());
142 };
143 }, [activeTabId, refreshTabMetas, setProjectRevision, workspaceScopeKey]);
144
145 const handleRemoteStatus = useCommittedCommand<RemoteStatusListener>((status) => {
146 useRemoteStore.getState().applyStatus(status);
147 if (status.state === "stopped" && status.error) useRemoteStore.getState().requestStatusPopover(status.hostId);
148 });
149 const handleRemoteForwards = useCommittedCommand<RemoteForwardsListener>((event) => useRemoteStore.getState().setForwards(event.hostId, event.forwards));
150 const handleRemoteServer = useCommittedCommand<RemoteServerListener>((server) => useRemoteStore.getState().setServer(server));
151 const handleInitialRemoteHosts = useCommittedCommand((hosts: Awaited<ReturnType<typeof app.RemoteHosts>>) => useRemoteStore.getState().setHosts(hosts));
152 const handleInitialRemoteStatuses = useCommittedCommand((statuses: Awaited<ReturnType<typeof app.RemoteConnectionStatuses>>) => useRemoteStore.getState().hydrateStatuses(statuses));
153
154 return {
155 refreshTabMetas,
156 seedActiveTabMeta,
157 registerRemoteTabMeta,
158 updateRemoteTabMeta,
159 handleRuntimeEvent,
160 handleRuntimeReady,
161 handleRuntimeRebuilt,
162 handleRemoteStatus,
163 handleRemoteForwards,
164 handleRemoteServer,
165 handleInitialRemoteHosts,
166 handleInitialRemoteStatuses,
167 };
168 }
169
169 lines TYPESCRIPT