@golba98/codexa 1.0.3 → 1.0.5

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 (125) 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 +642 -303
  5. package/src/commands/handler.ts +7 -18
  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/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -4,7 +4,6 @@ import type { BackendRunHandlers } from "../providers/types.js";
4
4
  import { ANTHROPIC_FALLBACK_MODELS } from "./models.js";
5
5
  import type { ProviderBackendKind, ProviderChatRequest, ProviderModel, ProviderModelDiscoveryResult, ProviderRouteValidationResult, ProviderRuntime } from "./types.js";
6
6
  import { buildClaudeSpawnSpec, resetClaudeExecutableCacheForTests } from "../executables/claudeExecutable.js";
7
- import { CLAUDE_CODE_EFFORT_IDS } from "./reasoning.js";
8
7
  import {
9
8
  claudeCodeModelsToProviderModels,
10
9
  discoverClaudeCodeCapabilities,
@@ -19,6 +18,7 @@ const ANTHROPIC_MAX_TOKENS = 1024;
19
18
  const ANTHROPIC_TIMEOUT_MS = 120_000;
20
19
  const ANTHROPIC_AUTH_CHECK_TIMEOUT_MS = 10_000;
21
20
  const ANTHROPIC_ROUTE_VALIDATION_TIMEOUT_MS = 15_000;
21
+ const DISCOVERY_FAILURE_MESSAGE = "Claude Code model version discovery failed; using fallback aliases with unknown versions.";
22
22
  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
23
  export { parseClaudeAuthStatus } from "./claudeCodeDiscovery.js";
24
24
 
@@ -55,10 +55,9 @@ export function mapModelIdToClaudeArg(modelId: string): string {
55
55
  return modelId;
56
56
  }
57
57
 
58
- export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
59
- if (reasoning && CLAUDE_CODE_EFFORT_IDS.has(reasoning)) return reasoning;
60
- return null;
61
- }
58
+ export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
59
+ return reasoning?.trim() || null;
60
+ }
62
61
 
