1import { type ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
2import { memo } from "react";
3import { InfoIcon, XIcon } from "lucide-react";
4import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert";
5import { Button, InlineButton } from "../ui/button";
6import { formatProviderDriverKindLabel } from "../../providerModels";
7import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
8
9/** Unsupported and broken versions fail mid-turn, so they warn even when ready. */
10function 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
19export 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
46export 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
54export 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. */
63export 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
91export 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});