1import {
2 ANTIGRAVITY_DEFAULT_MODEL,
3 ProviderDriverKind,
4 type AntigravitySettings,
5 type ProviderSetupError,
6 type ServerProvider,
7 type ServerProviderModel,
8 type ServerProviderSlashCommand,
9 type ServerProviderUsageLimits,
10} from "@t3tools/contracts";
11import { createModelCapabilities } from "@t3tools/shared/model";
12import * as DateTime from "effect/DateTime";
13import * as Effect from "effect/Effect";
14import * as Option from "effect/Option";
15import * as Predicate from "effect/Predicate";
16import * as Result from "effect/Result";
17import * as Stream from "effect/Stream";
18import * as SubscriptionRef from "effect/SubscriptionRef";
19import type * as EffectAcpErrors from "effect-acp/errors";
20import type * as EffectAcpSchema from "effect-acp/compat";
21
22import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
23import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
24import {
25 makeManualOnlyProviderMaintenanceCapabilities,
26 type ProviderMaintenanceCapabilities,
27} from "@t3tools/provider-core/server/maintenanceResolver";
28import {
29 buildServerProvider,
30 isCommandMissingCause,
31 type ServerProviderDraft,
32} from "@t3tools/provider-core/server/snapshotProbe";
33import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits";
34
35const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
36const MAX_WORKSPACE_SNAPSHOTS = 32;
37const HEALTH_CHECK_TIMEOUT = "90 seconds";
38const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
39const AUTH_UNCHECKED_MESSAGE =
40 "Antigravity is installed. Google account access is not checked yet.";
41
42type SessionSetupResult = Pick<
43 AcpSessionRuntime.AcpSessionRuntimeStartResult["sessionSetupResult"],
44 "configOptions" | "models"
45>;
46
47/** Keep the native model IDs, including model-specific thinking levels. */
48export function buildAntigravityModelsFromSession(
49 setup: SessionSetupResult,
50): ReadonlyArray<ServerProviderModel> {
51 const config = setup.configOptions?.find(
52 (option) => option.id === "model" || option.category === "model",
53 );
54 const currentValue =
55 config?.type === "select" ? config.currentValue : setup.models?.currentModelId;
56 const entries =
57 config?.type === "select"
58 ? config.options.flatMap((entry) => ("value" in entry ? [entry] : entry.options))
59 : config === undefined
60 ? (setup.models?.availableModels.map((model) => ({
61 value: model.modelId,
62 name: model.name,
63 })) ?? [])
64 : [];
65 const seen = new Set<string>();
66 return entries.flatMap((entry): ServerProviderModel[] => {
67 if (!entry.value.trim() || seen.has(entry.value)) return [];
68 seen.add(entry.value);
69 return [
70 {
71 slug: entry.value,
72 name: entry.name.trim() ? entry.name : entry.value,
73 isCustom: false,
74 ...(entry.value === currentValue
75 ? { isDefault: true, aliases: [ANTIGRAVITY_DEFAULT_MODEL] }
76 : {}),
77 capabilities: EMPTY_MODEL_CAPABILITIES,
78 },
79 ];
80 });
81}
82
83function nativeCommands(
84 commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
85): ReadonlyArray<ServerProviderSlashCommand> {
86 const seen = new Set<string>();
87 return commands.flatMap((command): ServerProviderSlashCommand[] => {
88 if (!command.name.trim() || seen.has(command.name)) return [];
89 seen.add(command.name);
90 const description = command.description.trim();
91 const hint = command.input?.hint.trim();
92 return [
93 {
94 name: command.name,
95 ...(description ? { description } : {}),
96 ...(hint ? { input: { hint } } : {}),
97 },
98 ];
99 });
100}
101
102function isMissingInstallation(error: EffectAcpErrors.AcpError | ProviderSetupError): boolean {
103 if (error._tag === "AcpSpawnError") {
104 return (
105 isCommandMissingCause(error.cause) ||
106 (Predicate.isObject(error.cause) && error.cause.code === "ENOENT")
107 );
108 }
109 return (
110 error._tag === "ProviderSetupError" &&
111 error.operation === "resolve" &&
112 /not installed|missing|incomplete|does not publish/i.test(error.detail)
113 );
114}
115
116interface AntigravityProviderState {
117 readonly draft: ServerProviderDraft;
118 readonly authRevision: number;
119}
120
121interface AntigravityProviderOptions {
122 readonly stampIdentity: (snapshot: ServerProviderDraft) => Effect.Effect<ServerProvider>;
123 readonly probe: Effect.Effect<
124 EffectAcpSchema.InitializeResponse,
125 EffectAcpErrors.AcpError | ProviderSetupError
126 >;
127 readonly supportsTextGeneration: Effect.Effect<boolean>;
128 readonly probeUsage?: Effect.Effect<ServerProviderUsageLimits>;
129 readonly clearUsage?: Effect.Effect<void>;
130 readonly maintenanceCapabilities?: ProviderMaintenanceCapabilities;
131 /** Auth type and label published once a session authenticates. */
132 readonly auth?: { readonly type: string; readonly label: string };
133}
134
135/**
136 * Health uses initialize only. Session callbacks supply account-specific
137 * metadata, which a disabled instance never shows.
138 */
139export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(function* (
140 settings: AntigravitySettings,
141 options: AntigravityProviderOptions,
142) {
143 const checkedAt = DateTime.formatIso(yield* DateTime.now);
144 const initialDraft = {
145 ...buildServerProvider({
146 presentation: { displayName: "Antigravity", showInteractionModeToggle: false },
147 enabled: settings.enabled,
148 checkedAt,
149 models: [],
150 probe: {
151 installed: false,
152 version: null,
153 status: "warning",
154 // The configured method rides along so the registry can tell a saved
155 // account for this method from one left by a previous configuration.
156 auth: { status: "unknown", ...(options.auth ? { type: options.auth.type } : {}) },
157 message: settings.enabled
158 ? "Checking Antigravity availability."
159 : "Antigravity is disabled in T3 Code settings.",
160 },
161 }),
162 setup: { canAuthenticate: true, canInstall: true },
163 supportsConversationRollback: false,
164 supportsTextGeneration: false,
165 workspaceSnapshots: [],
166 } satisfies ServerProviderDraft;
167 const metadata = yield* SubscriptionRef.make<AntigravityProviderState>({
168 draft: initialDraft,
169 authRevision: 0,
170 });
171 // Skills the driver discovered on disk per workspace. Session callbacks
172 // rewrite the workspace entry with native commands and must keep these, or
173 // the registry drops the suggestions and never re-reads the workspace.
174 const discoveredSkills = new Map<string, ServerProvider["skills"]>();
175 const getSnapshot = SubscriptionRef.get(metadata).pipe(
176 Effect.flatMap((state) => options.stampIdentity(state.draft)),
177 );
178
179 const checkProvider = Effect.fn("checkAntigravityProvider")(function* () {
180 if (!settings.enabled) return yield* getSnapshot;
181 const before = yield* SubscriptionRef.get(metadata);
182 const result = yield* options.probe.pipe(
183 Effect.timeoutOption(HEALTH_CHECK_TIMEOUT),
184 Effect.result,
185 );
186 const initialized =
187 Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined;
188 const failure = Result.isFailure(result) ? result.failure : undefined;
189 const missingInstallation = failure !== undefined && isMissingInstallation(failure);
190 const errorMessage =
191 initialized !== undefined
192 ? undefined
193 : failure?._tag === "ProviderSetupError"
194 ? failure.detail.trim() || "Antigravity could not complete its local health check."
195 : missingInstallation
196 ? "Antigravity is not installed or its executable could not be found."
197 : failure
198 ? "Antigravity could not complete its local health check."
199 : `Antigravity did not respond to its local health check within ${HEALTH_CHECK_TIMEOUT}.`;
200 const supportsTextGeneration =
201 initialized !== undefined ? yield* options.supportsTextGeneration : false;
202 const usageLimits =
203 initialized !== undefined && options.probeUsage ? yield* options.probeUsage : undefined;
204 const updatedAt = DateTime.formatIso(yield* DateTime.now);
205 const next = yield* SubscriptionRef.updateAndGet(metadata, (state) => {
206 if (state.authRevision !== before.authRevision) return state;
207 const { message: _previousMessage, ...draft } = state.draft;
208 const authenticated = draft.auth.status === "authenticated";
209 const message =
210 errorMessage ??
211 (authenticated
212 ? undefined
213 : draft.auth.status === "unauthenticated"
214 ? SIGN_IN_MESSAGE
215 : AUTH_UNCHECKED_MESSAGE);
216 return {
217 ...state,
218 draft: {
219 ...draft,
220 installed: !missingInstallation,
221 version: initialized?.agentInfo?.version || draft.version,
222 status: errorMessage ? "error" : authenticated ? "ready" : "warning",
223 checkedAt: updatedAt,
224 usageLimits: missingInstallation
225 ? undefined
226 : resolveUsageLimitsAfterProbe({
227 published: draft.usageLimits,
228 probed: usageLimits ?? draft.usageLimits,
229 }),
230 ...(missingInstallation
231 ? {
232 models: [],
233 slashCommands: [],
234 skills: [],
235 workspaceSnapshots: [],
236 supportsTextGeneration: false,
237 }
238 : {}),
239 ...(initialized !== undefined
240 ? {
241 supportsTextGeneration:
242 supportsTextGeneration && draft.auth.status !== "unauthenticated",
243 }
244 : {}),
245 ...(message ? { message } : {}),
246 },
247 } satisfies AntigravityProviderState;
248 });
249 return yield* options.stampIdentity(next.draft);
250 });
251
252 const maintenanceCapabilities =
253 options.maintenanceCapabilities ??
254 makeManualOnlyProviderMaintenanceCapabilities({
255 provider: ProviderDriverKind.make("antigravity"),
256 packageName: null,
257 });
258 const managed = yield* makeManagedServerProvider({
259 resolveMaintenance: () => Effect.succeed(maintenanceCapabilities),
260 getSettings: Effect.succeed(settings),
261 streamSettings: Stream.empty,
262 haveSettingsChanged: () => false,
263 initialSnapshot: () => getSnapshot,
264 checkProvider: checkProvider(),
265 enrichSnapshot: ({ publishSnapshot }) =>
266 SubscriptionRef.changes(metadata).pipe(
267 Stream.runForEach((state) =>
268 options.stampIdentity(state.draft).pipe(Effect.flatMap(publishSnapshot)),
269 ),
270 ),
271 });
272
273 const onSessionStarted = Effect.fn("AntigravityProvider.onSessionStarted")(function* (
274 started: AcpSessionRuntime.AcpSessionRuntimeStartResult,
275 cwd?: string,
276 ) {
277 if (!settings.enabled) return;
278 const before = yield* SubscriptionRef.get(metadata);
279 const supportsTextGeneration = yield* options.supportsTextGeneration;
280 const usageLimits =
281 options.probeUsage && (!before.draft.usageLimits || before.draft.usageLimits.unavailable)
282 ? yield* options.probeUsage
283 : before.draft.usageLimits;
284 const updatedAt = DateTime.formatIso(yield* DateTime.now);
285 yield* SubscriptionRef.update(metadata, (state) => {
286 if (
287 state.authRevision !== before.authRevision &&
288 state.draft.auth.status === "unauthenticated"
289 ) {
290 return state;
291 }
292 const { message: _previousMessage, ...draft } = state.draft;
293 const workspaces = draft.workspaceSnapshots ?? [];
294 const workspace = cwd ? workspaces.find((entry) => entry.cwd === cwd) : undefined;
295 return {
296 authRevision: state.authRevision + 1,
297 draft: {
298 ...draft,
299 installed: true,
300 status: "ready",
301 version: started.initializeResult.agentInfo?.version || draft.version,
302 auth: {
303 status: "authenticated",
304 type: options.auth?.type ?? "oauth-personal",
305 label: options.auth?.label ?? "Google account",
306 },
307 checkedAt: updatedAt,
308 usageLimits: resolveUsageLimitsAfterProbe({
309 published: draft.usageLimits,
310 probed: usageLimits ?? draft.usageLimits,
311 }),
312 models: buildAntigravityModelsFromSession(started.sessionSetupResult),
313 supportsTextGeneration,
314 ...(cwd
315 ? {
316 workspaceSnapshots: [
317 ...workspaces.filter((entry) => entry.cwd !== cwd),
318 {
319 cwd,
320 checkedAt: updatedAt,
321 slashCommands: workspace?.slashCommands ?? draft.slashCommands,
322 skills: workspace?.skills ?? discoveredSkills.get(cwd) ?? [],
323 },
324 ].slice(-MAX_WORKSPACE_SNAPSHOTS),
325 }
326 : {}),
327 },
328 } satisfies AntigravityProviderState;
329 });
330 });
331
332 const onConfigOptionsUpdated = Effect.fn("AntigravityProvider.onConfigOptionsUpdated")(function* (
333 configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
334 ) {
335 const models = buildAntigravityModelsFromSession({ configOptions });
336 yield* SubscriptionRef.update(metadata, (state) => {
337 if (state.draft.auth.status !== "authenticated") return state;
338 return { ...state, draft: { ...state.draft, models } };
339 });
340 });
341
342 const onAvailableCommands = Effect.fn("AntigravityProvider.onAvailableCommands")(function* (
343 commands: ReadonlyArray<EffectAcpSchema.AvailableCommand>,
344 cwd?: string,
345 ) {
346 if (!settings.enabled) return;
347 const slashCommands = nativeCommands(commands);
348 const updatedAt = DateTime.formatIso(yield* DateTime.now);
349 yield* SubscriptionRef.update(metadata, (state) => {
350 if (state.draft.auth.status === "unauthenticated") return state;
351 return {
352 ...state,
353 draft: {
354 ...state.draft,
355 slashCommands,
356 ...(cwd
357 ? {
358 workspaceSnapshots: [
359 ...(state.draft.workspaceSnapshots ?? []).filter((entry) => entry.cwd !== cwd),
360 {
361 cwd,
362 checkedAt: updatedAt,
363 slashCommands,
364 skills:
365 state.draft.workspaceSnapshots?.find((entry) => entry.cwd === cwd)?.skills ??
366 discoveredSkills.get(cwd) ??
367 [],
368 },
369 ].slice(-MAX_WORKSPACE_SNAPSHOTS),
370 }
371 : {}),
372 },
373 };
374 });
375 });
376
377 const clearAccountMetadata = Effect.fn("AntigravityProvider.clearAccountMetadata")(function* () {
378 if (options.clearUsage) yield* options.clearUsage;
379 if (!settings.enabled) return;
380 const updatedAt = DateTime.formatIso(yield* DateTime.now);
381 yield* SubscriptionRef.update(
382 metadata,
383 (state) =>
384 ({
385 authRevision: state.authRevision + 1,
386 draft: {
387 ...state.draft,
388 auth: { status: "unauthenticated" },
389 status: "warning",
390 message: SIGN_IN_MESSAGE,
391 checkedAt: updatedAt,
392 models: [],
393 slashCommands: [],
394 skills: [],
395 workspaceSnapshots: [],
396 supportsTextGeneration: false,
397 usageLimits: undefined,
398 },
399 }) satisfies AntigravityProviderState,
400 );
401 discoveredSkills.clear();
402 });
403
404 const snapshotForCwd = Effect.fn("AntigravityProvider.snapshotForCwd")(function* (
405 cwd: string,
406 skills?: ServerProvider["skills"],
407 ) {
408 if (skills) {
409 discoveredSkills.set(cwd, skills);
410 // A rescan replaces the stored entry's skills. Session callbacks and
411 // health checks republish that entry, so it must not keep old ones.
412 yield* SubscriptionRef.update(metadata, (state) =>
413 state.draft.workspaceSnapshots?.some((entry) => entry.cwd === cwd)
414 ? {
415 ...state,
416 draft: {
417 ...state.draft,
418 workspaceSnapshots: state.draft.workspaceSnapshots.map((entry) =>
419 entry.cwd === cwd ? { ...entry, skills } : entry,
420 ),
421 },
422 }
423 : state,
424 );
425 }
426 const snapshot = yield* getSnapshot;
427 const workspace = snapshot.workspaceSnapshots?.find((entry) => entry.cwd === cwd);
428 const resolvedSkills = skills ?? workspace?.skills ?? discoveredSkills.get(cwd) ?? [];
429 return workspace
430 ? { ...snapshot, slashCommands: workspace.slashCommands, skills: resolvedSkills }
431 : { ...snapshot, skills: resolvedSkills };
432 });
433
434 return {
435 snapshot: { ...managed, getSnapshot },
436 onSessionStarted,
437 onConfigOptionsUpdated,
438 onAvailableCommands,
439 onSignedOut: clearAccountMetadata(),
440 onAuthRequired: clearAccountMetadata(),
441 snapshotForCwd,
442 };
443});