Skip to content
Merged
14 changes: 12 additions & 2 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { environmentPresentations } from "../../state/presentation";
import { ResetCredits } from "./UsageLimitsSection";
import { useProviderColors } from "./usageProviders";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const DRIVER_LABEL: Partial<Record<string, string>> = {
codex: "Codex",
claudeAgent: "Claude",
antigravity: "Antigravity",
};
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;

function accountName(account: LimitAccount) {
Expand Down Expand Up @@ -264,7 +268,13 @@ export function UsageLimitsSection({
<PoolWindowCard
key={`${window.kind}:${window.id}`}
pool={window}
color={pool.driver === "claudeAgent" ? colors.claude : colors.codex}
color={
pool.driver === "claudeAgent"
? colors.claude
: pool.driver === "antigravity"
? colors.antigravity
: colors.codex
}
now={now}
environmentIds={
selectedEnvironmentIds === null ? null : [...selectedEnvironmentIds]
Expand Down
77 changes: 76 additions & 1 deletion apps/server/src/provider/AntigravityProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ProviderDriverKind,
ProviderInstanceId,
ProviderSetupError,
type ServerProviderUsageLimits,
} from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -128,7 +129,11 @@ const layerTest = ProviderHostLive.layer.pipe(
type ProbeError = EffectAcpErrors.AcpError | ProviderSetupError;

const makeHarness = Effect.fn("makeAntigravityProviderHarness")(function* (
options: { readonly enabled?: boolean; readonly safe?: boolean } = {},
options: {
readonly enabled?: boolean;
readonly safe?: boolean;
readonly probeUsage?: Effect.Effect<ServerProviderUsageLimits>;
} = {},
) {
const initialProbe = yield* Deferred.make<EffectAcpSchema.InitializeResponse, ProbeError>();
const probeCalls = yield* Ref.make(0);
Expand All @@ -141,6 +146,7 @@ const makeHarness = Effect.fn("makeAntigravityProviderHarness")(function* (
decodeSettings({ enabled: options.enabled ?? true, customModels: ["do-not-seed-me"] }),
{
stampIdentity: (snapshot) => Effect.succeed({ ...snapshot, instanceId, driver }),
...(options.probeUsage ? { probeUsage: options.probeUsage } : {}),
probe: Ref.update(probeCalls, (count) => count + 1).pipe(
Effect.andThen(Ref.get(probe)),
Effect.flatten,
Expand Down Expand Up @@ -670,4 +676,73 @@ it.layer(layerTest)("Antigravity provider snapshots", (it) => {
}),
),
);

it.effect(
"publishes limits, retains a successful read on failure, and clears them on sign-out",
() =>
Effect.scoped(
Effect.gen(function* () {
const limits: ServerProviderUsageLimits = {
checkedAt: "2026-10-09T12:00:00.000Z",
windows: [
{
id: "3p-weekly",
kind: "weekly",
label: "Claude & GPT (shared) · Weekly",
usedPercent: 75,
},
],
};
const usage = yield* Ref.make(limits);
const harness = yield* makeHarness({ probeUsage: Ref.get(usage) });
yield* harness.initialize;
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toEqual(limits);
yield* Ref.set(harness.probe, Effect.succeed(initializeResult));
yield* Ref.set(usage, {
checkedAt: "2026-10-09T12:01:00.000Z",
windows: [],
unavailable: { reason: "probeFailed" },
});
expect((yield* harness.provider.snapshot.refresh).usageLimits).toEqual(limits);
yield* harness.provider.onSignedOut;
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toBeUndefined();
yield* Ref.set(usage, {
checkedAt: "2026-10-09T12:02:00.000Z",
windows: [],
unavailable: { reason: "unsupported" },
});
expect((yield* harness.provider.snapshot.refresh).usageLimits?.windows).toEqual([]);
}),
),
);

it.effect("a quota read finishing after sign-out cannot restore account limits", () =>
Effect.scoped(
Effect.gen(function* () {
const readStarted = yield* Deferred.make<void>();
const readResult = yield* Deferred.make<ServerProviderUsageLimits>();
const usage = yield* Ref.make<Effect.Effect<ServerProviderUsageLimits>>(
Effect.succeed({ checkedAt: "2026-10-09T12:00:00.000Z", windows: [] }),
);
const harness = yield* makeHarness({ probeUsage: Ref.get(usage).pipe(Effect.flatten) });
yield* harness.initialize;
yield* Ref.set(harness.probe, Effect.succeed(initializeResult));
yield* Ref.set(
usage,
Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(readResult))),
);
const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
yield* Deferred.await(readStarted);
yield* harness.provider.onSignedOut;
yield* Deferred.succeed(readResult, {
checkedAt: "2026-10-09T12:01:00.000Z",
windows: [
{ id: "gemini-5h", kind: "session", label: "Gemini · 5-hour", usedPercent: 10 },
],
});
yield* Fiber.join(refresh);
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toBeUndefined();
}),
),
);
});
22 changes: 22 additions & 0 deletions apps/server/src/provider/AntigravityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ServerProvider,
type ServerProviderModel,
type ServerProviderSlashCommand,
type ServerProviderUsageLimits,
} from "@t3tools/contracts";
import { createModelCapabilities } from "@t3tools/shared/model";
import * as DateTime from "effect/DateTime";
Expand All @@ -29,6 +30,7 @@ import {
isCommandMissingCause,
type ServerProviderDraft,
} from "@t3tools/provider-core/server/snapshotProbe";
import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits";

const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
const MAX_WORKSPACE_SNAPSHOTS = 32;
Expand Down Expand Up @@ -123,6 +125,8 @@ interface AntigravityProviderOptions {
EffectAcpErrors.AcpError | ProviderSetupError
>;
readonly supportsTextGeneration: Effect.Effect<boolean>;
readonly probeUsage?: Effect.Effect<ServerProviderUsageLimits>;
readonly clearUsage?: Effect.Effect<void>;
readonly maintenanceCapabilities?: ProviderMaintenanceCapabilities;
/** Auth type and label published once a session authenticates. */
readonly auth?: { readonly type: string; readonly label: string };
Expand Down Expand Up @@ -192,6 +196,8 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
: `Antigravity did not respond to its local health check within ${HEALTH_CHECK_TIMEOUT}.`;
const supportsTextGeneration =
initialized !== undefined ? yield* options.supportsTextGeneration : false;
const usageLimits =
initialized !== undefined && options.probeUsage ? yield* options.probeUsage : undefined;
const updatedAt = DateTime.formatIso(yield* DateTime.now);
const next = yield* SubscriptionRef.updateAndGet(metadata, (state) => {
if (state.authRevision !== before.authRevision) return state;
Expand All @@ -212,6 +218,12 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
version: initialized?.agentInfo?.version || draft.version,
status: errorMessage ? "error" : authenticated ? "ready" : "warning",
checkedAt: updatedAt,
usageLimits: missingInstallation
? undefined
: resolveUsageLimitsAfterProbe({
published: draft.usageLimits,
probed: usageLimits ?? draft.usageLimits,
}),
...(missingInstallation
? {
models: [],
Expand Down Expand Up @@ -261,6 +273,10 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
) {
const before = yield* SubscriptionRef.get(metadata);
const supportsTextGeneration = yield* options.supportsTextGeneration;
const usageLimits =
options.probeUsage && (!before.draft.usageLimits || before.draft.usageLimits.unavailable)
? yield* options.probeUsage
: before.draft.usageLimits;
const updatedAt = DateTime.formatIso(yield* DateTime.now);
yield* SubscriptionRef.update(metadata, (state) => {
if (
Expand All @@ -285,6 +301,10 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
label: options.auth?.label ?? "Google account",
},
checkedAt: updatedAt,
usageLimits: resolveUsageLimitsAfterProbe({
published: draft.usageLimits,
probed: usageLimits ?? draft.usageLimits,
}),
models: buildAntigravityModelsFromSession(started.sessionSetupResult),
supportsTextGeneration,
...(cwd
Expand Down Expand Up @@ -350,6 +370,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
});

const clearAccountMetadata = Effect.fn("AntigravityProvider.clearAccountMetadata")(function* () {
if (options.clearUsage) yield* options.clearUsage;
const updatedAt = DateTime.formatIso(yield* DateTime.now);
yield* SubscriptionRef.update(
metadata,
Expand All @@ -367,6 +388,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
skills: [],
workspaceSnapshots: [],
supportsTextGeneration: false,
usageLimits: undefined,
},
}) satisfies AntigravityProviderState,
);
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import { HttpClient } from "effect/http";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import * as ServerConfig from "../../config.ts";
Expand Down Expand Up @@ -246,6 +247,12 @@ const layerDeps = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-antigravity-driver-config-",
}).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("Unexpected quota request")),
),
),
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provideMerge(
Layer.mock(BackgroundPolicy.BackgroundPolicy)({
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import { HttpClient } from "effect/http";
import type { AcpError } from "effect-acp/errors";

import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
Expand Down Expand Up @@ -53,6 +54,7 @@ import { makeAntigravityAdapterV2 } from "../../orchestration-v2/Adapters/Antigr
import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging";
import { ProviderDriverError } from "../Errors.ts";
import { makeAntigravityProvider } from "../AntigravityProvider.ts";
import { makeAntigravityUsageProbe } from "../antigravityUsageLimits.ts";
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog";
import {
Expand All @@ -74,6 +76,7 @@ export type AntigravityDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| IdAllocator.IdAllocatorV2
| McpProviderSessions.McpProviderSessions
| ModelCatalog.ModelCatalog
Expand All @@ -95,6 +98,7 @@ export const AntigravityDriver: ProviderDriver<
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const fileSystem = yield* FileSystem.FileSystem;
const httpClient = yield* HttpClient.HttpClient;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const host = yield* ProviderHost.ProviderHost;
Expand Down Expand Up @@ -384,9 +388,20 @@ export const AntigravityDriver: ProviderDriver<
};
});

const usage = yield* makeAntigravityUsageProbe({
profileDirectory,
authMethod: auth.authMethod,
}).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(HttpClient.HttpClient, httpClient),
);
const provider = yield* makeAntigravityProvider(settings, {
stampIdentity: classifyModels,
probe,
probeUsage: usage.probe,
clearUsage: usage.clear,
auth: { type: auth.authMethod, label: antigravityAuthLabel(auth.authMethod) },
supportsTextGeneration: isAntigravityTextGenerationAvailable(profileDirectory).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Expand Down
Loading
Loading