authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-11 21:56:20-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-11 21:56:20-03:00
log4d16e6bc6f0be52c5d44b8864000c70c7825e4f7
tree3f97f369efd9272d3f50661f56636bc98b9290db
parent1f73a89fc4d5c8b3e188282aeb8173f8f8bf257b
signature Signed by PGP key B5690EEEBB952194

fix(web): align provider settings with shared settings rows (#10571)


5 files changed, 386 insertions(+), 327 deletions(-)

apps/web/src/components/settings/ProviderInstanceCard.tsx+94-98
...@@ -252,93 +252,98 @@ function ProviderEnvironmentSection(props: {...@@ -252,93 +252,98 @@ function ProviderEnvironmentSection(props: {
252 ]);252 ]);
253253
254 return (254 return (
255 <div className="mt-3 min-w-0 space-y-2">255 <SettingsRow
256 {rows.map((variable, index) => (256 title="Variables"
257 <div key={variable.id} className="flex min-w-0 items-center gap-1.5">257 description="API keys, base URLs, and other per-instance CLI settings."
258 <DraftInput258 control={
259 size="sm"259 <Button type="button" size="sm" variant="outline" onClick={addVariable}>
260 className="w-44 shrink-0 font-mono"
261 value={variable.name}
262 onCommit={(name) => updateVariable(variable.id, { name: name.trim() })}
263 placeholder="VARIABLE_NAME"
264 spellCheck={false}
265 aria-label={`Environment variable name ${index + 1}`}
266 />
267 <span className="text-xs text-muted-foreground" aria-hidden>
268 =
269 </span>
270 <DraftInput
271 size="sm"
272 className="min-w-0 flex-1 font-mono"
273 value={variable.valueRedacted ? "" : variable.value}
274 onCommit={(value) => updateVariable(variable.id, { value })}
275 type={variable.sensitive ? "password" : undefined}
276 autoComplete="off"
277 placeholder={
278 variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value"
279 }
280 spellCheck={false}
281 aria-label={`Environment variable value ${index + 1}`}
282 />
283 <Tooltip>
284 <TooltipTrigger
285 render={
286 <Button
287 type="button"
288 size="icon-micro"
289 variant="ghost-muted"
290 className={cn(
291 "[--control-icon-color:currentColor]",
292 variable.sensitive && "text-foreground",
293 )}
294 onClick={() => {
295 const sensitive = !variable.sensitive;
296 updateVariable(variable.id, {
297 sensitive,
298 ...(sensitive && variable.valueRedacted === undefined
299 ? {}
300 : { valueRedacted: sensitive ? variable.valueRedacted : false }),
301 });
302 }}
303 aria-pressed={variable.sensitive}
304 aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`}
305 >
306 {variable.sensitive ? (
307 <LockIcon className="size-3" />
308 ) : (
309 <LockOpenIcon className="size-3" />
310 )}
311 </Button>
312 }
313 />
314 <TooltipPopup side="top">
315 {variable.sensitive ? "Sensitive, stored separately" : "Plain text"}
316 </TooltipPopup>
317 </Tooltip>
318 <Button
319 type="button"
320 size="icon-micro"
321 variant="ghost-muted"
322 className="[--control-icon-color:currentColor] hover:text-destructive"
323 onClick={() => removeVariable(variable.id)}
324 aria-label={`Remove environment variable ${variable.name || index + 1}`}
325 >
326 <XIcon className="size-3" />
327 </Button>
328 </div>
329 ))}
330 <div className="flex min-h-[1.875rem] flex-wrap items-center justify-end gap-x-3 gap-y-1">
331 {rows.length > 0 ? (
332 <span className="mr-auto text-xs text-muted-foreground">
333 Sensitive values are stored separately and never returned to the app.
334 </span>
335 ) : null}
336 <Button type="button" size="xs" variant="ghost-muted" onClick={addVariable}>
337 <PlusIcon className="size-3" />260 <PlusIcon className="size-3" />
338 Add variable261 Add variable
339 </Button>262 </Button>
340 </div>263 }
341 </div>264 >
265 {rows.length > 0 ? (
266 <div className="mt-3 min-w-0 space-y-2 pb-2">
267 {rows.map((variable, index) => (
268 <div key={variable.id} className="flex min-w-0 flex-wrap items-center gap-1.5">
269 <DraftInput
270 size="sm"
271 className="w-full min-w-0 font-mono sm:w-44 sm:shrink-0"
272 value={variable.name}
273 onCommit={(name) => updateVariable(variable.id, { name: name.trim() })}
274 placeholder="VARIABLE_NAME"
275 spellCheck={false}
276 aria-label={`Environment variable name ${index + 1}`}
277 />
278 <span className="hidden text-xs text-muted-foreground sm:inline" aria-hidden>
279 =
280 </span>
281 <DraftInput
282 size="sm"
283 className="min-w-0 flex-1 font-mono"
284 value={variable.valueRedacted ? "" : variable.value}
285 onCommit={(value) => updateVariable(variable.id, { value })}
286 type={variable.sensitive ? "password" : undefined}
287 autoComplete="off"
288 placeholder={
289 variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value"
290 }
291 spellCheck={false}
292 aria-label={`Environment variable value ${index + 1}`}
293 />
294 <Tooltip>
295 <TooltipTrigger
296 render={
297 <Button
298 type="button"
299 size="icon-micro"
300 variant="ghost-muted"
301 className={cn(
302 "[--control-icon-color:currentColor]",
303 variable.sensitive && "text-foreground",
304 )}
305 onClick={() => {
306 const sensitive = !variable.sensitive;
307 updateVariable(variable.id, {
308 sensitive,
309 ...(sensitive && variable.valueRedacted === undefined
310 ? {}
311 : { valueRedacted: sensitive ? variable.valueRedacted : false }),
312 });
313 }}
314 aria-pressed={variable.sensitive}
315 aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`}
316 >
317 {variable.sensitive ? (
318 <LockIcon className="size-3" />
319 ) : (
320 <LockOpenIcon className="size-3" />
321 )}
322 </Button>
323 }
324 />
325 <TooltipPopup side="top">
326 {variable.sensitive ? "Sensitive, stored separately" : "Plain text"}
327 </TooltipPopup>
328 </Tooltip>
329 <Button
330 type="button"
331 size="icon-micro"
332 variant="ghost-muted"
333 className="[--control-icon-color:currentColor] hover:text-destructive"
334 onClick={() => removeVariable(variable.id)}
335 aria-label={`Remove environment variable ${variable.name || index + 1}`}
336 >
337 <XIcon className="size-3" />
338 </Button>
339 </div>
340 ))}
341 <p className="text-xs text-muted-foreground">
342 Sensitive values are stored separately and never returned to the app.
343 </p>
344 </div>
345 ) : null}
346 </SettingsRow>
342 );347 );
343}348}
344349
...@@ -837,11 +842,7 @@ export function ProviderInstanceCard({...@@ -837,11 +842,7 @@ export function ProviderInstanceCard({
837 />842 />
838 </SettingsSection>843 </SettingsSection>
839844
840 {setup ? (845 {setup ? <SettingsSection title="Setup">{setup}</SettingsSection> : null}
841 <SettingsSection title="Setup">
842 <div className="px-3 py-3 sm:px-4">{setup}</div>
843 </SettingsSection>
844 ) : null}
845846
846 <SettingsSection847 <SettingsSection
847 title="Runtime"848 title="Runtime"
...@@ -877,15 +878,10 @@ export function ProviderInstanceCard({...@@ -877,15 +878,10 @@ export function ProviderInstanceCard({
877 aria-disabled={readOnly || undefined}878 aria-disabled={readOnly || undefined}
878 className={readOnly ? "opacity-50 select-none" : undefined}879 className={readOnly ? "opacity-50 select-none" : undefined}
879 >880 >
880 <SettingsRow881 <ProviderEnvironmentSection
881 title="Variables"882 environment={instance.environment ?? []}
882 description="API keys, base URLs, and other per-instance CLI settings."883 onChange={updateEnvironment}
883 >884 />
884 <ProviderEnvironmentSection
885 environment={instance.environment ?? []}
886 onChange={updateEnvironment}
887 />
888 </SettingsRow>
889 </SettingsSection>885 </SettingsSection>
890886
891 {driverOption !== undefined ? (887 {driverOption !== undefined ? (
apps/web/src/components/settings/ProviderModelsSection.tsx+14-13
...@@ -514,7 +514,7 @@ export function ProviderModelsSection({...@@ -514,7 +514,7 @@ export function ProviderModelsSection({
514 onChange={(event) => setFilter(event.target.value)}514 onChange={(event) => setFilter(event.target.value)}
515 placeholder="Filter models"515 placeholder="Filter models"
516 size="sm"516 size="sm"
517 className="w-56"517 className="w-56 max-w-full"
518 spellCheck={false}518 spellCheck={false}
519 aria-label="Filter models"519 aria-label="Filter models"
520 />520 />
...@@ -540,6 +540,18 @@ export function ProviderModelsSection({...@@ -540,6 +540,18 @@ export function ProviderModelsSection({
540 {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}540 {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}
541 </span>541 </span>
542 </div>542 </div>
543 {driverKind !== "antigravity" && !isAdding ? (
544 <Button
545 type="button"
546 size="xs"
547 variant="ghost-muted"
548 className="ml-auto"
549 onClick={() => setIsAdding(true)}
550 >
551 <PlusIcon className="size-3" />
552 Add custom model
553 </Button>
554 ) : null}
543 </div>555 </div>
544 <div556 <div
545 ref={listRef}557 ref={listRef}
...@@ -619,18 +631,7 @@ export function ProviderModelsSection({...@@ -619,18 +631,7 @@ export function ProviderModelsSection({
619 </Button>631 </Button>
620 </div>632 </div>
621 </div>633 </div>
622 ) : (634 ) : null}
623 <Button
624 type="button"
625 size="xs"
626 variant="ghost-muted"
627 className="mt-2 -ml-2"
628 onClick={() => setIsAdding(true)}
629 >
630 <PlusIcon className="size-3" />
631 Add custom model
632 </Button>
633 )}
634635
635 {driverKind !== "antigravity" && error ? (636 {driverKind !== "antigravity" && error ? (
636 <p className="mt-2 text-xs text-destructive">{error}</p>637 <p className="mt-2 text-xs text-destructive">{error}</p>
apps/web/src/components/settings/ProviderSetupSection.test.tsx+3-1
...@@ -138,7 +138,9 @@ function renderSetup(...@@ -138,7 +138,9 @@ function renderSetup(
138function button(view: unknown, label: string) {138function button(view: unknown, label: string) {
139 return visitElements(139 return visitElements(
140 view,140 view,
141 (element) => element.props.children === label && typeof element.props.onClick === "function",141 (element) =>
142 (element.props.children === label || element.props["aria-label"] === label) &&
143 typeof element.props.onClick === "function",
142 );144 );
143}145}
144146
apps/web/src/components/settings/ProviderSetupSection.tsx+271-209
...@@ -12,6 +12,7 @@ import {...@@ -12,6 +12,7 @@ import {
12 type ServerProvider,12 type ServerProvider,
13} from "@t3tools/contracts";13} from "@t3tools/contracts";
14import { useRef, useState } from "react";14import { useRef, useState } from "react";
15import { Trash2Icon } from "lucide-react";
1516
16import { writeTextToClipboard } from "../../hooks/useCopyToClipboard";17import { writeTextToClipboard } from "../../hooks/useCopyToClipboard";
17import { ensureLocalApi } from "../../localApi";18import { ensureLocalApi } from "../../localApi";
...@@ -20,6 +21,8 @@ import { serverEnvironment } from "../../state/server";...@@ -20,6 +21,8 @@ import { serverEnvironment } from "../../state/server";
20import { useAtomCommand } from "../../state/use-atom-command";21import { useAtomCommand } from "../../state/use-atom-command";
21import { Button } from "../ui/button";22import { Button } from "../ui/button";
22import { Input } from "../ui/input";23import { Input } from "../ui/input";
24import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
25import { SettingsRow } from "./settingsLayout";
2326
24interface ProviderSetupSectionProps {27interface ProviderSetupSectionProps {
25 readonly environmentId: EnvironmentId;28 readonly environmentId: EnvironmentId;
...@@ -68,24 +71,34 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho...@@ -68,24 +71,34 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho
68/** Setup state belongs to the selected environment and is never saved in client settings. */71/** Setup state belongs to the selected environment and is never saved in client settings. */
69export function ProviderSetupSection(props: ProviderSetupSectionProps) {72export function ProviderSetupSection(props: ProviderSetupSectionProps) {
70 return (73 return (
71 <section aria-label="Antigravity setup" className="grid gap-3 text-xs">74 <section
72 <p>Antigravity runs on {props.environmentLabel}.</p>75 aria-label="Antigravity setup"
73 {!props.enabled ? (76 className="@container/setup divide-y divide-border/50 text-xs"
74 <div className="flex flex-wrap items-center gap-2">77 >
75 <span className="text-muted-foreground">Enable it to use it in threads.</span>78 <SettingsRow
76 {!props.readOnly ? (79 className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3"
77 <Button size="xs" variant="outline" onClick={props.onEnable}>80 title="Environment"
78 Enable Antigravity81 description="Device that runs this provider."
79 </Button>82 control={
80 ) : null}83 <div className="flex min-w-0 flex-col gap-2 sm:items-end">
81 </div>84 <span className="text-muted-foreground [overflow-wrap:anywhere]">
82 ) : null}85 {props.environmentLabel}
86 </span>
87 {!props.enabled && !props.readOnly ? (
88 <Button size="sm" variant="outline" onClick={props.onEnable}>
89 Enable Antigravity
90 </Button>
91 ) : null}
92 </div>
93 }
94 />
83 {props.readOnly ? (95 {props.readOnly ? (
84 <p className="text-muted-foreground">This connection cannot change provider setup.</p>96 <SettingsRow title="Setup unavailable" description="Provider setup is read-only." />
85 ) : props.provider?.setup === undefined ? (97 ) : props.provider?.setup === undefined ? (
86 <p className="text-muted-foreground">98 <SettingsRow
87 Update this environment to install Antigravity and sign in with Google here.99 title="Update required"
88 </p>100 description="Update this environment to manage Antigravity."
101 />
89 ) : (102 ) : (
90 <ProviderSetupActions103 <ProviderSetupActions
91 key={`${props.environmentId}:${props.instanceId}`}104 key={`${props.environmentId}:${props.instanceId}`}
...@@ -177,12 +190,14 @@ function ProviderSetupActions({...@@ -177,12 +190,14 @@ function ProviderSetupActions({
177 : installation?.phase === "verifying"190 : installation?.phase === "verifying"
178 ? "Checking the downloaded runtime."191 ? "Checking the downloaded runtime."
179 : installed192 : installed
180 ? "Antigravity is installed."193 ? "Installed."
181 : usesCustomBinary194 : usesCustomBinary
182 ? enabled195 ? enabled
183 ? "The configured Antigravity runtime is unavailable."196 ? "The configured Antigravity runtime is unavailable."
184 : "The configured Antigravity runtime has not been checked."197 : "The configured Antigravity runtime has not been checked."
185 : "Install the official Antigravity runtime before signing in.";198 : installation?.totalBytes
199 ? `${Math.ceil(installation.totalBytes / 1_000_000)} MB download.`
200 : "Not installed.";
186201
187 async function runCommand<A, E>(202 async function runCommand<A, E>(
188 label: string,203 label: string,
...@@ -262,210 +277,257 @@ function ProviderSetupActions({...@@ -262,210 +277,257 @@ function ProviderSetupActions({
262 }277 }
263278
264 return (279 return (
265 <div className="grid gap-3">280 <div className="divide-y divide-border/50">
266 <div className="grid gap-2">281 <SettingsRow
267 <p className="font-medium">Runtime</p>282 title="Runtime"
268 <p role="status" className="text-muted-foreground">283 className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3"
269 {installationStatusMessage}284 description="Install and manage Antigravity."
270 </p>285 status={
271 {installation?.phase === "downloading" &&286 <div className="space-y-2">
272 installation.totalBytes !== null &&287 {usesCustomBinary ? (
273 installation.totalBytes > 0 ? (288 <p className="text-muted-foreground">
274 <progress289 Uses the custom binary path below. Installation keeps that path.
275 aria-label="Antigravity download"290 </p>
276 className="h-1 w-full accent-foreground"291 ) : null}
277 value={installation.downloadedBytes}292 {!installed && !provider.setup?.canInstall ? (
278 max={installation.totalBytes}293 <p className="text-muted-foreground">
279 />294 Automatic installation unavailable. Set a binary path or use another environment.
280 ) : null}295 </p>
281 {installation?.message && installation.message !== installationStatusMessage ? (296 ) : null}
282 <p className="text-muted-foreground [overflow-wrap:anywhere]">{installation.message}</p>297 </div>
283 ) : null}298 }
284 {usesCustomBinary ? (299 control={
285 <p className="text-muted-foreground">300 <div className="flex w-full min-w-0 flex-col gap-2 sm:w-56 sm:text-right">
286 This instance uses the binary path below. Installing a managed runtime does not change301 <p role="status" className="min-h-4 text-muted-foreground tabular-nums">
287 that path.302 {installationStatusMessage}
288 </p>303 </p>
289 ) : null}304 <div className="h-1">
290 {!installed && !usesCustomBinary && !installActive && installation?.totalBytes ? (305 {installation?.phase === "downloading" &&
291 <p className="text-muted-foreground">306 installation.totalBytes !== null &&
292 Downloads {Math.ceil(installation.totalBytes / 1_000_000)} MB from Google.307 installation.totalBytes > 0 ? (
293 </p>308 <progress
294 ) : null}309 aria-label="Antigravity download"
295 {!installed && !provider.setup?.canInstall ? (310 className="block h-1 w-full accent-foreground"
296 <p className="text-muted-foreground">311 value={installation.downloadedBytes}
297 Automatic installation is unavailable here. Set an existing binary path below or use a312 max={installation.totalBytes}
298 supported remote environment.313 />
299 </p>314 ) : null}
300 ) : null}315 </div>
301 <div className="flex flex-wrap gap-2">316 {!installActive &&
302 {installActive && installation.operationId ? (317 installation?.message &&
303 <Button318 installation.message !== installationStatusMessage ? (
304 size="xs"319 <p className="text-muted-foreground [overflow-wrap:anywhere]">
305 variant="outline"320 {installation.message}
306 disabled={actionsDisabled}321 </p>
307 onClick={() => {322 ) : null}
308 const operationId = installation.operationId;323 <div className="grid min-h-7 grid-cols-[1.75rem_minmax(0,1fr)] gap-2">
309 if (!operationId) return;324 <div className="col-start-2 row-start-1 grid">
310 void runCommand("Cancelling installation", () =>325 {installActive && installation.operationId ? (
311 cancelInstall({ environmentId, input: { instanceId, operationId } }),326 <Button
312 );327 size="sm"
313 }}328 variant="outline"
314 >329 disabled={actionsDisabled}
315 Cancel installation330 onClick={() => {
316 </Button>331 const operationId = installation.operationId;
317 ) : !installActive && provider.setup?.canInstall ? (332 if (!operationId) return;
318 <Button333 void runCommand("Cancelling installation", () =>
319 size="xs"334 cancelInstall({ environmentId, input: { instanceId, operationId } }),
320 variant="outline"335 );
321 disabled={actionsDisabled || installation === null || authActive}336 }}
322 onClick={() => void runCommand("Starting installation", () => startInstall(target))}337 >
323 >338 Cancel installation
324 {installation?.installedVersion339 </Button>
325 ? installation.version && installation.version !== installation.installedVersion340 ) : !installActive && provider.setup?.canInstall ? (
326 ? "Update Antigravity"341 <Button
327 : "Reinstall Antigravity"342 size="sm"
328 : installation?.phase === "failed" || installation?.phase === "cancelled"343 variant="outline"
329 ? "Retry installation"344 disabled={actionsDisabled || installation === null || authActive}
330 : installed345 onClick={() =>
331 ? "Install managed runtime"346 void runCommand("Starting installation", () => startInstall(target))
332 : "Install Antigravity"}347 }
333 </Button>348 >
334 ) : null}349 {installation?.installedVersion
335 {installation?.canRemove && !installActive ? (350 ? installation.version &&
336 <Button351 installation.version !== installation.installedVersion
337 size="xs"352 ? "Update Antigravity"
338 variant="ghost"353 : "Reinstall Antigravity"
339 disabled={actionsDisabled || authActive}354 : installation?.phase === "failed" || installation?.phase === "cancelled"
340 onClick={() => void removeRuntime()}355 ? "Retry installation"
341 >356 : installed
342 Remove downloaded runtime357 ? "Install managed runtime"
343 </Button>358 : "Install Antigravity"}
344 ) : null}359 </Button>
345 </div>360 ) : null}
346 </div>361 </div>
362 {installation?.canRemove && !installActive ? (
363 <Tooltip>
364 <TooltipTrigger
365 render={
366 <Button
367 size="icon-sm"
368 variant="ghost"
369 className="col-start-1 row-start-1"
370 aria-label="Remove downloaded runtime"
371 disabled={actionsDisabled || authActive}
372 onClick={() => void removeRuntime()}
373 />
374 }
375 >
376 <Trash2Icon className="size-3.5" />
377 </TooltipTrigger>
378 <TooltipPopup>Remove downloaded runtime</TooltipPopup>
379 </Tooltip>
380 ) : null}
381 </div>
382 </div>
383 }
384 />
347385
348 <div className="grid gap-2 border-t border-border/60 pt-3">386 <SettingsRow
349 <p className="font-medium">{methodLabel}</p>387 title={methodLabel}
350 <p role="status" className="text-muted-foreground [overflow-wrap:anywhere]">388 className="@max-lg/setup:[&>div:first-child]:flex @max-lg/setup:[&>div:first-child]:items-stretch @max-lg/setup:[&>div:first-child]:gap-3"
351 {authStatusMessage}389 description={
352 </p>390 usesBrowser ? "Connect your Google account." : "Connect with the credentials below."
353 {authorizationUrl ? (391 }
354 <>392 control={
355 <div className="flex flex-wrap gap-2">393 <div className="flex min-w-0 flex-col gap-2 sm:max-w-56 sm:items-end sm:text-right xl:max-w-72">
356 <Button size="xs" variant="outline" onClick={() => void openSignInPage()}>394 <p
357 Open sign-in page395 role="status"
358 </Button>396 className={
359 <Button size="xs" variant="ghost" onClick={() => void copySignInLink()}>397 authStatusMessage === phaseLabels.idle
360 {copiedFlowId === auth?.flowId ? "Link copied" : "Copy sign-in link"}398 ? "sr-only"
361 </Button>399 : "text-muted-foreground [overflow-wrap:anywhere]"
400 }
401 >
402 {authStatusMessage}
403 </p>
404 {authorizationUrl ? (
405 <div className="flex flex-wrap gap-2 sm:justify-end">
406 <Button size="sm" variant="outline" onClick={() => void openSignInPage()}>
407 Open sign-in page
408 </Button>
409 <Button size="sm" variant="ghost" onClick={() => void copySignInLink()}>
410 {copiedFlowId === auth?.flowId ? "Link copied" : "Copy sign-in link"}
411 </Button>
412 </div>
413 ) : null}
414 <div className="flex flex-wrap gap-2 sm:justify-end">
415 {authActive && auth?.flowId ? (
416 <Button
417 size="sm"
418 variant="ghost"
419 disabled={actionsDisabled}
420 onClick={() => {
421 const flowId = auth.flowId;
422 if (!flowId) return;
423 void runCommand("Cancelling sign-in", () =>
424 cancelAuth({ environmentId, input: { instanceId, flowId } }),
425 );
426 }}
427 >
428 Cancel sign-in
429 </Button>
430 ) : !authActive && !authenticated && provider.setup?.canAuthenticate ? (
431 <Button
432 size="sm"
433 variant="outline"
434 disabled={actionsDisabled || !installed || auth === null || installActive}
435 onClick={() => void runCommand("Starting sign-in", () => startAuth(target))}
436 >
437 {usesBrowser
438 ? auth?.phase === "failed" || auth?.phase === "cancelled"
439 ? "Retry Google sign-in"
440 : "Sign in with Google"
441 : auth?.phase === "failed" || auth?.phase === "cancelled"
442 ? "Retry connection"
443 : "Connect"}
444 </Button>
445 ) : null}
446 {!authActive && provider.setup?.canAuthenticate ? (
447 <Button
448 size="sm"
449 variant={authenticated ? "outline" : "ghost"}
450 disabled={actionsDisabled || auth === null}
451 onClick={() => void signOut()}
452 >
453 {usesBrowser ? "Sign out of Google" : "Disconnect"}
454 </Button>
455 ) : null}
362 </div>456 </div>
363 {auth?.expiresAt ? (457 </div>
458 }
459 >
460 {authorizationUrl || auth?.phase === "waiting" ? (
461 <div className="space-y-2 pb-2">
462 {authorizationUrl ? (
463 <>
464 {auth?.expiresAt ? (
465 <p className="text-muted-foreground">
466 Link expires at{" "}
467 <time dateTime={auth.expiresAt}>
468 {new Date(auth.expiresAt).toLocaleTimeString([], {
469 hour: "numeric",
470 minute: "2-digit",
471 })}
472 </time>
473 .
474 </p>
475 ) : null}
476 <form
477 className="grid gap-2"
478 onSubmit={(event) => {
479 event.preventDefault();
480 void submitCallback();
481 }}
482 >
483 <label htmlFor={`provider-callback-${instanceId}`}>
484 If the final localhost page does not load, paste its full URL here.
485 </label>
486 <Input
487 id={`provider-callback-${instanceId}`}
488 size="sm"
489 type="url"
490 autoComplete="off"
491 spellCheck={false}
492 placeholder="http://127.0.0.1:..."
493 value={callbackUrl}
494 maxLength={16_384}
495 disabled={actionsDisabled}
496 onChange={(event) =>
497 setCallbackDraft({ flowId: auth?.flowId ?? null, value: event.target.value })
498 }
499 />
500 <Button
501 size="sm"
502 variant="outline"
503 type="submit"
504 className="w-fit"
505 disabled={actionsDisabled || !callbackUrl.trim()}
506 >
507 Continue
508 </Button>
509 </form>
510 </>
511 ) : auth?.phase === "waiting" ? (
364 <p className="text-muted-foreground">512 <p className="text-muted-foreground">
365 Link expires at{" "}513 Sign-in is open in another client. Complete or cancel it there.
366 <time dateTime={auth.expiresAt}>
367 {new Date(auth.expiresAt).toLocaleTimeString([], {
368 hour: "numeric",
369 minute: "2-digit",
370 })}
371 </time>
372 .
373 </p>514 </p>
374 ) : null}515 ) : null}
375 <form516 </div>
376 className="grid gap-2"
377 onSubmit={(event) => {
378 event.preventDefault();
379 void submitCallback();
380 }}
381 >
382 <label htmlFor={`provider-callback-${instanceId}`}>
383 If the final localhost page does not load, paste its full URL here.
384 </label>
385 <Input
386 id={`provider-callback-${instanceId}`}
387 size="sm"
388 type="url"
389 autoComplete="off"
390 spellCheck={false}
391 placeholder="http://127.0.0.1:..."
392 value={callbackUrl}
393 maxLength={16_384}
394 disabled={actionsDisabled}
395 onChange={(event) =>
396 setCallbackDraft({ flowId: auth?.flowId ?? null, value: event.target.value })
397 }
398 />
399 <Button
400 size="xs"
401 variant="outline"
402 type="submit"
403 className="w-fit"
404 disabled={actionsDisabled || !callbackUrl.trim()}
405 >
406 Continue
407 </Button>
408 </form>
409 </>
410 ) : auth?.phase === "waiting" ? (
411 <p className="text-muted-foreground">
412 Sign-in is open in another client. Complete or cancel it there.
413 </p>
414 ) : null}517 ) : null}
415 <div className="flex flex-wrap gap-2">518 </SettingsRow>
416 {authActive && auth?.flowId ? (
417 <Button
418 size="xs"
419 variant="ghost"
420 disabled={actionsDisabled}
421 onClick={() => {
422 const flowId = auth.flowId;
423 if (!flowId) return;
424 void runCommand("Cancelling sign-in", () =>
425 cancelAuth({ environmentId, input: { instanceId, flowId } }),
426 );
427 }}
428 >
429 Cancel sign-in
430 </Button>
431 ) : !authActive && !authenticated && provider.setup?.canAuthenticate ? (
432 <Button
433 size="xs"
434 variant="outline"
435 disabled={actionsDisabled || !installed || auth === null || installActive}
436 onClick={() => void runCommand("Starting sign-in", () => startAuth(target))}
437 >
438 {usesBrowser
439 ? auth?.phase === "failed" || auth?.phase === "cancelled"
440 ? "Retry Google sign-in"
441 : "Sign in with Google"
442 : auth?.phase === "failed" || auth?.phase === "cancelled"
443 ? "Retry connection"
444 : "Connect"}
445 </Button>
446 ) : null}
447 {!authActive && provider.setup?.canAuthenticate ? (
448 <Button
449 size="xs"
450 variant={authenticated ? "outline" : "ghost"}
451 disabled={actionsDisabled || auth === null}
452 onClick={() => void signOut()}
453 >
454 {usesBrowser ? "Sign out of Google" : "Disconnect"}
455 </Button>
456 ) : null}
457 </div>
458 </div>
459519
460 {pendingLabel ? <p role="status">{pendingLabel}.</p> : null}520 <p className="sr-only" role="status">
521 {pendingLabel ? `${pendingLabel}.` : null}
522 </p>
461 {error || queryError ? (523 {error || queryError ? (
462 <div className="grid gap-2">524 <div className="grid gap-2 px-3 py-3 sm:px-4">
463 <p role="alert" className="text-destructive [overflow-wrap:anywhere]">525 <p role="alert" className="text-destructive [overflow-wrap:anywhere]">
464 {error ?? queryError}526 {error ?? queryError}
465 </p>527 </p>
466 {queryError ? (528 {queryError ? (
467 <Button529 <Button
468 size="xs"530 size="sm"
469 variant="outline"531 variant="outline"
470 className="w-fit"532 className="w-fit"
471 onClick={() => {533 onClick={() => {
packages/contracts/src/settings.ts+4-6
...@@ -739,7 +739,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(...@@ -739,7 +739,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(
739 Schema.annotateKey({739 Schema.annotateKey({
740 title: "Sign-in method",740 title: "Sign-in method",
741 description:741 description:
742 "Google account uses your Antigravity subscription. Gemini Enterprise needs a GCP project and location. API key and Agent Platform bill the credential you enter.",742 "Google accounts use your subscription; API keys and Agent Platform bill usage.",
743 providerSettingsForm: {743 providerSettingsForm: {
744 control: "select",744 control: "select",
745 options: ANTIGRAVITY_AUTH_METHODS,745 options: ANTIGRAVITY_AUTH_METHODS,
...@@ -751,8 +751,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(...@@ -751,8 +751,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(
751 Schema.withDecodingDefault(Effect.succeed("")),751 Schema.withDecodingDefault(Effect.succeed("")),
752 Schema.annotateKey({752 Schema.annotateKey({
753 title: "API key",753 title: "API key",
754 description:754 description: "Gemini or Vertex AI express key. Stored in plain text.",
755 "Gemini API key, or a Vertex AI express key for Agent Platform. Stored in plain text on this environment.",
756 providerSettingsForm: {755 providerSettingsForm: {
757 control: "password",756 control: "password",
758 placeholder: "Optional",757 placeholder: "Optional",
...@@ -773,7 +772,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(...@@ -773,7 +772,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(
773 Schema.withDecodingDefault(Effect.succeed("")),772 Schema.withDecodingDefault(Effect.succeed("")),
774 Schema.annotateKey({773 Schema.annotateKey({
775 title: "GCP location",774 title: "GCP location",
776 description: "Region for Gemini Enterprise or Agent Platform, such as us-central1.",775 description: "Region for Gemini Enterprise or Agent Platform.",
777 providerSettingsForm: { placeholder: "us-central1", clearWhenEmpty: "omit" },776 providerSettingsForm: { placeholder: "us-central1", clearWhenEmpty: "omit" },
778 }),777 }),
779 ),778 ),
...@@ -781,8 +780,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(...@@ -781,8 +780,7 @@ export const AntigravitySettings = makeProviderSettingsSchema(
781 Schema.withDecodingDefault(Effect.succeed("")),780 Schema.withDecodingDefault(Effect.succeed("")),
782 Schema.annotateKey({781 Schema.annotateKey({
783 title: "Binary path",782 title: "Binary path",
784 description:783 description: "Custom ACP executable. Leave empty to select automatically.",
785 "Optional path to the official Antigravity ACP executable. Leave empty for automatic selection.",
786 providerSettingsForm: { placeholder: "Automatic", clearWhenEmpty: "persist" },784 providerSettingsForm: { placeholder: "Automatic", clearWhenEmpty: "persist" },
787 }),785 }),
788 ),786 ),