@golba98/codexa 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,10 +1,11 @@
1
- export type ProviderId = "openai" | "anthropic" | "google" | "local";
1
+ export type ProviderId = "openai" | "anthropic" | "google" | "local" | "antigravity";
2
2
 
3
3
  export type ProviderBackendType =
4
- | "codex-cli-auth"
5
- | "gemini-cli-auth"
6
- | "claude-code-auth"
7
- | "openai-api-key"
4
+ | "codex-cli-auth"
5
+ | "gemini-cli-auth"
6
+ | "claude-code-auth"
7
+ | "antigravity-cli-auth"
8
+ | "openai-api-key"
8
9
  | "gemini-api-key"
9
10
  | "anthropic-api-key"
10
11
  | "local-openai-compatible"
@@ -69,10 +70,11 @@ export interface ProviderWorkspaceOverride {
69
70
  defaultModel?: string;
70
71
  models?: Record<string, ProviderModelWorkspaceOverride>;
71
72
  command?: string | ProviderLaunchCommand | null;
72
- claudeCommandPath?: string;
73
- geminiCommandPath?: string;
74
- codexCommandPath?: string;
75
- }
73
+ claudeCommandPath?: string;
74
+ geminiCommandPath?: string;
75
+ codexCommandPath?: string;
76
+ antigravityCommandPath?: string;
77
+ }
76
78
 
77
79
  export interface ProviderModelWorkspaceOverride {
78
80
  contextLength?: number;
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import { DEFAULT_MODEL } from "../../config/settings.js";
4
- import { normalizeWorkspaceRoot } from "../workspaceRoot.js";
4
+ import { normalizeWorkspaceRoot } from "../workspace/workspaceRoot.js";
5
5
  import { isKnownProviderId } from "./registry.js";
6
6
  import { getDefaultRouteModel, getProviderRuntime, isProviderRouteConfigured, isProviderRoutableInCodexa } from "../providerRuntime/registry.js";
7
7
  import { normalizeGeminiModelId } from "../providerRuntime/models.js";
@@ -13,8 +13,9 @@ import type {
13
13
  ProviderWorkspaceOverride,
14
14
  } from "./types.js";
15
15
 
16
- const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
- const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
16
+ const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
+ const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
18
+ const DEPRECATED_GOOGLE_PROVIDER_ID = "google";
18
19
 
19
20
  export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
20
21
  return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
@@ -28,16 +29,20 @@ function isDeprecatedAntigravityProviderId(value: unknown): boolean {
28
29
  return value === DEPRECATED_ANTIGRAVITY_PROVIDER_ID;
29
30
  }
30
31
 
31
- function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
+ function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
33
  if (!isRecord(value)) return false;
33
34
  return isDeprecatedAntigravityProviderId(value.providerId ?? value.provider_id)
34
35
  || DEPRECATED_ANTIGRAVITY_BACKENDS.has(String(value.backendKind ?? value.backend_kind));
35
- }
36
+ }
37
+
38
+ function isDeprecatedGoogleRoute(value: unknown): boolean {
39
+ return isRecord(value) && (value.providerId ?? value.provider_id) === DEPRECATED_GOOGLE_PROVIDER_ID;
40
+ }
36
41
 
