返回 DeepSeek-Reasonix
AppRuntime.tsx
根目录 / desktop / frontend / src / AppRuntime.tsx
1 import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2 import { useRuntimeStateSync } from "./lib/useRuntimeState";
3 import { useCommittedCommand } from "./lib/useCommittedCommand";
4 import { app, onLegacyEmptySessionCleanupChanged, openExternal } from "./lib/bridge";
5 import type { LegacyEmptySessionCleanupStatus } from "./generated/desktopContract.generated";
6 import { useT, useI18n } from "./lib/i18n";
7 import { useToast } from "./lib/toast";
8 import { useGoalActionHandler } from "./lib/goalAction";
9 import { useActiveRemoteSession } from "./lib/useRemoteSession";
10 import { useWarmTerminalPanel } from "./lib/useWarmTerminalPanel";
11 import { setReasoningDisplayPending } from "./lib/reasoningDisplayPreference";
12 import type { ComposerProfile, UserPlanModeIntents } from "./lib/composerProfile";
13 import type { TabMeta } from "./lib/types";
14 import type { HistoryViewState } from "./app-runtime/historyViewProjection";
15 import { useNavigationSurface } from "./lib/useNavigationSurface";
16 import { projectNavigationSurfaceTarget } from "./app-runtime/conversationProjection";
17 import { useSessionOperations } from "./app-runtime/useSessionOperations";
18 import { createSessionSurfaceFence, sessionIdentityKey } from "./app-runtime/sessionTarget";
19 import { commitAppRenderToken, createAppRenderToken } from "./app-runtime/appLifecycleProbe";
20 import { useAppRuntimeAdapter } from "./app-runtime/useAppRuntimeAdapter";
21 import { useAppShellStores } from "./app-runtime/useAppShellStores";
22 import { useAppSessionComposition } from "./app-runtime/useAppSessionComposition";
23 import { useAppNavigationComposition } from "./app-runtime/useAppNavigationComposition";
24 import { useSessionComposerLifecycle } from "./app-runtime/useSessionComposerLifecycle";
25 import { useRetiredProjectTreeUiMigration } from "./app-runtime/useLocalUiLifecycles";
26 import logoSymbol from "./assets/logo-symbol.svg";
27
28 const AppRuntimeView = lazy(() => import("./app-shell/AppRuntimeView").then((module) => ({ default: module.AppRuntimeView })));
29
30 function AppRuntimeViewFallback() {
31 return (
32 <div className="boot-shell" role="status" aria-label="Reasonix is starting">
33 <div className="boot-shell__card">
34 <div className="boot-shell__mark" aria-hidden="true"><img src={logoSymbol} alt="" draggable={false} /></div>
35 <div className="boot-shell__name">Reasonix</div>
36 <div className="boot-shell__dots" aria-hidden="true"><span /><span /><span /></div>
37 </div>
38 </div>
39 );
40 }
41
42 // Hold reasoning UI until the authoritative desktop startup settings arrive;
43 // this prevents a hidden preference from flashing content during first paint.
44 setReasoningDisplayPending();
45
46 /**
47 * Composition root: owns the controller adapter, the session identity/fence,
48 * the navigation surface and every store-backed state, then delegates all
49 * command domains to the session/navigation compositions and the tree to the
50 * shell view. Wiring only — no domain logic lives here.
51 */
52 export function AppRuntime() {
53 useRuntimeStateSync();
54 const appRenderToken = createAppRenderToken();
55 useLayoutEffect(() => commitAppRenderToken(appRenderToken));
56 const runtime = useAppRuntimeAdapter();
57 const { state, liveStore, activeTabId, notice } = runtime.snapshot;
58 const t = useT();
59 const { locale } = useI18n();
60 const { showToast } = useToast();
61 const { runGoalAction, handleGoalActionError } = useGoalActionHandler();
62 const [composerProfilesByTab, setComposerProfilesByTab] = useState<Record<string, ComposerProfile>>({});
63 const userPlanModeByTabRef = useRef<UserPlanModeIntents>({});
64 const [tabMetas, setTabMetas] = useState<TabMeta[]>([]);
65 const [tabOrderIds, setTabOrderIds] = useState<string[]>([]);
66 const activeTab = useMemo(
67 () => tabMetas.find((tab) => tab.id === activeTabId) ?? tabMetas.find((tab) => tab.active),
68 [activeTabId, tabMetas],
69 );
70 const { active: remoteSurfaceActive, session: remoteSession, ready: remoteComposerReady, onSend: remoteSend, onCancel: remoteCancel } = useActiveRemoteSession(activeTab, showToast);
71 const activeSessionIdentity = sessionIdentityKey({
72 tabId: activeTabId,
73 session: activeTab?.session ?? state.meta?.session,
74 sessionId: activeTab?.sessionId,
75 remote: activeTab?.remote,
76 sessionPath: activeTab?.sessionPath ?? state.meta?.sessionPath,
77 sessionGeneration: activeTab?.sessionGeneration ?? state.meta?.sessionGeneration ?? state.sessionGen,
78 scope: activeTab?.scope,
79 workspaceRoot: activeTab?.workspaceRoot ?? state.meta?.cwd,
80 topicId: activeTab?.topicId,
81 });
82 const sessionSurfaceFenceRef = useRef<ReturnType<typeof createSessionSurfaceFence> | null>(null);
83 if (!sessionSurfaceFenceRef.current) sessionSurfaceFenceRef.current = createSessionSurfaceFence();
84 const sessionSurfaceFence = sessionSurfaceFenceRef.current;
85 const sessionOperations = useSessionOperations({
86 visible: { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
87 resources: [
88 { tabId: activeTabId ?? "", sessionKey: activeSessionIdentity },
89 ...tabMetas.filter(tab => tab.id !== activeTabId).map(tab => ({
90 tabId: tab.id,
91 sessionKey: sessionIdentityKey({ tabId: tab.id, sessionPath: tab.sessionPath,
92 session: tab.session, sessionId: tab.sessionId, remote: tab.remote, sessionGeneration: tab.sessionGeneration,
93 scope: tab.scope, workspaceRoot: tab.workspaceRoot, topicId: tab.topicId }),
94 })),
95 ],
96 });
97 useLayoutEffect(() => {
98 sessionSurfaceFence.commit(activeTabId, activeSessionIdentity);
99 return () => sessionSurfaceFence.dispose();
100 }, [activeSessionIdentity, activeTabId, sessionSurfaceFence]);
101 const navigationSurface = useNavigationSurface(projectNavigationSurfaceTarget({
102 activeTabId, sessionKey: activeSessionIdentity, local: state, remote: remoteSurfaceActive ? remoteSession : undefined,
103 }));
104 const shell = useAppShellStores();
105 const [tabRevealSignal, setTabRevealSignal] = useState(0);
106 const [histView, setHistView] = useState<HistoryViewState | null>(null);
107 const [sidebarImDetailConnectionId, setSidebarImDetailConnectionId] = useState("");
108 useRetiredProjectTreeUiMigration();
109 const [tasksOpen, setTasksOpen] = useState<false | "session" | "all">(false);
110 const workspaceScopeActiveTabRef = useRef(activeTabId);
111 const [workspaceControllerEpoch, setWorkspaceControllerEpoch] = useState(0);
112 workspaceScopeActiveTabRef.current = activeTabId;
113 const { mounted: terminalContentVisible, fitEnabled: terminalFitEnabled, prefetch: prefetchTerminalPanel } = useWarmTerminalPanel(shell.terminalPanelOpen, shell.terminalResizing, !shell.managementActive);
114 const [dockRefreshKey, setDockRefreshKey] = useState(0);
115 const [fileRefRefreshKey, setFileRefRefreshKey] = useState(0);
116 const refreshComposerFileRefs = useCommittedCommand(() => setFileRefRefreshKey((value) => value + 1));
117 const composerFileRefRefreshKey = `${dockRefreshKey}:${fileRefRefreshKey}`;
118 const [projectRevision, setProjectRevision] = useState(0);
119 useSessionComposerLifecycle();
120
121 const session = useAppSessionComposition({
122 runtime,
123 t,
124 showToast,
125 shell,
126 core: {
127 state, liveStore, activeTabId, notice, activeTab, remoteSurfaceActive, remoteSession, remoteComposerReady,
128 remoteSend, remoteCancel, activeSessionIdentity, sessionSurfaceFence, sessionOperations,
129 },
130 surface: navigationSurface,
131 stores: {
132 composerProfilesByTab, setComposerProfilesByTab, tabMetas, setTabMetas, tabOrderIds, setTabOrderIds,
133 userPlanModeByTabRef,
134 },
135 local: {
136 setHistView, setTabRevealSignal,
137 sidebarImDetailConnectionId, setSidebarImDetailConnectionId,
138 workspaceScopeActiveTabRef, workspaceControllerEpoch, setWorkspaceControllerEpoch,
139 dockRefreshKey, setDockRefreshKey, fileRefRefreshKey, setFileRefRefreshKey, projectRevision, setProjectRevision,
140 },
141 goal: { runGoalAction, handleGoalActionError },
142 });
143 const navigation = useAppNavigationComposition({
144 runtime,
145 t,
146 notice,
147 showToast,
148 shell,
149 state,
150 activeTab,
151 activeTabId,
152 activeSessionIdentity,
153 remoteSurfaceActive,
154 surface: navigationSurface,
155 local: {
156 setHistView, setProjectRevision,
157 setSidebarImDetailConnectionId, setTasksOpen,
158 },
159 session,
160 });
161 const openCleanupTrash = useCommittedCommand(() => navigation.historyCommands.openTrash());
162 const cleanupNoticeBatchRef = useRef("");
163 useEffect(() => {
164 let live = true;
165 const showCleanupNotice = (status: LegacyEmptySessionCleanupStatus) => {
166 if (!live || status.removed <= 0 || !status.batchId) return;
167 const storageKey = `reasonix.legacy-empty-session-cleanup.notice.${status.batchId}`;
168 let shown = false;
169 try {
170 shown = window.localStorage.getItem(storageKey) === "shown";
171 } catch {
172 // Hardened webviews may disable storage. The in-memory fence still
173 // prevents duplicate notices for this renderer lifetime.
174 }
175 if (cleanupNoticeBatchRef.current === status.batchId || shown) return;
176 cleanupNoticeBatchRef.current = status.batchId;
177 try { window.localStorage.setItem(storageKey, "shown"); } catch { /* best effort */ }
178 showToast(t("history.legacyCleanupComplete", { n: status.removed }), "info", {
179 actionLabel: t("history.viewTrash"),
180 onAction: () => void openCleanupTrash(),
181 durationMs: 8000,
182 });
183 };
184 const unsubscribe = onLegacyEmptySessionCleanupChanged(showCleanupNotice);
185 void app.GetLegacyEmptySessionCleanupStatus().then(showCleanupNotice).catch(() => {});
186 return () => { live = false; unsubscribe(); };
187 }, [openCleanupTrash, showToast, t]);
188
189 return (
190 <Suspense fallback={<AppRuntimeViewFallback />}>
191 <AppRuntimeView
192 core={{
193 state, activeTab, activeTabId, liveStore, remoteSurfaceActive, remoteSession, remoteComposerReady,
194 remoteCancel, surface: navigationSurface, t, locale, onOpenLink: openExternal,
195 }}
196 shell={shell}
197 session={session}
198 navigation={navigation}
199 runtime={runtime}
200 local={{
201 tasksOpen, setTasksOpen,
202 sidebarImDetailConnectionId, setSidebarImDetailConnectionId,
203 tabRevealSignal, histView,
204 projectRevision, dockRefreshKey, composerFileRefRefreshKey, refreshComposerFileRefs,
205 terminalContentVisible, terminalFitEnabled, prefetchTerminalPanel,
206 }}
207 />
208 </Suspense>
209 );
210 }
211
211 lines Plain Text