1import {
2 isAtomCommandInterrupted,
3 squashAtomCommandFailure,
4 type AtomCommandResult,
5} from "@t3tools/client-runtime/state/runtime";
6import {
7 ANTIGRAVITY_AUTH_METHODS,
8 type AntigravityAuthMethod,
9 type EnvironmentId,
10 type ProviderInstanceId,
11 type ServerProvider,
12} from "@t3tools/contracts";
13import { useRef, useState } from "react";
14import { Trash2Icon } from "lucide-react";
15
16import { ensureLocalApi } from "../../localApi";
17import { useEnvironmentQuery } from "../../state/query";
18import { serverEnvironment } from "../../state/server";
19import { useAtomCommand } from "../../state/use-atom-command";
20import { Button } from "../ui/button";
21import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
22import { SettingsRow } from "./settingsLayout";
23import { ProviderAuthenticationSection } from "./ProviderAuthenticationSection";
24
25interface ProviderSetupSectionProps {
26 readonly environmentId: EnvironmentId;
27 readonly environmentLabel: string;
28 readonly instanceId: ProviderInstanceId;
29 readonly provider: ServerProvider | undefined;
30 readonly binaryPath?: string | undefined;
31 readonly authMethod?: AntigravityAuthMethod | undefined;
32 readonly enabled: boolean;
33 readonly readOnly: boolean;
34 readonly onEnable: () => void;
35}
36
37/** Read the configured method from the instance config. Unknown values fall back to personal. */
38export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMethod {
39 const value =
40 config !== null && typeof config === "object" && "authMethod" in config
41 ? config.authMethod
42 : undefined;
43 return (
44 ANTIGRAVITY_AUTH_METHODS.find((method) => method.value === value)?.value ?? "oauth-personal"
45 );
46}
47
48/** Setup state belongs to the selected environment and is never saved in client settings. */
49export function ProviderSetupSection(props: ProviderSetupSectionProps) {
50 return (
51 <section
52 aria-label="Antigravity setup"
53 className="@container/setup divide-y divide-border/50 text-xs"
54 >
55 <SettingsRow
56 className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3"
57 title="Environment"
58 description="Device that runs this provider."
59 control={
60 <div className="flex min-w-0 flex-col gap-2 sm:items-end">
61 <span className="text-muted-foreground [overflow-wrap:anywhere]">
62 {props.environmentLabel}
63 </span>
64 {!props.enabled && !props.readOnly ? (
65 <Button size="sm" variant="outline" onClick={props.onEnable}>
66 Enable Antigravity
67 </Button>
68 ) : null}
69 </div>
70 }
71 />
72 {props.readOnly ? (
73 <SettingsRow title="Setup unavailable" description="Provider setup is read-only." />
74 ) : props.provider?.setup === undefined ? (
75 <SettingsRow
76 title="Update required"
77 description="Update this environment to manage Antigravity."
78 />
79 ) : (
80 <ProviderSetupActions
81 key={`${props.environmentId}:${props.instanceId}`}
82 environmentId={props.environmentId}
83 environmentLabel={props.environmentLabel}
84 instanceId={props.instanceId}
85 provider={props.provider}
86 binaryPath={props.binaryPath}
87 authMethod={props.authMethod ?? "oauth-personal"}
88 enabled={props.enabled}
89 />
90 )}
91 </section>
92 );
93}
94
95function ProviderSetupActions({
96 environmentId,
97 environmentLabel,
98 instanceId,
99 provider,
100 enabled,
101 binaryPath,
102}: Pick<
103 ProviderSetupSectionProps,
104 "environmentId" | "environmentLabel" | "instanceId" | "enabled" | "binaryPath"
105> & {
106 readonly provider: ServerProvider;
107 readonly authMethod: AntigravityAuthMethod;
108}) {
109 const target = { environmentId, input: { instanceId } };
110 const authQuery = useEnvironmentQuery(serverEnvironment.providerAuthState(target));
111 const installQuery = useEnvironmentQuery(serverEnvironment.providerInstallState(target));
112 const auth = authQuery.data;
113 const installation = installQuery.data;
114 const commandOptions = { reportFailure: false, reportDefect: false };
115 const startInstall = useAtomCommand(serverEnvironment.startProviderInstall, commandOptions);
116 const cancelInstall = useAtomCommand(serverEnvironment.cancelProviderInstall, commandOptions);
117 const removeInstall = useAtomCommand(
118 serverEnvironment.removeProviderInstallation,
119 commandOptions,
120 );
121 const [pendingLabel, setPendingLabel] = useState<string | null>(null);
122 const pendingRef = useRef(false);
123 const [error, setError] = useState<string | null>(null);
124 const authActive =
125 auth?.phase === "starting" || auth?.phase === "waiting" || auth?.phase === "verifying";
126 const installActive =
127 installation?.phase === "downloading" ||
128 installation?.phase === "extracting" ||
129 installation?.phase === "verifying";
130 const usesCustomBinary = Boolean(binaryPath?.trim());
131 const installed =
132 provider.installed || (!usesCustomBinary && installation?.installedVersion != null);
133 const queryError = authQuery.error ?? installQuery.error;
134 const actionsDisabled = pendingLabel !== null || queryError !== null;
135 const installationStatusMessage =
136 installation?.phase === "downloading"
137 ? `Downloading ${(installation.downloadedBytes / 1_000_000).toFixed(1)} MB${installation.totalBytes === null ? "" : ` of ${(installation.totalBytes / 1_000_000).toFixed(1)} MB`}.`
138 : installation?.phase === "extracting"
139 ? "Extracting Antigravity."
140 : installation?.phase === "verifying"
141 ? "Checking the downloaded runtime."
142 : installed
143 ? "Installed."
144 : usesCustomBinary
145 ? enabled
146 ? "The configured Antigravity runtime is unavailable."
147 : "The configured Antigravity runtime has not been checked."
148 : installation?.totalBytes
149 ? `${Math.ceil(installation.totalBytes / 1_000_000)} MB download.`
150 : "Not installed.";
151
152 async function runCommand<A, E>(
153 label: string,
154 request: () => Promise<AtomCommandResult<A, E>>,
155 ): Promise<boolean> {
156 if (pendingRef.current) return false;
157 pendingRef.current = true;
158 setPendingLabel(label);
159 setError(null);
160 try {
161 const result = await request();
162 if (result._tag === "Failure") {
163 if (!isAtomCommandInterrupted(result)) {
164 const failure = squashAtomCommandFailure(result);
165 setError(failure instanceof Error ? failure.message : "Provider setup failed.");
166 }
167 return false;
168 }
169 return true;
170 } catch {
171 setError("Provider setup failed. Try again.");
172 return false;
173 } finally {
174 pendingRef.current = false;
175 setPendingLabel(null);
176 }
177 }
178
179 async function removeRuntime() {
180 const confirmed = await ensureLocalApi().dialogs.confirm(
181 `Remove the downloaded Antigravity runtime from ${environmentLabel}? Google sign-in and thread history are kept.`,
182 );
183 if (confirmed) {
184 await runCommand("Removing runtime", () => removeInstall(target));
185 }
186 }
187
188 return (
189 <div className="divide-y divide-border/50">
190 <SettingsRow
191 title="Runtime"
192 className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3"
193 description="Install and manage Antigravity."
194 status={
195 <div className="space-y-2">
196 {usesCustomBinary ? (
197 <p className="text-muted-foreground">
198 Uses the custom binary path below. Installation keeps that path.
199 </p>
200 ) : null}
201 {!installed && !provider.setup?.canInstall ? (
202 <p className="text-muted-foreground">
203 Automatic installation unavailable. Set a binary path or use another environment.
204 </p>
205 ) : null}
206 </div>
207 }
208 control={
209 <div className="flex w-full min-w-0 flex-col gap-2 @min-[32rem]/settings-row:w-56 @min-[32rem]/settings-row:text-right">
210 <p role="status" className="min-h-4 text-muted-foreground tabular-nums">
211 {installationStatusMessage}
212 </p>
213 <div className="h-1">
214 {installation?.phase === "downloading" &&
215 installation.totalBytes !== null &&
216 installation.totalBytes > 0 ? (
217 <progress
218 aria-label="Antigravity download"
219 className="block h-1 w-full accent-foreground"
220 value={installation.downloadedBytes}
221 max={installation.totalBytes}
222 />
223 ) : null}
224 </div>
225 {!installActive &&
226 installation?.message &&
227 installation.message !== installationStatusMessage ? (
228 <p className="text-muted-foreground [overflow-wrap:anywhere]">
229 {installation.message}
230 </p>
231 ) : null}
232 <div className="grid min-h-7 grid-cols-[1.75rem_minmax(0,1fr)] gap-2">
233 <div className="col-start-2 row-start-1 grid">
234 {installActive && installation.operationId ? (
235 <Button
236 size="sm"
237 variant="outline"
238 disabled={actionsDisabled}
239 onClick={() => {
240 const operationId = installation.operationId;
241 if (!operationId) return;
242 void runCommand("Cancelling installation", () =>
243 cancelInstall({ environmentId, input: { instanceId, operationId } }),
244 );
245 }}
246 >
247 Cancel installation
248 </Button>
249 ) : !installActive && provider.setup?.canInstall ? (
250 <Button
251 size="sm"
252 variant="outline"
253 disabled={actionsDisabled || installation === null || authActive}
254 onClick={() =>
255 void runCommand("Starting installation", () => startInstall(target))
256 }
257 >
258 {installation?.installedVersion
259 ? installation.version &&
260 installation.version !== installation.installedVersion
261 ? "Update Antigravity"
262 : "Reinstall Antigravity"
263 : installation?.phase === "failed" || installation?.phase === "cancelled"
264 ? "Retry installation"
265 : installed
266 ? "Install managed runtime"
267 : "Install Antigravity"}
268 </Button>
269 ) : null}
270 </div>
271 {installation?.canRemove && !installActive ? (
272 <Tooltip>
273 <TooltipTrigger
274 render={
275 <Button
276 size="icon-sm"
277 variant="ghost"
278 className="col-start-1 row-start-1"
279 aria-label="Remove downloaded runtime"
280 disabled={actionsDisabled || authActive}
281 onClick={() => void removeRuntime()}
282 />
283 }
284 >
285 <Trash2Icon className="size-3.5" />
286 </TooltipTrigger>
287 <TooltipPopup>Remove downloaded runtime</TooltipPopup>
288 </Tooltip>
289 ) : null}
290 </div>
291 </div>
292 }
293 />
294
295 <ProviderAuthenticationSection
296 environmentId={environmentId}
297 environmentLabel={environmentLabel}
298 instanceId={instanceId}
299 provider={provider}
300 readOnly={false}
301 />
302
303 <p className="sr-only" role="status">
304 {pendingLabel ? `${pendingLabel}.` : null}
305 </p>
306 {error || queryError ? (
307 <div className="grid gap-2 px-3 py-3 sm:px-4">
308 <p role="alert" className="text-destructive [overflow-wrap:anywhere]">
309 {error ?? queryError}
310 </p>
311 {queryError ? (
312 <Button
313 size="sm"
314 variant="outline"
315 className="w-fit"
316 onClick={() => {
317 authQuery.refresh();
318 installQuery.refresh();
319 }}
320 >
321 Retry setup status
322 </Button>
323 ) : null}
324 </div>
325 ) : null}
326 </div>
327 );
328}