Ctrl K
ComunidadGuías…

src/renderer.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { router } from "./router";
import { RouterProvider } from "@tanstack/react-router";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
import {
  getTelemetryUserId,
  isTelemetryOptedIn,
  isDyadProUser,
} from "./hooks/useSettings";

// Initialize i18next before any rendering
import "./i18n";
import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
  MutationCache,
  useQueryClient,
} from "@tanstack/react-query";
import { showError } from "./lib/toast";
import { ipc } from "./ipc/types";
import { useStore } from "jotai";
import { queryKeys } from "./lib/queryKeys";
import {
  createExceptionFromTelemetry,
  getExceptionTelemetryContext,
  getPostHogTelemetryStorage,
  PostHogErrorDeduper,
  shouldBypassNonProTelemetrySampling,
  shouldFilterPostHogExceptionEvent,
} from "./lib/posthogTelemetry";
import { registerRendererIpcListeners } from "./app_wiring/registerRendererIpcListeners";
import {
  ChatStreamProvider,
  useChatStreamManager,
} from "./chat_stream/ChatStreamProvider";
import {
  EntityDisposalProvider,
  useEntityDisposal,
  useRegisterEntityDisposer,
} from "./state_machines/react";
import { clearTestRuntimeForAppAtom } from "./atoms/testRuntimeAtoms";
import {
  ensureRecentViewedChatIdAtom,
  initializeChatTabSessionStorageAtom,
} from "./atoms/chatAtoms";
import {
  configureChatTabWindowSession,
  promoteMostRecentChatTabSession,
  pruneChatTabWindowSessions,
} from "./window_infrastructure/chat_tab_session_storage";
import type { VisibleEntity } from "./window_infrastructure/types";
import { initialWindowNavigation } from "./window_infrastructure/initial_window_navigation";
import {
  earlyTelemetryEvents,
  registerEarlyRendererEvents,
} from "./app_wiring/early_renderer_events";
import { clearRecorderForAppAtom } from "./atoms/recorderAtoms";

// @ts-ignore
console.log("Running in mode:", import.meta.env.MODE);
registerEarlyRendererEvents();

interface MyMeta extends Record<string, unknown> {
  showErrorToast: boolean;
}

declare module "@tanstack/react-query" {
  interface Register {
    queryMeta: MyMeta;
    mutationMeta: MyMeta;
  }
}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      retry: false,
    },
    mutations: {
      retry: false,
    },
  },
  queryCache: new QueryCache({
    onError: (error, query) => {
      if (query.meta?.showErrorToast) {
        showError(error);
      }
    },
  }),
  mutationCache: new MutationCache({
    onError: (error, _variables, _context, mutation) => {
      if (mutation.meta?.showErrorToast) {
        showError(error);
      }
    },
  }),
});

const postHogErrorDeduper = new PostHogErrorDeduper(
  getPostHogTelemetryStorage(window),
);
window.addEventListener("pagehide", () => postHogErrorDeduper.flush());
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    postHogErrorDeduper.flush();
  }
});

const posthogClient = posthog.init(
  "phc_5Vxx0XT8Ug3eWROhP6mm4D6D2DgIIKT232q4AKxC2ab",
  {
    api_host: "https://us.i.posthog.com",
    // @ts-ignore
    debug: import.meta.env.MODE === "development",
    autocapture: false,
    capture_exceptions: true,
    capture_pageview: false,
    before_send: (event) => {
      if (!isTelemetryOptedIn()) {
        console.debug("Telemetry not opted in, skipping event");
        return null;
      }

      if (shouldFilterPostHogExceptionEvent(event)) {
        console.debug(
          "Filtering generic fetch failed exception from telemetry",
        );
        return null;
      }
      const telemetryUserId = getTelemetryUserId();
      if (telemetryUserId) {
        posthogClient.identify(telemetryUserId);
      }

      if (event?.properties["$ip"]) {
        event.properties["$ip"] = null;
      }

      const isPro = isDyadProUser();
      const dedupedEvent = postHogErrorDeduper.process(event, isPro);
      if (!dedupedEvent) {
        console.debug("Deduplicating PostHog error event", event?.event);
        return null;
      }
      event = dedupedEvent;

      // For non-Pro users, sample routine events at 10%. The helper keeps
      // correlated diagnostics and other exempt events.
      if (!isPro) {
        if (
          !shouldBypassNonProTelemetrySampling(event) &&
          Math.random() > 0.1
        ) {
          console.debug("Non-Pro user: sampling out event", event?.event);
          return null;
        }
      }

      console.debug(
        "Telemetry opted in - UUID:",
        telemetryUserId,
        "sending event",
        event,
      );
      return event;
    },
    persistence: "localStorage",
  },
);

function App() {
  return (
    <ChatStreamProvider>
      <RendererServices />
    </ChatStreamProvider>
  );
}

