@golba98/codexa 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -0,0 +1,48 @@
1
+ import { runCommand } from "../process/CommandRunner.js";
2
+ import { resolveExecutable } from "./executableResolver.js";
3
+
4
+ type CommandRunner = typeof runCommand;
5
+
6
+ let cachedExecutable: string | null = null;
7
+
8
+ export function resetAgyExecutableCacheForTests(): void {
9
+ cachedExecutable = null;
10
+ }
11
+
12
+ /**
13
+ * Returns the resolved Antigravity CLI executable (full path or bare name).
14
+ *
15
+ * Priority:
16
+ * 1. Configured path override (antigravityCommandPath)
17
+ * 2. AGY_EXECUTABLE env var
18
+ * 3. Windows PATH lookup for real files: agy.exe, agy.cmd, agy.bat, agy
19
+ * 4. Bare name fallback "agy" (Unix PATH resolution)
20
+ */
21
+ export async function resolveAgyExecutable(options?: {
22
+ runCommandImpl?: CommandRunner;
23
+ cwd?: string;
24
+ configuredPath?: string | null;
25
+ }): Promise<string> {
26
+ if (!options?.configuredPath && !options?.runCommandImpl && cachedExecutable !== null) {
27
+ return cachedExecutable;
28
+ }
29
+
30
+ const result = await resolveExecutable({
31
+ runCommandImpl: options?.runCommandImpl,
32
+ cwd: options?.cwd,
33
+ configuredPath: options?.configuredPath,
34
+ envOverrides: ["AGY_EXECUTABLE"],
35
+ commandNames: process.platform === "win32"
36
+ ? ["agy.exe", "agy.cmd", "agy.bat", "agy"]
37
+ : ["agy"],
38
+ knownPathDirectories: [],
39
+ knownFilePaths: [],
40
+ label: "antigravity",
41
+ allowBareFallback: true,
42
+ });
43
+
44
+ if (!options?.configuredPath && !options?.runCommandImpl) {
45
+ cachedExecutable = result;
46
+ }
47
+ return result;
48
+ }
@@ -101,6 +101,7 @@ export function spawnCodexProcess(
101
101
  options: SpawnOptions,
102
102
  ): ReturnType<typeof spawn> {
103
103
  const spec = buildSpawnSpec(executable, args);
104
+ if (!spec.executable) throw new Error("Codex executable path is empty.");
104
105
  return spawn(spec.executable, spec.args, { ...options, shell: false });
105
106
  }
106
107
 
@@ -21,18 +21,27 @@ export interface ExecutableResolverOptions {
21
21
  requireResolvedFile?: boolean;
22
22
  }
23
23
 