63
62
  function buildClaudeCodeBaseArgs(request: ProviderChatRequest): string[] {
64
63
  const effort = mapReasoningToEffort(request.route.reasoning ?? null);
@@ -498,6 +497,9 @@ export const anthropicRuntime: ProviderRuntime = {
498
497
  providerId: "anthropic",
499
498
  backendKind: getAnthropicRuntimeBackendKind(),
500
499
  models: getActiveAnthropicModels(),
500
+ message: (claudeCapabilityDiscovery?.modelSource ?? "fallback") === "fallback"
501
+ ? DISCOVERY_FAILURE_MESSAGE
502
+ : undefined,
501
503
  diagnostics: claudeCapabilityDiscovery ? {
502
504
  resolvedCommand: claudeCapabilityDiscovery.resolvedCommand,
503
505
  modelSource: claudeCapabilityDiscovery.modelSource,
@@ -534,7 +536,9 @@ export const anthropicRuntime: ProviderRuntime = {
534
536
  providerId: "anthropic",
535
537
  backendKind: getAnthropicRuntimeBackendKind(),
536
538
  models: discoveredAnthropicModels,
537
- message: `Refreshed Claude capabilities (${discovery.modelSource}).`,
539
+ message: discovery.modelSource === "fallback"
540
+ ? DISCOVERY_FAILURE_MESSAGE
541
+ : `Refreshed Claude capabilities (${discovery.modelSource}).`,
538
542
  diagnostics: {
539
543
  resolvedCommand: discovery.resolvedCommand,
540
544
  modelSource: discovery.modelSource,
@@ -0,0 +1,461 @@
1
+ import { runCommand } from "../process/CommandRunner.js";
2
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
3
+ import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
4
+ import { loadCachedProviderModels } from "../models/providerModelCache.js";
5
+ import type { BackendRunHandlers } from "../providers/types.js";
6
+ import type {
7
+ ProviderBackendKind,
8
+ ProviderChatRequest,
9
+ ProviderModel,
10
+ ProviderModelDiscoveryResult,
11
+ ProviderRouteValidationResult,
12
+ ProviderRuntime,
13
+ } from "./types.js";
14
+ import {
15
+ resolveAgyExecutable,
16
+ resetAgyExecutableCacheForTests,
17
+ } from "../executables/antigravityExecutable.js";
18
+ import { buildSpawnSpec } from "../executables/executableResolver.js";
19
+
20
+ export { resetAgyExecutableCacheForTests };
21
+
22
+ const ANTIGRAVITY_TIMEOUT_MS = 120_000;
23
+ const ANTIGRAVITY_VALIDATION_TIMEOUT_MS = 10_000;
24
+ const ANTIGRAVITY_ROUTE_SETUP_MESSAGE =
25
+ "`agy` command not found. Install Antigravity CLI or set AGY_EXECUTABLE to the full path.";
26
+
27
+ export const ANTIGRAVITY_DEFAULT_MODEL_ID = "gemini-3.5-flash";
28
+ export const ANTIGRAVITY_DEFAULT_REASONING = "high";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Model definitions
32
+ // ---------------------------------------------------------------------------
33
+
34
+ interface AgySelectorMetadata {
35
+ provider: "antigravity";
36
+ selectors: Record<string, string>;
37
+ }
38
+
39
+ function normalizeAgyId(value: string): string {
40
+ return value
41
+ .trim()
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9.]+/g, "-")
44
+ .replace(/^-+|-+$/g, "");
45
+ }
46
+
47
+ function formatAgyVariantLabel(value: string): string {
48
+ return value
49
+ .split(/[-_\s]+/)
50
+ .filter(Boolean)
51
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1).toLowerCase()}`)
52
+ .join(" ") || value;
53
+ }
54
+
55
+ function readAgySelectorMetadata(model: ProviderModel): AgySelectorMetadata | null {
56
+ if (!model.raw || typeof model.raw !== "object" || Array.isArray(model.raw)) return null;
57
+ const raw = model.raw as Partial<AgySelectorMetadata>;
58
+ if (raw.provider !== "antigravity" || !raw.selectors || typeof raw.selectors !== "object") return null;
59
+ return { provider: "antigravity", selectors: raw.selectors };
60
+ }
61
+
62
+ function preferredAgyDefault(modelId: string, efforts: readonly string[]): string {
63
+ if ((modelId === "gemini-3.5-flash" || modelId === "gemini-3.1-pro") && efforts.includes(ANTIGRAVITY_DEFAULT_REASONING)) {
64
+ return ANTIGRAVITY_DEFAULT_REASONING;
65
+ }
66
+ return efforts[0] ?? ANTIGRAVITY_DEFAULT_REASONING;
67
+ }
68
+
69
+ const AGY_REASONING_DISPLAY_ORDER = ["low", "medium", "high", "xhigh", "max"] as const;
70
+
71
+ function sortAgyReasoningLevels(levels: readonly ReasoningEffortCapability[]): ReasoningEffortCapability[] {
72
+ const rank = new Map<string, number>(AGY_REASONING_DISPLAY_ORDER.map((id, index) => [id, index]));
73
+ return levels
74
+ .map((level, index) => ({ level, index }))
75
+ .sort((left, right) => (rank.get(left.level.id) ?? AGY_REASONING_DISPLAY_ORDER.length + left.index)
76
+ - (rank.get(right.level.id) ?? AGY_REASONING_DISPLAY_ORDER.length + right.index))
77
+ .map(({ level }) => level);
78
+ }
79
+
80
+ export function parseAgyModelsOutput(stdout: string): ProviderModel[] {
81
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
82
+ const parsed = lines.map((selector) => {
83
+ const match = selector.match(/^(.*?)\s+\(([^()]+)\)$/);
84
+ return { selector, base: match?.[1]?.trim() ?? selector, variant: match?.[2]?.trim() ?? null };
85
+ });
86
+ const baseCounts = new Map<string, number>();
87
+ for (const item of parsed) baseCounts.set(item.base, (baseCounts.get(item.base) ?? 0) + 1);
88
+
89
+ const models: ProviderModel[] = [];
90
+ const grouped = new Map<string, ProviderModel>();
91
+ for (const item of parsed) {
92
+ const isVariantGroup = item.variant !== null && (baseCounts.get(item.base) ?? 0) > 1;
93
+ if (!isVariantGroup) {
94
+ const modelId = normalizeAgyId(item.selector);
95
+ models.push({
96
+ id: modelId,
97
+ modelId,
98
+ label: item.selector,
99
+ description: `Discovered from agy models: ${item.selector}`,
100
+ defaultReasoningLevel: null,
101
+ supportedReasoningLevels: null,
102
+ source: "discovered",
103
+ raw: { provider: "antigravity", selectors: { "": item.selector } } satisfies AgySelectorMetadata,
104
+ });
105
+ continue;
106
+ }
107
+
108
+ const modelId = normalizeAgyId(item.base);
109
+ const effortId = normalizeAgyId(item.variant ?? "");
110
+ const existing = grouped.get(item.base);
111
+ if (existing) {
112
+ const metadata = readAgySelectorMetadata(existing);
113
+ const levels = sortAgyReasoningLevels([...(existing.supportedReasoningLevels ?? []), {
114
+ id: effortId,
115
+ label: formatAgyVariantLabel(item.variant ?? effortId),
116
+ description: null,
117
+ }]);
118
+ const selectors = { ...(metadata?.selectors ?? {}), [effortId]: item.selector };
119
+ const updated = {
120
+ ...existing,
121
+ defaultReasoningLevel: preferredAgyDefault(modelId, levels.map((level) => level.id)),
122
+ supportedReasoningLevels: levels,
123
+ raw: { provider: "antigravity", selectors } satisfies AgySelectorMetadata,
124
+ };
125
+ grouped.set(item.base, updated);
126
+ models[models.indexOf(existing)] = updated;
127
+ continue;
128
+ }
129
+
130
+ const level: ReasoningEffortCapability = {
131
+ id: effortId,
132
+ label: formatAgyVariantLabel(item.variant ?? effortId),
133
+ description: null,
134
+ };
135
+ const model: ProviderModel = {
136
+ id: modelId,
137
+ modelId,
138
+ label: item.base,
139
+ description: `Discovered from agy models. Select an advertised variant with ←/→.`,
140
+ defaultReasoningLevel: preferredAgyDefault(modelId, [effortId]),
141
+ supportedReasoningLevels: [level],
142
+ source: "discovered",
143
+ raw: { provider: "antigravity", selectors: { [effortId]: item.selector } } satisfies AgySelectorMetadata,
144
+ };
145
+ grouped.set(item.base, model);
146
+ models.push(model);
147
+ }
148
+ return models;
149
+ }
150
+
151
+ // Resolve persisted model/reasoning state to the exact selector advertised by `agy models`.
152
+ export function getAgyModelSelector(
153
+ modelId: string,
154
+ reasoning: string | null | undefined,
155
+ models: readonly ProviderModel[] = getActiveAgyModels(),
156
+ ): string | null {
157
+ const model = models.find((item) => item.modelId === modelId || item.id === modelId);
158
+ if (!model) return null;
159
+ const metadata = readAgySelectorMetadata(model);
160
+ if (!metadata) return null;
161
+ if (!model.supportedReasoningLevels?.length) return metadata.selectors[""] ?? null;
162
+ if (reasoning) return metadata.selectors[reasoning] ?? null;
163
+ const effort = model.defaultReasoningLevel;
164
+ return effort ? metadata.selectors[effort] ?? null : null;
165
+ }
166
+
167
+ export function getAntigravityModelLabel(modelId: string): string {
168
+ return getActiveAgyModels().find((m) => m.id === modelId)?.label ?? modelId;
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Legacy model ID migration
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Migrates legacy compound Antigravity model IDs (from feat/antigravity-cli-provider)
177
+ * to the new family + reasoning format.
178
+ *
179
+ * Old IDs encoded effort in the model ID (e.g., "gemini-3.5-flash-high").
180
+ * New IDs use the base family ("gemini-3.5-flash") with reasoning stored separately.
181
+ */
182
+ export function migrateAntigravityLegacyModelId(modelId: string): { modelId: string; reasoning?: string } {
183
+ const legacy: Record<string, { modelId: string; reasoning?: string }> = {
184
+ "gemini-3.5-flash-high": { modelId: "gemini-3.5-flash", reasoning: "high" },
185
+ "gemini-3.5-flash-medium": { modelId: "gemini-3.5-flash", reasoning: "medium" },
186
+ "gemini-3.5-flash-low": { modelId: "gemini-3.5-flash", reasoning: "low" },
187
+ "gemini-3.1-pro-high": { modelId: "gemini-3.1-pro", reasoning: "high" },
188
+ "gemini-3.1-pro-low": { modelId: "gemini-3.1-pro", reasoning: "low" },
189
+ "claude-sonnet-4-6-think": { modelId: "claude-sonnet-4.6-thinking" },
190
+ "claude-opus-4-6-think": { modelId: "claude-opus-4.6-thinking" },
191
+ "gpt-oss-120b": { modelId: "gpt-oss-120b-medium" },
192
+ };
193
+ return legacy[modelId] ?? { modelId };
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Module-level state
198
+ // ---------------------------------------------------------------------------
199
+
200
+ let agyRouteValidated = false;
201
+ let resolvedAgyExecutable: string = "agy";
202
+ let discoveredAgyModels: readonly ProviderModel[] | null = null;
203
+
204
+ function getActiveAgyModels(): readonly ProviderModel[] {
205
+ if (discoveredAgyModels?.length) return discoveredAgyModels;
206
+ return loadCachedProviderModels("antigravity")?.models ?? [];
207
+ }
208
+
209
+ export async function discoverAgyModels(options: {
210
+ executable: string;
211
+ cwd: string;
212
+ runCommandImpl: typeof runCommand;
213
+ platform: NodeJS.Platform;
214
+ }): Promise<ProviderModelDiscoveryResult> {
215
+ const spawnSpec = buildSpawnSpec(options.executable, ["models"], options.platform);
216
+ const result = await options.runCommandImpl({
217
+ executable: spawnSpec.executable,
218
+ args: spawnSpec.args,
219
+ cwd: options.cwd,
220
+ timeoutMs: ANTIGRAVITY_VALIDATION_TIMEOUT_MS,
221
+ }).result;
222
+ const models = result.status === "completed" && result.exitCode === 0
223
+ ? parseAgyModelsOutput(result.stdout)
224
+ : [];
225
+ if (models.length > 0) {
226
+ discoveredAgyModels = models;
227
+ return {
228
+ status: "ready",
229
+ providerId: "antigravity",
230
+ backendKind: "antigravity-cli-auth",
231
+ models,
232
+ message: `Loaded ${models.length} models from agy models.`,
233
+ diagnostics: { modelSource: "agy-models-command", modelsExitCode: result.exitCode, modelsStatus: result.status },
234
+ };
235
+ }
236
+ const cached = loadCachedProviderModels("antigravity")?.models ?? [];
237
+ return {
238
+ status: cached.length > 0 ? "ready" : "not-configured",
239
+ providerId: "antigravity",
240
+ backendKind: cached.length > 0 ? "antigravity-cli-auth" : "unavailable",
241
+ models: cached,
242
+ message: cached.length > 0
243
+ ? "Live agy model metadata is unavailable; using the last successful discovery."
244
+ : "Antigravity model metadata is unavailable. Run Refresh models after checking `agy models`.",
245
+ diagnostics: { modelSource: cached.length > 0 ? "cache" : "unavailable", modelsExitCode: result.exitCode, modelsStatus: result.status },
246
+ };
247
+ }
248
+
249
+ export function isAntigravityRouteConfigured(): boolean {
250
+ return agyRouteValidated;
251
+ }
252
+
253
+ export function resetAntigravityRouteValidationCacheForTests(): void {
254
+ agyRouteValidated = false;
255
+ resolvedAgyExecutable = "agy";
256
+ discoveredAgyModels = null;
257
+ resetAgyExecutableCacheForTests();
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Route validation
262
+ // ---------------------------------------------------------------------------
263
+
264
+ export async function validateAntigravityRoute(options: {
265
+ cwd?: string;
266
+ configuredPath?: string | null;
267
+ runCommandImpl?: typeof runCommand;
268
+ platform?: NodeJS.Platform;
269
+ }): Promise<ProviderRouteValidationResult> {
270
+ // Already validated this session: skip the executable probe and model
271
+ // re-discovery so re-activating antigravity is instant. "Refresh models"
272
+ // bypasses this via runtime.refreshModels, so a stale catalog stays
273
+ // user-recoverable.
274
+ if (agyRouteValidated && discoveredAgyModels?.length) {
275
+ return {
276
+ status: "ready",
277
+ providerId: "antigravity",
278
+ backendKind: "antigravity-cli-auth",
279
+ message: `Antigravity CLI found at: ${resolvedAgyExecutable}`,
280
+ diagnostics: {
281
+ resolvedCommand: resolvedAgyExecutable,
282
+ modelSource: "session-cache",
283
+ discoveredModelCount: discoveredAgyModels.length,
284
+ },
285
+ };
286
+ }
287
+
288
+ let resolved: string;
289
+ try {
290
+ resolved = await resolveAgyExecutable({
291
+ cwd: options.cwd,
292
+ configuredPath: options.configuredPath,
293
+ runCommandImpl: options.runCommandImpl,
294
+ });
295
+ } catch {
296
+ return {
297
+ status: "not-configured",
298
+ providerId: "antigravity",
299
+ backendKind: "unavailable",
300
+ message: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
301
+ diagnostics: { resolvedCommand: null },
302
+ };
303
+ }
304
+
305
+ // Probe the binary to confirm it's actually installed. Running --help has no
306
+ // auth side effects and exits 0 when agy is present. buildSpawnSpec wraps
307
+ // .cmd/.bat shims in `cmd.exe /d /s /c call` on Windows (no-op elsewhere) so
308
+ // the probe can actually launch the resolved executable.
309
+ const runCommandImpl = options.runCommandImpl ?? runCommand;
310
+ const probeSpec = buildSpawnSpec(resolved, ["--help"], options.platform ?? process.platform);
311
+ const probe = runCommandImpl({
312
+ executable: probeSpec.executable,
313
+ args: probeSpec.args,
314
+ cwd: options.cwd ?? process.cwd(),
315
+ timeoutMs: ANTIGRAVITY_VALIDATION_TIMEOUT_MS,
316
+ });
317
+ const probeResult = await probe.result;
318
+
319
+ if (probeResult.status === "spawn_error") {
320
+ return {
321
+ status: "not-configured",
322
+ providerId: "antigravity",
323
+ backendKind: "unavailable",
324
+ message: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
325
+ diagnostics: { resolvedCommand: resolved },
326
+ };
327
+ }
328
+
329
+ resolvedAgyExecutable = resolved;
330
+ agyRouteValidated = true;
331
+ const modelDiscovery = await discoverAgyModels({
332
+ executable: resolved,
333
+ cwd: options.cwd ?? process.cwd(),
334
+ runCommandImpl,
335
+ platform: options.platform ?? process.platform,
336
+ });
337
+
338
+ return {
339
+ status: "ready",
340
+ providerId: "antigravity",
341
+ backendKind: "antigravity-cli-auth",
342
+ message: `Antigravity CLI found at: ${resolved}`,
343
+ diagnostics: {
344
+ resolvedCommand: resolved,
345
+ modelSource: modelDiscovery.diagnostics?.modelSource ?? "unavailable",
346
+ discoveredModelCount: modelDiscovery.models.length,
347
+ },
348
+ };
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // run()
353
+ // ---------------------------------------------------------------------------
354
+
355
+ export function runAntigravityWithRunner(
356
+ request: ProviderChatRequest,
357
+ handlers: BackendRunHandlers,
358
+ runCommandImpl: typeof runCommand = runCommand,
359
+ executable: string = resolvedAgyExecutable,
360
+ platform: NodeJS.Platform = process.platform,
361
+ models: readonly ProviderModel[] = getActiveAgyModels(),
362
+ ): () => void {
363
+ const selector = getAgyModelSelector(request.route.modelId, request.route.reasoning, models);
364
+ if (!selector) {
365
+ handlers.onError(
366
+ `Antigravity has no verified selector for ${request.route.modelId}${request.route.reasoning ? ` / ${request.route.reasoning}` : ""}. Refresh models and try again.`,
367
+ );
368
+ return () => undefined;
369
+ }
370
+ const spawnSpec = buildSpawnSpec(executable, ["--model", selector, "-p", request.prompt], platform);
371
+
372
+ const runner = runCommandImpl(
373
+ {
374
+ executable: spawnSpec.executable,
375
+ args: spawnSpec.args,
376
+ cwd: request.workspaceRoot,
377
+ env: { ...process.env },
378
+ timeoutMs: ANTIGRAVITY_TIMEOUT_MS,
379
+ },
380
+ );
381
+
382
+ runner.result.then((result) => {
383
+ if (result.status === "canceled") return;
384
+
385
+ if (result.status !== "completed" || result.exitCode !== 0) {
386
+ const message = result.userMessage || result.stderr || "Antigravity CLI execution failed.";
387
+ handlers.onError(message, `agy command: ${JSON.stringify([spawnSpec.executable, ...spawnSpec.args])}`);
388
+ return;
389
+ }
390
+
391
+ const text = sanitizeTerminalOutput(result.stdout).trim();
392
+ if (text) {
393
+ handlers.onAssistantDelta?.(text);
394
+ }
395
+ handlers.onFinalAnswerObserved?.(text);
396
+ handlers.onResponse(text);
397
+ }).catch((error) => {
398
+ const message = error instanceof Error ? error.message : "Antigravity CLI execution failed.";
399
+ handlers.onError(message);
400
+ });
401
+
402
+ return runner.cancel;
403
+ }
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // Runtime
407
+ // ---------------------------------------------------------------------------
408
+
409
+ export const antigravityRuntime: ProviderRuntime = {
410
+ providerId: "antigravity",
411
+ label: "Antigravity CLI",
412
+ modelPickerLabel: "Antigravity",
413
+ backendKind: "antigravity-cli-auth",
414
+ routeAvailable: true,
415
+ routeStatus: "Routes through the Antigravity CLI (`agy`) when installed.",
416
+ routeSetupMessage: ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
417
+ launchAvailable: true,
418
+ isRouteConfigured: isAntigravityRouteConfigured,
419
+ validateRoute: async ({ workspaceRoot, antigravityCommandPath }) => validateAntigravityRoute({
420
+ cwd: workspaceRoot,
421
+ configuredPath: antigravityCommandPath ?? null,
422
+ }),
423
+ discoverModels: (): ProviderModelDiscoveryResult => {
424
+ const models = getActiveAgyModels();
425
+ return {
426
+ status: models.length > 0 ? "ready" : "not-configured",
427
+ providerId: "antigravity",
428
+ backendKind: models.length > 0 ? "antigravity-cli-auth" : "unavailable",
429
+ models,
430
+ ...(models.length === 0 ? { message: "Antigravity model metadata is unavailable. Run Refresh models." } : {}),
431
+ };
432
+ },
433
+ refreshModels: async ({ cwd }): Promise<ProviderModelDiscoveryResult> => {
434
+ let executable = resolvedAgyExecutable;
435
+ try {
436
+ executable = await resolveAgyExecutable({ cwd });
437
+ resolvedAgyExecutable = executable;
438
+ } catch {
439
+ const cached = loadCachedProviderModels("antigravity")?.models ?? [];
440
+ return {
441
+ status: cached.length > 0 ? "ready" : "not-configured",
442
+ providerId: "antigravity",
443
+ backendKind: cached.length > 0 ? "antigravity-cli-auth" : "unavailable",
444
+ models: cached,
445
+ message: cached.length > 0
446
+ ? "Antigravity CLI is unavailable; using the last successful model discovery."
447
+ : ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
448
+ diagnostics: { modelSource: cached.length > 0 ? "cache" : "unavailable", resolvedCommand: null },
449
+ };
450
+ }
451
+ return discoverAgyModels({ executable, cwd, runCommandImpl: runCommand, platform: process.platform });
452
+ },
453
+ run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
454
+ handlers.onProgress?.({
455
+ id: "antigravity-route",
456
+ source: "stdout",
457
+ text: "Starting Antigravity CLI",
458
+ });
459
+ return runAntigravityWithRunner(request, handlers);
460
+ },
461
+ };