@golba98/codexa 1.0.4 → 1.0.6

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 (78) hide show
  1. package/README.md +6 -10
  2. package/package.json +1 -1
  3. package/src/app.tsx +188 -121
  4. package/src/commands/handler.ts +11 -20
  5. package/src/config/appVersion.ts +65 -0
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/settings.ts +5 -1
  8. package/src/config/updateCheckCache.ts +18 -8
  9. package/src/core/models/codexModelCapabilities.ts +57 -4
  10. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  11. package/src/core/models/providerModelCache.ts +90 -0
  12. package/src/core/process/CommandRunner.ts +12 -1
  13. package/src/core/providerLauncher/registry.ts +37 -7
  14. package/src/core/providerLauncher/types.ts +2 -1
  15. package/src/core/providerRuntime/anthropic.ts +3 -5
  16. package/src/core/providerRuntime/antigravity.ts +257 -90
  17. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
  18. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  19. package/src/core/providerRuntime/models.ts +18 -4
  20. package/src/core/providerRuntime/reasoning.ts +9 -6
  21. package/src/core/providerRuntime/registry.ts +93 -44
  22. package/src/core/providerRuntime/types.ts +1 -0
  23. package/src/core/version/packageManager.ts +119 -0
  24. package/src/core/version/updateCheck.ts +15 -9
  25. package/src/index.tsx +23 -9
  26. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  27. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  28. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  29. package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
  30. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
  31. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  32. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  33. package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
  34. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  35. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +15 -13
  36. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +6 -5
  37. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  38. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  39. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  40. package/src/ui/{slashCommands.ts → input/slashCommands.ts} +1 -0
  41. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
  42. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  43. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  44. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  45. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  46. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
  47. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  48. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  49. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  50. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  51. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  52. package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
  53. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  54. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
  55. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
  56. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
  57. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  58. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +54 -44
  59. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  60. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  61. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  62. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  63. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
  64. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  65. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  66. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  67. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  68. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
  69. package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
  70. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  71. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  72. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  73. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
  74. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  75. /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
  76. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
  77. /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
  78. /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
@@ -16,14 +16,15 @@ import {
16
16
  } from "../providerRuntime/registry.js";
17
17
  import { normalizeGeminiModelId } from "../providerRuntime/models.js";
18
18
  import { ANTIGRAVITY_DEFAULT_MODEL_ID } from "../providerRuntime/antigravity.js";
19
+ import { discoverMistralVibeModels } from "../providerRuntime/mistralVibe.js";
19
20
  import { setLocalProviderConfig } from "../providerRuntime/local.js";
20
21
  import { formatContextLength, resolveModelContextLengthCached } from "../providerRuntime/contextMetadata.js";
21
22
  import { resolveModelCapabilityProfileCached } from "../providerRuntime/capabilityProfile.js";
22
23
 
23
24
  // Google/Gemini remains a recognized legacy config value so existing workspace
24
25
  // files can be migrated, but it is no longer a selectable Codexa provider.
25
- const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "local", "antigravity"];
26
- const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "local", "antigravity"];
26
+ const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "mistral", "local", "antigravity"];
27
+ const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "mistral", "local", "antigravity"];
27
28
 
28
29
  const DEFAULT_PROVIDER_ID: ProviderId = "openai";
29
30
 
@@ -78,6 +79,17 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
78
79
  isActiveRoute: false,
79
80
  routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
80
81
  },
82
+ mistral: {
83
+ id: "mistral",
84
+ displayName: "Mistral Vibe CLI",
85
+ currentModel: () => "Vibe default",
86
+ backendType: "mistral-vibe-cli-auth",
87
+ routeMode: "in-codexa",
88
+ enabled: true,
89
+ launchCommand: { executable: "vibe", args: [] },
90
+ isActiveRoute: false,
91
+ routeUnavailableReason: null,
92
+ },
81
93
  antigravity: {
82
94
  id: "antigravity",
83
95
  displayName: "Antigravity",
@@ -137,9 +149,11 @@ function applyOverride(
137
149
  : provider.currentModel,
138
150
  enabled: nextEnabled,
139
151
  launchCommand: nextCommand,
140
- statusLabel: nextEnabled
141
- ? (provider.routeUnavailableReason ? "Needs config" : "Enabled")
142
- : "Disabled",
152
+ statusLabel: !nextEnabled
153
+ ? "Disabled"
154
+ : provider.routeMode === "launch-only"
155
+ ? provider.statusLabel
156
+ : (provider.routeUnavailableReason ? "Needs config" : "Enabled"),
143
157
  };
144
158
  }