24
- function validateConfiguredExecutable(value: string, label: string, cwd: string): string {
25
- return normalizeExecutableValue(value, {
24
+ function validateConfiguredExecutable(value: string, label: string, cwd: string): string {
25
+ return normalizeExecutableValue(value, {
26
26
  label,
27
27
  cwd,
28
28
  requireExistingPath: /[\\/]/.test(value) || /^[\s"']*[A-Za-z]:/.test(value),
29
29
  allowBareExecutable: true,
30
- });
31
- }
32
-
33
- async function resolveWithWhere(
34
- runCommandImpl: CommandRunner,
35
- cwd: string,
30
+ });
31
+ }
32
+
33
+ function validateResolvedExecutable(value: string, label: string, cwd: string): string {
34
+ return normalizeExecutableValue(value, {
35
+ label,
36
+ cwd,
37
+ requireExistingPath: false,
38
+ allowBareExecutable: true,
39
+ });
40
+ }
41
+
42
+ async function resolveWithWhere(
43
+ runCommandImpl: CommandRunner,
44
+ cwd: string,
36
45
  query: string,
37
46
  requireResolvedFile: boolean,
38
47
  ): Promise<string | null> {
@@ -47,13 +56,19 @@ async function resolveWithWhere(
47
56
  const lines = whereResult.stdout
48
57
  .trim()
49
58
  .split(/[\r\n]+/)
50
- .map((l) => l.trim())
51
- .filter(Boolean);
52
- for (const line of lines) {
53
- if (!requireResolvedFile || existsSync(line)) return line;
54
- }
55
- return null;
56
- }
59
+ .map((l) => l.trim())
60
+ .filter(Boolean);
61
+ for (const line of lines) {
62
+ let candidate: string;
63
+ try {
64
+ candidate = validateResolvedExecutable(line, "where.exe result", cwd);
65
+ } catch {
66
+ continue;
67
+ }
68
+ if (!requireResolvedFile || existsSync(candidate)) return candidate;
69
+ }
70
+ return null;
71
+ }
57
72
 
58
73
  /**
59
74
  * Resolves an executable's location.
@@ -98,41 +113,48 @@ export async function resolveExecutable(options: ExecutableResolverOptions): Pro
98
113
  for (const candidate of options.commandNames) {
99
114
  knownCandidates.push(join(dir, candidate));
100
115
  }
101
- }
102
-
103
- for (const candidate of knownCandidates) {
104
- if (existsSync(candidate)) return candidate;
105
- }
106
- }
107
-
108
- // 5. Explicit known file fallbacks
109
- for (const candidate of options.knownFilePaths ?? []) {
110
- if (existsSync(candidate)) return candidate;
111
- }
116
+ }
117
+
118
+ for (const candidate of knownCandidates) {
119
+ const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
120
+ if (existsSync(validated)) return validated;
121
+ }
122
+ }
123
+
124
+ // 5. Explicit known file fallbacks
125
+ for (const candidate of options.knownFilePaths ?? []) {
126
+ const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
127
+ if (existsSync(validated)) return validated;
128
+ }
112
129
 
113
130
  if (options.allowBareFallback === false) {
114
131
  throw new Error(`${options.label} executable was not found.`);
115
132
  }
116
133
 
117
- // 6. Bare name fallback — prefer the name without an extension (works on Unix).
118
- const bareName = options.commandNames.find((c) => !c.includes(".")) ?? options.commandNames[0];
119
- return bareName!;
120
- }
134
+ // 6. Bare name fallback — prefer the name without an extension (works on Unix).
135
+ const bareName = options.commandNames.find((c) => !c.includes(".")) ?? options.commandNames[0];
136
+ return validateResolvedExecutable(bareName!, `${options.label} executable`, cwd);
137
+ }
121
138
 
122
139
  /**
123
140
  * Builds the spawn spec for a resolved executable.
124
141
  * .cmd and .bat files must be invoked via `cmd.exe /d /s /c` on Windows.
142
+ *
143
+ * `platform` defaults to the host platform; it is injectable so the Windows
144
+ * wrapping branch can be unit-tested from any OS.
125
145
  */
126
- export function buildSpawnSpec(
127
- executable: string,
128
- args: string[],
129
- ): { executable: string; args: string[] } {
130
- if (process.platform === "win32") {
131
- const lower = executable.toLowerCase();
132
- if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
133
- validateWindowsBatchExecutableForCmd(executable, "Windows batch executable");
134
- return { executable: "cmd.exe", args: ["/d", "/s", "/c", "call", executable, ...args] };
135
- }
136
- }
137
- return { executable, args };
138
- }
146
+ export function buildSpawnSpec(
147
+ executable: string,
148
+ args: string[],
149
+ platform: NodeJS.Platform = process.platform,
150
+ ): { executable: string; args: string[] } {
151
+ const validatedExecutable = validateResolvedExecutable(executable, "Executable", process.cwd());
152
+ if (platform === "win32") {
153
+ const lower = validatedExecutable.toLowerCase();
154
+ if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
155
+ validateWindowsBatchExecutableForCmd(validatedExecutable, "Windows batch executable");
156
+ return { executable: "cmd.exe", args: ["/d", "/s", "/c", "call", validatedExecutable, ...args] };
157
+ }
158
+ }
159
+ return { executable: validatedExecutable, args };
160
+ }
@@ -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
  }
@@ -23,10 +23,14 @@ function stripBalancedWrappingQuotes(value: string): string {
23
23
  return value;
24
24
  }
25
25
 
26
- function looksLikePath(value: string): boolean {
27
- return /[\\/]/.test(value) || /^[A-Za-z]:/.test(value);
28
- }
29
-
26
+ function looksLikePath(value: string): boolean {
27
+ return /[\\/]/.test(value) || /^[A-Za-z]:/.test(value);
28
+ }
29
+
30
+ function isWindowsAbsolutePath(value: string): boolean {
31
+ return /^[A-Za-z]:[\\/]/.test(value);
32
+ }
33
+
30
34
  function hasWindowsBatchExtension(value: string): boolean {
31
35
  return /\.(?:cmd|bat)$/i.test(value);
32
36
  }
