| 1 | import { type ProviderInstanceId, type ServerProvider } from "@t3tools/contracts"; |
| 2 | import { memo } from "react"; |
| 3 | import { InfoIcon, XIcon } from "lucide-react"; |
| 4 | import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert"; |
| 5 | import { Button, InlineButton } from "../ui/button"; |
| 6 | import { formatProviderDriverKindLabel } from "../../providerModels"; |
| 7 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 8 | |
| 9 | /** Unsupported and broken versions fail mid-turn, so they warn even when ready. */ |
| 10 | function getIncompatibleVersion(status: ServerProvider) { |
| 11 | const compatibility = status.compatibilityAdvisory; |
| 12 | if (status.status === "error" && status.auth.status === "unauthenticated") return null; |
| 13 | return compatibility?.status === "broken" || |
| 14 | (status.status === "ready" && compatibility?.status === "unsupported") |
| 15 | ? compatibility |
| 16 | : null; |
| 17 | } |
| 18 | |
| 19 | export function getProviderStatusBannerKey(status: ServerProvider | null): string | null { |
| 20 | if (!status || status.status === "disabled") return null; |
| 21 | const incompatible = getIncompatibleVersion(status); |
| 22 | if (incompatible) { |
| 23 | return [ |
| 24 | status.instanceId, |
| 25 | incompatible.status, |
| 26 | status.version ?? "", |
| 27 | incompatible.message ?? "", |
| 28 | ].join("\u0000"); |
| 29 | } |
| 30 | if (status.status === "ready") return null; |
| 31 | // Antigravity checks saved credentials when a session starts. Its local |
| 32 | // health check leaves auth unknown after a restart, which is not a failure. |
| 33 | if ( |
| 34 | status.driver === "antigravity" && |
| 35 | status.installed && |
| 36 | status.status === "warning" && |
| 37 | status.auth.status === "unknown" |
| 38 | ) { |
| 39 | return null; |
| 40 | } |
| 41 | return [status.instanceId, status.status, status.auth.status, status.message ?? ""].join( |
| 42 | "\u0000", |
| 43 | ); |
| 44 | } |
| 45 | |
| 46 | export function shouldShowProviderStatusBanner( |
| 47 | status: ServerProvider | null, |
| 48 | dismissedBannerKey: string | null, |
| 49 | ): boolean { |
| 50 | const bannerKey = getProviderStatusBannerKey(status); |
| 51 | return bannerKey !== null && bannerKey !== dismissedBannerKey; |
| 52 | } |
| 53 | |
| 54 | export function hasProviderSetup(status: ServerProvider): boolean { |
| 55 | return ( |
| 56 | status.driver === "antigravity" || |
| 57 | status.setup?.canAuthenticate === true || |
| 58 | status.setup?.canInstall === true |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | /** Broken-version guidance takes precedence over startup failures it can cause. */ |
| 63 | export function getProviderStatusMessage(status: ServerProvider): string { |
| 64 | if ( |
| 65 | status.auth.status !== "unauthenticated" && |
| 66 | status.compatibilityAdvisory?.status === "broken" && |
| 67 | status.compatibilityAdvisory.message |
| 68 | ) { |
| 69 | return status.compatibilityAdvisory.message; |
| 70 | } |
| 71 | if (status.message) return status.message; |
| 72 | const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver); |
| 73 | if (!status.installed && hasProviderSetup(status)) { |
| 74 | return `Open provider setup to install ${formatProviderDriverKindLabel(status.driver)} on this environment.`; |
| 75 | } |
| 76 | if (status.auth.status === "unauthenticated") { |
| 77 | if (hasProviderSetup(status)) { |
| 78 | return status.driver === "antigravity" |
| 79 | ? "Open provider setup to sign in with Google." |
| 80 | : "Open provider setup to sign in."; |
| 81 | } |
| 82 | return "Sign in via the CLI to authenticate again."; |
| 83 | } |
| 84 | return status.status === "ready" |
| 85 | ? "No models are available for this provider." |
| 86 | : status.status === "error" |
| 87 | ? `${providerName} provider is unavailable.` |
| 88 | : `${providerName} provider has limited availability.`; |
| 89 | } |
| 90 | |
| 91 | export const ProviderStatusBanner = memo(function ProviderStatusBanner({ |
| 92 | onDismiss, |
| 93 | onOpenProviderSetup, |
| 94 | status, |
| 95 | }: { |
| 96 | onDismiss: () => void; |
| 97 | onOpenProviderSetup?: (instanceId: ProviderInstanceId) => void; |
| 98 | status: ServerProvider | null; |
| 99 | }) { |
| 100 | if (!status || getProviderStatusBannerKey(status) === null) { |
| 101 | return null; |
| 102 | } |
| 103 | |
| 104 | const providerName = status.displayName?.trim() || formatProviderDriverKindLabel(status.driver); |
| 105 | const isUnauthenticated = status.status === "error" && status.auth.status === "unauthenticated"; |
| 106 | const incompatible = getIncompatibleVersion(status); |
| 107 | const title = isUnauthenticated |
| 108 | ? `${providerName} is unauthenticated` |
| 109 | : incompatible |
| 110 | ? `${providerName} ${status.version ?? ""} is ${incompatible.status === "broken" ? "known to be broken" : "unsupported"}` |
| 111 | : `${providerName} provider status`; |
| 112 | const message = incompatible?.message ?? getProviderStatusMessage(status); |
| 113 | const isWarning = |
| 114 | incompatible?.status !== "broken" && (status.status === "warning" || incompatible !== null); |
| 115 | |
| 116 | return ( |
| 117 | <div className="pointer-events-auto mx-auto w-fit max-w-[calc(100%-2rem)] pt-3"> |
| 118 | <Alert |
| 119 | variant={isWarning ? "warning" : "error"} |
| 120 | role={incompatible && incompatible.status !== "broken" ? "status" : "alert"} |
| 121 | surface="glass" |
| 122 | controlAlignment="first-line" |
| 123 | > |
| 124 | <InfoIcon /> |
| 125 | <AlertTitle>{title}</AlertTitle> |
| 126 | <AlertDescription> |
| 127 | <Tooltip> |
| 128 | <TooltipTrigger render={<div className="line-clamp-3" />}>{message}</TooltipTrigger> |
| 129 | <TooltipPopup side="top" className="whitespace-pre-wrap"> |
| 130 | {message} |
| 131 | </TooltipPopup> |
| 132 | </Tooltip> |
| 133 | {onOpenProviderSetup && hasProviderSetup(status) ? ( |
| 134 | <InlineButton onClick={() => onOpenProviderSetup(status.instanceId)}> |
| 135 | Open provider setup |
| 136 | </InlineButton> |
| 137 | ) : null} |
| 138 | </AlertDescription> |
| 139 | <AlertAction> |
| 140 | <Button |
| 141 | aria-label={`Dismiss ${providerName} provider ${status.status}`} |
| 142 | onClick={onDismiss} |
| 143 | size="icon-xs" |
| 144 | variant="ghost-muted" |
| 145 | > |
| 146 | <XIcon /> |
| 147 | </Button> |
| 148 | </AlertAction> |
| 149 | </Alert> |
| 150 | </div> |
| 151 | ); |
| 152 | }); |