463 lines · 15.4 KB · TypeScript
RawDownload
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;}