function RendererServices() {
  const queryClient = useQueryClient();
  const store = useStore();
  const chatStreamManager = useChatStreamManager();
  const entityDisposal = useEntityDisposal();
  const [windowReady, setWindowReady] = useState(false);
  const clearAppRuntime = useCallback(
    (appId: number) => {
      store.set(clearTestRuntimeForAppAtom, appId);
      // Recorded interactions can carry whatever the user typed into the app;
      // a deleted app must not leave them (or its draft) resident for the rest
      // of the renderer's life.
      store.set(clearRecorderForAppAtom, appId);
      // The main process holds its own copies: a parked draft, and possibly a
      // live session still serving an isolated database and holding the app's
      // lock. Clearing the atoms only takes away the UI that could have ended
      // them. (`stopApp` during deletion ends the session too, but deletion
      // paths that never started the app wouldn't have.)
      void ipc.recording.discardRecordedTestDraft({ appId }).catch(() => {});
      void ipc.recording.stopRecording({ appId }).catch(() => {});
    },
    [store],
  );
  useRegisterEntityDisposer("app", clearAppRuntime);

  // Fetch user budget on app load
  useEffect(() => {
    queryClient.prefetchQuery({
      queryKey: queryKeys.userBudget.info,
      queryFn: () => ipc.system.getUserBudget(),
    });
  }, [queryClient]);

  useEffect(() => {
    // Subscribe to navigation state changes
    const unsubscribe = router.subscribe("onResolved", (navigation) => {
      // Capture the navigation event in PostHog
      posthog.capture("navigation", {
        toPath: navigation.toLocation.pathname,
        fromPath: navigation.fromLocation?.pathname,
      });

      // Optionally capture as a standard pageview as well
      posthog.capture("$pageview", {
        path: navigation.toLocation.pathname,
      });
    });

    // Clean up subscription when component unmounts
    return () => {
      unsubscribe();
    };
  }, []);

  useEffect(
    () =>
      earlyTelemetryEvents.subscribe(({ eventName, properties }) => {
        if (eventName === "$exception") {
          posthog.captureException(
            createExceptionFromTelemetry(properties),
            getExceptionTelemetryContext(properties),
          );
          return;
        }

        posthog.capture(eventName, properties);
      }),
    [],
  );

  useEffect(() => {
    let disposed = false;
    let retryTimer: ReturnType<typeof setTimeout> | undefined;
    let retryDelayMs = 100;
    const bootstrapWindow = () => {
      void ipc.windowInfrastructure
        .bootstrap({})
        .then((bootstrap) => {
          if (disposed) return;
          configureChatTabWindowSession(bootstrap.windowSessionId, {
            mayMigrateLegacySession: bootstrap.mayMigrateLegacyChatTabSession,
          });
          try {
            if (bootstrap.mayMigrateLegacyChatTabSession) {
              promoteMostRecentChatTabSession(
                window.localStorage,
                bootstrap.windowSessionId,
              );
            }
            pruneChatTabWindowSessions(
              window.localStorage,
              bootstrap.restorableWindowSessionIds,
            );
            store.set(initializeChatTabSessionStorageAtom);
          } catch (error) {
            // Browser storage is optional presentation state. A denied or full
            // localStorage must not turn a successful main-process bootstrap
            // into a permanently blank product window.
            console.error(
              "Failed to initialize chat tab session storage",
              error,
            );
          }
          const entity: VisibleEntity | undefined = bootstrap.initialEntity;
          if (entity?.kind === "chat") {
            // Seed the tab before route navigation. ChatTabs hydration merges
            // pre-hydration opens, so this works even with a collapsed sidebar.
            store.set(ensureRecentViewedChatIdAtom, entity.id);
          }
          const navigation = initialWindowNavigation(
            entity,
            bootstrap.initialChatAppId,
          );
          if (navigation) {
            void router.navigate({ ...navigation, replace: true });
          }
          setWindowReady(true);
        })
        .catch((error) => {
          if (disposed) return;
          console.error("Failed to initialize window session", error);
          retryTimer = setTimeout(bootstrapWindow, retryDelayMs);
          retryDelayMs = Math.min(retryDelayMs * 2, 5_000);
        });
    };
    bootstrapWindow();
    return () => {
      disposed = true;
      if (retryTimer) clearTimeout(retryTimer);
    };
  }, []);

  useEffect(() => {
    if (!windowReady) return;
    return registerRendererIpcListeners({
      ipcClient: ipc,
      store,
      queryClient,
      chatStreamManager,
      entityDisposal,
      getCurrentPathname: () => router.state.location.pathname,
      subscribeToNavigation: (listener) =>
        router.subscribe("onResolved", listener),
    });
  }, [chatStreamManager, entityDisposal, queryClient, store, windowReady]);

  return windowReady ? <RouterProvider router={router} /> : null;
}

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <PostHogProvider client={posthogClient}>
        <EntityDisposalProvider>
          <App />
        </EntityDisposalProvider>
      </PostHogProvider>
    </QueryClientProvider>
  </StrictMode>,
);