import { OrchestrationEvent, ThreadId, type ServerLifecycleWelcomePayload,} from "@t3tools/contracts";import { Outlet, createRootRouteWithContext, type ErrorComponentProps, useNavigate, useLocation,} from "@tanstack/react-router";import { useEffect, useEffectEvent, useRef } from "react";import { QueryClient, useQueryClient } from "@tanstack/react-query";import { Throttler } from "@tanstack/react-pacer"; import { APP_DISPLAY_NAME } from "../branding";import { AppSidebarLayout } from "../components/AppSidebarLayout";import { Button } from "../components/ui/button";import { AnchoredToastProvider, ToastProvider, toastManager } from "../components/ui/toast";import { resolveAndPersistPreferredEditor } from "../editorPreferences";import { readNativeApi } from "../nativeApi";import { type ServerConfigUpdateSource, useServerConfig, useServerConfigUpdatedSubscription, useServerWelcomeSubscription,} from "../rpc/serverState";import { ServerStateBootstrap } from "../rpc/serverStateBootstrap";import { clearPromotedDraftThread, clearPromotedDraftThreads, useComposerDraftStore,} from "../composerDraftStore";import { useStore } from "../store";import { useUiStateStore } from "../uiStateStore";import { useTerminalStateStore } from "../terminalStateStore";import { terminalRunningSubprocessFromEvent } from "../terminalActivity";import { migrateLocalSettingsToServer } from "../hooks/useSettings";import { providerQueryKeys } from "../lib/providerReactQuery";import { projectQueryKeys } from "../lib/projectReactQuery";import { collectActiveTerminalThreadIds } from "../lib/terminalStateCleanup";import { deriveOrchestrationBatchEffects } from "../orchestrationEventEffects";import { createOrchestrationRecoveryCoordinator } from "../orchestrationRecovery"; export const Route = createRootRouteWithContext<{ queryClient: QueryClient;}>()({ component: RootRouteView, errorComponent: RootRouteErrorView, head: () => ({ meta: [{ name: "title", content: APP_DISPLAY_NAME }], }),}); function RootRouteView() { if (!readNativeApi()) { return ( <div className="flex h-screen flex-col bg-background text-foreground"> <div className="flex flex-1 items-center justify-center"> <p className="text-sm text-muted-foreground"> Connecting to {APP_DISPLAY_NAME} server... </p> </div> </div> ); } return ( <> <ServerStateBootstrap /> <ToastProvider> <AnchoredToastProvider> <EventRouter /> <DesktopProjectBootstrap /> <AppSidebarLayout> <Outlet /> </AppSidebarLayout> </AnchoredToastProvider> </ToastProvider> </> );} function RootRouteErrorView({ error, reset }: ErrorComponentProps) { const message = errorMessage(error); const details = errorDetails(error); return ( <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6"> <div className="pointer-events-none absolute inset-0 opacity-80"> <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]" /> <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" /> </div> <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8"> <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase"> {APP_DISPLAY_NAME} </p> <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl"> Something went wrong. </h1> <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p> <div className="mt-5 flex flex-wrap gap-2"> <Button size="sm" onClick={() => reset()}> Try again </Button> <Button size="sm" variant="outline" onClick={() => window.location.reload()}> Reload app </Button> </div> <details className="group mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55"> <summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground"> <span className="group-open:hidden">Show error details</span> <span className="hidden group-open:inline">Hide error details</span> </summary> <pre className="max-h-56 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs text-foreground/85"> {details} </pre> </details> </section> </div> );} function errorMessage(error: unknown): string { if (error instanceof Error && error.message.trim().length > 0) { return error.message; } if (typeof error === "string" && error.trim().length > 0) { return error; } return "An unexpected router error occurred.";} function errorDetails(error: unknown): string { if (error instanceof Error) { return error.stack ?? error.message; } if (typeof error === "string") { return error; } try { return JSON.stringify(error, null, 2); } catch { return "No additional error details are available."; }} function EventRouter() { const applyOrchestrationEvents = useStore((store) => store.applyOrchestrationEvents); const syncServerReadModel = useStore((store) => store.syncServerReadModel); const setProjectExpanded = useUiStateStore((store) => store.setProjectExpanded); const syncProjects = useUiStateStore((store) => store.syncProjects); const syncThreads = useUiStateStore((store) => store.syncThreads); const clearThreadUi = useUiStateStore((store) => store.clearThreadUi); const removeTerminalState = useTerminalStateStore((store) => store.removeTerminalState); const removeOrphanedTerminalStates = useTerminalStateStore( (store) => store.removeOrphanedTerminalStates, ); const queryClient = useQueryClient(); const navigate = useNavigate(); const pathname = useLocation({ select: (loc) => loc.pathname }); const pathnameRef = useRef(pathname); const handledBootstrapThreadIdRef = useRef<string | null>(null); const handledConfigReplayRef = useRef(false); const disposedRef = useRef(false); const bootstrapFromSnapshotRef = useRef<() => Promise<void>>(async () => undefined); const serverConfig = useServerConfig(); pathnameRef.current = pathname; const handleWelcome = useEffectEvent((payload: ServerLifecycleWelcomePayload) => { migrateLocalSettingsToServer(); void (async () => { await bootstrapFromSnapshotRef.current(); if (disposedRef.current) { return; } if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) { return; } setProjectExpanded(payload.bootstrapProjectId, true); if (pathnameRef.current !== "/") { return; } if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { return; } await navigate({ to: "/$threadId", params: { threadId: payload.bootstrapThreadId }, replace: true, }); handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; })().catch(() => undefined); }); const handleServerConfigUpdated = useEffectEvent( ({ payload, source, }: { readonly payload: import("@t3tools/contracts").ServerConfigUpdatedPayload; readonly source: ServerConfigUpdateSource; }) => { const isReplay = !handledConfigReplayRef.current; handledConfigReplayRef.current = true; if (isReplay || source !== "keybindingsUpdated") { return; } const issue = payload.issues.find((entry) => entry.kind.startsWith("keybindings.")); if (!issue) { toastManager.add({ type: "success", title: "Keybindings updated", description: "Keybindings configuration reloaded successfully.", }); return; } toastManager.add({ type: "warning", title: "Invalid keybindings configuration", description: issue.message, actionProps: { children: "Open keybindings.json", onClick: () => { const api = readNativeApi(); if (!api) { return; } void Promise.resolve(serverConfig ?? api.server.getConfig()) .then((config) => { const editor = resolveAndPersistPreferredEditor(config.availableEditors); if (!editor) { throw new Error("No available editors found."); } return api.shell.openInEditor(config.keybindingsConfigPath, editor); }) .catch((error) => { toastManager.add({ type: "error", title: "Unable to open keybindings file", description: error instanceof Error ? error.message : "Unknown error opening file.", }); }); }, }, }); }, ); useEffect(() => { const api = readNativeApi(); if (!api) return; let disposed = false; disposedRef.current = false; const recovery = createOrchestrationRecoveryCoordinator(); let needsProviderInvalidation = false; const reconcileSnapshotDerivedState = () => { const threads = useStore.getState().threads; const projects = useStore.getState().projects; syncProjects(projects.map((project) => ({ id: project.id, cwd: project.cwd }))); syncThreads( threads.map((thread) => ({ id: thread.id, seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); clearPromotedDraftThreads(threads.map((thread) => thread.id)); const draftThreadIds = Object.keys( useComposerDraftStore.getState().draftThreadsByThreadId, ) as ThreadId[]; const activeThreadIds = collectActiveTerminalThreadIds({ snapshotThreads: threads.map((thread) => ({ id: thread.id, deletedAt: null })), draftThreadIds, }); removeOrphanedTerminalStates(activeThreadIds); }; const queryInvalidationThrottler = new Throttler( () => { if (!needsProviderInvalidation) { return; } needsProviderInvalidation = false; void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all }); // Invalidate workspace entry queries so the @-mention file picker // reflects files created, deleted, or restored during this turn. void queryClient.invalidateQueries({ queryKey: projectQueryKeys.all }); }, { wait: 100, leading: false, trailing: true, }, ); const applyEventBatch = (events: ReadonlyArray<OrchestrationEvent>) => { const nextEvents = recovery.markEventBatchApplied(events); if (nextEvents.length === 0) { return; } const batchEffects = deriveOrchestrationBatchEffects(nextEvents); const needsProjectUiSync = nextEvents.some( (event) => event.type === "project.created" || event.type === "project.meta-updated" || event.type === "project.deleted", ); if (batchEffects.needsProviderInvalidation) { needsProviderInvalidation = true; void queryInvalidationThrottler.maybeExecute(); } applyOrchestrationEvents(nextEvents); if (needsProjectUiSync) { const projects = useStore.getState().projects; syncProjects(projects.map((project) => ({ id: project.id, cwd: project.cwd }))); } const needsThreadUiSync = nextEvents.some( (event) => event.type === "thread.created" || event.type === "thread.deleted", ); if (needsThreadUiSync) { const threads = useStore.getState().threads; syncThreads( threads.map((thread) => ({ id: thread.id, seedVisitedAt: thread.updatedAt ?? thread.createdAt, })), ); } const draftStore = useComposerDraftStore.getState(); for (const threadId of batchEffects.clearPromotedDraftThreadIds) { clearPromotedDraftThread(threadId); } for (const threadId of batchEffects.clearDeletedThreadIds) { draftStore.clearDraftThread(threadId); clearThreadUi(threadId); } for (const threadId of batchEffects.removeTerminalStateThreadIds) { removeTerminalState(threadId); } }; const recoverFromSequenceGap = async (): Promise<void> => { if (!recovery.beginReplayRecovery("sequence-gap")) { return; } try { const events = await api.orchestration.replayEvents(recovery.getState().latestSequence); if (!disposed) { applyEventBatch(events); } } catch { recovery.failReplayRecovery(); void fallbackToSnapshotRecovery(); return; } if (!disposed && recovery.completeReplayRecovery()) { void recoverFromSequenceGap(); } }; const runSnapshotRecovery = async (reason: "bootstrap" | "replay-failed"): Promise<void> => { if (!recovery.beginSnapshotRecovery(reason)) { return; } try { const snapshot = await api.orchestration.getSnapshot(); if (!disposed) { syncServerReadModel(snapshot); reconcileSnapshotDerivedState(); if (recovery.completeSnapshotRecovery(snapshot.snapshotSequence)) { void recoverFromSequenceGap(); } } } catch { // Keep prior state and wait for welcome or a later replay attempt. recovery.failSnapshotRecovery(); } }; const bootstrapFromSnapshot = async (): Promise<void> => { await runSnapshotRecovery("bootstrap"); }; bootstrapFromSnapshotRef.current = bootstrapFromSnapshot; const fallbackToSnapshotRecovery = async (): Promise<void> => { await runSnapshotRecovery("replay-failed"); }; const unsubDomainEvent = api.orchestration.onDomainEvent((event) => { const action = recovery.classifyDomainEvent(event.sequence); if (action === "apply") { applyEventBatch([event]); return; } if (action === "recover") { void recoverFromSequenceGap(); } }); const unsubTerminalEvent = api.terminal.onEvent((event) => { const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event); if (hasRunningSubprocess === null) { return; } useTerminalStateStore .getState() .setTerminalActivity( ThreadId.makeUnsafe(event.threadId), event.terminalId, hasRunningSubprocess, ); }); return () => { disposed = true; disposedRef.current = true; needsProviderInvalidation = false; queryInvalidationThrottler.cancel(); unsubDomainEvent(); unsubTerminalEvent(); }; }, [ applyOrchestrationEvents, navigate, queryClient, removeTerminalState, removeOrphanedTerminalStates, clearThreadUi, setProjectExpanded, syncProjects, syncServerReadModel, syncThreads, ]); useServerWelcomeSubscription(handleWelcome); useServerConfigUpdatedSubscription(handleServerConfigUpdated); return null;} function DesktopProjectBootstrap() { // Desktop hydration runs through EventRouter project + orchestration sync. return null;}
import {
OrchestrationEvent,
ThreadId,
type ServerLifecycleWelcomePayload,
} from "@t3tools/contracts";
import {
Outlet,
createRootRouteWithContext,
type ErrorComponentProps,
useNavigate,
useLocation,
} from "@tanstack/react-router";
import { useEffect, useEffectEvent, useRef } from "react";
import { QueryClient, useQueryClient } from "@tanstack/react-query";
import { Throttler } from "@tanstack/react-pacer";
import { APP_DISPLAY_NAME } from "../branding";
import { AppSidebarLayout } from "../components/AppSidebarLayout";
import { Button } from "../components/ui/button";
import { AnchoredToastProvider, ToastProvider, toastManager } from "../components/ui/toast";
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
import { readNativeApi } from "../nativeApi";
import {
type ServerConfigUpdateSource,
useServerConfig,
useServerConfigUpdatedSubscription,
useServerWelcomeSubscription,
} from "../rpc/serverState";
import { ServerStateBootstrap } from "../rpc/serverStateBootstrap";
import {
clearPromotedDraftThread,
clearPromotedDraftThreads,
useComposerDraftStore,
} from "../composerDraftStore";
import { useStore } from "../store";
import { useUiStateStore } from "../uiStateStore";
import { useTerminalStateStore } from "../terminalStateStore";
import { terminalRunningSubprocessFromEvent } from "../terminalActivity";
import { migrateLocalSettingsToServer } from "../hooks/useSettings";
import { providerQueryKeys } from "../lib/providerReactQuery";
import { projectQueryKeys } from "../lib/projectReactQuery";
import { collectActiveTerminalThreadIds } from "../lib/terminalStateCleanup";
import { deriveOrchestrationBatchEffects } from "../orchestrationEventEffects";
import { createOrchestrationRecoveryCoordinator } from "../orchestrationRecovery";
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
component: RootRouteView,
errorComponent: RootRouteErrorView,
head: () => ({
meta: [{ name: "title", content: APP_DISPLAY_NAME }],
}),
});
function RootRouteView() {
if (!readNativeApi()) {
return (
<div className="flex h-screen flex-col bg-background text-foreground">
<div className="flex flex-1 items-center justify-center">
<p className="text-sm text-muted-foreground">
Connecting to {APP_DISPLAY_NAME} server...
</p>
</div>
</div>
);
}
return (
<>
<ServerStateBootstrap />
<ToastProvider>
<AnchoredToastProvider>
<EventRouter />
<DesktopProjectBootstrap />
<AppSidebarLayout>
<Outlet />
</AppSidebarLayout>
</AnchoredToastProvider>
</ToastProvider>
</>
);
}
function RootRouteErrorView({ error, reset }: ErrorComponentProps) {
const message = errorMessage(error);
const details = errorDetails(error);
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
<div className="pointer-events-none absolute inset-0 opacity-80">
<div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]" />
<div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
</div>
<section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
<p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
{APP_DISPLAY_NAME}
</p>
<h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
Something went wrong.
</h1>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>
<div className="mt-5 flex flex-wrap gap-2">
<Button size="sm" onClick={() => reset()}>
Try again
</Button>
<Button size="sm" variant="outline" onClick={() => window.location.reload()}>
Reload app
</Button>
</div>
<details className="group mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
<summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
<span className="group-open:hidden">Show error details</span>
<span className="hidden group-open:inline">Hide error details</span>
</summary>
<pre className="max-h-56 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs text-foreground/85">
{details}
</pre>
</details>
</section>
</div>
);
}
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim().length > 0) {
return error.message;
}
if (typeof error === "string" && error.trim().length > 0) {
return error;
}
return "An unexpected router error occurred.";
}
function errorDetails(error: unknown): string {
if (error instanceof Error) {
return error.stack ?? error.message;
}
if (typeof error === "string") {
return error;
}
try {
return JSON.stringify(error, null, 2);
} catch {
return "No additional error details are available.";
}
}
function EventRouter() {
const applyOrchestrationEvents = useStore((store) => store.applyOrchestrationEvents);
const syncServerReadModel = useStore((store) => store.syncServerReadModel);
const setProjectExpanded = useUiStateStore((store) => store.setProjectExpanded);
const syncProjects = useUiStateStore((store) => store.syncProjects);
const syncThreads = useUiStateStore((store) => store.syncThreads);
const clearThreadUi = useUiStateStore((store) => store.clearThreadUi);
const removeTerminalState = useTerminalStateStore((store) => store.removeTerminalState);
const removeOrphanedTerminalStates = useTerminalStateStore(
(store) => store.removeOrphanedTerminalStates,
);
const queryClient = useQueryClient();
const navigate = useNavigate();
const pathname = useLocation({ select: (loc) => loc.pathname });
const pathnameRef = useRef(pathname);
const handledBootstrapThreadIdRef = useRef<string | null>(null);
const handledConfigReplayRef = useRef(false);
const disposedRef = useRef(false);
const bootstrapFromSnapshotRef = useRef<() => Promise<void>>(async () => undefined);
const serverConfig = useServerConfig();
pathnameRef.current = pathname;
const handleWelcome = useEffectEvent((payload: ServerLifecycleWelcomePayload) => {
migrateLocalSettingsToServer();
void (async () => {
await bootstrapFromSnapshotRef.current();
if (disposedRef.current) {
return;
}
if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) {
return;
}
setProjectExpanded(payload.bootstrapProjectId, true);
if (pathnameRef.current !== "/") {
return;
}
if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) {
return;
}
await navigate({
to: "/$threadId",
params: { threadId: payload.bootstrapThreadId },
replace: true,
});
handledBootstrapThreadIdRef.current = payload.bootstrapThreadId;
})().catch(() => undefined);
});
const handleServerConfigUpdated = useEffectEvent(
({
payload,
source,
}: {
readonly payload: import("@t3tools/contracts").ServerConfigUpdatedPayload;
readonly source: ServerConfigUpdateSource;
}) => {
const isReplay = !handledConfigReplayRef.current;
handledConfigReplayRef.current = true;
if (isReplay || source !== "keybindingsUpdated") {
return;
}
const issue = payload.issues.find((entry) => entry.kind.startsWith("keybindings."));
if (!issue) {
toastManager.add({
type: "success",
title: "Keybindings updated",
description: "Keybindings configuration reloaded successfully.",
});
return;
}
toastManager.add({
type: "warning",
title: "Invalid keybindings configuration",
description: issue.message,
actionProps: {
children: "Open keybindings.json",
onClick: () => {
const api = readNativeApi();
if (!api) {
return;
}
void Promise.resolve(serverConfig ?? api.server.getConfig())
.then((config) => {
const editor = resolveAndPersistPreferredEditor(config.availableEditors);
if (!editor) {
throw new Error("No available editors found.");
}
return api.shell.openInEditor(config.keybindingsConfigPath, editor);
})
.catch((error) => {
toastManager.add({
type: "error",
title: "Unable to open keybindings file",
description:
error instanceof Error ? error.message : "Unknown error opening file.",
});
});
},
},
});
},
);
useEffect(() => {
const api = readNativeApi();
if (!api) return;
let disposed = false;
disposedRef.current = false;
const recovery = createOrchestrationRecoveryCoordinator();
let needsProviderInvalidation = false;
const reconcileSnapshotDerivedState = () => {
const threads = useStore.getState().threads;
const projects = useStore.getState().projects;
syncProjects(projects.map((project) => ({ id: project.id, cwd: project.cwd })));
syncThreads(
threads.map((thread) => ({
id: thread.id,
seedVisitedAt: thread.updatedAt ?? thread.createdAt,
})),
);
clearPromotedDraftThreads(threads.map((thread) => thread.id));
const draftThreadIds = Object.keys(
useComposerDraftStore.getState().draftThreadsByThreadId,
) as ThreadId[];
const activeThreadIds = collectActiveTerminalThreadIds({
snapshotThreads: threads.map((thread) => ({ id: thread.id, deletedAt: null })),
draftThreadIds,
});
removeOrphanedTerminalStates(activeThreadIds);
};
const queryInvalidationThrottler = new Throttler(
() => {
if (!needsProviderInvalidation) {
return;
}
needsProviderInvalidation = false;
void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all });
// Invalidate workspace entry queries so the @-mention file picker
// reflects files created, deleted, or restored during this turn.
void queryClient.invalidateQueries({ queryKey: projectQueryKeys.all });
},
{
wait: 100,
leading: false,
trailing: true,
},
);
const applyEventBatch = (events: ReadonlyArray<OrchestrationEvent>) => {
const nextEvents = recovery.markEventBatchApplied(events);
if (nextEvents.length === 0) {
return;
}
const batchEffects = deriveOrchestrationBatchEffects(nextEvents);
const needsProjectUiSync = nextEvents.some(
(event) =>
event.type === "project.created" ||
event.type === "project.meta-updated" ||
event.type === "project.deleted",
);
if (batchEffects.needsProviderInvalidation) {
needsProviderInvalidation = true;
void queryInvalidationThrottler.maybeExecute();
}
applyOrchestrationEvents(nextEvents);
if (needsProjectUiSync) {
const projects = useStore.getState().projects;
syncProjects(projects.map((project) => ({ id: project.id, cwd: project.cwd })));
}
const needsThreadUiSync = nextEvents.some(
(event) => event.type === "thread.created" || event.type === "thread.deleted",
);
if (needsThreadUiSync) {
const threads = useStore.getState().threads;
syncThreads(
threads.map((thread) => ({
id: thread.id,
seedVisitedAt: thread.updatedAt ?? thread.createdAt,
})),
);
}
const draftStore = useComposerDraftStore.getState();
for (const threadId of batchEffects.clearPromotedDraftThreadIds) {
clearPromotedDraftThread(threadId);
}
for (const threadId of batchEffects.clearDeletedThreadIds) {
draftStore.clearDraftThread(threadId);
clearThreadUi(threadId);
}
for (const threadId of batchEffects.removeTerminalStateThreadIds) {
removeTerminalState(threadId);
}
};
const recoverFromSequenceGap = async (): Promise<void> => {
if (!recovery.beginReplayRecovery("sequence-gap")) {
return;
}
try {
const events = await api.orchestration.replayEvents(recovery.getState().latestSequence);
if (!disposed) {
applyEventBatch(events);
}
} catch {
recovery.failReplayRecovery();
void fallbackToSnapshotRecovery();
return;
}
if (!disposed && recovery.completeReplayRecovery()) {
void recoverFromSequenceGap();
}
};
const runSnapshotRecovery = async (reason: "bootstrap" | "replay-failed"): Promise<void> => {
if (!recovery.beginSnapshotRecovery(reason)) {
return;
}
try {
const snapshot = await api.orchestration.getSnapshot();
if (!disposed) {
syncServerReadModel(snapshot);
reconcileSnapshotDerivedState();
if (recovery.completeSnapshotRecovery(snapshot.snapshotSequence)) {
void recoverFromSequenceGap();
}
}
} catch {
// Keep prior state and wait for welcome or a later replay attempt.
recovery.failSnapshotRecovery();
}
};
const bootstrapFromSnapshot = async (): Promise<void> => {
await runSnapshotRecovery("bootstrap");
};
bootstrapFromSnapshotRef.current = bootstrapFromSnapshot;
const fallbackToSnapshotRecovery = async (): Promise<void> => {
await runSnapshotRecovery("replay-failed");
};
const unsubDomainEvent = api.orchestration.onDomainEvent((event) => {
const action = recovery.classifyDomainEvent(event.sequence);
if (action === "apply") {
applyEventBatch([event]);
return;
}
if (action === "recover") {
void recoverFromSequenceGap();
}
});
const unsubTerminalEvent = api.terminal.onEvent((event) => {
const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event);
if (hasRunningSubprocess === null) {
return;
}
useTerminalStateStore
.getState()
.setTerminalActivity(
ThreadId.makeUnsafe(event.threadId),
event.terminalId,
hasRunningSubprocess,
);
});
return () => {
disposed = true;
disposedRef.current = true;
needsProviderInvalidation = false;
queryInvalidationThrottler.cancel();
unsubDomainEvent();
unsubTerminalEvent();
};
}, [
applyOrchestrationEvents,
navigate,
queryClient,
removeTerminalState,
removeOrphanedTerminalStates,
clearThreadUi,
setProjectExpanded,
syncProjects,
syncServerReadModel,
syncThreads,
]);
useServerWelcomeSubscription(handleWelcome);
useServerConfigUpdatedSubscription(handleServerConfigUpdated);
return null;
}
function DesktopProjectBootstrap() {
// Desktop hydration runs through EventRouter project + orchestration sync.
return null;
}