ubume 0.1.0

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