145
159
 
@@ -157,6 +171,7 @@ export function getActiveRouteProviderId(config: ProviderWorkspaceConfig | null
157
171
 
158
172
  export function buildProviderRegistry(options: {
159
173
  activeModel: string;
174
+ workspaceRoot?: string;
160
175
  workspaceConfig?: ProviderWorkspaceConfig | null;
161
176
  diagnostics?: Record<string, Record<string, string | number | boolean | null>>;
162
177
  routeErrors?: Record<string, string>;
@@ -170,7 +185,9 @@ export function buildProviderRegistry(options: {
170
185
  }
171
186
  const defaults = DEFAULT_PROVIDERS[id];
172
187
  const runtime = getProviderRuntime(id);
173
- const discovery = runtime.discoverModels();
188
+ const discovery = id === "mistral"
189
+ ? discoverMistralVibeModels(options.workspaceRoot ?? process.cwd())
190
+ : runtime.discoverModels();
174
191
 
175
192
  const activeRoute = options.workspaceConfig?.activeRoute;
176
193
  const isThisActive = activeRoute?.providerId === id;
@@ -205,6 +222,10 @@ export function buildProviderRegistry(options: {
205
222
  }
206
223
  }
207
224
 
225
+ if (id === "mistral") {
226
+ currentModelLabel = discovery.models[0]?.modelId ?? "Vibe default";
227
+ }
228
+
208
229
  const rawMetadataForModel = discovery.models.find((model) => model.modelId === currentModelLabel)?.raw;
209
230
  const contextMetadata = resolveModelContextLengthCached({
210
231
  providerId: id,
@@ -230,7 +251,16 @@ export function buildProviderRegistry(options: {
230
251
 
231
252
  const enabled = id === "local" ? discovery.status === "ready" : defaults.enabled;
232
253
 
233
- const statusLabel = id === "local"
254
+ const availabilityStatus = options.diagnostics?.[id]?.availabilityStatus;
255
+ const statusLabel = id === "mistral"
256
+ ? availabilityStatus === "checking"
257
+ ? "Checking"
258
+ : availabilityStatus === "unavailable"
259
+ ? "Missing"
260
+ : routeUnavailableReason
261
+ ? "Needs config"
262
+ : "Enabled"
263
+ : id === "local"
234
264
  ? (discovery.status === "ready" ? "Enabled" : "Disabled")
235
265
  : !defaults.enabled
236
266
  ? "Disabled"
@@ -1,9 +1,10 @@
1
- export type ProviderId = "openai" | "anthropic" | "google" | "local" | "antigravity";
1
+ export type ProviderId = "openai" | "anthropic" | "google" | "mistral" | "local" | "antigravity";
2
2
 
3
3
  export type ProviderBackendType =
4
4
  | "codex-cli-auth"
5
5
  | "gemini-cli-auth"
6
6
  | "claude-code-auth"
7
+ | "mistral-vibe-cli-auth"
7
8
  | "antigravity-cli-auth"
8
9
  | "openai-api-key"
9
10
  | "gemini-api-key"
@@ -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,
@@ -56,10 +55,9 @@ export function mapModelIdToClaudeArg(modelId: string): string {
56
55
  return modelId;
57
56
  }
58
57
 
59
- export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
60
- if (reasoning && CLAUDE_CODE_EFFORT_IDS.has(reasoning)) return reasoning;
61
- return null;
62
- }
58
+ export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
59
+ return reasoning?.trim() || null;
60
+ }
63
61
 
64
62
  function buildClaudeCodeBaseArgs(request: ProviderChatRequest): string[] {
65
63
  const effort = mapReasoningToEffort(request.route.reasoning ?? null);
@@ -1,6 +1,7 @@
1
1
  import { runCommand } from "../process/CommandRunner.js";
2
2
  import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
3
3
  import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
4
+ import { loadCachedProviderModels } from "../models/providerModelCache.js";
4
5
  import type { BackendRunHandlers } from "../providers/types.js";
5
6
  import type {
6
7
  ProviderBackendKind,
@@ -27,97 +28,155 @@ export const ANTIGRAVITY_DEFAULT_MODEL_ID = "gemini-3.5-flash";
27
28
  export const ANTIGRAVITY_DEFAULT_REASONING = "high";
28
29
 
29
30
  // ---------------------------------------------------------------------------
30
- // Effort levels for Antigravity Gemini models
31
+ // Model definitions
31
32
  // ---------------------------------------------------------------------------
32
33
 
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 };
34
+ interface AgySelectorMetadata {
35
+ provider: "antigravity";
36
+ selectors: Record<string, string>;
37
+ }
36
38
 
37
- // ---------------------------------------------------------------------------
38
- // Model definitions
39
- // ---------------------------------------------------------------------------
39
+ function normalizeAgyId(value: string): string {
40
+ return value
41
+ .trim()
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9.]+/g, "-")
44
+ .replace(/^-+|-+$/g, "");
45
+ }
40
46
 
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
- ];
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
+ }
88
54
 
89
- // ---------------------------------------------------------------------------
90
- // AGY_MODEL env mapping
91
- // ---------------------------------------------------------------------------
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
+ }
92
61
 
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
- };
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
+ }
106
79
 
