From 42748290a405ea281885b7b0815e3d640a26a1bd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 12:58:26 +0000 Subject: [PATCH 1/2] fix(web): Antigravity sign-in required status + Sign in with Google MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Treat a healthy Antigravity install with unchecked Google auth as informational in Settings (Installed · Sign-in required) instead of amber Needs attention. Keep the server health probe as warning so readiness semantics stay unchanged. Add a Sign in with Google action on the Antigravity settings card that starts the existing OAuth flow. Co-authored-by: maco --- .../settings/ProviderInstanceCard.test.ts | 42 +++++++ .../settings/ProviderInstanceCard.tsx | 48 +++++--- .../settings/ProviderSettingsPanel.tsx | 22 +++- .../settings/ProviderSetupSection.test.tsx | 111 +++++++++++++++++- .../settings/ProviderSetupSection.tsx | 89 ++++++++++++-- .../settings/providerStatus.test.ts | 70 ++++++++++- .../src/components/settings/providerStatus.ts | 22 ++++ 7 files changed, 375 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/settings/ProviderInstanceCard.test.ts b/apps/web/src/components/settings/ProviderInstanceCard.test.ts index a5085774f0c6..9a82b02ff32a 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.test.ts +++ b/apps/web/src/components/settings/ProviderInstanceCard.test.ts @@ -121,6 +121,48 @@ describe("deriveProviderModelsForDisplay", () => { expect(markup).toContain("blur-[2px]"); expect(markup).not.toContain("developer@example.com"); }); + + it("shows informational Antigravity sign-in copy instead of amber Needs attention", () => { + const instanceId = ProviderInstanceId.make("antigravity"); + const driver = ProviderDriverKind.make("antigravity"); + const liveProvider: ServerProvider = { + instanceId, + driver, + enabled: true, + installed: true, + version: "1.0.0", + status: "warning", + auth: { status: "unknown" }, + checkedAt: "2026-08-27T12:00:00.000Z", + models: [], + slashCommands: [], + skills: [], + message: "Antigravity is installed. Google account access is not checked yet.", + }; + const props = { + instanceId, + instance: { driver, enabled: true }, + driverOption: undefined, + liveProvider, + signInAction: createElement("button", { type: "button" }, "Sign in with Google"), + onUpdate: () => undefined, + hiddenModels: [], + favoriteModels: [], + modelOrder: [], + onHiddenModelsChange: () => undefined, + onFavoriteModelsChange: () => undefined, + onModelOrderChange: () => undefined, + } as const; + + for (const mode of ["list", "editor"] as const) { + const markup = renderToStaticMarkup(createElement(ProviderInstanceCard, { ...props, mode })); + expect(markup).toContain("Installed · Sign-in required"); + expect(markup).toContain("Sign in with Google"); + expect(markup).not.toContain("Needs attention"); + expect(markup).not.toContain("bg-warning"); + } + }); + it("surfaces a failed probe message in both the list row and the editor", () => { const instanceId = ProviderInstanceId.make("codex_work"); const driver = ProviderDriverKind.make("codex"); diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index 0a8c8fcbe404..99e72d89a85a 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -53,6 +53,7 @@ import { PROVIDER_STATUS_STYLES, getProviderSummary, getProviderVersionLabel, + isAntigravityUncheckedAuth, type ProviderStatusKey, } from "./providerStatus"; @@ -372,6 +373,12 @@ interface ProviderInstanceCardProps { * omit it. */ readonly headerAction?: ReactNode | undefined; + /** + * Optional Google sign-in control for Antigravity. Rendered on the list row + * and at the top of the editor so OAuth can start from Settings without a + * chat session. + */ + readonly signInAction?: ReactNode | undefined; readonly setup?: ReactNode; readonly hiddenModels: ReadonlyArray; readonly favoriteModels: ReadonlyArray; @@ -414,6 +421,7 @@ export function ProviderInstanceCard({ onUpdate, onDelete, headerAction, + signInAction, setup, hiddenModels, favoriteModels, @@ -434,6 +442,8 @@ export function ProviderInstanceCard({ const summary = enabled ? getProviderSummary(liveProvider) : { headline: "Disabled", detail: null }; + // Keep the server's warning status, but do not paint this as amber attention. + const informationalSignInRequired = enabled && isAntigravityUncheckedAuth(liveProvider); const authEmail = liveProvider?.auth.email?.trim(); const isAuthenticated = enabled && liveProvider?.auth.status === "authenticated"; const authLabel = @@ -567,14 +577,14 @@ export function ProviderInstanceCard({ ) : null; // Healthy and disabled rows read fine from their text; only trouble gets a dot. - const statusDotNode = - statusKey === "warning" || statusKey === "error" ? ( - - ) : null; - // Trouble states carry the server's explanation (a failed probe, a shadow - // home entry that is not a symlink, a missing binary). Show it wherever the - // headline shows so the user can act without opening the editor. - const needsAttention = statusKey === "warning" || statusKey === "error"; + // Unchecked Antigravity Google auth stays informational, not amber attention. + // Trouble states still carry the server's explanation (a failed probe, a + // shadow home entry that is not a symlink, a missing binary). + const needsAttention = + !informationalSignInRequired && (statusKey === "warning" || statusKey === "error"); + const statusDotNode = needsAttention ? ( + + ) : null; const editorStatusNode = isAuthenticated && authEmail ? ( <> @@ -670,13 +680,16 @@ export function ProviderInstanceCard({ - - updateEnabled(Boolean(checked))} - aria-label={`Enable ${displayName}`} - /> + + {signInAction} + + updateEnabled(Boolean(checked))} + aria-label={`Enable ${displayName}`} + /> + ); @@ -810,7 +823,10 @@ export function ProviderInstanceCard({ {editorStatusNode} +
+
{editorStatusNode}
+ {signInAction} +
} control={
setSelectedInstanceId(row.instanceId) : undefined} readOnly={readOnly} + signInAction={ + !readOnly && offersAntigravityGoogleSignIn(liveProvider, authMethod) ? ( + setSelectedInstanceId(row.instanceId) : undefined} + /> + ) : null + } setup={ mode === "editor" && row.driver === "antigravity" ? ( updateProviderInstance(row, { ...row.instance, enabled: true })} diff --git a/apps/web/src/components/settings/ProviderSetupSection.test.tsx b/apps/web/src/components/settings/ProviderSetupSection.test.tsx index 501b6c8dc496..b7cdb141dea3 100644 --- a/apps/web/src/components/settings/ProviderSetupSection.test.tsx +++ b/apps/web/src/components/settings/ProviderSetupSection.test.tsx @@ -73,7 +73,11 @@ vi.mock("../../localApi", () => ({ ensureLocalApi: () => ({ dialogs: { confirm: setup.confirm } }), })); -import { ProviderSetupSection } from "./ProviderSetupSection"; +import { + AntigravityGoogleSignInButton, + offersAntigravityGoogleSignIn, + ProviderSetupSection, +} from "./ProviderSetupSection"; const environmentId = EnvironmentId.make("remote-google"); const instanceId = ProviderInstanceId.make("antigravity_work"); @@ -386,4 +390,109 @@ describe("Antigravity setup", () => { expect(setup.startAuth).not.toHaveBeenCalled(); }, ); + + it("keeps Google sign-in in setup when the binary is healthy and auth is unchecked", () => { + setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null }); + const view = renderSetup({ + provider: { ...provider, status: "warning", auth: { status: "unknown" } }, + }); + expect(button(view, "Sign in with Google")).not.toBeNull(); + expect( + visitElements( + view, + (element) => element.props.children === "Sign in with your Google account.", + ), + ).not.toBeNull(); + }); +}); + +function renderCardSignIn( + options: { + provider?: ServerProvider; + authMethod?: "oauth-personal" | "gemini-api-key"; + onStarted?: () => void; + } = {}, +) { + hooks.beginRender(); + return AntigravityGoogleSignInButton({ + environmentId, + instanceId, + provider: options.provider ?? { ...provider, status: "warning", auth: { status: "unknown" } }, + authMethod: options.authMethod ?? "oauth-personal", + onStarted: options.onStarted, + }); +} + +describe("Antigravity Google sign-in on the provider card", () => { + beforeEach(() => { + hooks.reset(); + vi.clearAllMocks(); + setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null }); + setup.startAuth.mockReset().mockResolvedValue({ _tag: "Success", value: undefined }); + }); + + it("starts Google OAuth from Settings when auth has not been checked", async () => { + const onStarted = vi.fn(); + const view = renderCardSignIn({ + provider: { ...provider, status: "warning", auth: { status: "unknown" } }, + onStarted, + }); + click(view, "Sign in with Google"); + await flushPromises(); + + expect(onStarted).toHaveBeenCalledTimes(1); + expect(setup.startAuth).toHaveBeenCalledWith({ environmentId, input: { instanceId } }); + }); + + it("coalesces repeated card sign-in clicks while start is pending", async () => { + let completeStart: (value: { _tag: "Success"; value: undefined }) => void = () => { + throw new Error("Missing start resolver."); + }; + const pending = new Promise<{ _tag: "Success"; value: undefined }>((resolve) => { + completeStart = resolve; + }); + setup.startAuth.mockReturnValueOnce(pending); + const view = renderCardSignIn(); + click(view, "Sign in with Google"); + click(view, "Sign in with Google"); + + expect(setup.startAuth).toHaveBeenCalledTimes(1); + completeStart({ _tag: "Success", value: undefined }); + await flushPromises(); + }); + + it("does not offer Google sign-in for an API key method or a signed-in account", () => { + expect( + renderCardSignIn({ + provider: { ...provider, status: "ready", auth: { status: "authenticated" } }, + }), + ).toBeNull(); + expect(renderCardSignIn({ authMethod: "gemini-api-key" })).toBeNull(); + expect(setup.startAuth).not.toHaveBeenCalled(); + }); +}); + +describe("offersAntigravityGoogleSignIn", () => { + it("requires an installed Antigravity instance that can run Google OAuth", () => { + expect(offersAntigravityGoogleSignIn(provider, "oauth-personal")).toBe(true); + expect( + offersAntigravityGoogleSignIn( + { ...provider, status: "warning", auth: { status: "unknown" } }, + "oauth-business", + ), + ).toBe(true); + expect(offersAntigravityGoogleSignIn({ ...provider, installed: false }, "oauth-personal")).toBe( + false, + ); + expect( + offersAntigravityGoogleSignIn( + { ...provider, auth: { status: "authenticated" } }, + "oauth-personal", + ), + ).toBe(false); + expect(offersAntigravityGoogleSignIn(provider, "gemini-api-key")).toBe(false); + const { setup: _setup, ...withoutSetup } = provider; + expect(offersAntigravityGoogleSignIn(withoutSetup, "oauth-personal")).toBe(false); + expect(offersAntigravityGoogleSignIn(undefined, "oauth-personal")).toBe(false); + }); }); diff --git a/apps/web/src/components/settings/ProviderSetupSection.tsx b/apps/web/src/components/settings/ProviderSetupSection.tsx index 0bbd160b4593..75a199da43c9 100644 --- a/apps/web/src/components/settings/ProviderSetupSection.tsx +++ b/apps/web/src/components/settings/ProviderSetupSection.tsx @@ -57,6 +57,25 @@ const CREDENTIAL_PHASE_LABELS: Record = { cancelled: "Connection cancelled.", }; +/** Personal and Enterprise open Google in the browser; API key methods do not. */ +export function usesAntigravityBrowserAuth(authMethod: AntigravityAuthMethod): boolean { + return authMethod === "oauth-personal" || authMethod === "oauth-business"; +} + +/** Offer Google sign-in when the instance can authenticate and is not already signed in. */ +export function offersAntigravityGoogleSignIn( + provider: ServerProvider | undefined, + authMethod: AntigravityAuthMethod, +): boolean { + return ( + provider?.driver === "antigravity" && + provider.installed && + provider.auth.status !== "authenticated" && + provider.setup?.canAuthenticate === true && + usesAntigravityBrowserAuth(authMethod) + ); +} + /** Read the configured method from the instance config. Unknown values fall back to personal. */ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMethod { const value = @@ -68,6 +87,65 @@ export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMetho ); } +/** + * Starts Google OAuth from the provider settings card (list row and editor + * header) without opening a chat. The setup section still owns waiting-state + * UI such as the sign-in link and callback field. + */ +export function AntigravityGoogleSignInButton({ + environmentId, + instanceId, + provider, + authMethod, + size = "xs", + onStarted, +}: { + readonly environmentId: EnvironmentId; + readonly instanceId: ProviderInstanceId; + readonly provider: ServerProvider | undefined; + readonly authMethod: AntigravityAuthMethod; + readonly size?: "xs" | "sm" | undefined; + readonly onStarted?: (() => void) | undefined; +}) { + const target = { environmentId, input: { instanceId } }; + const authQuery = useEnvironmentQuery(serverEnvironment.providerAuthState(target)); + const startAuth = useAtomCommand(serverEnvironment.startProviderAuth, { + reportFailure: false, + reportDefect: false, + }); + const [pending, setPending] = useState(false); + const pendingRef = useRef(false); + const auth = authQuery.data; + const authActive = + auth?.phase === "starting" || auth?.phase === "waiting" || auth?.phase === "verifying"; + + if (!offersAntigravityGoogleSignIn(provider, authMethod) || authActive) { + return null; + } + + return ( + + ); +} + /** Setup state belongs to the selected environment and is never saved in client settings. */ export function ProviderSetupSection(props: ProviderSetupSectionProps) { return ( @@ -131,7 +209,7 @@ function ProviderSetupActions({ readonly authMethod: AntigravityAuthMethod; }) { const target = { environmentId, input: { instanceId } }; - const usesBrowser = authMethod === "oauth-personal" || authMethod === "oauth-business"; + const usesBrowser = usesAntigravityBrowserAuth(authMethod); const phaseLabels = usesBrowser ? AUTH_PHASE_LABELS : CREDENTIAL_PHASE_LABELS; const methodLabel = ANTIGRAVITY_AUTH_METHODS.find((method) => method.value === authMethod)?.label ?? @@ -391,14 +469,7 @@ function ProviderSetupActions({ } control={
-

+

{authStatusMessage}

{authorizationUrl ? ( diff --git a/apps/web/src/components/settings/providerStatus.test.ts b/apps/web/src/components/settings/providerStatus.test.ts index 46dc7e262512..a48d27e44de8 100644 --- a/apps/web/src/components/settings/providerStatus.test.ts +++ b/apps/web/src/components/settings/providerStatus.test.ts @@ -1,7 +1,7 @@ import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; -import { getProviderSummary } from "./providerStatus"; +import { getProviderSummary, isAntigravityUncheckedAuth } from "./providerStatus"; const provider: ServerProvider = { instanceId: ProviderInstanceId.make("codex"), @@ -25,6 +25,63 @@ describe("getProviderSummary", () => { }); }); + it("treats healthy Antigravity with unchecked Google auth as sign-in required, not attention", () => { + const message = "Antigravity is installed. Google account access is not checked yet."; + const antigravity = { + ...provider, + instanceId: ProviderInstanceId.make("antigravity"), + driver: ProviderDriverKind.make("antigravity"), + status: "warning" as const, + auth: { status: "unknown" as const }, + message, + }; + + expect(isAntigravityUncheckedAuth(antigravity)).toBe(true); + expect(getProviderSummary(antigravity)).toEqual({ + headline: "Installed · Sign-in required", + detail: message, + }); + }); + + it("keeps confirmed Antigravity sign-out and real probe failures as they are", () => { + const antigravity = { + ...provider, + instanceId: ProviderInstanceId.make("antigravity"), + driver: ProviderDriverKind.make("antigravity"), + }; + + expect( + getProviderSummary({ + ...antigravity, + status: "warning", + auth: { status: "unauthenticated" }, + message: "Sign in with Google to use Antigravity.", + }), + ).toEqual({ + headline: "Not authenticated", + detail: "Sign in with Google to use Antigravity.", + }); + expect( + isAntigravityUncheckedAuth({ + ...antigravity, + status: "error", + auth: { status: "unknown" }, + message: "Antigravity could not complete its local health check.", + }), + ).toBe(false); + expect( + getProviderSummary({ + ...antigravity, + status: "error", + auth: { status: "unknown" }, + message: "Antigravity could not complete its local health check.", + }), + ).toEqual({ + headline: "Unavailable", + detail: "Antigravity could not complete its local health check.", + }); + }); + it("does not hide a provider error behind a previous authenticated state", () => { expect( getProviderSummary({ @@ -49,6 +106,17 @@ describe("getProviderSummary", () => { headline: "Needs attention", detail: "The provider version is unsupported.", }); + expect( + getProviderSummary({ + ...provider, + status: "warning", + auth: { status: "unknown" }, + message: "The provider version is unsupported.", + }), + ).toEqual({ + headline: "Needs attention", + detail: "The provider version is unsupported.", + }); }); it("keeps authentication failures actionable when their provider status is error", () => { diff --git a/apps/web/src/components/settings/providerStatus.ts b/apps/web/src/components/settings/providerStatus.ts index 90c618f5daa7..d4db7e7c2295 100644 --- a/apps/web/src/components/settings/providerStatus.ts +++ b/apps/web/src/components/settings/providerStatus.ts @@ -21,6 +21,22 @@ export const PROVIDER_STATUS_STYLES = { export type ProviderStatusKey = keyof typeof PROVIDER_STATUS_STYLES; +/** + * Antigravity's periodic health probe only calls ACP `initialize()`. A passed + * probe with Google auth still `unknown` is published as `warning`, which + * Settings would otherwise render as amber "Needs attention". Chat already + * hides that state; Settings should treat it as informational too. + */ +export function isAntigravityUncheckedAuth(provider: ServerProvider | undefined) { + return ( + provider !== undefined && + provider.driver === "antigravity" && + provider.installed && + provider.status === "warning" && + provider.auth.status === "unknown" + ); +} + /** * Derive the headline + detail copy shown under a provider's name in the * settings page. Prefers `provider.message` for server-supplied detail and @@ -55,6 +71,12 @@ export function getProviderSummary(provider: ServerProvider | undefined) { detail: provider.message ?? null, }; } + if (isAntigravityUncheckedAuth(provider)) { + return { + headline: "Installed · Sign-in required", + detail: provider.message ?? null, + }; + } if (provider.status === "warning") { return { headline: "Needs attention", From ca0267cfefdac05fd34eaa47c7bb6dc652499f81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 13:02:13 +0000 Subject: [PATCH 2/2] style(web): format Antigravity settings sign-in status markup Co-authored-by: maco --- apps/web/src/components/settings/ProviderInstanceCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index 99e72d89a85a..2475fe6833f3 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -824,7 +824,9 @@ export function ProviderInstanceCard({ title="Display name" status={
-
{editorStatusNode}
+
+ {editorStatusNode} +
{signInAction}
}