@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
@@ -0,0 +1,153 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { formatReasoningLabel } from "../../config/settings.js";
5
+ import type { ProviderModel } from "../providerRuntime/types.js";
6
+ import type {
7
+ CodexModelCapabilities,
8
+ CodexModelCapability,
9
+ ReasoningEffortCapability,
10
+ } from "./codexModelCapabilities.js";
11
+ import { loadCachedProviderModels, type CachedProviderModels } from "./providerModelCache.js";
12
+
13
+ // The codex CLI maintains its own model catalog cache with slugs, labels and
14
+ // reasoning levels. Reading it seeds Codexa's OpenAI model list instantly —
15
+ // no subprocess — and stays current because the codex CLI refreshes the file
16
+ // on its own runs.
17
+ // Resolved per call from env (matching claudeCodeDiscovery) so HOME
18
+ // redirection in tests holds — Bun's homedir() ignores runtime HOME changes.
19
+ export function getCodexModelsCacheFile(): string {
20
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? homedir();
21
+ return join(home, ".codex", "models_cache.json");
22
+ }
23
+
24
+ interface CodexSeed {
25
+ fetchedAt: number;
26
+ models: readonly ProviderModel[];
27
+ }
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ function asString(value: unknown): string | null {
34
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
35
+ }
36
+
37
+ function parseReasoningLevels(raw: unknown): readonly ReasoningEffortCapability[] | null {
38
+ if (!Array.isArray(raw)) {
39
+ return null;
40
+ }
41
+ const levels = raw
42
+ .map((entry): ReasoningEffortCapability | null => {
43
+ if (!isRecord(entry)) {
44
+ return null;
45
+ }
46
+ const id = asString(entry.effort);
47
+ if (!id) {
48
+ return null;
49
+ }
50
+ return { id, label: formatReasoningLabel(id), description: asString(entry.description) };
51
+ })
52
+ .filter((entry): entry is ReasoningEffortCapability => entry !== null);
53
+ return levels.length > 0 ? levels : null;
54
+ }
55
+
56
+ function parseSeedModel(raw: unknown): ProviderModel | null {
57
+ if (!isRecord(raw)) {
58
+ return null;
59
+ }
60
+ // Models with visibility "hide" are internal codex routes (e.g. auto-review).
61
+ if (asString(raw.visibility) === "hide") {
62
+ return null;
63
+ }
64
+ const modelId = asString(raw.slug);
65
+ if (!modelId) {
66
+ return null;
67
+ }
68
+ return {
69
+ id: modelId,
70
+ modelId,
71
+ label: asString(raw.display_name) ?? modelId,
72
+ description: asString(raw.description),
73
+ defaultReasoningLevel: asString(raw.default_reasoning_level),
74
+ supportedReasoningLevels: parseReasoningLevels(raw.supported_reasoning_levels),
75
+ source: "discovered",
76
+ };
77
+ }
78
+
79
+ export function loadCodexSeedModels(cacheFile = getCodexModelsCacheFile()): CodexSeed | null {
80
+ try {
81
+ if (!existsSync(cacheFile)) {
82
+ return null;
83
+ }
84
+ const parsed: unknown = JSON.parse(readFileSync(cacheFile, "utf8"));
85
+ if (!isRecord(parsed) || !Array.isArray(parsed.models)) {
86
+ return null;
87
+ }
88
+ const models = parsed.models
89
+ .map(parseSeedModel)
90
+ .filter((model): model is ProviderModel => model !== null);
91
+ if (models.length === 0) {
92
+ return null;
93
+ }
94
+ const fetchedAtRaw = asString(parsed.fetched_at);
95
+ const fetchedAt = fetchedAtRaw ? Date.parse(fetchedAtRaw) : Number.NaN;
96
+ return { fetchedAt: Number.isFinite(fetchedAt) ? fetchedAt : 0, models };
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ // Freshest locally known OpenAI models without spawning a subprocess:
103
+ // codex's own cache file vs Codexa's persisted last-good discovery.
104
+ export function loadSeededOpenAiModels(options: {
105
+ codexCacheFile?: string;
106
+ providerCacheFile?: string;
107
+ } = {}): CachedProviderModels | null {
108
+ const seed = loadCodexSeedModels(options.codexCacheFile);
109
+ const persisted = options.providerCacheFile === undefined
110
+ ? loadCachedProviderModels("openai")
111
+ : loadCachedProviderModels("openai", options.providerCacheFile);
112
+ if (seed && (!persisted || seed.fetchedAt >= persisted.discoveredAt)) {
113
+ return { discoveredAt: seed.fetchedAt, models: seed.models };
114
+ }
115
+ return persisted;
116
+ }
117
+
118
+ function toCapability(model: ProviderModel, index: number): CodexModelCapability {
119
+ return {
120
+ id: model.id,
121
+ model: model.modelId,
122
+ label: model.label,
123
+ description: model.description,
124
+ available: true,
125
+ hidden: false,
126
+ isDefault: index === 0,
127
+ defaultReasoningLevel: model.defaultReasoningLevel,
128
+ supportedReasoningLevels: model.supportedReasoningLevels,
129
+ reasoningLevelCount: model.supportedReasoningLevels ? model.supportedReasoningLevels.length : null,
130
+ source: "runtime",
131
+ raw: model,
132
+ };
133
+ }
134
+
135
+ // Capabilities for the OpenAI picker sourced purely from local caches.
136
+ // Returns null when nothing is cached yet (first ever launch).
137
+ export function loadSeededCodexCapabilities(options: {
138
+ codexCacheFile?: string;
139
+ providerCacheFile?: string;
140
+ } = {}): CodexModelCapabilities | null {
141
+ const seeded = loadSeededOpenAiModels(options);
142
+ if (!seeded) {
143
+ return null;
144
+ }
145
+ return {
146
+ status: "ready",
147
+ source: "runtime",
148
+ models: seeded.models.map(toCapability),
149
+ discoveredAt: seeded.discoveredAt,
150
+ executable: null,
151
+ error: null,
152
+ };
153
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { ProviderId } from "../providerLauncher/types.js";
5
+ import type { ProviderModel } from "../providerRuntime/types.js";
6
+
7
+ // Persistent last-good model discovery results, one entry per provider.
8
+ // Lets pickers open instantly with the previous session's discovered models
9
+ // while a background refresh runs. Corrupt or missing cache degrades to null.
10
+ // Resolved per call from env (matching claudeCodeDiscovery) so HOME
11
+ // redirection in tests holds — Bun's homedir() ignores runtime HOME changes.
12
+ export function getProviderModelCacheFile(): string {
13
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? homedir();
14
+ return join(home, ".codexa-model-cache.json");
15
+ }
16
+
17
+ const CACHE_VERSION = 1;
18
+
19
+ export interface CachedProviderModels {
20
+ discoveredAt: number;
21
+ models: readonly ProviderModel[];
22
+ }
23
+
24
+ interface ProviderModelCacheFile {
25
+ version: number;
26
+ providers: Partial<Record<ProviderId, CachedProviderModels>>;
27
+ }
28
+
29
+ function readCacheFile(cacheFile: string): ProviderModelCacheFile | null {
30
+ try {
31
+ if (!existsSync(cacheFile)) {
32
+ return null;
33
+ }
34
+ const parsed: unknown = JSON.parse(readFileSync(cacheFile, "utf8"));
35
+ if (
36
+ typeof parsed !== "object" || parsed === null || Array.isArray(parsed)
37
+ || (parsed as ProviderModelCacheFile).version !== CACHE_VERSION
38
+ || typeof (parsed as ProviderModelCacheFile).providers !== "object"
39
+ || (parsed as ProviderModelCacheFile).providers === null
40
+ ) {
41
+ return null;
42
+ }
43
+ return parsed as ProviderModelCacheFile;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // A model id is passed straight to a provider CLI, so it must be a bare token.
50
+ // Discovery scrapes CLI output, and a buggy build can bake an ANSI escape into
51
+ // an id (e.g. "claude-fable-5\x1b[1m"). Reject rather than repair: a cached id
52
+ // we cannot vouch for is dropped so discovery re-probes for a clean one.
53
+ const VALID_MODEL_ID = /^[A-Za-z0-9._:\/-]+$/;
54
+
55
+ function isUsableModel(model: unknown): model is ProviderModel {
56
+ if (typeof model !== "object" || model === null) {
57
+ return false;
58
+ }
59
+ const candidate = model as ProviderModel;
60
+ return typeof candidate.id === "string"
61
+ && typeof candidate.modelId === "string"
62
+ && typeof candidate.label === "string"
63
+ && VALID_MODEL_ID.test(candidate.id)
64
+ && VALID_MODEL_ID.test(candidate.modelId);
65
+ }
66
+
67
+ function isValidEntry(entry: unknown): entry is CachedProviderModels {
68
+ if (typeof entry !== "object" || entry === null) {
69
+ return false;
70
+ }
71
+ const candidate = entry as CachedProviderModels;
72
+ return typeof candidate.discoveredAt === "number" && Array.isArray(candidate.models);
73
+ }
74
+
75
+ export function loadCachedProviderModels(
76
+ providerId: ProviderId,
77
+ cacheFile = getProviderModelCacheFile(),
78
+ ): CachedProviderModels | null {
79
+ const cache = readCacheFile(cacheFile);
80
+ const entry = cache?.providers?.[providerId];
81
+ if (!entry || !isValidEntry(entry)) {
82
+ return null;
83
+ }
84
+ const models = entry.models.filter(isUsableModel);
85
+ if (models.length === 0) {
86
+ return null;
87
+ }
88
+ return { discoveredAt: entry.discoveredAt, models };
89
+ }
90
+
91
+ export function saveCachedProviderModels(
92
+ providerId: ProviderId,
93
+ entry: CachedProviderModels,
94
+ cacheFile = getProviderModelCacheFile(),
95
+ ): void {
96
+ if (entry.models.length === 0) {
97
+ return;
98
+ }
99
+ try {
100
+ const cache = readCacheFile(cacheFile) ?? { version: CACHE_VERSION, providers: {} };
101
+ cache.providers[providerId] = entry;
102
+ writeFileSync(cacheFile, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
103
+ } catch {
104
+ // Persistence is best-effort; discovery still works without it.
105
+ }
106
+ }
@@ -4,14 +4,14 @@ import { useEffect, useRef } from "react";
4
4
 
5
5
  type DebugEnv = Record<string, string | undefined>;
6
6
 
7
- // Global debug state — populated lazily on first check, or eagerly via configureRenderDebug().
7
+ // Global debug state — populated lazily on first check, or eagerly via configureRenderDebug().
8
8
  let configured = false;
9
9
  let enabled = false;
10
10
  let renderTraceEnabled = false;
11
11
  let lifecycleEnabled = false;
12
12
  let flickerEnabled = false;
13
13
  let plainActionsEnabled = false;
14
- let logPath = join(process.cwd(), ".codexa-debug", "render-debug.log");
14
+ let logPath = join(process.cwd(), ".codexa", "debug", "render-status.log");
15
15
  let sessionId = `${Date.now()}-${process.pid}`;
16
16
  const counters = new Map<string, number>();
17
17
 
@@ -19,13 +19,17 @@ function configureFromEnv(env: DebugEnv = process.env): void {
19
19
  renderTraceEnabled = env["CODEXA_DEBUG_RENDER_TRACE"] === "1";
20
20
  // Both CODEXA_RENDER_DEBUG and CODEXA_DEBUG_RENDER activate render debugging —
21
21
  // two names exist for historical reasons; either one is sufficient.
22
- enabled = env["CODEXA_RENDER_DEBUG"] === "1"
23
- || env["CODEXA_DEBUG_RENDER"] === "1"
24
- || renderTraceEnabled;
22
+ // CODEXA_TERMINAL_TRACE is a focused alias for diagnosing terminal/clear/resize
23
+ // render-state issues; it lights up the same `terminal` trace channel.
24
+ enabled = env["CODEXA_RENDER_DEBUG"] === "1"
25
+ || env["CODEXA_DEBUG_MODEL_STATE"] === "1"
26
+ || env["CODEXA_DEBUG_RENDER"] === "1"
27
+ || env["CODEXA_TERMINAL_TRACE"] === "1"
28
+ || renderTraceEnabled;
25
29
  lifecycleEnabled = env["CODEXA_DEBUG_LIFECYCLE"] === "1";
26
30
  flickerEnabled = env["CODEXA_DEBUG_FLICKER"] === "1";
27
31
  plainActionsEnabled = env["CODEXA_DEBUG_PLAIN_ACTIONS"] === "1";
28
- logPath = env["CODEXA_RENDER_DEBUG_FILE"]?.trim() || join(process.cwd(), ".codexa-debug", "render-debug.log");
32
+ logPath = env["CODEXA_RENDER_DEBUG_FILE"]?.trim() || join(process.cwd(), ".codexa", "debug", "render-status.log");
29
33
  sessionId = `${Date.now()}-${process.pid}`;
30
34
  configured = true;
31
35
  }
@@ -9,6 +9,7 @@ export interface CommandSpec {
9
9
  cwd: string;
10
10
  env?: NodeJS.ProcessEnv;
11
11
  timeoutMs?: number;
12
+ stdinData?: string;
12
13
  }
13
14
 
14
15
  export interface CommandResult {
@@ -177,10 +178,20 @@ function runProcess(
177
178
  cwd: spec.cwd,
178
179
  env: spec.env,
179
180
  shell: false,
180
- stdio: ["ignore", "pipe", "pipe"],
181
+ stdio: [spec.stdinData !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
181
182
  });
182
183
  handlers.onProcessLifecycle?.("spawned");
183
184
 
185
+ if (spec.stdinData !== undefined) {
186
+ try {
187
+ child.stdin?.on("error", () => { /* EPIPE when the process exits before reading stdin */ });
188
+ child.stdin?.write(spec.stdinData);
189
+ child.stdin?.end();
190
+ } catch {
191
+ // stdin already closed; the close/error handlers report the outcome
192
+ }
193
+ }
194
+
184
195
  const result = new Promise<CommandResult>((resolve) => {
185
196
  const finish = (partial: Omit<CommandResult, "stdout" | "stderr" | "startedAt" | "endedAt" | "durationMs" | "userMessage"> & { endedAt?: number }) => {
186
197
  if (timeoutHandle) clearTimeout(timeoutHandle);
@@ -14,12 +14,17 @@ import {
14
14
  isProviderRoutableInCodexa,
15
15
  isProviderRouteConfigured,
16
16
  } from "../providerRuntime/registry.js";
17
- import { normalizeGeminiModelId } from "../providerRuntime/models.js";
18
- import { setLocalProviderConfig } from "../providerRuntime/local.js";
17
+ import { normalizeGeminiModelId } from "../providerRuntime/models.js";
18
+ import { ANTIGRAVITY_DEFAULT_MODEL_ID } from "../providerRuntime/antigravity.js";
19
+ import { discoverMistralVibeModels } from "../providerRuntime/mistralVibe.js";
20
+ import { setLocalProviderConfig } from "../providerRuntime/local.js";
19
21
  import { formatContextLength, resolveModelContextLengthCached } from "../providerRuntime/contextMetadata.js";
20
22
  import { resolveModelCapabilityProfileCached } from "../providerRuntime/capabilityProfile.js";
21
23
 
22
- const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "google", "local"];
24
+ // Google/Gemini remains a recognized legacy config value so existing workspace
25
+ // files can be migrated, but it is no longer a selectable Codexa provider.
26
+ const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "mistral", "local", "antigravity"];
27
+ const KNOWN_PROVIDER_IDS: readonly ProviderId[] = ["openai", "anthropic", "google", "mistral", "local", "antigravity"];
23
28
 
24
29
  const DEFAULT_PROVIDER_ID: ProviderId = "openai";
25
30
 
@@ -63,8 +68,8 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
63
68
  isActiveRoute: false,
64
69
  routeUnavailableReason: null,
65
70
  },
66
- local: {
67
- id: "local",
71
+ local: {
72
+ id: "local",
68
73
  displayName: "Local",
69
74
  currentModel: () => "Local default",
70
75
  backendType: "local-openai-compatible",
@@ -72,12 +77,34 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
72
77
  enabled: false,
73
78
  launchCommand: null,
74
79
  isActiveRoute: false,
75
- routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
80
+ routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
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,
76
92
  },
77
- };
93
+ antigravity: {
94
+ id: "antigravity",
95
+ displayName: "Antigravity",
96
+ currentModel: () => ANTIGRAVITY_DEFAULT_MODEL_ID,
97
+ backendType: "antigravity-cli-auth",
98
+ routeMode: "in-codexa",
99
+ enabled: true,
100
+ launchCommand: { executable: "agy", args: [] },
101
+ isActiveRoute: false,
102
+ routeUnavailableReason: null,
103
+ },
104
+ };
78
105
 
79
- function isProviderId(value: unknown): value is ProviderId {
80
- return typeof value === "string" && PROVIDER_ORDER.includes(value as ProviderId);
106
+ function isProviderId(value: unknown): value is ProviderId {
107
+ return typeof value === "string" && KNOWN_PROVIDER_IDS.includes(value as ProviderId);
81
108
  }
82
109
 
83
110
  function normalizeLaunchCommand(value: ProviderWorkspaceOverride["command"] | undefined): ProviderLaunchCommand | null | undefined {
@@ -122,25 +149,29 @@ function applyOverride(
122
149
  : provider.currentModel,
123
150
  enabled: nextEnabled,
124
151
  launchCommand: nextCommand,
125
- statusLabel: nextEnabled
126
- ? (provider.routeUnavailableReason ? "Needs config" : "Enabled")
127
- : "Disabled",
152
+ statusLabel: !nextEnabled
153
+ ? "Disabled"
154
+ : provider.routeMode === "launch-only"
155
+ ? provider.statusLabel
156
+ : (provider.routeUnavailableReason ? "Needs config" : "Enabled"),
128
157
  };
129
158
  }
130
159
 
131
- export function getDefaultProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
132
- return isProviderId(config?.workspaceDefaultProviderId) ? config.workspaceDefaultProviderId : DEFAULT_PROVIDER_ID;
133
- }
160
+ export function getDefaultProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
161
+ const providerId = config?.workspaceDefaultProviderId;
162
+ return isProviderId(providerId) && providerId !== "google" ? providerId : DEFAULT_PROVIDER_ID;
163
+ }
134
164
 
135
165
  export function getActiveRouteProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
136
166
  const providerId = config?.activeRoute?.providerId;
137
- return isProviderId(providerId) && isProviderRoutableInCodexa(providerId)
167
+ return isProviderId(providerId) && providerId !== "google" && isProviderRoutableInCodexa(providerId)
138
168
  ? providerId
139
169
  : DEFAULT_PROVIDER_ID;
140
170
  }
141
171
 
142
172
  export function buildProviderRegistry(options: {
143
173
  activeModel: string;
174
+ workspaceRoot?: string;
144
175
  workspaceConfig?: ProviderWorkspaceConfig | null;
145
176
  diagnostics?: Record<string, Record<string, string | number | boolean | null>>;
146
177
  routeErrors?: Record<string, string>;
@@ -154,7 +185,9 @@ export function buildProviderRegistry(options: {
154
185
  }
155
186
  const defaults = DEFAULT_PROVIDERS[id];
156
187
  const runtime = getProviderRuntime(id);
157
- const discovery = runtime.discoverModels();
188
+ const discovery = id === "mistral"
189
+ ? discoverMistralVibeModels(options.workspaceRoot ?? process.cwd())
190
+ : runtime.discoverModels();
158
191
 
159
192
  const activeRoute = options.workspaceConfig?.activeRoute;
160
193
  const isThisActive = activeRoute?.providerId === id;
@@ -189,6 +222,10 @@ export function buildProviderRegistry(options: {
189
222
  }
190
223
  }
191
224
 
225
+ if (id === "mistral") {
226
+ currentModelLabel = discovery.models[0]?.modelId ?? "Vibe default";
227
+ }
228
+
192
229
  const rawMetadataForModel = discovery.models.find((model) => model.modelId === currentModelLabel)?.raw;
193
230
  const contextMetadata = resolveModelContextLengthCached({
194
231
  providerId: id,
@@ -214,7 +251,16 @@ export function buildProviderRegistry(options: {
214
251
 
215
252
  const enabled = id === "local" ? discovery.status === "ready" : defaults.enabled;
216
253
 
217
- 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"
218
264
  ? (discovery.status === "ready" ? "Enabled" : "Disabled")
219
265
  : !defaults.enabled
220
266
  ? "Disabled"
@@ -1,10 +1,12 @@
1
- export type ProviderId = "openai" | "anthropic" | "google" | "local";
1
+ export type ProviderId = "openai" | "anthropic" | "google" | "mistral" | "local" | "antigravity";
2
2
 
3
3
  export type ProviderBackendType =
4
- | "codex-cli-auth"
5
- | "gemini-cli-auth"
6
- | "claude-code-auth"
7
- | "openai-api-key"
4
+ | "codex-cli-auth"
5
+ | "gemini-cli-auth"
6
+ | "claude-code-auth"
7
+ | "mistral-vibe-cli-auth"
8
+ | "antigravity-cli-auth"
9
+ | "openai-api-key"
8
10
  | "gemini-api-key"
9
11
  | "anthropic-api-key"
10
12
  | "local-openai-compatible"
@@ -69,10 +71,11 @@ export interface ProviderWorkspaceOverride {
69
71
  defaultModel?: string;
70
72
  models?: Record<string, ProviderModelWorkspaceOverride>;
71
73
  command?: string | ProviderLaunchCommand | null;
72
- claudeCommandPath?: string;
73
- geminiCommandPath?: string;
74
- codexCommandPath?: string;
75
- }
74
+ claudeCommandPath?: string;
75
+ geminiCommandPath?: string;
76
+ codexCommandPath?: string;
77
+ antigravityCommandPath?: string;
78
+ }
76
79
 
77
80
  export interface ProviderModelWorkspaceOverride {
78
81
  contextLength?: number;
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import { DEFAULT_MODEL } from "../../config/settings.js";
4
- import { normalizeWorkspaceRoot } from "../workspaceRoot.js";
4
+ import { normalizeWorkspaceRoot } from "../workspace/workspaceRoot.js";
5
5
  import { isKnownProviderId } from "./registry.js";
6
6
  import { getDefaultRouteModel, getProviderRuntime, isProviderRouteConfigured, isProviderRoutableInCodexa } from "../providerRuntime/registry.js";
7
7
  import { normalizeGeminiModelId } from "../providerRuntime/models.js";
@@ -13,8 +13,9 @@ import type {
13
13
  ProviderWorkspaceOverride,
14
14
  } from "./types.js";
15
15
 
16
- const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
- const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
16
+ const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
+ const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
18
+ const DEPRECATED_GOOGLE_PROVIDER_ID = "google";
18
19
 
19
20
  export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
20
21
  return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
@@ -28,16 +29,20 @@ function isDeprecatedAntigravityProviderId(value: unknown): boolean {
28
29
  return value === DEPRECATED_ANTIGRAVITY_PROVIDER_ID;
29
30
  }
30
31
 
31
- function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
+ function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
33
  if (!isRecord(value)) return false;
33
34
  return isDeprecatedAntigravityProviderId(value.providerId ?? value.provider_id)
34
35
  || DEPRECATED_ANTIGRAVITY_BACKENDS.has(String(value.backendKind ?? value.backend_kind));
35
- }
36
+ }
37
+
38
+ function isDeprecatedGoogleRoute(value: unknown): boolean {
39
+ return isRecord(value) && (value.providerId ?? value.provider_id) === DEPRECATED_GOOGLE_PROVIDER_ID;
40
+ }
36
41
 
37
42
  function resolveDeprecatedProviderFallback(
38
43
  providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>>,
39
44
  ): ProviderId {
40
- const candidates: readonly ProviderId[] = ["openai", "google", "anthropic", "local"];
45
+ const candidates: readonly ProviderId[] = ["openai", "anthropic", "local"];
41
46
  return candidates.find((providerId) =>
42
47
  isProviderRoutableInCodexa(providerId)
43
48
  && (providerId === "openai" || providers[providerId] !== undefined || isProviderRouteConfigured(providerId))
@@ -214,22 +219,24 @@ function parseActiveRoute(value: unknown): ProviderActiveRoute | undefined {
214
219
  export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceConfig {
215
220
  if (!isRecord(data)) return {};
216
221
 
217
- const config: ProviderWorkspaceConfig = {};
218
- const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
219
- let foundDeprecatedAntigravity = false;
222
+ const config: ProviderWorkspaceConfig = {};
223
+ const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
224
+ let foundDeprecatedAntigravity = false;
225
+ let foundDeprecatedGoogle = false;
220
226
 
221
227
  if (isRecord(data.providers)) {
222
228
  for (const [id, value] of Object.entries(data.providers)) {
223
- if (isDeprecatedAntigravityProviderId(id)) {
224
- foundDeprecatedAntigravity = true;
225
- continue;
226
- }
227
- if (!isKnownProviderId(id)) continue;
228
- const override = parseProviderOverride(value);
229
- if (id === "google" && override?.currentModel) {
230
- override.currentModel = normalizeGeminiModelId(override.currentModel);
231
- }
232
- if (override) providers[id] = override;
229
+ if (isDeprecatedAntigravityProviderId(id)) {
230
+ foundDeprecatedAntigravity = true;
231
+ continue;
232
+ }
233
+ if (id === DEPRECATED_GOOGLE_PROVIDER_ID) {
234
+ foundDeprecatedGoogle = true;
235
+ continue;
236
+ }
237
+ if (!isKnownProviderId(id)) continue;
238
+ const override = parseProviderOverride(value);
239
+ if (override) providers[id] = override;
233
240
  }
234
241
  config.providers = providers;
235
242
  }
@@ -238,7 +245,10 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
238
245
  ?? data.workspace_default_provider_id
239
246
  ?? data.defaultProviderId
240
247
  ?? data.default_provider_id;
241
- if (isDeprecatedAntigravityProviderId(defaultProvider)) {
248
+ if (defaultProvider === DEPRECATED_GOOGLE_PROVIDER_ID) {
249
+ foundDeprecatedGoogle = true;
250
+ config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
251
+ } else if (isDeprecatedAntigravityProviderId(defaultProvider)) {
242
252
  foundDeprecatedAntigravity = true;
243
253
  config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
244
254
  } else if (typeof defaultProvider === "string" && isKnownProviderId(defaultProvider)) {
@@ -246,7 +256,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
246
256
  }
247
257
 
248
258
  const rawActiveRoute = data.activeRoute ?? data.active_route;
249
- if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
259
+ if (isDeprecatedGoogleRoute(rawActiveRoute)) {
260
+ foundDeprecatedGoogle = true;
261
+ const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
262
+ config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
263
+ config.workspaceDefaultProviderId ??= fallbackProviderId;
264
+ } else if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
250
265
  foundDeprecatedAntigravity = true;
251
266
  const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
252
267
  config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
@@ -258,12 +273,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
258
273
  }
259
274
  }
260
275
 
261
- if (foundDeprecatedAntigravity) {
276
+ if (foundDeprecatedGoogle || foundDeprecatedAntigravity) {
262
277
  const revertedProviderId = config.activeRoute?.providerId
263
278
  ?? config.workspaceDefaultProviderId
264
279
  ?? resolveDeprecatedProviderFallback(providers);
265
280
  config.migrationNotice = {
266
- deprecatedProviderId: DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
281
+ deprecatedProviderId: foundDeprecatedGoogle ? DEPRECATED_GOOGLE_PROVIDER_ID : DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
267
282
  revertedProviderId,
268
283
  };
269
284
  }
@@ -348,13 +363,13 @@ export function saveProviderWorkspaceConfig(workspaceRoot: string, config: Provi
348
363
  renameSync(tmpFile, filePath);
349
364
  }
350
365
 
351
- export function setProviderWorkspaceDefault(
366
+ export function setProviderWorkspaceDefault(
352
367
  config: ProviderWorkspaceConfig,
353
368
  providerId: ProviderId,
354
369
  ): ProviderWorkspaceConfig {
355
370
  return {
356
371
  ...config,
357
- workspaceDefaultProviderId: providerId,
372
+ workspaceDefaultProviderId: providerId === DEPRECATED_GOOGLE_PROVIDER_ID ? "openai" : providerId,
358
373
  };
359
374
  }
360
375
 
@@ -396,7 +411,7 @@ export function setProviderActiveRoute(
396
411
  config: ProviderWorkspaceConfig,
397
412
  activeRoute: ProviderActiveRoute,
398
413
  ): ProviderWorkspaceConfig {
399
- if (!isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
414
+ if (activeRoute.providerId === DEPRECATED_GOOGLE_PROVIDER_ID || !isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
400
415
  return config;
401
416
  }
402
417