107
- export function getAgyModelEnvValue(modelId: string): string | null {
108
- return AGY_MODEL_ENV_MAP[modelId] ?? null;
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;
109
149
  }
110
150
 
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 };
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;
117
165
  }
118
166
 
119
167
  export function getAntigravityModelLabel(modelId: string): string {
120
- return ANTIGRAVITY_MODELS.find((m) => m.id === modelId)?.label ?? modelId;
168
+ const discovered = getActiveAgyModels().find((m) => m.id === modelId || m.modelId === modelId);
169
+ if (discovered) return discovered.label;
170
+
171
+ const normalizedId = migrateAntigravityLegacyModelId(modelId).modelId;
172
+ const knownLabels: Record<string, string> = {
173
+ "gemini-3.5-flash": "Gemini 3.5 Flash",
174
+ "gemini-3.1-pro": "Gemini 3.1 Pro",
175
+ "claude-sonnet-4.6-thinking": "Claude Sonnet 4.6 (Thinking)",
176
+ "claude-opus-4.6-thinking": "Claude Opus 4.6 (Thinking)",
177
+ "gpt-oss-120b-medium": "GPT-OSS 120B",
178
+ };
179
+ return knownLabels[normalizedId] ?? modelId;
121
180
  }
122
181
 
123
182
  // ---------------------------------------------------------------------------
@@ -138,7 +197,9 @@ export function migrateAntigravityLegacyModelId(modelId: string): { modelId: str
138
197
  "gemini-3.5-flash-low": { modelId: "gemini-3.5-flash", reasoning: "low" },
139
198
  "gemini-3.1-pro-high": { modelId: "gemini-3.1-pro", reasoning: "high" },
140
199
  "gemini-3.1-pro-low": { modelId: "gemini-3.1-pro", reasoning: "low" },
141
- "gpt-oss-120b-medium": { modelId: "gpt-oss-120b" },
200
+ "claude-sonnet-4-6-think": { modelId: "claude-sonnet-4.6-thinking" },
201
+ "claude-opus-4-6-think": { modelId: "claude-opus-4.6-thinking" },
202
+ "gpt-oss-120b": { modelId: "gpt-oss-120b-medium" },
142
203
  };
143
204
  return legacy[modelId] ?? { modelId };
144
205
  }