37
42
  function resolveDeprecatedProviderFallback(
38
43
  providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>>,
39
44
  ): ProviderId {
40
- const candidates: readonly ProviderId[] = ["openai", "google", "anthropic", "local"];
45
+ const candidates: readonly ProviderId[] = ["openai", "anthropic", "local"];
41
46
  return candidates.find((providerId) =>
42
47
  isProviderRoutableInCodexa(providerId)
43
48
  && (providerId === "openai" || providers[providerId] !== undefined || isProviderRouteConfigured(providerId))
@@ -214,22 +219,24 @@ function parseActiveRoute(value: unknown): ProviderActiveRoute | undefined {
214
219
  export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceConfig {
215
220
  if (!isRecord(data)) return {};
216
221
 
217
- const config: ProviderWorkspaceConfig = {};
218
- const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
219
- let foundDeprecatedAntigravity = false;
222
+ const config: ProviderWorkspaceConfig = {};
223
+ const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
224
+ let foundDeprecatedAntigravity = false;
225
+ let foundDeprecatedGoogle = false;
220
226
 
221
227
  if (isRecord(data.providers)) {
222
228
  for (const [id, value] of Object.entries(data.providers)) {
223
- if (isDeprecatedAntigravityProviderId(id)) {
224
- foundDeprecatedAntigravity = true;
225
- continue;
226
- }
227
- if (!isKnownProviderId(id)) continue;
228
- const override = parseProviderOverride(value);
229
- if (id === "google" && override?.currentModel) {
230
- override.currentModel = normalizeGeminiModelId(override.currentModel);
231
- }
232
- if (override) providers[id] = override;
229
+ if (isDeprecatedAntigravityProviderId(id)) {
230
+ foundDeprecatedAntigravity = true;
231
+ continue;
232
+ }
233
+ if (id === DEPRECATED_GOOGLE_PROVIDER_ID) {
234
+ foundDeprecatedGoogle = true;
235
+ continue;
236
+ }
237
+ if (!isKnownProviderId(id)) continue;
238
+ const override = parseProviderOverride(value);
239
+ if (override) providers[id] = override;
233
240
  }
234
241
  config.providers = providers;
235
242
  }
@@ -238,7 +245,10 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
238
245
  ?? data.workspace_default_provider_id
239
246
  ?? data.defaultProviderId
240
247
  ?? data.default_provider_id;
241
- if (isDeprecatedAntigravityProviderId(defaultProvider)) {
248
+ if (defaultProvider === DEPRECATED_GOOGLE_PROVIDER_ID) {
249
+ foundDeprecatedGoogle = true;
250
+ config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
251
+ } else if (isDeprecatedAntigravityProviderId(defaultProvider)) {
242
252
  foundDeprecatedAntigravity = true;
243
253
  config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
244
254
  } else if (typeof defaultProvider === "string" && isKnownProviderId(defaultProvider)) {
@@ -246,7 +256,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
246
256
  }
247
257
 
248
258
  const rawActiveRoute = data.activeRoute ?? data.active_route;
249
- if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
259
+ if (isDeprecatedGoogleRoute(rawActiveRoute)) {
260
+ foundDeprecatedGoogle = true;
261
+ const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
262
+ config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
263
+ config.workspaceDefaultProviderId ??= fallbackProviderId;
264
+ } else if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
250
265
  foundDeprecatedAntigravity = true;
251
266
  const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
252
267
  config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
@@ -258,12 +273,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
258
273
  }
259
274
  }
260
275
 
261
- if (foundDeprecatedAntigravity) {
276
+ if (foundDeprecatedGoogle || foundDeprecatedAntigravity) {
262
277
  const revertedProviderId = config.activeRoute?.providerId
263
278
  ?? config.workspaceDefaultProviderId
264
279
  ?? resolveDeprecatedProviderFallback(providers);
265
280
  config.migrationNotice = {
266
- deprecatedProviderId: DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
281
+ deprecatedProviderId: foundDeprecatedGoogle ? DEPRECATED_GOOGLE_PROVIDER_ID : DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
267
282
  revertedProviderId,
268
283
  };
269
284
  }
@@ -348,13 +363,13 @@ export function saveProviderWorkspaceConfig(workspaceRoot: string, config: Provi
348
363
  renameSync(tmpFile, filePath);
349
364
  }
350
365
 
351
- export function setProviderWorkspaceDefault(
366
+ export function setProviderWorkspaceDefault(
352
367
  config: ProviderWorkspaceConfig,
353
368
  providerId: ProviderId,
354
369
  ): ProviderWorkspaceConfig {
355
370
  return {
356
371
  ...config,
357
- workspaceDefaultProviderId: providerId,
372
+ workspaceDefaultProviderId: providerId === DEPRECATED_GOOGLE_PROVIDER_ID ? "openai" : providerId,
358
373
  };
359
374
  }
360
375
 
@@ -396,7 +411,7 @@ export function setProviderActiveRoute(
396
411
  config: ProviderWorkspaceConfig,
397
412
  activeRoute: ProviderActiveRoute,
398
413
  ): ProviderWorkspaceConfig {
399
- if (!isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
414
+ if (activeRoute.providerId === DEPRECATED_GOOGLE_PROVIDER_ID || !isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
400
415
  return config;
401
416
  }
402
417
 
@@ -19,6 +19,7 @@ const ANTHROPIC_MAX_TOKENS = 1024;
19
19
  const ANTHROPIC_TIMEOUT_MS = 120_000;
20
20
  const ANTHROPIC_AUTH_CHECK_TIMEOUT_MS = 10_000;
21
21
  const ANTHROPIC_ROUTE_VALIDATION_TIMEOUT_MS = 15_000;
22
+ const DISCOVERY_FAILURE_MESSAGE = "Claude Code model version discovery failed; using fallback aliases with unknown versions.";
22
23
  export const ANTHROPIC_ROUTE_SETUP_MESSAGE = "Anthropic/Claude is not configured for in-Codexa routing.\nSign in with Claude Code or set ANTHROPIC_API_KEY.";
23
24
  export { parseClaudeAuthStatus } from "./claudeCodeDiscovery.js";
24
25
 
@@ -498,6 +499,9 @@ export const anthropicRuntime: ProviderRuntime = {
498
499
  providerId: "anthropic",
499
500
  backendKind: getAnthropicRuntimeBackendKind(),
500
501
  models: getActiveAnthropicModels(),
502
+ message: (claudeCapabilityDiscovery?.modelSource ?? "fallback") === "fallback"
503
+ ? DISCOVERY_FAILURE_MESSAGE
504
+ : undefined,
501
505
  diagnostics: claudeCapabilityDiscovery ? {
502
506
  resolvedCommand: claudeCapabilityDiscovery.resolvedCommand,
503
507
  modelSource: claudeCapabilityDiscovery.modelSource,
@@ -534,7 +538,9 @@ export const anthropicRuntime: ProviderRuntime = {
534
538
  providerId: "anthropic",
535
539
  backendKind: getAnthropicRuntimeBackendKind(),
536
540
  models: discoveredAnthropicModels,
537
- message: `Refreshed Claude capabilities (${discovery.modelSource}).`,
541
+ message: discovery.modelSource === "fallback"
542
+ ? DISCOVERY_FAILURE_MESSAGE
543
+ : `Refreshed Claude capabilities (${discovery.modelSource}).`,
538
544
  diagnostics: {
539
545
  resolvedCommand: discovery.resolvedCommand,
540
546
  modelSource: discovery.modelSource,
@@ -0,0 +1,305 @@
1
+ import { runCommand } from "../process/CommandRunner.js";
2
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
3
+ import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
4
+ import type { BackendRunHandlers } from "../providers/types.js";
5
+ import type {
6
+ ProviderBackendKind,
7
+ ProviderChatRequest,
8
+ ProviderModel,
9
+ ProviderModelDiscoveryResult,
10
+ ProviderRouteValidationResult,
11
+ ProviderRuntime,
12
+ } from "./types.js";
13
+ import {
14
+ resolveAgyExecutable,
15
+ resetAgyExecutableCacheForTests,
16
+ } from "../executables/antigravityExecutable.js";
17
+ import { buildSpawnSpec } from "../executables/executableResolver.js";
18
+
19
+ export { resetAgyExecutableCacheForTests };
20
+
21
+ const ANTIGRAVITY_TIMEOUT_MS = 120_000;
22
+ const ANTIGRAVITY_VALIDATION_TIMEOUT_MS = 10_000;
23
+ const ANTIGRAVITY_ROUTE_SETUP_MESSAGE =
24
+ "`agy` command not found. Install Antigravity CLI or set AGY_EXECUTABLE to the full path.";
25
+
26
+ export const ANTIGRAVITY_DEFAULT_MODEL_ID = "gemini-3.5-flash";
27
+ export const ANTIGRAVITY_DEFAULT_REASONING = "high";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Effort levels for Antigravity Gemini models
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const AGY_LOW: ReasoningEffortCapability = { id: "low", label: "Low", description: null };
34
+ const AGY_MEDIUM: ReasoningEffortCapability = { id: "medium", label: "Medium", description: null };
35
+ const AGY_HIGH: ReasoningEffortCapability = { id: "high", label: "High", description: null };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Model definitions
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export const ANTIGRAVITY_MODELS: readonly ProviderModel[] = [
42
+ {
43
+ id: "gemini-3.5-flash",
44
+ modelId: "gemini-3.5-flash",
45
+ label: "Gemini 3.5 Flash",
46
+ description: "Antigravity-routed Gemini 3.5 Flash. Select effort with ←/→.",
47
+ defaultReasoningLevel: "high",
48
+ supportedReasoningLevels: [AGY_LOW, AGY_MEDIUM, AGY_HIGH],
49
+ source: "fallback",
50
+ },
51
+ {
52
+ id: "gemini-3.1-pro",
53
+ modelId: "gemini-3.1-pro",
54
+ label: "Gemini 3.1 Pro",
55
+ description: "Antigravity-routed Gemini 3.1 Pro. Select effort with ←/→.",
56
+ defaultReasoningLevel: "high",
57
+ supportedReasoningLevels: [AGY_LOW, AGY_HIGH],
58
+ source: "fallback",
59
+ },
60
+ {
61
+ id: "claude-sonnet-4-6-think",
62
+ modelId: "claude-sonnet-4-6-think",
63
+ label: "Claude Sonnet 4.6 (Thinking)",
64
+ description: "Antigravity-routed Claude Sonnet 4.6 with extended thinking.",
65
+ defaultReasoningLevel: null,
66
+ supportedReasoningLevels: null,
67
+ source: "fallback",
68
+ },
69
+ {
70
+ id: "claude-opus-4-6-think",
71
+ modelId: "claude-opus-4-6-think",
72
+ label: "Claude Opus 4.6 (Thinking)",
73
+ description: "Antigravity-routed Claude Opus 4.6 with extended thinking.",
74
+ defaultReasoningLevel: null,
75
+ supportedReasoningLevels: null,
76
+ source: "fallback",
77
+ },
78
+ {
79
+ id: "gpt-oss-120b",
80
+ modelId: "gpt-oss-120b",
81
+ label: "GPT-OSS 120B",
82
+ description: "Antigravity-routed GPT-OSS 120B.",
83
+ defaultReasoningLevel: null,
84
+ supportedReasoningLevels: null,
85
+ source: "fallback",
86
+ },
87
+ ];
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // AGY_MODEL env mapping
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Maps Codexa model IDs to the AGY_MODEL env value passed to the agy subprocess.
95
+ *
96
+ * Verified:
97
+ * gemini-3.5-flash → "gemini-3.5-flash" (confirmed via: AGY_MODEL=gemini-3.5-flash agy -p "say hello back")
98
+ *
99
+ * Unverified (same pattern, not independently smoke-tested):
100
+ * gemini-3.1-pro → "gemini-3.1-pro"
101
+ */
102
+ const AGY_MODEL_ENV_MAP: Readonly<Record<string, string>> = {
103
+ "gemini-3.5-flash": "gemini-3.5-flash",
104
+ "gemini-3.1-pro": "gemini-3.1-pro",
105
+ };
106
+
107
+ export function getAgyModelEnvValue(modelId: string): string | null {
108
+ return AGY_MODEL_ENV_MAP[modelId] ?? null;
109
+ }
110
+
111
+ export function buildAgyEnv(modelId: string, _reasoning?: string): NodeJS.ProcessEnv {
112
+ // _reasoning is stored in route state and surfaced in the footer UI, but agy
113
+ // has no verified CLI flag or env var for passing effort level to the subprocess.
114
+ // TODO: wire _reasoning here once Antigravity exposes a stable mechanism.
115
+ const envValue = getAgyModelEnvValue(modelId);
116
+ return envValue ? { ...process.env, AGY_MODEL: envValue } : { ...process.env };
117
+ }
118
+
119
+ export function getAntigravityModelLabel(modelId: string): string {
120
+ return ANTIGRAVITY_MODELS.find((m) => m.id === modelId)?.label ?? modelId;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Legacy model ID migration
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Migrates legacy compound Antigravity model IDs (from feat/antigravity-cli-provider)
129
+ * to the new family + reasoning format.
130
+ *
131
+ * Old IDs encoded effort in the model ID (e.g., "gemini-3.5-flash-high").
132
+ * New IDs use the base family ("gemini-3.5-flash") with reasoning stored separately.
133
+ */
134
+ export function migrateAntigravityLegacyModelId(modelId: string): { modelId: string; reasoning?: string } {
135
+ const legacy: Record<string, { modelId: string; reasoning?: string }> = {
136
+ "gemini-3.5-flash-high": { modelId: "gemini-3.5-flash", reasoning: "high" },
137
+ "gemini-3.5-flash-medium": { modelId: "gemini-3.5-flash", reasoning: "medium" },
138
+ "gemini-3.5-flash-low": { modelId: "gemini-3.5-flash", reasoning: "low" },
139
+ "gemini-3.1-pro-high": { modelId: "gemini-3.1-pro", reasoning: "high" },
140
+ "gemini-3.1-pro-low": { modelId: "gemini-3.1-pro", reasoning: "low" },
141
+ "gpt-oss-120b-medium": { modelId: "gpt-oss-120b" },
142
+ };
143
+ return legacy[modelId] ?? { modelId };
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Module-level state
148
+ // ---------------------------------------------------------------------------
149
+
150
+ let agyRouteValidated = false;
151
+ let resolvedAgyExecutable: string = "agy";
152
+
153
+ export function isAntigravityRouteConfigured(): boolean {
154
+ return agyRouteValidated;
155
+ }
156
+
157
+ export function resetAntigravityRouteValidationCacheForTests(): void {
158
+ agyRouteValidated = false;
159
+ resolvedAgyExecutable = "agy";
160
+ resetAgyExecutableCacheForTests();
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Route validation
165
+ // ---------------------------------------------------------------------------
166
+
167
+ export async function validateAntigravityRoute(options: {
168
+ cwd?: string;
169
+ configuredPath?: string | null;
170
+ runCommandImpl?: typeof runCommand;
171
+ platform?: NodeJS.Platform;
172
+ }): Promise<ProviderRouteValidationResult> {
173
+ let resolved: string;
174
+ try {
175
+ resolved = await resolveAgyExecutable({
176
+ cwd: options.cwd,
177
+ configuredPath: options.configuredPath,
178
+ runCommandImpl: options.runCommandImpl,
179
+ });
180
+ } catch {
181
+ return {
182
+ status: "not-configured",
183
+ providerId: "antigravity",
184
+ backendKind: "unavailable",
185
+ message: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
186
+ diagnostics: { resolvedCommand: null },
187
+ };
188
+ }
189
+
190
+ // Probe the binary to confirm it's actually installed. Running --help has no
191
+ // auth side effects and exits 0 when agy is present. buildSpawnSpec wraps
192
+ // .cmd/.bat shims in `cmd.exe /d /s /c call` on Windows (no-op elsewhere) so
193
+ // the probe can actually launch the resolved executable.
194
+ const runCommandImpl = options.runCommandImpl ?? runCommand;
195
+ const probeSpec = buildSpawnSpec(resolved, ["--help"], options.platform ?? process.platform);
196
+ const probe = runCommandImpl({
197
+ executable: probeSpec.executable,
198
+ args: probeSpec.args,
199
+ cwd: options.cwd ?? process.cwd(),
200
+ timeoutMs: ANTIGRAVITY_VALIDATION_TIMEOUT_MS,
201
+ });
202
+ const probeResult = await probe.result;
203
+
204
+ if (probeResult.status === "spawn_error") {
205
+ return {
206
+ status: "not-configured",
207
+ providerId: "antigravity",
208
+ backendKind: "unavailable",
209
+ message: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
210
+ diagnostics: { resolvedCommand: resolved },
211
+ };
212
+ }
213
+
214
+ resolvedAgyExecutable = resolved;
215
+ agyRouteValidated = true;
216
+
217
+ return {
218
+ status: "ready",
219
+ providerId: "antigravity",
220
+ backendKind: "antigravity-cli-auth",
221
+ message: `Antigravity CLI found at: ${resolved}`,
222
+ diagnostics: { resolvedCommand: resolved },
223
+ };
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // run()
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export function runAntigravityWithRunner(
231
+ request: ProviderChatRequest,
232
+ handlers: BackendRunHandlers,
233
+ runCommandImpl: typeof runCommand = runCommand,
234
+ executable: string = resolvedAgyExecutable,
235
+ platform: NodeJS.Platform = process.platform,
236
+ ): () => void {
237
+ const spawnSpec = buildSpawnSpec(executable, ["-p", request.prompt], platform);
238
+ const env = buildAgyEnv(request.route.modelId, request.route.reasoning);
239
+
240
+ const runner = runCommandImpl(
241
+ {
242
+ executable: spawnSpec.executable,
243
+ args: spawnSpec.args,
244
+ cwd: request.workspaceRoot,
245
+ env,
246
+ timeoutMs: ANTIGRAVITY_TIMEOUT_MS,
247
+ },
248
+ );
249
+
250
+ runner.result.then((result) => {
251
+ if (result.status === "canceled") return;
252
+
253
+ if (result.status !== "completed" || result.exitCode !== 0) {
254
+ const message = result.userMessage || result.stderr || "Antigravity CLI execution failed.";
255
+ handlers.onError(message, `agy command: ${JSON.stringify([spawnSpec.executable, ...spawnSpec.args])}`);
256
+ return;
257
+ }
258
+
259
+ const text = sanitizeTerminalOutput(result.stdout).trim();
260
+ if (text) {
261
+ handlers.onAssistantDelta?.(text);
262
+ }
263
+ handlers.onFinalAnswerObserved?.(text);
264
+ handlers.onResponse(text);
265
+ }).catch((error) => {
266
+ const message = error instanceof Error ? error.message : "Antigravity CLI execution failed.";
267
+ handlers.onError(message);
268
+ });
269
+
270
+ return runner.cancel;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Runtime
275
+ // ---------------------------------------------------------------------------
276
+
277
+ export const antigravityRuntime: ProviderRuntime = {
278
+ providerId: "antigravity",
279
+ label: "Antigravity CLI",
280
+ modelPickerLabel: "Antigravity",
281
+ backendKind: "antigravity-cli-auth",
282
+ routeAvailable: true,
283
+ routeStatus: "Routes through the Antigravity CLI (`agy`) when installed.",
284
+ routeSetupMessage: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
285
+ launchAvailable: true,
286
+ isRouteConfigured: isAntigravityRouteConfigured,
287
+ validateRoute: async ({ workspaceRoot, antigravityCommandPath }) => validateAntigravityRoute({
288
+ cwd: workspaceRoot,
289
+ configuredPath: antigravityCommandPath ?? null,
290
+ }),
291
+ discoverModels: (): ProviderModelDiscoveryResult => ({
292
+ status: "ready",
293
+ providerId: "antigravity",
294
+ backendKind: "antigravity-cli-auth",
295
+ models: ANTIGRAVITY_MODELS,
296
+ }),
297
+ run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
298
+ handlers.onProgress?.({
299
+ id: "antigravity-route",
300
+ source: "stdout",
301
+ text: "Starting Antigravity CLI",
302
+ });
303
+ return runAntigravityWithRunner(request, handlers);
304
+ },
305
+ };