diff --git a/apps/server/src/provider/Layers/AntigravityProvider.test.ts b/apps/server/src/provider/Layers/AntigravityProvider.test.ts index 363afbee1106..34fd81a424ca 100644 --- a/apps/server/src/provider/Layers/AntigravityProvider.test.ts +++ b/apps/server/src/provider/Layers/AntigravityProvider.test.ts @@ -264,6 +264,23 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { ), ); + it.effect("publishes the configured sign-in method before any account is checked", () => + Effect.scoped( + Effect.gen(function* () { + const provider = yield* makeAntigravityProvider(decodeSettings({ enabled: true }), { + stampIdentity: (snapshot) => Effect.succeed({ ...snapshot, instanceId, driver }), + probe: Effect.succeed(initializeResult), + supportsTextGeneration: Effect.succeed(true), + auth: { type: "gemini-api-key", label: "Gemini API key" }, + }); + expect((yield* provider.snapshot.getSnapshot).auth).toEqual({ + status: "unknown", + type: "gemini-api-key", + }); + }), + ), + ); + it.effect("treats initialize as installation proof, not account or model discovery", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/AntigravityProvider.ts b/apps/server/src/provider/Layers/AntigravityProvider.ts index bd886a123808..956a5d81d3f4 100644 --- a/apps/server/src/provider/Layers/AntigravityProvider.ts +++ b/apps/server/src/provider/Layers/AntigravityProvider.ts @@ -144,7 +144,9 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func installed: false, version: null, status: "warning", - auth: { status: "unknown" }, + // The configured method rides along so the registry can tell a saved + // account for this method from one left by a previous configuration. + auth: { status: "unknown", ...(options.auth ? { type: options.auth.type } : {}) }, message: settings.enabled ? "Checking Antigravity availability." : "Antigravity is disabled in T3 Code settings.", diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index ffabf6d8d4f3..988c89e1e679 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -1179,6 +1179,113 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te }); }); + describe("Antigravity saved account", () => { + const signedIn = { + instanceId: ProviderInstanceId.make("antigravity-personal"), + driver: ProviderDriverKind.make("antigravity"), + status: "ready", + enabled: true, + installed: true, + auth: { status: "authenticated", type: "oauth-personal", label: "Google account" }, + checkedAt: "2026-09-05T00:00:00.000Z", + version: "agy_acp_server_1.1.1", + models: [ + { + slug: "gemini-3.7-flash-high", + name: "Gemini 3.7 Flash", + isCustom: false, + capabilities: null, + }, + ], + slashCommands: [{ name: "plan" }], + skills: [], + } as const satisfies ServerProvider; + const uncheckedMessage = + "Antigravity is installed. Google account access is not checked yet."; + const restartProbe = { + ...signedIn, + status: "warning", + auth: { status: "unknown" }, + checkedAt: "2026-09-05T00:01:00.000Z", + message: uncheckedMessage, + models: [], + } as const satisfies ServerProvider; + + it("keeps the saved Google account through restart health checks", () => { + const merged = mergeProviderSnapshot(signedIn, restartProbe); + const { message: _uncheckedMessage, ...probeWithoutMessage } = restartProbe; + assert.deepStrictEqual(merged, { + ...probeWithoutMessage, + status: "ready", + auth: signedIn.auth, + models: signedIn.models, + }); + assert.equal("message" in merged, false); + // The next periodic probe reads the merged snapshot as its previous state. + assert.deepStrictEqual(mergeProviderSnapshot(merged, restartProbe), merged); + }); + + it("carries the account through the boot probe and a failed probe without hiding them", () => { + const booting = { + ...restartProbe, + installed: false, + version: null, + message: "Checking Antigravity availability.", + } satisfies ServerProvider; + assert.deepStrictEqual(mergeProviderSnapshot(signedIn, booting), { + ...booting, + auth: signedIn.auth, + models: signedIn.models, + }); + + const failed = { + ...restartProbe, + status: "error", + message: "Antigravity did not respond to its local health check within 90 seconds.", + } satisfies ServerProvider; + assert.deepStrictEqual(mergeProviderSnapshot(signedIn, failed), { + ...failed, + auth: signedIn.auth, + models: signedIn.models, + }); + }); + + it("does not invent an account after sign-out, disable, uninstall, or for other providers", () => { + const untouched = [ + { ...restartProbe, auth: { status: "unauthenticated" } }, + { ...restartProbe, status: "disabled", enabled: false }, + { ...restartProbe, status: "error", installed: false }, + { ...restartProbe, driver: ProviderDriverKind.make("codex") }, + // The instance was rebuilt with another sign-in method. + { ...restartProbe, auth: { status: "unknown", type: "gemini-api-key" } }, + ] satisfies ReadonlyArray; + for (const next of untouched) { + const merged = mergeProviderSnapshot(signedIn, next); + assert.deepStrictEqual(merged.auth, next.auth); + assert.equal(merged.status, next.status); + assert.equal(merged.message, next.message); + } + assert.deepStrictEqual( + mergeProviderSnapshot({ ...signedIn, auth: { status: "unknown" } }, restartProbe).auth, + { status: "unknown" }, + ); + assert.equal( + mergeProviderSnapshot( + { ...signedIn, driver: ProviderDriverKind.make("codex") }, + restartProbe, + ).auth.status, + "unknown", + ); + assert.deepStrictEqual( + mergeProviderSnapshot(signedIn, { + ...restartProbe, + auth: { status: "unknown", type: "oauth-personal" }, + }).auth, + signedIn.auth, + ); + }); + }); + it("fills missing capabilities from the previous provider snapshot", () => { const previousProvider = { instanceId: ProviderInstanceId.make("cursor"), diff --git a/apps/server/src/provider/Layers/ProviderRegistry.ts b/apps/server/src/provider/Layers/ProviderRegistry.ts index 94dde9f931bb..a8e6caf95aa7 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.ts @@ -162,31 +162,69 @@ const mergeProviderModels = ( : mergedModels; }; +/** + * Antigravity's health check only initializes the agent, so after a server + * restart it reports the account as unchecked. The saved Google login still + * works, and the previous snapshot proves it. Carry that account state until + * a session, refresh, or sign-out reports something new. A confirmed missing + * installation, sign-out, disabled instance, or a changed sign-in method is + * never overridden. + */ +const carrySavedAntigravityAccount = ( + previousProvider: ServerProvider, + nextProvider: ServerProvider, +): Pick | undefined => { + const antigravity = ProviderDriverKind.make("antigravity"); + if ( + nextProvider.driver !== antigravity || + previousProvider.driver !== antigravity || + !nextProvider.enabled || + nextProvider.auth.status !== "unknown" || + previousProvider.auth.status !== "authenticated" || + (nextProvider.auth.type !== undefined && + nextProvider.auth.type !== previousProvider.auth.type) || + (!nextProvider.installed && nextProvider.status !== "warning") + ) { + return undefined; + } + // The pending boot probe (`installed: false`, warning) and a failed probe + // keep their own status; only a passed health check reads as ready. + const status = + nextProvider.installed && nextProvider.status === "warning" ? "ready" : nextProvider.status; + return { auth: previousProvider.auth, status }; +}; + export const mergeProviderSnapshot = ( previousProvider: ServerProvider | undefined, nextProvider: ServerProvider, -): ServerProvider => - !previousProvider - ? nextProvider - : { - ...nextProvider, - models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models), - ...(nextProvider.workspaceSnapshots !== undefined - ? { workspaceSnapshots: nextProvider.workspaceSnapshots } - : previousProvider.workspaceSnapshots !== undefined - ? { workspaceSnapshots: previousProvider.workspaceSnapshots } - : {}), - ...(shouldRetainMissingOpenCodeMetadata(nextProvider) - ? { - slashCommands: - nextProvider.slashCommands.length === 0 - ? previousProvider.slashCommands - : nextProvider.slashCommands, - skills: - nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills, - } - : {}), - }; +): ServerProvider => { + if (!previousProvider) { + return nextProvider; + } + const savedAccount = carrySavedAntigravityAccount(previousProvider, nextProvider); + // "Google account access is not checked yet" describes the probe, not the + // account; it must not outlive the state it explained. + const { message: _uncheckedMessage, ...nextWithoutMessage } = nextProvider; + return { + ...(savedAccount?.status === "ready" ? nextWithoutMessage : nextProvider), + ...savedAccount, + models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models), + ...(nextProvider.workspaceSnapshots !== undefined + ? { workspaceSnapshots: nextProvider.workspaceSnapshots } + : previousProvider.workspaceSnapshots !== undefined + ? { workspaceSnapshots: previousProvider.workspaceSnapshots } + : {}), + ...(shouldRetainMissingOpenCodeMetadata(nextProvider) + ? { + slashCommands: + nextProvider.slashCommands.length === 0 + ? previousProvider.slashCommands + : nextProvider.slashCommands, + skills: nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills, + } + : {}), + }; +}; const haveProvidersChanged = ( previousProviders: ReadonlyArray, diff --git a/docs/user/providers-antigravity.md b/docs/user/providers-antigravity.md index 7466cba309ac..2336cb9b5c18 100644 --- a/docs/user/providers-antigravity.md +++ b/docs/user/providers-antigravity.md @@ -123,6 +123,9 @@ is refused while the runtime is in use. ## Check access and troubleshoot +A server restart keeps your Google sign-in. The provider shows the saved account +until a session, a refresh, or a sign-out reports something new. + To check access and reload models, use **Refresh provider status** in web or desktop provider settings, or **Refresh models** in mobile thread settings. If asked to sign in again, use setup on web or desktop.