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