@@ -149,6 +210,52 @@ export function migrateAntigravityLegacyModelId(modelId: string): { modelId: str
149
210
 
150
211
  let agyRouteValidated = false;
151
212
  let resolvedAgyExecutable: string = "agy";
213
+ let discoveredAgyModels: readonly ProviderModel[] | null = null;
214
+
215
+ function getActiveAgyModels(): readonly ProviderModel[] {
216
+ if (discoveredAgyModels?.length) return discoveredAgyModels;
217
+ return loadCachedProviderModels("antigravity")?.models ?? [];
218
+ }
219
+
220
+ export async function discoverAgyModels(options: {
221
+ executable: string;
222
+ cwd: string;
223
+ runCommandImpl: typeof runCommand;
224
+ platform: NodeJS.Platform;
225
+ }): Promise<ProviderModelDiscoveryResult> {
226
+ const spawnSpec = buildSpawnSpec(options.executable, ["models"], options.platform);
227
+ const result = await options.runCommandImpl({
228
+ executable: spawnSpec.executable,
229
+ args: spawnSpec.args,
230
+ cwd: options.cwd,
231
+ timeoutMs: ANTIGRAVITY_VALIDATION_TIMEOUT_MS,
232
+ }).result;
233
+ const models = result.status === "completed" && result.exitCode === 0
234
+ ? parseAgyModelsOutput(result.stdout)
235
+ : [];
236
+ if (models.length > 0) {
237
+ discoveredAgyModels = models;
238
+ return {
239
+ status: "ready",
240
+ providerId: "antigravity",
241
+ backendKind: "antigravity-cli-auth",
242
+ models,
243
+ message: `Loaded ${models.length} models from agy models.`,
244
+ diagnostics: { modelSource: "agy-models-command", modelsExitCode: result.exitCode, modelsStatus: result.status },
245
+ };
246
+ }
247
+ const cached = loadCachedProviderModels("antigravity")?.models ?? [];
248
+ return {
249
+ status: cached.length > 0 ? "ready" : "not-configured",
250
+ providerId: "antigravity",
251
+ backendKind: cached.length > 0 ? "antigravity-cli-auth" : "unavailable",
252
+ models: cached,
253
+ message: cached.length > 0
254
+ ? "Live agy model metadata is unavailable; using the last successful discovery."
255
+ : "Antigravity model metadata is unavailable. Run Refresh models after checking `agy models`.",
256
+ diagnostics: { modelSource: cached.length > 0 ? "cache" : "unavailable", modelsExitCode: result.exitCode, modelsStatus: result.status },
257
+ };
258
+ }
152
259
 
153
260
  export function isAntigravityRouteConfigured(): boolean {
154
261
  return agyRouteValidated;
@@ -157,6 +264,7 @@ export function isAntigravityRouteConfigured(): boolean {
157
264
  export function resetAntigravityRouteValidationCacheForTests(): void {
158
265
  agyRouteValidated = false;
159
266
  resolvedAgyExecutable = "agy";
267
+ discoveredAgyModels = null;
160
268
  resetAgyExecutableCacheForTests();
161
269
  }
162
270
 
@@ -170,6 +278,24 @@ export async function validateAntigravityRoute(options: {
170
278
  runCommandImpl?: typeof runCommand;
171
279
  platform?: NodeJS.Platform;
172
280
  }): Promise<ProviderRouteValidationResult> {
281
+ // Already validated this session: skip the executable probe and model
282
+ // re-discovery so re-activating antigravity is instant. "Refresh models"
283
+ // bypasses this via runtime.refreshModels, so a stale catalog stays
284
+ // user-recoverable.
285
+ if (agyRouteValidated && discoveredAgyModels?.length) {
286
+ return {
287
+ status: "ready",
288
+ providerId: "antigravity",
289
+ backendKind: "antigravity-cli-auth",
290
+ message: `Antigravity CLI found at: ${resolvedAgyExecutable}`,
291
+ diagnostics: {
292
+ resolvedCommand: resolvedAgyExecutable,
293
+ modelSource: "session-cache",
294
+ discoveredModelCount: discoveredAgyModels.length,
295
+ },
296
+ };
297
+ }
298
+
173
299
  let resolved: string;
174
300
  try {
175
301
  resolved = await resolveAgyExecutable({
@@ -213,13 +339,23 @@ export async function validateAntigravityRoute(options: {
213
339
 
214
340
  resolvedAgyExecutable = resolved;
215
341
  agyRouteValidated = true;
342
+ const modelDiscovery = await discoverAgyModels({
343
+ executable: resolved,
344
+ cwd: options.cwd ?? process.cwd(),
345
+ runCommandImpl,
346
+ platform: options.platform ?? process.platform,
347
+ });
216
348
 
217
349
  return {
218
350
  status: "ready",
219
351
  providerId: "antigravity",
220
352
  backendKind: "antigravity-cli-auth",
221
353
  message: `Antigravity CLI found at: ${resolved}`,
222
- diagnostics: { resolvedCommand: resolved },
354
+ diagnostics: {
355
+ resolvedCommand: resolved,
356
+ modelSource: modelDiscovery.diagnostics?.modelSource ?? "unavailable",
357
+ discoveredModelCount: modelDiscovery.models.length,
358
+ },
223
359
  };
224
360
  }
225
361
 
@@ -233,16 +369,23 @@ export function runAntigravityWithRunner(
233
369
  runCommandImpl: typeof runCommand = runCommand,
234
370
  executable: string = resolvedAgyExecutable,
235
371
  platform: NodeJS.Platform = process.platform,
372
+ models: readonly ProviderModel[] = getActiveAgyModels(),
236
373
  ): () => void {
237
- const spawnSpec = buildSpawnSpec(executable, ["-p", request.prompt], platform);
238
- const env = buildAgyEnv(request.route.modelId, request.route.reasoning);
374
+ const selector = getAgyModelSelector(request.route.modelId, request.route.reasoning, models);
375
+ if (!selector) {
376
+ handlers.onError(
377
+ `Antigravity has no verified selector for ${request.route.modelId}${request.route.reasoning ? ` / ${request.route.reasoning}` : ""}. Refresh models and try again.`,
378
+ );
379
+ return () => undefined;
380
+ }
381
+ const spawnSpec = buildSpawnSpec(executable, ["--model", selector, "-p", request.prompt], platform);
239
382
 
240
383
  const runner = runCommandImpl(
241
384
  {
242
385
  executable: spawnSpec.executable,
243
386
  args: spawnSpec.args,
244
387
  cwd: request.workspaceRoot,
245
- env,
388
+ env: { ...process.env },
246
389
  timeoutMs: ANTIGRAVITY_TIMEOUT_MS,
247
390
  },
248
391
  );
@@ -288,12 +431,36 @@ export const antigravityRuntime: ProviderRuntime = {
288
431
  cwd: workspaceRoot,
289
432
  configuredPath: antigravityCommandPath ?? null,
290
433
  }),
291
- discoverModels: (): ProviderModelDiscoveryResult => ({
292
- status: "ready",
293
- providerId: "antigravity",
294
- backendKind: "antigravity-cli-auth",
295
- models: ANTIGRAVITY_MODELS,
296
- }),
434
+ discoverModels: (): ProviderModelDiscoveryResult => {
435
+ const models = getActiveAgyModels();
436
+ return {
437
+ status: models.length > 0 ? "ready" : "not-configured",
438
+ providerId: "antigravity",
439
+ backendKind: models.length > 0 ? "antigravity-cli-auth" : "unavailable",
440
+ models,
441
+ ...(models.length === 0 ? { message: "Antigravity model metadata is unavailable. Run Refresh models." } : {}),
442
+ };
443
+ },
444
+ refreshModels: async ({ cwd }): Promise<ProviderModelDiscoveryResult> => {
445
+ let executable = resolvedAgyExecutable;
446
+ try {
447
+ executable = await resolveAgyExecutable({ cwd });
448
+ resolvedAgyExecutable = executable;
449
+ } catch {
450
+ const cached = loadCachedProviderModels("antigravity")?.models ?? [];
451
+ return {
452
+ status: cached.length > 0 ? "ready" : "not-configured",
453
+ providerId: "antigravity",
454
+ backendKind: cached.length > 0 ? "antigravity-cli-auth" : "unavailable",
455
+ models: cached,
456
+ message: cached.length > 0
457
+ ? "Antigravity CLI is unavailable; using the last successful model discovery."
458
+ : ANTIGRAVITY_ROUTE_SETUP_MESSAGE,
459
+ diagnostics: { modelSource: cached.length > 0 ? "cache" : "unavailable", resolvedCommand: null },
460
+ };
461
+ }
462
+ return discoverAgyModels({ executable, cwd, runCommandImpl: runCommand, platform: process.platform });
463
+ },
297
464
  run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
298
465
  handlers.onProgress?.({
299
466
  id: "antigravity-route",