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(); type ProviderUpdateToastId = ReturnType; type ActiveProviderUpdateToast = | { readonly kind: "prompt"; readonly key: string; readonly toastId: ProviderUpdateToastId } | { readonly kind: "update"; readonly key: string; readonly toastId: ProviderUpdateToastId; readonly providerInstanceIds: ReadonlySet; readonly providerCount: number; }; function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind }) { const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[provider]; if (!ProviderIcon) { return ( ); } return ( ); } 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(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 ? ( ) : 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; }