@@ -67,7 +71,7 @@ export function normalizeExecutableValue(
67
71
  return trimmed;
68
72
  }
69
73
 
70
- const normalized = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd, trimmed);
74
+ const normalized = isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed) ? trimmed : resolve(cwd, trimmed);
71
75
  validateCommonExecutableSyntax(normalized, options.label);
72
76
 
73
77
  if (requireExistingPath && !existsSync(normalized)) {
@@ -1,8 +1,9 @@
1
- import { spawn, type ChildProcess } from "child_process";
2
- import { existsSync } from "fs";
3
- import { buildSpawnSpec } from "../executables/executableResolver.js";
4
- import { normalizeExecutableValue } from "../process/processValidation.js";
5
- import type { ProviderConfig, ProviderLaunchCommand } from "./types.js";
1
+ import { spawn, type ChildProcess } from "child_process";
2
+ import { accessSync, constants, existsSync } from "fs";
3
+ import { delimiter, join } from "path";
4
+ import { buildSpawnSpec } from "../executables/executableResolver.js";
5
+ import { normalizeExecutableValue } from "../process/processValidation.js";
6
+ import type { ProviderConfig, ProviderLaunchCommand } from "./types.js";
6
7
 
7
8
  export interface ProviderLaunchSpec {
8
9
  executable: string;
@@ -75,43 +76,55 @@ function setRawMode(stdin: RawModeStream | null | undefined, enabled: boolean):
75
76
  }
76
77
  }
77
78
 
