@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,407 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { DEFAULT_MODEL } from "../../config/settings.js";
4
+ import { normalizeWorkspaceRoot } from "../workspaceRoot.js";
5
+ import { isKnownProviderId } from "./registry.js";
6
+ import { getDefaultRouteModel, getProviderRuntime, isProviderRouteConfigured, isProviderRoutableInCodexa } from "../providerRuntime/registry.js";
7
+ import { normalizeGeminiModelId } from "../providerRuntime/models.js";
8
+ import type {
9
+ ProviderActiveRoute,
10
+ ProviderId,
11
+ ProviderLaunchCommand,
12
+ ProviderWorkspaceConfig,
13
+ ProviderWorkspaceOverride,
14
+ } from "./types.js";
15
+
16
+ const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
+ const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
18
+
19
+ export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
20
+ return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
21
+ }
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function isDeprecatedAntigravityProviderId(value: unknown): boolean {
28
+ return value === DEPRECATED_ANTIGRAVITY_PROVIDER_ID;
29
+ }
30
+
31
+ function isDeprecatedAntigravityRoute(value: unknown): boolean {
32
+ if (!isRecord(value)) return false;
33
+ return isDeprecatedAntigravityProviderId(value.providerId ?? value.provider_id)
34
+ || DEPRECATED_ANTIGRAVITY_BACKENDS.has(String(value.backendKind ?? value.backend_kind));
35
+ }
36
+
37
+ function resolveDeprecatedProviderFallback(
38
+ providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>>,
39
+ ): ProviderId {
40
+ const candidates: readonly ProviderId[] = ["openai", "google", "anthropic", "local"];
41
+ return candidates.find((providerId) =>
42
+ isProviderRoutableInCodexa(providerId)
43
+ && (providerId === "openai" || providers[providerId] !== undefined || isProviderRouteConfigured(providerId))
44
+ ) ?? "openai";
45
+ }
46
+
47
+ function createFallbackActiveRoute(
48
+ providerId: ProviderId,
49
+ providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>>,
50
+ ): ProviderActiveRoute {
51
+ const override = providers[providerId];
52
+ const modelId = providerId === "openai"
53
+ ? override?.currentModel ?? DEFAULT_MODEL
54
+ : providerId === "google"
55
+ ? normalizeGeminiModelId(override?.currentModel ?? getDefaultRouteModel(providerId, DEFAULT_MODEL))
56
+ : override?.currentModel ?? getDefaultRouteModel(providerId, DEFAULT_MODEL);
57
+
58
+ return {
59
+ providerId,
60
+ modelId,
61
+ backendKind: getProviderRuntime(providerId).backendKind,
62
+ ...(override?.currentReasoning ? { reasoning: override.currentReasoning } : {}),
63
+ ...(providerId === "google" ? { modelSelection: { kind: "manual" as const, modelId } } : {}),
64
+ };
65
+ }
66
+
67
+ function parseLaunchCommand(value: unknown): ProviderWorkspaceOverride["command"] | undefined {
68
+ if (value === null) return null;
69
+ if (typeof value === "string") return value;
70
+ if (!isRecord(value)) return undefined;
71
+ if (typeof value.executable !== "string") return undefined;
72
+ return {
73
+ executable: value.executable,
74
+ args: Array.isArray(value.args)
75
+ ? value.args.filter((arg): arg is string => typeof arg === "string")
76
+ : [],
77
+ };
78
+ }
79
+
80
+ function parseProviderOverride(value: unknown): ProviderWorkspaceOverride | undefined {
81
+ if (!isRecord(value)) return undefined;
82
+ const override: ProviderWorkspaceOverride = {};
83
+
84
+ // Accept both camelCase and snake_case field names for compatibility with
85
+ // config files written by different tool versions.
86
+ if (typeof value.currentModel === "string") {
87
+ override.currentModel = value.currentModel;
88
+ } else if (typeof value.current_model === "string") {
89
+ override.currentModel = value.current_model;
90
+ }
91
+
92
+ if (typeof value.currentReasoning === "string") {
93
+ override.currentReasoning = value.currentReasoning;
94
+ } else if (typeof value.current_reasoning === "string") {
95
+ override.currentReasoning = value.current_reasoning;
96
+ }
97
+
98
+ if (typeof value.enabled === "boolean") {
99
+ override.enabled = value.enabled;
100
+ }
101
+
102
+ const providerType = value.type;
103
+ if (providerType === "openai-compatible") {
104
+ override.type = providerType;
105
+ }
106
+
107
+ const baseUrl = value.baseUrl ?? value.base_url;
108
+ if (typeof baseUrl === "string" && baseUrl.trim()) {
109
+ override.baseUrl = baseUrl.trim();
110
+ }
111
+
112
+ const apiKey = value.apiKey ?? value.api_key;
113
+ if (typeof apiKey === "string" && apiKey.trim()) {
114
+ override.apiKey = apiKey.trim();
115
+ }
116
+
117
+ const pinnedModel = value.pinnedModel ?? value.pinned_model;
118
+ if (typeof pinnedModel === "string" && pinnedModel.trim()) {
119
+ override.pinnedModel = pinnedModel.trim();
120
+ }
121
+
122
+ const defaultModel = value.defaultModel ?? value.default_model;
123
+ if (typeof defaultModel === "string" && defaultModel.trim()) {
124
+ override.defaultModel = defaultModel.trim();
125
+ }
126
+
127
+ if (isRecord(value.models)) {
128
+ const models: Record<string, import("./types.js").ProviderModelWorkspaceOverride> = {};
129
+ for (const [modelId, modelValue] of Object.entries(value.models)) {
130
+ if (!modelId.trim() || !isRecord(modelValue)) continue;
131
+ const entry: import("./types.js").ProviderModelWorkspaceOverride = {};
132
+
133
+ const rawContextLength = modelValue.contextLength ?? modelValue.context_length;
134
+ if (typeof rawContextLength === "number" && Number.isInteger(rawContextLength) && rawContextLength > 0) {
135
+ entry.contextLength = rawContextLength;
136
+ }
137
+
138
+ const rawMaxOutput = modelValue.maxOutputTokens ?? modelValue.max_output_tokens;
139
+ if (typeof rawMaxOutput === "number" && Number.isInteger(rawMaxOutput) && rawMaxOutput > 0) {
140
+ entry.maxOutputTokens = rawMaxOutput;
141
+ }
142
+
143
+ for (const [camelKey, snakeKey] of [
144
+ ["supportsStreaming", "supports_streaming"],
145
+ ["supportsToolCalls", "supports_tool_calls"],
146
+ ["supportsSystemPrompt", "supports_system_prompt"],
147
+ ["supportsVision", "supports_vision"],
148
+ ] as const) {
149
+ const raw = modelValue[camelKey] ?? modelValue[snakeKey];
150
+ if (typeof raw === "boolean") {
151
+ (entry as Record<string, unknown>)[camelKey] = raw;
152
+ }
153
+ }
154
+
155
+ if (Object.keys(entry).length > 0) {
156
+ models[modelId] = entry;
157
+ }
158
+ }
159
+ if (Object.keys(models).length > 0) {
160
+ override.models = models;
161
+ }
162
+ }
163
+
164
+ const command = parseLaunchCommand(value.command);
165
+ if (command !== undefined) {
166
+ override.command = command;
167
+ }
168
+
169
+ const claudeCommandPath = value.claudeCommandPath ?? value.claude_command_path;
170
+ if (typeof claudeCommandPath === "string" && claudeCommandPath.trim()) {
171
+ override.claudeCommandPath = claudeCommandPath.trim();
172
+ }
173
+
174
+ const geminiCommandPath = value.geminiCommandPath ?? value.gemini_command_path;
175
+ if (typeof geminiCommandPath === "string" && geminiCommandPath.trim()) {
176
+ override.geminiCommandPath = geminiCommandPath.trim();
177
+ }
178
+
179
+ const codexCommandPath = value.codexCommandPath ?? value.codex_command_path;
180
+ if (typeof codexCommandPath === "string" && codexCommandPath.trim()) {
181
+ override.codexCommandPath = codexCommandPath.trim();
182
+ }
183
+
184
+ return override;
185
+ }
186
+
187
+ function parseActiveRoute(value: unknown): ProviderActiveRoute | undefined {
188
+ if (!isRecord(value)) return undefined;
189
+ const providerId = value.providerId ?? value.provider_id;
190
+ const modelId = value.modelId ?? value.model_id;
191
+ const backendKind = value.backendKind ?? value.backend_kind;
192
+ const reasoning = value.reasoning;
193
+ const modelSelection = value.modelSelection ?? value.model_selection;
194
+
195
+ if (typeof providerId !== "string" || !isKnownProviderId(providerId) || !isProviderRoutableInCodexa(providerId)) return undefined;
196
+ if (typeof modelId !== "string" || !modelId.trim()) return undefined;
197
+
198
+ const normalizedModelId = providerId === "google" ? normalizeGeminiModelId(modelId.trim()) : modelId.trim();
199
+ const normalizedModelSelection = providerId === "google" && isRecord(modelSelection)
200
+ ? modelSelection.kind === "manual"
201
+ ? { kind: "manual" as const, modelId: normalizeGeminiModelId(typeof modelSelection.modelId === "string" ? modelSelection.modelId : null) }
202
+ : { kind: "auto" as const, family: modelSelection.family === "gemini-2.5" ? "gemini-2.5" as const : "gemini-3" as const }
203
+ : undefined;
204
+
205
+ return {
206
+ providerId,
207
+ modelId: normalizedModelId,
208
+ backendKind: getProviderRuntime(providerId).backendKind,
209
+ ...(typeof reasoning === "string" && reasoning.trim() ? { reasoning: reasoning.trim() } : {}),
210
+ ...(normalizedModelSelection ? { modelSelection: normalizedModelSelection } : {}),
211
+ };
212
+ }
213
+
214
+ export function parseProviderWorkspaceConfig(data: unknown): ProviderWorkspaceConfig {
215
+ if (!isRecord(data)) return {};
216
+
217
+ const config: ProviderWorkspaceConfig = {};
218
+ const providers: Partial<Record<ProviderId, ProviderWorkspaceOverride>> = {};
219
+ let foundDeprecatedAntigravity = false;
220
+
221
+ if (isRecord(data.providers)) {
222
+ 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;
233
+ }
234
+ config.providers = providers;
235
+ }
236
+
237
+ const defaultProvider = data.workspaceDefaultProviderId
238
+ ?? data.workspace_default_provider_id
239
+ ?? data.defaultProviderId
240
+ ?? data.default_provider_id;
241
+ if (isDeprecatedAntigravityProviderId(defaultProvider)) {
242
+ foundDeprecatedAntigravity = true;
243
+ config.workspaceDefaultProviderId = resolveDeprecatedProviderFallback(providers);
244
+ } else if (typeof defaultProvider === "string" && isKnownProviderId(defaultProvider)) {
245
+ config.workspaceDefaultProviderId = defaultProvider;
246
+ }
247
+
248
+ const rawActiveRoute = data.activeRoute ?? data.active_route;
249
+ if (isDeprecatedAntigravityRoute(rawActiveRoute)) {
250
+ foundDeprecatedAntigravity = true;
251
+ const fallbackProviderId = resolveDeprecatedProviderFallback(providers);
252
+ config.activeRoute = createFallbackActiveRoute(fallbackProviderId, providers);
253
+ config.workspaceDefaultProviderId ??= fallbackProviderId;
254
+ } else {
255
+ const activeRoute = parseActiveRoute(rawActiveRoute);
256
+ if (activeRoute) {
257
+ config.activeRoute = activeRoute;
258
+ }
259
+ }
260
+
261
+ if (foundDeprecatedAntigravity) {
262
+ const revertedProviderId = config.activeRoute?.providerId
263
+ ?? config.workspaceDefaultProviderId
264
+ ?? resolveDeprecatedProviderFallback(providers);
265
+ config.migrationNotice = {
266
+ deprecatedProviderId: DEPRECATED_ANTIGRAVITY_PROVIDER_ID,
267
+ revertedProviderId,
268
+ };
269
+ }
270
+
271
+ return config;
272
+ }
273
+
274
+ function serializeLaunchCommand(command: string | ProviderLaunchCommand | null | undefined): unknown {
275
+ if (command === undefined || command === null || typeof command === "string") {
276
+ return command;
277
+ }
278
+ return {
279
+ executable: command.executable,
280
+ args: command.args,
281
+ };
282
+ }
283
+
284
+ export function serializeProviderWorkspaceConfig(config: ProviderWorkspaceConfig): Record<string, unknown> {
285
+ const providers = Object.fromEntries(
286
+ Object.entries(config.providers ?? {}).map(([id, override]) => [
287
+ id,
288
+ {
289
+ ...(override.currentModel !== undefined ? { current_model: override.currentModel } : {}),
290
+ ...(override.currentReasoning !== undefined ? { current_reasoning: override.currentReasoning } : {}),
291
+ ...(override.enabled !== undefined ? { enabled: override.enabled } : {}),
292
+ ...(override.type !== undefined ? { type: override.type } : {}),
293
+ ...(override.baseUrl !== undefined ? { base_url: override.baseUrl } : {}),
294
+ ...(override.apiKey !== undefined ? { api_key: override.apiKey } : {}),
295
+ ...(override.pinnedModel !== undefined ? { pinned_model: override.pinnedModel } : {}),
296
+ ...(override.defaultModel !== undefined ? { default_model: override.defaultModel } : {}),
297
+ ...(override.models !== undefined ? { models: Object.fromEntries(
298
+ Object.entries(override.models).map(([modelId, model]) => [
299
+ modelId,
300
+ {
301
+ ...(model.contextLength !== undefined ? { contextLength: model.contextLength } : {}),
302
+ ...(model.maxOutputTokens !== undefined ? { maxOutputTokens: model.maxOutputTokens } : {}),
303
+ ...(model.supportsStreaming !== undefined ? { supportsStreaming: model.supportsStreaming } : {}),
304
+ ...(model.supportsToolCalls !== undefined ? { supportsToolCalls: model.supportsToolCalls } : {}),
305
+ ...(model.supportsSystemPrompt !== undefined ? { supportsSystemPrompt: model.supportsSystemPrompt } : {}),
306
+ ...(model.supportsVision !== undefined ? { supportsVision: model.supportsVision } : {}),
307
+ },
308
+ ]),
309
+ ) } : {}),
310
+ ...(override.command !== undefined ? { command: serializeLaunchCommand(override.command) } : {}),
311
+ ...(override.claudeCommandPath !== undefined ? { claude_command_path: override.claudeCommandPath } : {}),
312
+ ...(override.geminiCommandPath !== undefined ? { gemini_command_path: override.geminiCommandPath } : {}),
313
+ ...(override.codexCommandPath !== undefined ? { codex_command_path: override.codexCommandPath } : {}),
314
+ },
315
+ ]),
316
+ );
317
+
318
+ return {
319
+ ...(config.workspaceDefaultProviderId ? { workspaceDefaultProviderId: config.workspaceDefaultProviderId } : {}),
320
+ ...(config.activeRoute ? {
321
+ activeRoute: {
322
+ providerId: config.activeRoute.providerId,
323
+ modelId: config.activeRoute.modelId,
324
+ backendKind: config.activeRoute.backendKind ?? getProviderRuntime(config.activeRoute.providerId).backendKind,
325
+ ...(config.activeRoute.reasoning ? { reasoning: config.activeRoute.reasoning } : {}),
326
+ ...(config.activeRoute.modelSelection ? { modelSelection: config.activeRoute.modelSelection } : {}),
327
+ },
328
+ } : {}),
329
+ ...(Object.keys(providers).length > 0 ? { providers } : {}),
330
+ };
331
+ }
332
+
333
+ export function loadProviderWorkspaceConfig(workspaceRoot: string): ProviderWorkspaceConfig {
334
+ const filePath = getProviderWorkspaceConfigFile(workspaceRoot);
335
+ if (!existsSync(filePath)) return {};
336
+ try {
337
+ return parseProviderWorkspaceConfig(JSON.parse(readFileSync(filePath, "utf-8")));
338
+ } catch {
339
+ return {};
340
+ }
341
+ }
342
+
343
+ export function saveProviderWorkspaceConfig(workspaceRoot: string, config: ProviderWorkspaceConfig): void {
344
+ const filePath = getProviderWorkspaceConfigFile(workspaceRoot);
345
+ mkdirSync(dirname(filePath), { recursive: true });
346
+ const tmpFile = `${filePath}.tmp`;
347
+ writeFileSync(tmpFile, JSON.stringify(serializeProviderWorkspaceConfig(config), null, 2), "utf-8");
348
+ renameSync(tmpFile, filePath);
349
+ }
350
+
351
+ export function setProviderWorkspaceDefault(
352
+ config: ProviderWorkspaceConfig,
353
+ providerId: ProviderId,
354
+ ): ProviderWorkspaceConfig {
355
+ return {
356
+ ...config,
357
+ workspaceDefaultProviderId: providerId,
358
+ };
359
+ }
360
+
361
+ export function setProviderDefaultModel(
362
+ config: ProviderWorkspaceConfig,
363
+ providerId: ProviderId,
364
+ modelId: string,
365
+ ): ProviderWorkspaceConfig {
366
+ return {
367
+ ...config,
368
+ providers: {
369
+ ...config.providers,
370
+ [providerId]: {
371
+ ...config.providers?.[providerId],
372
+ currentModel: modelId,
373
+ },
374
+ },
375
+ };
376
+ }
377
+
378
+ export function setProviderDefaultReasoning(
379
+ config: ProviderWorkspaceConfig,
380
+ providerId: ProviderId,
381
+ reasoning: string,
382
+ ): ProviderWorkspaceConfig {
383
+ return {
384
+ ...config,
385
+ providers: {
386
+ ...config.providers,
387
+ [providerId]: {
388
+ ...config.providers?.[providerId],
389
+ currentReasoning: reasoning,
390
+ },
391
+ },
392
+ };
393
+ }
394
+
395
+ export function setProviderActiveRoute(
396
+ config: ProviderWorkspaceConfig,
397
+ activeRoute: ProviderActiveRoute,
398
+ ): ProviderWorkspaceConfig {
399
+ if (!isProviderRoutableInCodexa(activeRoute.providerId) || !isProviderRouteConfigured(activeRoute.providerId)) {
400
+ return config;
401
+ }
402
+
403
+ return {
404
+ ...config,
405
+ activeRoute,
406
+ };
407
+ }