331 lines · 10.5 KB · TypeScript
RawDownload
import { useNavigate } from "@tanstack/react-router";import { useAtomValue } from "@effect/atom-react";import { DownloadIcon } from "lucide-react";import { useCallback, useEffect, useMemo, useRef } from "react";import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts"; import { primaryServerProvidersAtom, serverEnvironment } from "../state/server";import { usePrimaryEnvironment } from "../state/environments";import { useDismissedProviderUpdateNotificationKeys } from "../providerUpdateDismissal";import { PROVIDER_ICON_BY_PROVIDER } from "./chat/providerIconUtils";import {  canOneClickUpdateProviderCandidate,  collectProviderUpdateCandidates,  collectUpdatedProviderSnapshots,  firstFailedProviderUpdateMessage,  getProviderUpdateInitialToastView,  getProviderUpdateProgressToastView,  getProviderUpdateRejectedToastView,  getProviderUpdateRunningToastView,  providerUpdateNotificationKey,  type ProviderUpdateToastView,} from "./ProviderUpdateLaunchNotification.logic";import { stackedThreadToast, toastManager } from "./ui/toast";import { useAtomCommand } from "../state/use-atom-command"; const seenProviderUpdateNotificationKeys = new Set<string>();type ProviderUpdateToastId = ReturnType<typeof toastManager.add>; type ActiveProviderUpdateToast =  | { readonly kind: "prompt"; readonly key: string; readonly toastId: ProviderUpdateToastId }  | {      readonly kind: "update";      readonly key: string;      readonly toastId: ProviderUpdateToastId;      readonly providerInstanceIds: ReadonlySet<ProviderInstanceId>;      readonly providerCount: number;    }; function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind }) {  const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[provider];   if (!ProviderIcon) {    return (      <span className="relative inline-flex size-4 shrink-0 items-center justify-center">        <DownloadIcon aria-hidden="true" className="size-4 text-success" strokeWidth={2.5} />      </span>    );  }   return (    <span className="relative inline-flex size-4 shrink-0 items-center justify-center">      <ProviderIcon aria-hidden="true" className="size-4" />      <span className="absolute -right-1 -bottom-1 inline-flex size-3 items-center justify-center rounded-full bg-popover">        <DownloadIcon aria-hidden="true" className="size-2.5 text-success" strokeWidth={2.5} />      </span>    </span>  );} function updateProviderUpdateToast(input: {  readonly toastId: ProviderUpdateToastId;  readonly view: ProviderUpdateToastView;  readonly openSettings: () => void;}) {  if (input.view.type === "loading" || input.view.type === "success") {    toastManager.update(input.toastId, {      type: input.view.type,      title: input.view.title,      description: input.view.description,      timeout: 0,      data: {        hideCopyButton: true,        ...(input.view.dismissAfterVisibleMs !== undefined          ? { dismissAfterVisibleMs: input.view.dismissAfterVisibleMs }          : {}),      },    });    return;  }   toastManager.update(    input.toastId,    stackedThreadToast({      type: input.view.type,      title: input.view.title,      description: input.view.description,      timeout: 0,      actionProps: {        children: "Settings",        onClick: input.openSettings,      },      actionVariant: "outline",      data: {        hideCopyButton: true,      },    }),  );} function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) {  return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded";} /** * The single-prompt provider update notification used when there is only one * local environment (no WSL backend). Non-WSL users see exactly this flow — the * per-environment split is gated behind WSL presence. */export function ProviderUpdatePrimaryNotification() {  const navigate = useNavigate();  const providers = useAtomValue(primaryServerProvidersAtom);  const primaryEnvironment = usePrimaryEnvironment();  const updateProvider = useAtomCommand(serverEnvironment.updateProvider, {    reportFailure: false,  });  const activeToastRef = useRef<ActiveProviderUpdateToast | null>(null);  const { dismissedNotificationKeys, dismissNotificationKey } =    useDismissedProviderUpdateNotificationKeys();   // If this flow unmounts (e.g. a WSL backend appears and we switch to the  // per-environment popover), close any prompt it owns so it does not linger.  useEffect(() => {    return () => {      const activeToast = activeToastRef.current;      if (activeToast) {        toastManager.close(activeToast.toastId);        activeToastRef.current = null;      }    };  }, []);   const updateProviders = useMemo(() => collectProviderUpdateCandidates(providers), [providers]);  const notificationKey = useMemo(    () => providerUpdateNotificationKey(updateProviders),    [updateProviders],  );  const oneClickProviders = useMemo(    () =>      updateProviders.filter((provider) => canOneClickUpdateProviderCandidate(provider, providers)),    [providers, updateProviders],  );   const openProviderSettings = useCallback(    (toastId?: ProviderUpdateToastId) => {      const activeToast = activeToastRef.current;      if (toastId !== undefined) {        toastManager.close(toastId);      } else if (activeToast) {        toastManager.close(activeToast.toastId);      }      if (activeToast && (toastId === undefined || activeToast.toastId === toastId)) {        activeToastRef.current = null;      }      void navigate({ to: "/settings/providers" });    },    [navigate],  );   useEffect(() => {    const activeToast = activeToastRef.current;    if (activeToast?.kind !== "update") {      return;    }     const activeProviders = providers.filter((provider) =>      activeToast.providerInstanceIds.has(provider.instanceId),    );    const view = getProviderUpdateProgressToastView({      providers: activeProviders,      providerCount: activeToast.providerCount,    });    updateProviderUpdateToast({      toastId: activeToast.toastId,      view,      openSettings: () => openProviderSettings(activeToast.toastId),    });     if (isTerminalProviderUpdateToastView(view)) {      activeToastRef.current = null;    }  }, [providers, openProviderSettings]);   useEffect(() => {    const activeToast = activeToastRef.current;    if (activeToast?.kind === "prompt" && activeToast.key !== notificationKey) {      toastManager.close(activeToast.toastId);      activeToastRef.current = null;    }     if (      !notificationKey ||      dismissedNotificationKeys.has(notificationKey) ||      seenProviderUpdateNotificationKeys.has(notificationKey) ||      activeToastRef.current    ) {      return;    }     seenProviderUpdateNotificationKeys.add(notificationKey);     const initialView = getProviderUpdateInitialToastView({ updateProviders, oneClickProviders });     let toastId!: ProviderUpdateToastId;    let updateStarted = false;    const openSettings = () => openProviderSettings(toastId);    const dismissPrompt = () => {      dismissNotificationKey(notificationKey);    };     const runUpdates = () => {      if (updateStarted || oneClickProviders.length === 0 || !primaryEnvironment) {        return;      }      updateStarted = true;       const providerCount = oneClickProviders.length;      const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId));      activeToastRef.current = {        kind: "update",        key: notificationKey,        toastId,        providerInstanceIds,        providerCount,      };       updateProviderUpdateToast({        toastId,        view: getProviderUpdateRunningToastView(providerCount),        openSettings,      });       void (async () => {        const results = [];        for (const provider of oneClickProviders) {          results.push(            await updateProvider({              environmentId: primaryEnvironment.environmentId,              input: {                provider: provider.driver,                instanceId: provider.instanceId,              },            }),          );        }         const activeUpdateToast = activeToastRef.current;        if (activeUpdateToast?.kind !== "update" || activeUpdateToast.toastId !== toastId) {          return;        }         const failedMessage = firstFailedProviderUpdateMessage(results);        if (failedMessage) {          updateProviderUpdateToast({            toastId,            view: getProviderUpdateRejectedToastView(providerCount, failedMessage),            openSettings,          });          activeToastRef.current = null;          return;        }         const updatedProviderSnapshots = collectUpdatedProviderSnapshots({          results,          providerInstanceIds,        });        const view = getProviderUpdateProgressToastView({          providers: updatedProviderSnapshots,          providerCount,        });        updateProviderUpdateToast({          toastId,          view,          openSettings,        });         if (isTerminalProviderUpdateToastView(view)) {          activeToastRef.current = null;        }      })();    };     toastId = toastManager.add(      stackedThreadToast({        type: initialView.type,        title: initialView.title,        description: initialView.description,        timeout: 0,        actionProps:          oneClickProviders.length > 0            ? {                children: "Update",                onClick: runUpdates,              }            : {                children: "Settings",                onClick: openSettings,              },        actionVariant: oneClickProviders.length > 0 ? "default" : "outline",        data: {          leadingIcon:            updateProviders.length === 1 ? (              <ProviderUpdateToastIcon provider={updateProviders[0]!.driver} />            ) : undefined,          hideCopyButton: true,          onClose: dismissPrompt,          ...(oneClickProviders.length > 0            ? {                secondaryActionProps: {                  children: "Settings",                  onClick: openSettings,                },                secondaryActionVariant: "outline" as const,              }            : {}),        },      }),    );    activeToastRef.current = { kind: "prompt", key: notificationKey, toastId };  }, [    updateProvider,    dismissNotificationKey,    dismissedNotificationKeys,    notificationKey,    oneClickProviders,    openProviderSettings,    primaryEnvironment,    updateProviders,  ]);   return null;}