| 1 | import { |
| 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"; |
| 11 | import { createModelCapabilities } from "@t3tools/shared/model"; |
| 12 | import * as DateTime from "effect/DateTime"; |
| 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Option from "effect/Option"; |
| 15 | import * as Predicate from "effect/Predicate"; |
| 16 | import * as Result from "effect/Result"; |
| 17 | import * as Stream from "effect/Stream"; |
| 18 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 19 | import type * as EffectAcpErrors from "effect-acp/errors"; |
| 20 | import type * as EffectAcpSchema from "effect-acp/compat"; |
| 21 | |
| 22 | import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 23 | import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; |
| 24 | import { |
| 25 | makeManualOnlyProviderMaintenanceCapabilities, |
| 26 | type ProviderMaintenanceCapabilities, |
| 27 | } from "@t3tools/provider-core/server/maintenanceResolver"; |
| 28 | import { |
| 29 | buildServerProvider, |
| 30 | isCommandMissingCause, |
| 31 | type ServerProviderDraft, |
| 32 | } from "@t3tools/provider-core/server/snapshotProbe"; |
| 33 | import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits"; |
| 34 | |
| 35 | const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); |
| 36 | const MAX_WORKSPACE_SNAPSHOTS = 32; |
| 37 | const HEALTH_CHECK_TIMEOUT = "90 seconds"; |
| 38 | const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; |
| 39 | const AUTH_UNCHECKED_MESSAGE = |
| 40 | "Antigravity is installed. Google account access is not checked yet."; |
| 41 | |
| 42 | type SessionSetupResult = Pick< |
| 43 | AcpSessionRuntime.AcpSessionRuntimeStartResult["sessionSetupResult"], |
| 44 | "configOptions" | "models" |
| 45 | >; |
| 46 | |
| 47 | /** Keep the native model IDs, including model-specific thinking levels. */ |
| 48 | export 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 | |
| 83 | function 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 | |
| 102 | function 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 | |
| 116 | interface AntigravityProviderState { |
| 117 | readonly draft: ServerProviderDraft; |
| 118 | readonly authRevision: number; |
| 119 | } |
| 120 | |
| 121 | interface 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 | */ |
| 139 | export 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 | }); |