@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,784 @@
1
+ import { appendFileSync } from "fs";
2
+ import { runCommand, type CommandResult, type CommandStreamHandlers } from "../process/CommandRunner.js";
3
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
4
+ import type { BackendRunHandlers } from "../providers/types.js";
5
+ import { GEMINI_DEFAULT_MODEL_ID, GEMINI_FALLBACK_MODELS, normalizeGeminiModelId } from "./models.js";
6
+ import type { ProviderBackendKind, ProviderChatRequest, ProviderRouteValidationResult, ProviderRuntime, ResolvedRuntimeConfig } from "./types.js";
7
+ import { resolveGeminiExecutable } from "../executables/geminiExecutable.js";
8
+
9
+ // ─── Diagnostics ─────────────────────────────────────────────────────────────
10
+
11
+ const GEMINI_DIAG_LOG = `${process.env.TEMP ?? process.env.TMPDIR ?? "/tmp"}/codexa-gemini-diag.log`;
12
+ function isGeminiDiagEnabled(): boolean {
13
+ return process.env.CODEXA_GEMINI_DEBUG === "1";
14
+ }
15
+
16
+ function diagLog(msg: string): void {
17
+ if (!isGeminiDiagEnabled()) return;
18
+ try { appendFileSync(GEMINI_DIAG_LOG, `[${new Date().toISOString()}] ${msg}\n`); } catch { /* ignore */ }
19
+ }
20
+
21
+ const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
22
+ const GEMINI_TIMEOUT_MS = Number(process.env.CODEXA_GEMINI_TIMEOUT_MS?.trim()) || 120_000;
23
+ const GEMINI_ROUTE_VALIDATION_TIMEOUT_MS = 30_000;
24
+ const GEMINI_READY_PROMPT = "Respond with READY only.";
25
+ const GEMINI_REASONING_UNSUPPORTED_DIAGNOSTIC = "Gemini reasoning control is not supported by this CLI version.";
26
+ export const GEMINI_ROUTE_SETUP_MESSAGE = "Google/Gemini is not configured for in-Codexa routing yet. Sign in with Gemini CLI headless auth or set GEMINI_API_KEY / GOOGLE_API_KEY.";
27
+
28
+ type CommandRunner = typeof runCommand;
29
+ export type GeminiApprovalMode = "default" | "plan" | "auto_edit" | "yolo";
30
+ export type GeminiOutputFormat = "text" | "json" | "stream-json";
31
+ export type GeminiCommandMode = "readiness" | "prompt";
32
+ type GeminiExtractionStatus = "assistant-text" | "completed-empty-assistant" | "not-completed";
33
+
34
+ export interface GeminiCommandSpec {
35
+ file: string;
36
+ args: string[];
37
+ cwd: string;
38
+ mode: GeminiCommandMode;
39
+ model?: string;
40
+ reasoning?: string;
41
+ approvalMode: GeminiApprovalMode;
42
+ outputFormat: GeminiOutputFormat;
43
+ includesPolicy: boolean;
44
+ }
45
+
46
+ interface GeminiPromptRunDiagnostics {
47
+ command: GeminiCommandSpec;
48
+ redactedArgs: string[];
49
+ exitCode: number | null;
50
+ signal: NodeJS.Signals | null;
51
+ status: CommandResult["status"];
52
+ stdoutSnippet: string;
53
+ stderrSnippet: string;
54
+ extractionStatus: GeminiExtractionStatus;
55
+ parseMode: "plain-text";
56
+ parsedEvents: number;
57
+ parsedMessages: number;
58
+ finalAssistantTextLength: number;
59
+ finalAssistantTextPreview: string;
60
+ stderrWarningTextPresent: boolean;
61
+ }
62
+
63
+ let geminiCliHeadlessValidated = false;
64
+ let resolvedGeminiCommand: string | null = null;
65
+ let lastPromptDiagnostics: GeminiPromptRunDiagnostics | null = null;
66
+
67
+ function getGeminiApiKey(env: NodeJS.ProcessEnv = process.env): string | null {
68
+ return env.GEMINI_API_KEY?.trim() || env.GOOGLE_API_KEY?.trim() || null;
69
+ }
70
+
71
+ export function hasGeminiApiKey(env: NodeJS.ProcessEnv = process.env): boolean {
72
+ return getGeminiApiKey(env) !== null;
73
+ }
74
+
75
+ export function isGeminiRouteConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
76
+ return hasGeminiApiKey(env) || geminiCliHeadlessValidated;
77
+ }
78
+
79
+ export function resetGeminiRouteValidationCacheForTests(): void {
80
+ geminiCliHeadlessValidated = false;
81
+ resolvedGeminiCommand = null;
82
+ lastPromptDiagnostics = null;
83
+ }
84
+
85
+ function parseGeminiJsonResponse(text: string): string | null {
86
+ try {
87
+ const parsed = JSON.parse(text) as unknown;
88
+ if (typeof parsed === "object" && parsed !== null && "response" in parsed) {
89
+ const response = (parsed as { response?: unknown }).response;
90
+ return typeof response === "string" ? response : null;
91
+ }
92
+ } catch {
93
+ return null;
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ function firstUsefulOutputLine(result: Pick<CommandResult, "stdout" | "stderr" | "userMessage">): string | null {
100
+ return sanitizeTerminalOutput(`${result.stderr}\n${result.stdout}\n${result.userMessage}`)
101
+ .split(/\r?\n/)
102
+ .map((line) => line.trim())
103
+ .find(Boolean) ?? null;
104
+ }
105
+
106
+ function getCombinedOutput(result: Pick<CommandResult, "stdout" | "stderr" | "userMessage">): string {
107
+ return sanitizeTerminalOutput(`${result.stderr}\n${result.stdout}\n${result.userMessage}`);
108
+ }
109
+
110
+ export function resolveGeminiApprovalMode(runtime?: ResolvedRuntimeConfig | boolean): GeminiApprovalMode {
111
+ if (typeof runtime === "boolean") {
112
+ return runtime ? "plan" : "default";
113
+ }
114
+
115
+ if (!runtime) return "default";
116
+ if (runtime.planMode || runtime.mode === "suggest" || runtime.policy.sandboxMode === "read-only") {
117
+ return "plan";
118
+ }
119
+ if (runtime.mode === "auto-edit") {
120
+ return "auto_edit";
121
+ }
122
+ if (
123
+ runtime.mode === "full-auto"
124
+ || (runtime.policy.approvalPolicy === "never" && runtime.policy.sandboxMode === "danger-full-access")
125
+ ) {
126
+ return "yolo";
127
+ }
128
+ return "default";
129
+ }
130
+
131
+ // ─── Command building ─────────────────────────────────────────────────────────
132
+
133
+ export function buildGeminiCliValidationArgs(): string[] {
134
+ return ["--model", GEMINI_DEFAULT_MODEL_ID, "-p", GEMINI_READY_PROMPT];
135
+ }
136
+
137
+ export function buildGeminiCliPromptArgs(
138
+ prompt: string,
139
+ modelId?: string | null,
140
+ runtime?: ResolvedRuntimeConfig | boolean,
141
+ ): string[] {
142
+ void runtime;
143
+ const resolvedModelId = normalizeGeminiModelId(modelId);
144
+ return [
145
+ "--model",
146
+ resolvedModelId,
147
+ "-p",
148
+ prompt,
149
+ ];
150
+ }
151
+
152
+ export async function buildGeminiCommand(options: {
153
+ cwd: string;
154
+ mode: GeminiCommandMode;
155
+ prompt?: string;
156
+ model?: string | null;
157
+ reasoning?: string | null;
158
+ runtime?: ResolvedRuntimeConfig | boolean;
159
+ configuredPath?: string | null;
160
+ runCommandImpl?: CommandRunner;
161
+ outputFormat?: GeminiOutputFormat;
162
+ }): Promise<GeminiCommandSpec> {
163
+ const file = await resolveGeminiExecutable({
164
+ runCommandImpl: options.runCommandImpl,
165
+ cwd: options.cwd,
166
+ configuredPath: options.configuredPath,
167
+ });
168
+ resolvedGeminiCommand = file;
169
+
170
+ const approvalMode = resolveGeminiApprovalMode(options.runtime);
171
+ const outputFormat = options.outputFormat ?? "text";
172
+ const model = options.mode === "readiness" ? GEMINI_DEFAULT_MODEL_ID : normalizeGeminiModelId(options.model);
173
+ const args = options.mode === "readiness"
174
+ ? ["--model", model, "-p", GEMINI_READY_PROMPT]
175
+ : [
176
+ "--model",
177
+ model,
178
+ "-p",
179
+ options.prompt ?? "",
180
+ ];
181
+
182
+ return {
183
+ file,
184
+ args,
185
+ cwd: options.cwd,
186
+ mode: options.mode,
187
+ model,
188
+ ...(options.reasoning ? { reasoning: options.reasoning } : {}),
189
+ approvalMode,
190
+ outputFormat,
191
+ includesPolicy: args.includes("--policy") || args.includes("--admin-policy") || args.some((arg) => /auto-saved\.toml/i.test(arg)),
192
+ };
193
+ }
194
+
195
+ function redactedPromptArgs(command: GeminiCommandSpec): string[] {
196
+ if (command.mode !== "prompt") return [...command.args];
197
+ const args = [...command.args];
198
+ const promptIndex = args.indexOf("-p");
199
+ if (promptIndex >= 0 && promptIndex + 1 < args.length) {
200
+ args[promptIndex + 1] = "<prompt>";
201
+ }
202
+ return args;
203
+ }
204
+
205
+ function diagnosticArgs(command: GeminiCommandSpec): string {
206
+ return JSON.stringify(command.mode === "prompt" ? redactedPromptArgs(command) : command.args);
207
+ }
208
+
209
+ function recordPromptDiagnostics(command: GeminiCommandSpec, result: CommandResult): void {
210
+ lastPromptDiagnostics = {
211
+ command,
212
+ redactedArgs: redactedPromptArgs(command),
213
+ exitCode: result.exitCode,
214
+ signal: result.signal,
215
+ status: result.status,
216
+ stdoutSnippet: sanitizeTerminalOutput(result.stdout).trim().slice(0, 500),
217
+ stderrSnippet: sanitizeTerminalOutput(result.stderr).trim().slice(0, 500),
218
+ extractionStatus: "not-completed",
219
+ parseMode: "plain-text",
220
+ parsedEvents: 0,
221
+ parsedMessages: 0,
222
+ finalAssistantTextLength: 0,
223
+ finalAssistantTextPreview: "",
224
+ stderrWarningTextPresent: /warning|ripgrep is not available|falling back to greptool/i.test(result.stderr),
225
+ };
226
+ }
227
+
228
+ function recordExtractionDiagnostics(result: CommandResult, text: string): GeminiExtractionStatus {
229
+ const extractionStatus: GeminiExtractionStatus = result.status === "completed" && result.exitCode === 0
230
+ ? text.trim()
231
+ ? "assistant-text"
232
+ : "completed-empty-assistant"
233
+ : "not-completed";
234
+
235
+ if (lastPromptDiagnostics) {
236
+ lastPromptDiagnostics = {
237
+ ...lastPromptDiagnostics,
238
+ extractionStatus,
239
+ finalAssistantTextLength: text.length,
240
+ finalAssistantTextPreview: "",
241
+ };
242
+ }
243
+
244
+ diagLog([
245
+ "PARSED:",
246
+ `parseMode=plain-text`,
247
+ `parsedEvents=0`,
248
+ `parsedMessages=0`,
249
+ `extractionStatus=${extractionStatus}`,
250
+ `finalExtractedAssistantText.length=${text.length}`,
251
+ `stderrWarningTextPresent=${/warning|ripgrep is not available|falling back to greptool/i.test(result.stderr)}`,
252
+ ].join(" "));
253
+
254
+ return extractionStatus;
255
+ }
256
+
257
+ // ─── Execution ───────────────────────────────────────────────────────────────
258
+
259
+ async function executeGeminiCommand(
260
+ command: GeminiCommandSpec,
261
+ runCommandImpl: CommandRunner,
262
+ timeoutMs: number,
263
+ handlers?: CommandStreamHandlers,
264
+ ): Promise<CommandResult> {
265
+ let stdoutChunkCount = 0;
266
+ let stderrChunkCount = 0;
267
+ const lifecycleEvents: string[] = [];
268
+ const streamHandlers: CommandStreamHandlers = {
269
+ onProcessLifecycle: (event) => {
270
+ lifecycleEvents.push(event);
271
+ diagLog(`PROCESS_LIFECYCLE: event=${event}`);
272
+ handlers?.onProcessLifecycle?.(event);
273
+ },
274
+ onStdout: (text) => {
275
+ stdoutChunkCount += 1;
276
+ diagLog(`STDOUT_RAW_CHUNK: index=${stdoutChunkCount} length=${text.length}`);
277
+ handlers?.onStdout?.(text);
278
+ },
279
+ onStderr: (text) => {
280
+ stderrChunkCount += 1;
281
+ diagLog(`STDERR_RAW_CHUNK: index=${stderrChunkCount} length=${text.length}`);
282
+ handlers?.onStderr?.(text);
283
+ },
284
+ };
285
+
286
+ diagLog([
287
+ "EXECUTE_COMMAND:",
288
+ `resolvedCommand=${command.file}`,
289
+ `argv=${diagnosticArgs(command)}`,
290
+ `cwd=${command.cwd}`,
291
+ `shell=false`,
292
+ ].join(" "));
293
+
294
+ const runner = runCommandImpl({
295
+ executable: command.file,
296
+ args: command.args,
297
+ cwd: command.cwd,
298
+ timeoutMs,
299
+ }, streamHandlers);
300
+ diagLog(`SPAWNED: pid=${runner.child?.pid ?? "unknown"} executable=${command.file} timeoutMs=${timeoutMs}`);
301
+ const result = await runner.result;
302
+ diagLog(`PROCESS_CLOSE_EVENT: closeObserved=true status=${result.status} exitCode=${result.exitCode} signal=${result.signal}`);
303
+ diagLog(`CLOSED: status=${result.status} exitCode=${result.exitCode} signal=${result.signal} durationMs=${result.durationMs} stdout.len=${result.stdout.length} stderr.len=${result.stderr.length} stdoutChunks=${stdoutChunkCount} stderrChunks=${stderrChunkCount} lifecycle=${JSON.stringify(lifecycleEvents)}`);
304
+ return result;
305
+ }
306
+
307
+ async function runGeminiApi(request: ProviderChatRequest): Promise<string> {
308
+ const apiKey = getGeminiApiKey();
309
+ if (!apiKey) {
310
+ throw new Error(GEMINI_ROUTE_SETUP_MESSAGE);
311
+ }
312
+
313
+ const modelId = normalizeGeminiModelId(request.route.modelId);
314
+ const response = await fetch(`${GEMINI_API_BASE_URL}/${encodeURIComponent(modelId)}:generateContent?key=${encodeURIComponent(apiKey)}`, {
315
+ method: "POST",
316
+ headers: { "Content-Type": "application/json" },
317
+ body: JSON.stringify({
318
+ contents: [
319
+ {
320
+ role: "user",
321
+ parts: [{ text: request.prompt }],
322
+ },
323
+ ],
324
+ }),
325
+ });
326
+
327
+ const body = await response.text();
328
+ if (!response.ok) {
329
+ throw new Error(`Gemini API request failed (${response.status}): ${sanitizeTerminalOutput(body).slice(0, 500)}`);
330
+ }
331
+
332
+ const parsed = JSON.parse(body) as {
333
+ candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;
334
+ };
335
+ const text = parsed.candidates?.flatMap((candidate) => candidate.content?.parts ?? [])
336
+ .map((part) => part.text ?? "")
337
+ .join("")
338
+ .trim();
339
+
340
+ if (!text) {
341
+ throw new Error("Gemini API returned no assistant text.");
342
+ }
343
+
344
+ return text;
345
+ }
346
+
347
+ function isPolicyFileError(result: CommandResult): boolean {
348
+ return /policy file error|auto-saved\.toml/i.test(getCombinedOutput(result));
349
+ }
350
+
351
+ function isInvalidModelError(result: CommandResult): boolean {
352
+ const combined = getCombinedOutput(result);
353
+ return /\b(model)\b[\s\S]{0,80}\b(not found|invalid|unknown|unsupported|does not exist|not supported)\b/i.test(combined)
354
+ || /\b(not found|invalid|unknown|unsupported)\b[\s\S]{0,80}\b(model)\b/i.test(combined);
355
+ }
356
+
357
+ function formatGeminiFailure(command: GeminiCommandSpec, result: CommandResult): string {
358
+ const combinedOutput = getCombinedOutput(result).trim();
359
+ if (isPolicyFileError(result)) {
360
+ return [
361
+ "Policy file error in Gemini CLI.",
362
+ `Command file: ${command.file}`,
363
+ `Command args: ${JSON.stringify(redactedPromptArgs(command))}`,
364
+ `Included --policy: ${command.args.includes("--policy")}`,
365
+ `Included --admin-policy: ${command.args.includes("--admin-policy")}`,
366
+ `Included policy file: ${command.includesPolicy}`,
367
+ `First useful output: ${firstUsefulOutputLine(result) ?? "Unknown error"}`,
368
+ ].join("\n");
369
+ }
370
+ if (result.status === "timeout") {
371
+ return [
372
+ `Gemini CLI prompt timed out after ${GEMINI_TIMEOUT_MS}ms.`,
373
+ `Command file: ${command.file}`,
374
+ `Command args: ${JSON.stringify(redactedPromptArgs(command))}`,
375
+ result.stderr.trim() ? `Stderr: ${result.stderr.trim().slice(0, 500)}` : null,
376
+ result.stdout.trim() ? `Stdout: ${result.stdout.trim().slice(0, 500)}` : null,
377
+ ].filter(Boolean).join("\n");
378
+ }
379
+ return combinedOutput || result.userMessage || "Gemini CLI headless route failed.";
380
+ }
381
+
382
+ async function runGeminiCliAttempt(
383
+ request: ProviderChatRequest,
384
+ runCommandImpl: CommandRunner,
385
+ modelId: string | null,
386
+ handlers?: CommandStreamHandlers,
387
+ ): Promise<{ command: GeminiCommandSpec; result: CommandResult }> {
388
+ const command = await buildGeminiCommand({
389
+ cwd: request.workspaceRoot,
390
+ mode: "prompt",
391
+ prompt: request.prompt,
392
+ model: modelId,
393
+ reasoning: request.route.reasoning,
394
+ runtime: request.runtime,
395
+ configuredPath: request.runtime.geminiCommandPath,
396
+ runCommandImpl,
397
+ outputFormat: "text",
398
+ });
399
+ const result = await executeGeminiCommand(command, runCommandImpl, GEMINI_TIMEOUT_MS, handlers);
400
+ recordPromptDiagnostics(command, result);
401
+ return { command, result };
402
+ }
403
+
404
+ export async function runGeminiCliWithRunner(
405
+ request: ProviderChatRequest,
406
+ runCommandImpl: CommandRunner = runCommand,
407
+ handlers?: CommandStreamHandlers,
408
+ ): Promise<string> {
409
+ const first = await runGeminiCliAttempt(request, runCommandImpl, request.route.modelId, handlers);
410
+ diagLog(`ATTEMPT1: status=${first.result.status} exitCode=${first.result.exitCode} stdout.len=${first.result.stdout.length} stderr.len=${first.result.stderr.length}`);
411
+ if (first.result.status === "completed" && first.result.exitCode === 0) {
412
+ const text = sanitizeTerminalOutput(first.result.stdout).trim();
413
+ const extractionStatus = recordExtractionDiagnostics(first.result, text);
414
+ if (!text) {
415
+ diagLog(`EMPTY_STDOUT: stdout is empty after sanitize+trim.`);
416
+ diagLog(`EMPTY_STDOUT: stderr.len=${first.result.stderr.length} stdout.len=${first.result.stdout.length}`);
417
+ diagLog(`EMPTY_STDOUT: extractionStatus=${extractionStatus}. Gemini likely wrote response to stderr or emitted no assistant text. Run will complete with no rendered assistant content unless app boundary treats this as an error.`);
418
+ }
419
+ diagLog(`SUCCESS: returning text.length=${text.length}`);
420
+ return text;
421
+ }
422
+
423
+ recordExtractionDiagnostics(first.result, "");
424
+ diagLog(`FAILURE: isInvalidModel=${request.route.modelId ? isInvalidModelError(first.result) : false} status=${first.result.status} exitCode=${first.result.exitCode}`);
425
+
426
+ throw new Error(formatGeminiFailure(first.command, first.result));
427
+ }
428
+
429
+ async function runGeminiCli(request: ProviderChatRequest, handlers?: CommandStreamHandlers): Promise<string> {
430
+ return runGeminiCliWithRunner(request, runCommand, handlers);
431
+ }
432
+
433
+ export function classifyGeminiProbeFailure(result: CommandResult): "auth required" | "quota/rate limit" | "bad flag" | "shell wrapper/function conflict" | "unknown" {
434
+ const combined = getCombinedOutput(result);
435
+ if (/parameter name 'p' is ambiguous|Possible matches include:[\s\S]*ProgressAction|PipelineVariable/i.test(combined)) {
436
+ return "shell wrapper/function conflict";
437
+ }
438
+ if (/\b(auth|authentication|login|sign in|signin|unauthorized|not authenticated)\b/i.test(combined)) {
439
+ return "auth required";
440
+ }
441
+ if (/\b(quota|rate limit|rate-limit|too many requests|resource exhausted|429)\b/i.test(combined)) {
442
+ return "quota/rate limit";
443
+ }
444
+ if (/\b(unknown|invalid|unrecognized|unexpected)\b/i.test(combined) && /\b(flag|option|argument|parameter)\b/i.test(combined)) {
445
+ return "bad flag";
446
+ }
447
+ return "unknown";
448
+ }
449
+
450
+ async function captureGeminiEnvironment(
451
+ cwd: string,
452
+ runCommandImpl: CommandRunner,
453
+ configuredPath?: string | null,
454
+ ): Promise<{ path: string | null; version: string | null; commandCheckOk: boolean; commandCheckOutput: string | null }> {
455
+ try {
456
+ const resolved = await resolveGeminiExecutable({ runCommandImpl, cwd, configuredPath });
457
+ const versionRunner = runCommandImpl({
458
+ executable: resolved,
459
+ args: ["--version"],
460
+ cwd,
461
+ timeoutMs: 5000,
462
+ });
463
+ const versionResult = await versionRunner.result;
464
+ if (versionResult.status === "completed" && versionResult.exitCode === 0) {
465
+ return {
466
+ path: resolved,
467
+ version: versionResult.stdout.trim() || versionResult.stderr.trim() || null,
468
+ commandCheckOk: true,
469
+ commandCheckOutput: versionResult.stdout.trim() || versionResult.stderr.trim() || null,
470
+ };
471
+ }
472
+
473
+ const helpRunner = runCommandImpl({
474
+ executable: resolved,
475
+ args: ["--help"],
476
+ cwd,
477
+ timeoutMs: 5000,
478
+ });
479
+ const helpResult = await helpRunner.result;
480
+ return {
481
+ path: resolved,
482
+ version: null,
483
+ commandCheckOk: helpResult.status === "completed" && helpResult.exitCode === 0,
484
+ commandCheckOutput: firstUsefulOutputLine(helpResult),
485
+ };
486
+ } catch {
487
+ return { path: null, version: null, commandCheckOk: false, commandCheckOutput: null };
488
+ }
489
+ }
490
+
491
+ // ─── Route validation ─────────────────────────────────────────────────────────
492
+
493
+ export async function validateGeminiRoute(options: {
494
+ cwd: string;
495
+ modelId: string;
496
+ env?: NodeJS.ProcessEnv;
497
+ runCommandImpl?: CommandRunner;
498
+ timeoutMs?: number;
499
+ configuredPath?: string | null;
500
+ }): Promise<ProviderRouteValidationResult> {
501
+ const runImpl = options.runCommandImpl ?? runCommand;
502
+ const envInfoPromise = captureGeminiEnvironment(options.cwd, runImpl, options.configuredPath);
503
+
504
+ let command: GeminiCommandSpec;
505
+ try {
506
+ command = await buildGeminiCommand({
507
+ cwd: options.cwd,
508
+ mode: "readiness",
509
+ model: options.modelId,
510
+ configuredPath: options.configuredPath,
511
+ runCommandImpl: runImpl,
512
+ outputFormat: "text",
513
+ });
514
+ } catch (error) {
515
+ geminiCliHeadlessValidated = false;
516
+ const message = error instanceof Error ? error.message : "Gemini CLI executable was not found.";
517
+ return {
518
+ status: hasGeminiApiKey(options.env) ? "ready" : "not-configured",
519
+ providerId: "google",
520
+ backendKind: hasGeminiApiKey(options.env) ? "gemini-api-key" : "unavailable",
521
+ message: hasGeminiApiKey(options.env) ? "Google/Gemini API key is configured." : `${message} Set GEMINI_EXECUTABLE to a known working Gemini CLI command/path.`,
522
+ diagnostics: {
523
+ resolvedCommand: null,
524
+ executablePath: null,
525
+ commandFile: null,
526
+ headlessPromptMode: "-p",
527
+ lastProbeCommandArgs: JSON.stringify(buildGeminiCliValidationArgs()),
528
+ outputFormat: "text",
529
+ includesPolicy: false,
530
+ failureReason: "unknown",
531
+ },
532
+ };
533
+ }
534
+
535
+ const result = await executeGeminiCommand(command, runImpl, options.timeoutMs ?? GEMINI_ROUTE_VALIDATION_TIMEOUT_MS);
536
+ const envInfo = await envInfoPromise;
537
+ const parsed = parseGeminiJsonResponse(result.stdout);
538
+ const probeText = sanitizeTerminalOutput(`${result.stdout}\n${result.stderr}\n${parsed ?? ""}`).trim();
539
+ const probeMatch = /\bREADY\b/.test(probeText);
540
+ const successfulGeminiResponse = result.status === "completed" && result.exitCode === 0 && probeMatch;
541
+ const failureReason = classifyGeminiProbeFailure(result);
542
+
543
+ const diagnostics: Record<string, string | number | boolean | null> = {
544
+ resolvedCommand: command.file,
545
+ executablePath: command.file,
546
+ commandFile: command.file,
547
+ version: envInfo.version,
548
+ commandCheckOk: envInfo.commandCheckOk,
549
+ commandCheckOutput: envInfo.commandCheckOutput,
550
+ headlessPromptMode: "-p",
551
+ probeStatus: successfulGeminiResponse ? "Ready" : result.status,
552
+ command: `${command.file} ${command.args.join(" ")}`,
553
+ lastProbeCommandFile: command.file,
554
+ lastProbeCommandArgs: JSON.stringify(command.args),
555
+ approvalMode: command.approvalMode,
556
+ outputFormat: command.outputFormat,
557
+ includesPolicy: command.includesPolicy,
558
+ reasoningDiagnostic: GEMINI_REASONING_UNSUPPORTED_DIAGNOSTIC,
559
+ status: result.status,
560
+ exitCode: result.exitCode,
561
+ timeout: result.status === "timeout",
562
+ stdoutSummary: result.stdout.trim().slice(0, 200),
563
+ stderrSummary: result.stderr.trim().slice(0, 200),
564
+ firstUsefulOutputLine: firstUsefulOutputLine(result),
565
+ failureReason,
566
+ probeMatch,
567
+ readyTokenObserved: probeMatch,
568
+ };
569
+
570
+ const looksFound = envInfo.path && (envInfo.commandCheckOk || (result.status !== "spawn_error" && result.errorCode !== "ENOENT"));
571
+ if (successfulGeminiResponse) {
572
+ geminiCliHeadlessValidated = true;
573
+ return {
574
+ status: "ready",
575
+ providerId: "google",
576
+ backendKind: "gemini-cli-auth",
577
+ message: "Gemini CLI auth is configured.",
578
+ diagnostics,
579
+ };
580
+ }
581
+
582
+ geminiCliHeadlessValidated = false;
583
+ if (hasGeminiApiKey(options.env)) {
584
+ return {
585
+ status: "ready",
586
+ providerId: "google",
587
+ backendKind: "gemini-api-key",
588
+ message: "Google/Gemini API key is configured.",
589
+ diagnostics,
590
+ };
591
+ }
592
+
593
+ let errorMessage = GEMINI_ROUTE_SETUP_MESSAGE;
594
+ if (failureReason === "shell wrapper/function conflict") {
595
+ errorMessage = `PowerShell wrapper detected. Codexa is bypassing it and using:\n${command.file}`;
596
+ } else if (result.status === "completed" && result.exitCode === 0) {
597
+ errorMessage = "Gemini CLI responded, but Codexa could not validate the headless route. The probe returned unexpected output.";
598
+ } else if (!looksFound) {
599
+ errorMessage = "Gemini CLI was not found as a real executable file. Install Gemini CLI or set GEMINI_EXECUTABLE to a known working command/path.";
600
+ } else if (result.status === "timeout") {
601
+ errorMessage = "Installed but headless probe timed out.";
602
+ } else if (result.status === "completed" && result.exitCode !== 0) {
603
+ errorMessage = `Gemini CLI installed, auth unknown or headless mode failed. Run: ${command.file} --model ${command.model ?? GEMINI_DEFAULT_MODEL_ID} -p "Respond with READY only."`;
604
+ } else if (result.status === "failed") {
605
+ errorMessage = `Gemini CLI installed, auth unknown or headless mode failed. Run: ${command.file} --model ${command.model ?? GEMINI_DEFAULT_MODEL_ID} -p "Respond with READY only."`;
606
+ }
607
+
608
+ return {
609
+ status: "not-configured",
610
+ providerId: "google",
611
+ backendKind: "unavailable",
612
+ message: errorMessage,
613
+ diagnostics,
614
+ };
615
+ }
616
+
617
+ function getGeminiRuntimeBackendKind(): ProviderBackendKind {
618
+ return geminiCliHeadlessValidated ? "gemini-cli-auth" : hasGeminiApiKey() ? "gemini-api-key" : "gemini-cli-auth";
619
+ }
620
+
621
+ function formatDiagnosticSnippet(label: string, value: string | null | undefined): string | null {
622
+ return value?.trim() ? `${label}: ${value.trim().slice(0, 500)}` : null;
623
+ }
624
+
625
+ export async function runGeminiDiagnostics(options: {
626
+ cwd: string;
627
+ selectedModel?: string | null;
628
+ selectedReasoning?: string | null;
629
+ runtime: ResolvedRuntimeConfig;
630
+ configuredPath?: string | null;
631
+ runCommandImpl?: CommandRunner;
632
+ }): Promise<string> {
633
+ const runImpl = options.runCommandImpl ?? runCommand;
634
+ const selectedModel = normalizeGeminiModelId(options.selectedModel);
635
+ const envInfo = await captureGeminiEnvironment(options.cwd, runImpl, options.configuredPath ?? options.runtime.geminiCommandPath);
636
+ const readiness = await validateGeminiRoute({
637
+ cwd: options.cwd,
638
+ modelId: selectedModel,
639
+ configuredPath: options.configuredPath ?? options.runtime.geminiCommandPath,
640
+ runCommandImpl: runImpl,
641
+ });
642
+ const readinessCommand = await buildGeminiCommand({
643
+ cwd: options.cwd,
644
+ mode: "readiness",
645
+ model: selectedModel,
646
+ runtime: options.runtime,
647
+ configuredPath: options.configuredPath ?? options.runtime.geminiCommandPath,
648
+ runCommandImpl: runImpl,
649
+ });
650
+ const promptPreview = await buildGeminiCommand({
651
+ cwd: options.cwd,
652
+ mode: "prompt",
653
+ prompt: "<prompt>",
654
+ model: selectedModel,
655
+ reasoning: options.selectedReasoning,
656
+ runtime: options.runtime,
657
+ configuredPath: options.configuredPath ?? options.runtime.geminiCommandPath,
658
+ runCommandImpl: runImpl,
659
+ });
660
+
661
+ return [
662
+ "Gemini diagnostics:",
663
+ ` Resolved executable path: ${envInfo.path ?? "Not found"}`,
664
+ ` Version output: ${envInfo.version ?? "Unknown"}`,
665
+ ` Readiness command file: ${readinessCommand.file}`,
666
+ ` Readiness command args: ${JSON.stringify(readinessCommand.args)}`,
667
+ ` Readiness result: ${readiness.status}${readiness.message ? ` - ${readiness.message}` : ""}`,
668
+ ` Selected model: ${selectedModel}`,
669
+ ` Selected approval mode: ${promptPreview.approvalMode}`,
670
+ ` Selected output format: ${promptPreview.outputFormat}`,
671
+ ` Policy args included: ${promptPreview.includesPolicy}`,
672
+ ` Reasoning: ${options.selectedReasoning ?? "none"} (${GEMINI_REASONING_UNSUPPORTED_DIAGNOSTIC})`,
673
+ ` Last prompt command file: ${lastPromptDiagnostics?.command.file ?? "none"}`,
674
+ ` Last prompt command args: ${lastPromptDiagnostics ? JSON.stringify(lastPromptDiagnostics.redactedArgs) : "none"}`,
675
+ ` Last exit code: ${lastPromptDiagnostics?.exitCode ?? "none"}`,
676
+ ` Last signal: ${lastPromptDiagnostics?.signal ?? "none"}`,
677
+ ` Last parse mode: ${lastPromptDiagnostics?.parseMode ?? "none"}`,
678
+ ` Last parsed events/messages: ${lastPromptDiagnostics ? `${lastPromptDiagnostics.parsedEvents}/${lastPromptDiagnostics.parsedMessages}` : "none"}`,
679
+ ` Last extraction status: ${lastPromptDiagnostics?.extractionStatus ?? "none"}`,
680
+ ` Last final assistant text length: ${lastPromptDiagnostics?.finalAssistantTextLength ?? "none"}`,
681
+ ` Last stderr warning text present: ${lastPromptDiagnostics?.stderrWarningTextPresent ?? "none"}`,
682
+ formatDiagnosticSnippet(" Last stdout", lastPromptDiagnostics?.stdoutSnippet),
683
+ formatDiagnosticSnippet(" Last stderr", lastPromptDiagnostics?.stderrSnippet),
684
+ formatDiagnosticSnippet(" Last final assistant text", lastPromptDiagnostics?.finalAssistantTextPreview),
685
+ ` Prompt preview command file: ${promptPreview.file}`,
686
+ ` Prompt preview command args: ${JSON.stringify(redactedPromptArgs(promptPreview))}`,
687
+ ].filter(Boolean).join("\n");
688
+ }
689
+
690
+ // ─── Runtime ─────────────────────────────────────────────────────────────────
691
+
692
+ export const geminiRuntime: ProviderRuntime = {
693
+ providerId: "google",
694
+ label: "Google/Gemini",
695
+ modelPickerLabel: "Gemini",
696
+ backendKind: "gemini-cli-auth",
697
+ routeAvailable: true,
698
+ routeStatus: "Uses Gemini CLI subscription-backed route when available, otherwise GEMINI_API_KEY or GOOGLE_API_KEY.",
699
+ routeSetupMessage: GEMINI_ROUTE_SETUP_MESSAGE,
700
+ launchAvailable: true,
701
+ isRouteConfigured: isGeminiRouteConfigured,
702
+ validateRoute: async ({ route, workspaceRoot, geminiCommandPath }) => validateGeminiRoute({
703
+ cwd: workspaceRoot,
704
+ modelId: route.modelId,
705
+ configuredPath: geminiCommandPath,
706
+ }),
707
+ discoverModels: () => ({
708
+ status: "ready",
709
+ providerId: "google",
710
+ backendKind: getGeminiRuntimeBackendKind(),
711
+ models: GEMINI_FALLBACK_MODELS,
712
+ }),
713
+ run: (request, handlers: BackendRunHandlers) => {
714
+ let cancelled = false;
715
+
716
+ diagLog(`=== RUN START: route=${JSON.stringify(request.route)} cwd=${request.workspaceRoot} geminiCommandPath=${request.runtime.geminiCommandPath ?? "unset"} geminiCliHeadlessValidated=${geminiCliHeadlessValidated} hasApiKey=${hasGeminiApiKey()}`);
717
+
718
+ handlers.onProgress?.({
719
+ id: "gemini-route",
720
+ source: "stdout",
721
+ text: "Starting Gemini CLI",
722
+ });
723
+ if (isGeminiDiagEnabled()) {
724
+ handlers.onProgress?.({
725
+ id: "gemini-diag",
726
+ source: "stdout",
727
+ text: `[DEBUG] Gemini diag log: ${GEMINI_DIAG_LOG}`,
728
+ });
729
+ }
730
+
731
+ if (request.route.reasoning) {
732
+ handlers.onProgress?.({
733
+ id: "gemini-reasoning",
734
+ source: "stdout",
735
+ text: GEMINI_REASONING_UNSUPPORTED_DIAGNOSTIC,
736
+ });
737
+ }
738
+
739
+ const childHandlers: CommandStreamHandlers = {
740
+ onProcessLifecycle: (event) => {
741
+ diagLog(`LIFECYCLE: ${event}`);
742
+ handlers.onProcessLifecycle?.(event === "cancel" ? "cleanup" : event);
743
+ },
744
+ onStdout: (text) => { diagLog(`STDOUT chunk length=${text.length}`); },
745
+ onStderr: (text) => { diagLog(`STDERR chunk length=${text.length}`); },
746
+ };
747
+
748
+ const execPath = geminiCliHeadlessValidated ? "runGeminiCli" : hasGeminiApiKey() ? "runGeminiApi" : "runGeminiCli (fallback-no-key)";
749
+ diagLog(`EXEC PATH: geminiCliHeadlessValidated=${geminiCliHeadlessValidated} → ${execPath}`);
750
+
751
+ const runGemini = geminiCliHeadlessValidated
752
+ ? runGeminiCli(request, childHandlers)
753
+ : hasGeminiApiKey()
754
+ ? runGeminiApi(request)
755
+ : runGeminiCli(request, childHandlers);
756
+
757
+ runGemini
758
+ .then((text) => {
759
+ diagLog(`RESOLVED: text.length=${text.length} cancelled=${cancelled}`);
760
+ if (!text) {
761
+ diagLog(`EMPTY_RESPONSE: text is empty → onAssistantDelta !chunk guard will block it → run will finalize silently with no rendered assistant content`);
762
+ diagLog(`NO_ERROR_CARD: runGeminiCliWithRunner returned "" (success path, not throw). .catch is NOT reached. handlers.onError is NOT called. finalizePromptRun will receive status="completed". RUN_FAILED is never dispatched. No error card appears.`);
763
+ }
764
+ if (cancelled) return;
765
+ diagLog(`CALLING: onAssistantDelta onFinalAnswerObserved onResponse`);
766
+ handlers.onAssistantDelta?.(text);
767
+ handlers.onFinalAnswerObserved?.(text);
768
+ handlers.onResponse(text);
769
+ diagLog(`HANDLERS CALLED OK`);
770
+ })
771
+ .catch((error) => {
772
+ diagLog(`REJECTED: cancelled=${cancelled} errorType=${error instanceof Error ? error.name : typeof error}`);
773
+ if (cancelled) return;
774
+ const message = error instanceof Error ? error.message : "Google/Gemini in-Codexa routing failed.";
775
+ diagLog(`CALLING: onError`);
776
+ handlers.onError(message);
777
+ });
778
+
779
+ return () => {
780
+ diagLog(`CANCEL CALLED`);
781
+ cancelled = true;
782
+ };
783
+ },
784
+ };