78
- function executableLooksLikePath(executable: string): boolean {
79
- return /[\\/]/.test(executable) || /^[A-Za-z]:/.test(executable);
80
- }
81
-
82
- // Probe-spawns the executable to check if it exits cleanly — used instead of `which`/`where`
83
- // because path-lookup alone doesn't verify the file is actually runnable.
84
- function captureProcessExit(executable: string, args: string[]): Promise<boolean> {
85
- return new Promise((resolve) => {
86
- let child: ChildProcess;
87
- try {
88
- child = spawn(executable, args, {
89
- stdio: "ignore",
90
- shell: false,
91
- });
92
- } catch {
93
- resolve(false);
94
- return;
95
- }
96
-
97
- child.once("error", () => resolve(false));
98
- child.once("close", (exitCode) => resolve(exitCode === 0));
99
- });
100
- }
101
-
102
- export async function commandExistsOnPath(executable: string): Promise<boolean> {
103
- const trimmed = executable.trim();
104
- if (!trimmed) return false;
105
- if (executableLooksLikePath(trimmed)) {
106
- return existsSync(trimmed);
107
- }
108
-
109
- if (process.platform === "win32") {
110
- return captureProcessExit("where.exe", [trimmed]);
111
- }
112
-
113
- return captureProcessExit("sh", ["-c", `command -v "$1" >/dev/null 2>&1`, "sh", trimmed]);
114
- }
79
+ function executableLooksLikePath(executable: string): boolean {
80
+ return /[\\/]/.test(executable) || /^[A-Za-z]:/.test(executable);
81
+ }
82
+
83
+ function canAccessExecutable(path: string): boolean {
84
+ try {
85
+ if (process.platform === "win32") {
86
+ return existsSync(path);
87
+ }
88
+ accessSync(path, constants.X_OK);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ function getPathExecutableCandidates(executable: string): string[] {
96
+ if (process.platform !== "win32" || /\.[A-Za-z0-9_-]+$/.test(executable)) {
97
+ return [executable];
98
+ }
99
+
100
+ const pathExt = process.env.PATHEXT?.split(";").filter(Boolean) ?? [".COM", ".EXE", ".BAT", ".CMD"];
101
+ return [executable, ...pathExt.map((ext) => `${executable}${ext.toLowerCase()}`)];
102
+ }
103
+
104
+ export async function commandExistsOnPath(executable: string): Promise<boolean> {
105
+ let candidate: string;
106
+ try {
107
+ candidate = normalizeExecutableValue(executable, {
108
+ label: "Provider launch command",
109
+ allowBareExecutable: true,
110
+ });
111
+ } catch {
112
+ return false;
113
+ }
114
+
115
+ const trimmed = candidate.trim();
116
+ if (executableLooksLikePath(trimmed)) {
117
+ return canAccessExecutable(trimmed);
118
+ }
119
+
120
+ const pathEntries = process.env.PATH?.split(delimiter).filter(Boolean) ?? [];
121
+ for (const dir of pathEntries) {
122
+ for (const name of getPathExecutableCandidates(trimmed)) {
123
+ if (canAccessExecutable(join(dir, name))) return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
115
128
 
116
129
  function formatSpawnError(provider: ProviderConfig, executable: string, error: NodeJS.ErrnoException): ProviderLaunchResult {
117
130
  if (error.code === "ENOENT") {
@@ -162,6 +175,10 @@ export async function launchProviderCli(
162
175
  let child: ChildProcess;
163
176
  try {
164
177
  const spawnSpec = buildSpawnSpec(spec.executable, spec.args);
178
+ if (!spawnSpec.executable) {
179
+ resolve({ status: "spawn-error", message: "Executable path is empty after resolution." });
180
+ return;
181
+ }
165
182
  child = spawnImpl(spawnSpec.executable, spawnSpec.args, {
166
183
  cwd: spec.cwd,
167
184
  shell: false,
@@ -14,12 +14,16 @@ 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 { setLocalProviderConfig } from "../providerRuntime/local.js";
19
20
  import { formatContextLength, resolveModelContextLengthCached } from "../providerRuntime/contextMetadata.js";
20
21
  import { resolveModelCapabilityProfileCached } from "../providerRuntime/capabilityProfile.js";
21
22
 
22
- const PROVIDER_ORDER: readonly ProviderId[] = ["openai", "anthropic", "google", "local"];
23
+ // Google/Gemini remains a recognized legacy config value so existing workspace
24
+ // 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"];
23
27
 
24
28
  const DEFAULT_PROVIDER_ID: ProviderId = "openai";
25
29
 
@@ -63,8 +67,8 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
63
67
  isActiveRoute: false,
64
68
  routeUnavailableReason: null,
65
69
  },
66
- local: {
67
- id: "local",
70
+ local: {
71
+ id: "local",
68
72
  displayName: "Local",
69
73
  currentModel: () => "Local default",
70
74
  backendType: "local-openai-compatible",
@@ -72,12 +76,23 @@ const DEFAULT_PROVIDERS: Record<ProviderId, ProviderDefault> = {
72
76
  enabled: false,
73
77
  launchCommand: null,
74
78
  isActiveRoute: false,
75
- routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
76
- },
77
- };
79
+ routeUnavailableReason: "Local provider unavailable. Start LM Studio, load a model, and enable the local server.",
80
+ },
81
+ antigravity: {
82
+ id: "antigravity",
83
+ displayName: "Antigravity",
84
+ currentModel: () => ANTIGRAVITY_DEFAULT_MODEL_ID,
85
+ backendType: "antigravity-cli-auth",
86
+ routeMode: "in-codexa",
87
+ enabled: true,
88
+ launchCommand: { executable: "agy", args: [] },
89
+ isActiveRoute: false,
90
+ routeUnavailableReason: null,
91
+ },
92
+ };
78
93
 
79
- function isProviderId(value: unknown): value is ProviderId {
80
- return typeof value === "string" && PROVIDER_ORDER.includes(value as ProviderId);
94
+ function isProviderId(value: unknown): value is ProviderId {
95
+ return typeof value === "string" && KNOWN_PROVIDER_IDS.includes(value as ProviderId);
81
96
  }
82
97
 
83
98
  function normalizeLaunchCommand(value: ProviderWorkspaceOverride["command"] | undefined): ProviderLaunchCommand | null | undefined {
@@ -128,13 +143,14 @@ function applyOverride(
128
143
  };
129
144
  }
130
145
 
131
- export function getDefaultProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
132
- return isProviderId(config?.workspaceDefaultProviderId) ? config.workspaceDefaultProviderId : DEFAULT_PROVIDER_ID;
133
- }
146
+ export function getDefaultProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
147
+ const providerId = config?.workspaceDefaultProviderId;
148
+ return isProviderId(providerId) && providerId !== "google" ? providerId : DEFAULT_PROVIDER_ID;
149
+ }
134
150
 
135
151
  export function getActiveRouteProviderId(config: ProviderWorkspaceConfig | null | undefined): ProviderId {
136
152
  const providerId = config?.activeRoute?.providerId;
137
- return isProviderId(providerId) && isProviderRoutableInCodexa(providerId)
153
+ return isProviderId(providerId) && providerId !== "google" && isProviderRoutableInCodexa(providerId)
138
154
  ? providerId
139
155
  : DEFAULT_PROVIDER_ID;
140
156
  }
@@ -1,10 +1,11 @@
1
- export type ProviderId = "openai" | "anthropic" | "google" | "local";
1
+ export type ProviderId = "openai" | "anthropic" | "google" | "local" | "antigravity";
2
2
 
3
3
  export type ProviderBackendType =
4
- | "codex-cli-auth"
5
- | "gemini-cli-auth"
6
- | "claude-code-auth"
7
- | "openai-api-key"
4
+ | "codex-cli-auth"
5
+ | "gemini-cli-auth"
6
+ | "claude-code-auth"
7
+ | "antigravity-cli-auth"
8
+ | "openai-api-key"
8
9
  | "gemini-api-key"
9
10
  | "anthropic-api-key"
10
11
  | "local-openai-compatible"
@@ -69,10 +70,11 @@ export interface ProviderWorkspaceOverride {
69
70
  defaultModel?: string;
70
71
  models?: Record<string, ProviderModelWorkspaceOverride>;
71
72
  command?: string | ProviderLaunchCommand | null;
72
- claudeCommandPath?: string;
73
- geminiCommandPath?: string;
74
- codexCommandPath?: string;
75
- }
73
+ claudeCommandPath?: string;
74
+ geminiCommandPath?: string;
75
+ codexCommandPath?: string;
76
+ antigravityCommandPath?: string;
77
+ }
76
78
 
77
79
  export interface ProviderModelWorkspaceOverride {
78
80
  contextLength?: number;
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import { DEFAULT_MODEL } from "../../config/settings.js";
4
- import { normalizeWorkspaceRoot } from "../workspaceRoot.js";
4
+ import { normalizeWorkspaceRoot } from "../workspace/workspaceRoot.js";
5
5
  import { isKnownProviderId } from "./registry.js";
6
6
  import { getDefaultRouteModel, getProviderRuntime, isProviderRouteConfigured, isProviderRoutableInCodexa } from "../providerRuntime/registry.js";
7
7
  import { normalizeGeminiModelId } from "../providerRuntime/models.js";
@@ -13,8 +13,9 @@ import type {
13
13
  ProviderWorkspaceOverride,
14
14
  } from "./types.js";
15
15
 
16
- const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
- const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
16
+ const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
+ const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
18
+ const DEPRECATED_GOOGLE_PROVIDER_ID = "google";
18
19
 
19
20
  export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
20
21
  return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
@@ -28,16 +29,20 @@ function isDeprecatedAntigravityProviderId(value: unknown): boolean {
28
29
  return value === DEPRECATED_ANTIGRAVITY_PROVIDER_ID;
29
30
  }
30
31
 
31
- function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
+ function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
33
  if (!isRecord(value)) return false;
33
34
  return isDeprecatedAntigravityProviderId(value.providerId ?? value.provider_id)
34
35
  || DEPRECATED_ANTIGRAVITY_BACKENDS.has(String(value.backendKind ?? value.backend_kind));
35
- }
36
+ }
37
+
38
+ function isDeprecatedGoogleRoute(value: unknown): boolean {
39
+ return isRecord(value) && (value.providerId ?? value.provider_id) === DEPRECATED_GOOGLE_PROVIDER_ID;
40
+ }
36
41
 
37
42
  function resolveDeprecatedProviderFallback(
38
43
  providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>>,
39
44
  ): ProviderId {
40
- const candidates: readonly ProviderId[] = ["openai", "google", "anthropic", "local"];
45
+ const candidates: readonly ProviderId[] = ["openai", "anthropic", "local"];
41
46
  return candidates.find((providerId) =>
42
47
  isProviderRoutableInCodexa(providerId)
43
48
  && (providerId === "openai" || providers[providerId] !== undefined || isProviderRouteConfigured(providerId))
@@ -214,22 +219,24 @@ function parseActiveRoute(value: unknown): ProviderActiveRoute | undefined {
214
219
  export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceConfig {
215
220
  if (!isRecord(data)) return {};
216
221
 
217
- const config: ProviderWorkspaceConfig = {};
218
- const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
219
- let foundDeprecatedAntigravity = false;
222
+ const config: ProviderWorkspaceConfig = {};
223
+ const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
224
+ let foundDeprecatedAntigravity = false;
225
+ let foundDeprecatedGoogle = false;
220
226
 
221
227
  if (isRecord(data.providers)) {
222
228
  for (const [id, value] of Object.entries(data.providers)) {
223
- if (isDeprecatedAntigravityProviderId(id)) {
224
- foundDeprecatedAntigravity = true;
225
- continue;
226
- }
227
- if (!isKnownProviderId(id)) continue;
228
- const override = parseProviderOverride(value);
229
- if (id === "google" && override?.currentModel) {
230
- override.currentModel = normalizeGeminiModelId(override.currentModel);
231
- }
232
- if (override) providers[id] = override;
229
+ if (isDeprecatedAntigravityProviderId(id)) {
230
+ foundDeprecatedAntigravity = true;
231
+ continue;
232
+ }
233
+ if (id === DEPRECATED_GOOGLE_PROVIDER_ID) {
234
+ foundDeprecatedGoogle = true;
235
+ continue;
236
+ }
237
+ if (!isKnownProviderId(id)) continue;
238
+ const override = parseProviderOverride(value);
239
+ if (override) providers[id] = override;
233
240
  }
234
241
  config.providers = providers;
235
242
  }
@@ -238,7 +245,10 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
238
245
  ?? data.workspace_default_provider_id
239
246
  ?? data.defaultProviderId
240
247
  ?? data.default_provider_id;
241
- if (isDeprecatedAntigravityProviderId(defaultProvider)) {
248
+ if (defaultProvider === DEPRECATED_GOOGLE_PROVIDER_ID) {
249
+ foundDeprecatedGoogle = true;
250
+ config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
251
+ } else if (isDeprecatedAntigravityProviderId(defaultProvider)) {
242
252
  foundDeprecatedAntigravity = true;
243
253
  config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
244
254
  } else if (typeof defaultProvider === "string" && isKnownProviderId(defaultProvider)) {
@@ -246,7 +256,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
246
256
  }
247
257
 
248
258
  const rawActiveRoute = data.activeRoute ?? data.active_route;
249
- if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
259
+ if (isDeprecatedGoogleRoute(rawActiveRoute)) {
260
+ foundDeprecatedGoogle = true;
261
+ const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
262
+ config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
263
+ config.workspaceDefaultProviderId ??= fallbackProviderId;
264
+ } else if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
250
265
  foundDeprecatedAntigravity = true;
251
266
  const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
252
267
  config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
@@ -258,12 +273,12 @@ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceCo
258
273
  }
259
274
  }
260
275
 
261
- if (foundDeprecatedAntigravity) {
276
+ if (foundDeprecatedGoogle || foundDeprecatedAntigravity) {
262
277
  const revertedProviderId = config.activeRoute?.providerId
263
278
  ?? config.workspaceDefaultProviderId
264
279
  ?? resolveDeprecatedProviderFallback(providers);
265
280
  config.migrationNotice = {
266
- deprecatedProviderId: DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
281
+ deprecatedProviderId: foundDeprecatedGoogle ? DEPRECATED_GOOGLE_PROVIDER_ID : DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
267
282
  revertedProviderId,
268
283
  };
269
284
  }
@@ -348,13 +363,13 @@ export function saveProviderWorkspaceConfig(workspaceRoot: string, config: Provi
348
363
  renameSync(tmpFile, filePath);
349
364
  }
350
365
 
351
- export function setProviderWorkspaceDefault(
366
+ export function setProviderWorkspaceDefault(
352
367
  config: ProviderWorkspaceConfig,
353
368
  providerId: ProviderId,
354
369
  ): ProviderWorkspaceConfig {
355
370
  return {
356
371
  ...config,
357
- workspaceDefaultProviderId: providerId,
372
+ workspaceDefaultProviderId: providerId === DEPRECATED_GOOGLE_PROVIDER_ID ? "openai" : providerId,
358
373
  };
359
374
  }
360
375
 
@@ -396,7 +411,7 @@ export function setProviderActiveRoute(
396
411
  config: ProviderWorkspaceConfig,
397
412
  activeRoute: ProviderActiveRoute,
398
413
  ): ProviderWorkspaceConfig {
399
- if (!isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
414
+ if (activeRoute.providerId === DEPRECATED_GOOGLE_PROVIDER_ID || !isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
400
415
  return config;
401
416
  }
402
417