@golba98/codexa 1.0.1 → 1.0.3

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 (234) hide show
  1. package/README.md +391 -94
  2. package/package.json +14 -8
  3. package/src/app.tsx +196 -63
  4. package/src/commands/handler.ts +9 -4
  5. package/src/config/buildInfo.ts +3 -0
  6. package/src/config/launchArgs.ts +10 -0
  7. package/src/config/layeredConfig.ts +0 -0
  8. package/src/config/persistence.ts +51 -0
  9. package/src/config/runtimeConfig.ts +4 -0
  10. package/src/config/settings.ts +6 -13
  11. package/src/config/toml-serialize.ts +0 -0
  12. package/src/config/trustStore.ts +0 -0
  13. package/src/config/updateCheckCache.ts +63 -0
  14. package/src/core/attachments.ts +0 -0
  15. package/src/core/auth/codexAuth.ts +0 -0
  16. package/src/core/channel.ts +23 -0
  17. package/src/core/cleanupFastFail.ts +0 -0
  18. package/src/core/clipboard.ts +0 -0
  19. package/src/core/codex.ts +0 -0
  20. package/src/core/codexExecArgs.ts +0 -0
  21. package/src/core/codexLaunch.ts +0 -0
  22. package/src/core/codexPrompt.ts +0 -0
  23. package/src/core/executables/claudeExecutable.ts +0 -0
  24. package/src/core/executables/codexExecutable.ts +1 -0
  25. package/src/core/executables/executableResolver.ts +61 -43
  26. package/src/core/executables/geminiExecutable.ts +0 -0
  27. package/src/core/githubDiagnostics.ts +0 -0
  28. package/src/core/hollowResponseFormat.ts +0 -0
  29. package/src/core/inputDebug.ts +0 -0
  30. package/src/core/launchContext.ts +33 -33
  31. package/src/core/models/codexCapabilities.ts +0 -0
  32. package/src/core/models/codexModelCapabilities.ts +0 -0
  33. package/src/core/models/modelSpecs.ts +0 -0
  34. package/src/core/perf/profiler.ts +0 -0
  35. package/src/core/perf/renderDebug.ts +0 -0
  36. package/src/core/planStorage.ts +0 -0
  37. package/src/core/process/CommandRunner.ts +0 -0
  38. package/src/core/process/processValidation.ts +9 -5
  39. package/src/core/projectInstructions.ts +0 -0
  40. package/src/core/providerLauncher/launcher.ts +59 -42
  41. package/src/core/providerLauncher/registry.ts +0 -0
  42. package/src/core/providerLauncher/types.ts +0 -0
  43. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  44. package/src/core/providerRuntime/anthropic.ts +0 -0
  45. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  46. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  47. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  48. package/src/core/providerRuntime/gemini.ts +0 -0
  49. package/src/core/providerRuntime/lmstudio.ts +0 -0
  50. package/src/core/providerRuntime/local.ts +0 -0
  51. package/src/core/providerRuntime/models.ts +0 -0
  52. package/src/core/providerRuntime/reasoning.ts +0 -0
  53. package/src/core/providerRuntime/registry.ts +0 -0
  54. package/src/core/providerRuntime/types.ts +0 -0
  55. package/src/core/providers/codexJsonStream.ts +0 -0
  56. package/src/core/providers/codexSubprocess.ts +0 -0
  57. package/src/core/providers/codexTranscript.ts +0 -0
  58. package/src/core/providers/openaiNative.ts +0 -0
  59. package/src/core/providers/registry.ts +0 -0
  60. package/src/core/providers/types.ts +0 -0
  61. package/src/core/terminal/startupClear.ts +20 -0
  62. package/src/core/terminal/terminalCapabilities.ts +0 -0
  63. package/src/core/terminal/terminalControl.ts +0 -0
  64. package/src/core/terminal/terminalSanitize.ts +0 -0
  65. package/src/core/terminal/terminalSelection.ts +0 -0
  66. package/src/core/terminal/terminalTitle.ts +0 -0
  67. package/src/core/updateCheck.ts +188 -0
  68. package/src/core/workspaceActivity.ts +0 -0
  69. package/src/core/workspaceGuard.ts +0 -0
  70. package/src/core/workspaceRoot.ts +0 -0
  71. package/src/exec.ts +0 -0
  72. package/src/headless/execArgs.ts +1 -0
  73. package/src/headless/execRunner.ts +0 -0
  74. package/src/index.tsx +13 -9
  75. package/src/session/appSession.ts +0 -0
  76. package/src/session/chatLifecycle.ts +0 -0
  77. package/src/session/liveRenderScheduler.ts +0 -0
  78. package/src/session/planFlow.ts +0 -0
  79. package/src/session/planTranscript.ts +0 -0
  80. package/src/session/promptRunSchedule.ts +0 -0
  81. package/src/session/types.ts +2 -1
  82. package/src/test/runtimeTestUtils.ts +0 -0
  83. package/src/types/react-dom.d.ts +0 -0
  84. package/src/ui/ActionRequiredBlock.tsx +5 -5
  85. package/src/ui/ActivityBars.tsx +3 -3
  86. package/src/ui/ActivityIndicator.tsx +6 -6
  87. package/src/ui/AgentBlock.tsx +6 -6
  88. package/src/ui/AnimatedStatusText.tsx +1 -1
  89. package/src/ui/AppShell.tsx +13 -5
  90. package/src/ui/AttachmentImportPanel.tsx +8 -8
  91. package/src/ui/AuthPanel.tsx +20 -20
  92. package/src/ui/BackendPicker.tsx +0 -0
  93. package/src/ui/BottomComposer.tsx +117 -78
  94. package/src/ui/DashCard.tsx +3 -3
  95. package/src/ui/Markdown.tsx +17 -17
  96. package/src/ui/ModePicker.tsx +0 -0
  97. package/src/ui/ModelPicker.tsx +0 -0
  98. package/src/ui/ModelPickerScreen.tsx +10 -10
  99. package/src/ui/ModelReasoningPicker.tsx +14 -14
  100. package/src/ui/Panel.tsx +3 -3
  101. package/src/ui/PermissionsPanel.tsx +0 -0
  102. package/src/ui/PlanActionPicker.tsx +6 -6
  103. package/src/ui/PlanReviewPanel.tsx +8 -8
  104. package/src/ui/ProviderPicker.tsx +18 -18
  105. package/src/ui/ReasoningPicker.tsx +0 -0
  106. package/src/ui/RunFooter.tsx +3 -3
  107. package/src/ui/SelectionPanel.tsx +4 -4
  108. package/src/ui/SettingsPanel.tsx +8 -8
  109. package/src/ui/Spinner.tsx +1 -1
  110. package/src/ui/StaticIntroItem.tsx +0 -0
  111. package/src/ui/StaticTranscriptItem.tsx +0 -0
  112. package/src/ui/TextEntryPanel.tsx +10 -10
  113. package/src/ui/ThemePicker.tsx +0 -0
  114. package/src/ui/ThinkingBlock.tsx +8 -8
  115. package/src/ui/Timeline.tsx +21 -17
  116. package/src/ui/TopHeader.tsx +421 -257
  117. package/src/ui/TurnGroup.tsx +31 -31
  118. package/src/ui/UpdateAvailableCard.tsx +41 -0
  119. package/src/ui/UpdatePromptPanel.tsx +203 -0
  120. package/src/ui/busyStatusAnimation.ts +0 -0
  121. package/src/ui/commandNormalize.ts +0 -0
  122. package/src/ui/diffRenderer.ts +0 -0
  123. package/src/ui/focus.ts +3 -0
  124. package/src/ui/inputBuffer.ts +0 -0
  125. package/src/ui/layout.ts +1 -1
  126. package/src/ui/logoVariants.ts +107 -0
  127. package/src/ui/modeDisplay.ts +12 -12
  128. package/src/ui/outputPipeline.ts +0 -0
  129. package/src/ui/progressEntries.ts +0 -0
  130. package/src/ui/runActivityView.ts +0 -0
  131. package/src/ui/runtimeDisplay.ts +100 -0
  132. package/src/ui/slashCommands.ts +0 -0
  133. package/src/ui/terminalAnswerFormat.ts +0 -0
  134. package/src/ui/textLayout.ts +0 -0
  135. package/src/ui/theme.tsx +274 -395
  136. package/src/ui/themeFlow.ts +0 -0
  137. package/src/ui/timelineMeasure.ts +29 -19
  138. package/src/ui/useThrottledValue.ts +0 -0
  139. package/scripts/audit-codexa-capabilities.mjs +0 -466
  140. package/scripts/smoke-terminal-bench.mjs +0 -35
  141. package/src/appRenderStability.test.ts +0 -131
  142. package/src/commands/handler.test.ts +0 -643
  143. package/src/config/launchArgs.test.ts +0 -158
  144. package/src/config/layeredConfig.test.ts +0 -143
  145. package/src/config/persistence.test.ts +0 -110
  146. package/src/config/runtimeConfig.test.ts +0 -218
  147. package/src/config/settings.test.ts +0 -155
  148. package/src/config/trustStore.test.ts +0 -29
  149. package/src/core/attachments.test.ts +0 -155
  150. package/src/core/auth/codexAuth.test.ts +0 -68
  151. package/src/core/cleanupFastFail.test.ts +0 -76
  152. package/src/core/codexExecArgs.test.ts +0 -195
  153. package/src/core/codexLaunch.test.ts +0 -205
  154. package/src/core/codexPrompt.test.ts +0 -252
  155. package/src/core/executables/codexExecutable.test.ts +0 -212
  156. package/src/core/executables/executableResolver.test.ts +0 -129
  157. package/src/core/executables/geminiExecutable.test.ts +0 -116
  158. package/src/core/executables/pathSanityScan.test.ts +0 -47
  159. package/src/core/githubDiagnostics.test.ts +0 -92
  160. package/src/core/hollowResponseFormat.test.ts +0 -58
  161. package/src/core/launchContext.test.ts +0 -157
  162. package/src/core/models/codexCapabilities.test.ts +0 -45
  163. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  164. package/src/core/models/modelSpecs.test.ts +0 -283
  165. package/src/core/perf/renderDebug.test.ts +0 -230
  166. package/src/core/planStorage.test.ts +0 -143
  167. package/src/core/process/CommandRunner.test.ts +0 -105
  168. package/src/core/projectInstructions.test.ts +0 -50
  169. package/src/core/providerLauncher/launcher.test.ts +0 -238
  170. package/src/core/providerLauncher/registry.test.ts +0 -324
  171. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  172. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  173. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  174. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  175. package/src/core/providerRuntime/gemini.test.ts +0 -437
  176. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  177. package/src/core/providerRuntime/local.test.ts +0 -787
  178. package/src/core/providerRuntime/registry.test.ts +0 -233
  179. package/src/core/providers/codexJsonStream.test.ts +0 -148
  180. package/src/core/providers/codexSubprocess.test.ts +0 -68
  181. package/src/core/providers/codexTranscript.test.ts +0 -284
  182. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  183. package/src/core/terminal/terminalControl.test.ts +0 -75
  184. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  185. package/src/core/terminal/terminalSelection.test.ts +0 -42
  186. package/src/core/terminal/terminalTitle.test.ts +0 -328
  187. package/src/core/workspaceActivity.test.ts +0 -163
  188. package/src/core/workspaceGuard.test.ts +0 -151
  189. package/src/core/workspaceRoot.test.ts +0 -23
  190. package/src/exec.test.ts +0 -13
  191. package/src/headless/execArgs.test.ts +0 -147
  192. package/src/headless/execRunner.test.ts +0 -434
  193. package/src/index.test.tsx +0 -618
  194. package/src/session/appSession.test.ts +0 -897
  195. package/src/session/chatLifecycle.test.ts +0 -64
  196. package/src/session/liveRenderScheduler.test.ts +0 -201
  197. package/src/session/planFlow.test.ts +0 -103
  198. package/src/session/planTranscript.test.ts +0 -65
  199. package/src/session/promptRunSchedule.test.ts +0 -36
  200. package/src/ui/ActivityIndicator.test.tsx +0 -58
  201. package/src/ui/AgentBlock.test.ts +0 -6
  202. package/src/ui/AnimatedStatusText.test.ts +0 -16
  203. package/src/ui/AppShell.test.tsx +0 -1739
  204. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  205. package/src/ui/BottomComposer.test.ts +0 -674
  206. package/src/ui/CodexLogo.tsx +0 -55
  207. package/src/ui/Markdown.test.ts +0 -157
  208. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  209. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  210. package/src/ui/ModelPickerState.test.tsx +0 -151
  211. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  212. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  213. package/src/ui/PromptCardBorder.test.tsx +0 -161
  214. package/src/ui/ProviderPicker.test.tsx +0 -289
  215. package/src/ui/ProviderShortcut.test.tsx +0 -143
  216. package/src/ui/SettingsPanel.test.tsx +0 -233
  217. package/src/ui/Timeline.test.ts +0 -2067
  218. package/src/ui/TimelineNavigation.test.tsx +0 -201
  219. package/src/ui/TopHeader.test.tsx +0 -239
  220. package/src/ui/TurnGroup.test.tsx +0 -365
  221. package/src/ui/busyStatusAnimation.test.ts +0 -30
  222. package/src/ui/commandNormalize.test.ts +0 -142
  223. package/src/ui/diffRenderer.test.ts +0 -102
  224. package/src/ui/focusFlow.test.tsx +0 -1098
  225. package/src/ui/inputBuffer.test.ts +0 -151
  226. package/src/ui/layout.test.ts +0 -145
  227. package/src/ui/modeDisplay.test.ts +0 -42
  228. package/src/ui/runActivityView.test.ts +0 -89
  229. package/src/ui/runLifecycleView.test.tsx +0 -237
  230. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  231. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  232. package/src/ui/textLayout.test.ts +0 -18
  233. package/src/ui/themeFlow.test.ts +0 -53
  234. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -1,55 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import type { LayoutMode } from "./layout.js";
5
-
6
- // Full 4-line ASCII banner — rendered only in "full" mode.
7
- const BANNER = [
8
- " ____ ___ ____ _____ _ __ _ ",
9
- " / ___| / _ \\| _ \\| ____| |/ / / \\ ",
10
- "| | | | | | | | | _| | ' / / _ \\ ",
11
- "|_| \\___/|_| |_|_____|_|\\_\\ /_/ \\_\\ "
12
- ];
13
-
14
-
15
- interface CodexLogoProps {
16
- /** Which layout mode we're in. Defaults to "full". */
17
- layout?: LayoutMode;
18
- }
19
-
20
- export function CodexLogo({ layout = "full" }: CodexLogoProps) {
21
- const theme = useTheme();
22
-
23
- function colorForIndex(index: number): string {
24
- return theme.LOGO[index % theme.LOGO.length] ?? theme.LOGO[0];
25
- }
26
-
27
- // Micro mode — no logo rendered at all
28
- if (layout === "micro") return null;
29
-
30
- // Compact mode — single colourful line: ✦ CODEXA
31
- if (layout === "compact") {
32
- const letters = ["C", "O", "D", "E", "X", "A"];
33
- return (
34
- <Box>
35
- <Text color={theme.LOGO[0]} bold>{"✦ "}</Text>
36
- {letters.map((char, index) => (
37
- <Text key={index} color={colorForIndex(index)} bold>
38
- {char}
39
- </Text>
40
- ))}
41
- </Box>
42
- );
43
- }
44
-
45
- // Full mode — ASCII banner to stay stable across terminals/codepages.
46
- return (
47
- <Box flexDirection="column" overflow="hidden">
48
- {BANNER.map((line, index) => (
49
- <Text key={line} color={colorForIndex(index)} wrap="truncate">
50
- {line}
51
- </Text>
52
- ))}
53
- </Box>
54
- );
55
- }
@@ -1,157 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { parseMarkdown } from "./Markdown.js";
4
- import type { ParaSegment, CodeSegment, Segment } from "./Markdown.js";
5
-
6
- const SAMPLE = [
7
- "# Summary",
8
- "",
9
- "The current rendering path is flattening the assistant response into a dense block.",
10
- "",
11
- "What needs to improve:",
12
- "",
13
- "- Preserve paragraph spacing.",
14
- "- Keep bullet lists readable.",
15
- "- Keep code blocks separate.",
16
- "- Avoid merging activity logs with final answers.",
17
- "",
18
- "Steps:",
19
- "",
20
- "1. Sanitize unsafe control characters.",
21
- "2. Preserve markdown structure.",
22
- "3. Render semantic blocks with spacing.",
23
- "4. Verify streaming still works.",
24
- "",
25
- "Example command:",
26
- "",
27
- "```powershell",
28
- "npm run typecheck",
29
- "npm run build",
30
- "```",
31
- "",
32
- "Assistant: Done. Formatting pipeline is now applied.",
33
- ].join("\n");
34
-
35
- function segmentText(segments: Segment[]): string {
36
- return segments.flatMap((segment) => {
37
- if (segment.type === "code") return segment.lines;
38
- if (segment.type === "header") return segment.parts.map((part) => part.text);
39
- if (segment.type === "list") return segment.items.flatMap((item) => item.parts.map((part) => part.text));
40
- return segment.lines.flatMap((line) => line.map((part) => part.text));
41
- }).join("\n");
42
- }
43
-
44
- test("sample response produces separate segments, not one dense para", () => {
45
- const segments = parseMarkdown(SAMPLE);
46
- const types = segments.map((s) => s.type);
47
-
48
- assert.ok(
49
- segments.length >= 7,
50
- `expected ≥7 segments, got ${segments.length}: ${JSON.stringify(types)}`,
51
- );
52
- assert.equal(segments[0]?.type, "header", "first segment must be the header");
53
- assert.ok(types.includes("code"), "expected a code segment");
54
- assert.ok(types.includes("list"), "expected a list segment");
55
- });
56
-
57
- test("blank lines between paragraphs produce separate ParaSegments", () => {
58
- const segments = parseMarkdown(SAMPLE);
59
- const paras = segments.filter((s): s is ParaSegment => s.type === "para");
60
-
61
- for (const para of paras) {
62
- const text = para.lines.flat().map((p) => p.text).join(" ");
63
- assert.ok(
64
- !(text.includes("rendering path") && text.includes("What needs to improve")),
65
- "two distinct paragraphs must not share one ParaSegment",
66
- );
67
- }
68
- });
69
-
70
- test("blank lines inside code blocks are preserved", () => {
71
- const input = "```\nline1\n\nline3\n```";
72
- const segments = parseMarkdown(input);
73
- assert.equal(segments.length, 1);
74
- assert.equal(segments[0]?.type, "code");
75
- const code = segments[0] as CodeSegment;
76
- assert.deepEqual(code.lines, ["line1", "", "line3"]);
77
- });
78
-
79
- test("plain paragraphs with no blank lines stay in one segment", () => {
80
- const input = "Line one.\nLine two.\nLine three.";
81
- const segments = parseMarkdown(input);
82
- assert.equal(segments.length, 1);
83
- assert.equal(segments[0]?.type, "para");
84
- const para = segments[0] as ParaSegment;
85
- assert.equal(para.lines.length, 3);
86
- });
87
-
88
- test("header immediately followed by text produces two segments", () => {
89
- const input = "# Title\n\nSome text here.";
90
- const segments = parseMarkdown(input);
91
- assert.equal(segments[0]?.type, "header");
92
- assert.equal(segments[1]?.type, "para");
93
- });
94
-
95
- test("cleans local markdown file links into compact terminal paths", () => {
96
- const input = "The app shell lives in [`src/App.tsx`](C:/Users/Example/Projects/Project/src/App.tsx#L22).";
97
- const text = segmentText(parseMarkdown(input));
98
-
99
- assert.match(text, /src\/App\.tsx:22/);
100
- assert.doesNotMatch(text, /C:\/Users/);
101
- assert.doesNotMatch(text, /\]\(/);
102
- });
103
-
104
- test("cleans Windows absolute paths in prose", () => {
105
- const input = "Formal proof: C:\\Users\\Example\\Projects\\Project\\docs\\proof.md#L26";
106
- const text = segmentText(parseMarkdown(input));
107
-
108
- assert.match(text, /docs\/proof\.md:26/);
109
- assert.doesNotMatch(text, /C:\\Users/);
110
- });
111
-
112
- test("cleans file paths with encoded spaces", () => {
113
- const input = "Overview: [README.md](file:///C:/Users/Example/Projects/5-Date%20Verification/README.md)";
114
- const text = segmentText(parseMarkdown(input));
115
-
116
- assert.match(text, /README\.md/);
117
- assert.doesNotMatch(text, /file:\/\//);
118
- assert.doesNotMatch(text, /5-Date%20Verification/);
119
- });
120
-
121
- test("cleans local line ranges", () => {
122
- const input = "See [proof](C:/Users/Example/Projects/Project/docs/proof.md#L26-L31).";
123
- const text = segmentText(parseMarkdown(input));
124
-
125
- assert.match(text, /proof \(docs\/proof\.md:26-31\)/);
126
- assert.doesNotMatch(text, /C:\/Users/);
127
- });
128
-
129
- test("external web markdown links remain unchanged", () => {
130
- const input = "Docs: [OpenAI](https://platform.openai.com/docs).";
131
- const text = segmentText(parseMarkdown(input));
132
-
133
- assert.match(text, /\[OpenAI\]\(https:\/\/platform\.openai\.com\/docs\)/);
134
- });
135
-
136
- test("inline-code web links remain unchanged", () => {
137
- const input = "Use `https://platform.openai.com/docs` for reference.";
138
- const text = segmentText(parseMarkdown(input));
139
-
140
- assert.match(text, /https:\/\/platform\.openai\.com\/docs/);
141
- });
142
-
143
- test("code blocks are not rewritten by terminal answer cleanup", () => {
144
- const input = [
145
- "```text",
146
- "C:/Users/Example/Project/src/App.tsx#L22",
147
- "[README.md](file:///C:/Project/README.md)",
148
- "```",
149
- ].join("\n");
150
- const segments = parseMarkdown(input);
151
- const code = segments[0] as CodeSegment;
152
-
153
- assert.deepEqual(code.lines, [
154
- "C:/Users/Example/Project/src/App.tsx#L22",
155
- "[README.md](file:///C:/Project/README.md)",
156
- ]);
157
- });
@@ -1,411 +0,0 @@
1
- /**
2
- * Tests that the model picker is correctly scoped to each provider:
3
- * - model lists contain only models for the correct provider
4
- * - picker title/copy uses the provider-specific label
5
- * - modelPickerLabel is set on each runtime
6
- */
7
-
8
- import assert from "node:assert/strict";
9
- import { PassThrough } from "node:stream";
10
- import test from "node:test";
11
- import React from "react";
12
- import { render } from "ink";
13
- import { ThemeProvider } from "./theme.js";
14
- import { ModelPickerScreen } from "./ModelPickerScreen.js";
15
- import { createLayoutSnapshot } from "./layout.js";
16
- import { ANTHROPIC_FALLBACK_MODELS, GEMINI_FALLBACK_MODELS, providerModelsToCodexCapabilities } from "../core/providerRuntime/models.js";
17
- import { anthropicRuntime } from "../core/providerRuntime/anthropic.js";
18
- import { geminiRuntime } from "../core/providerRuntime/gemini.js";
19
- import { getSelectableModelCapabilities } from "../core/models/codexModelCapabilities.js";
20
- import { ProviderPicker } from "./ProviderPicker.js";
21
- import type { ProviderConfig } from "../core/providerLauncher/types.js";
22
-
23
- class TestInput extends PassThrough {
24
- readonly isTTY = true;
25
- setRawMode(): this { return this; }
26
- override resume(): this { return this; }
27
- override pause(): this { return this; }
28
- ref(): this { return this; }
29
- unref(): this { return this; }
30
- }
31
-
32
- class TestOutput extends PassThrough {
33
- readonly isTTY = true;
34
- columns = 120;
35
- rows = 40;
36
- }
37
-
38
- function stripAnsi(value: string): string {
39
- return value.replace(/\[[0-?]*[ -/]*[@-~]/g, "");
40
- }
41
-
42
- function sleep(ms = 50): Promise<void> {
43
- return new Promise((resolve) => setTimeout(resolve, ms));
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Model list content — no cross-provider leakage
48
- // ---------------------------------------------------------------------------
49
-
50
- test("ANTHROPIC_FALLBACK_MODELS uses short CLI alias model IDs (not full API IDs)", () => {
51
- const ids = ANTHROPIC_FALLBACK_MODELS.map((m) => m.modelId);
52
- for (const id of ids) {
53
- assert.ok(
54
- !id.startsWith("gpt-"),
55
- `Anthropic model list must not contain OpenAI model: "${id}"`,
56
- );
57
- }
58
- // Short aliases, not full versioned API IDs
59
- assert.ok(ids.includes("opus"), "Missing opus alias");
60
- assert.ok(ids.includes("sonnet"), "Missing sonnet alias");
61
- assert.ok(ids.includes("haiku"), "Missing haiku alias");
62
- });
63
-
64
- test("ANTHROPIC_FALLBACK_MODELS contains the expected models", () => {
65
- const ids = ANTHROPIC_FALLBACK_MODELS.map((m) => m.modelId);
66
- assert.ok(ids.includes("opus"), "Missing opus");
67
- assert.ok(ids.includes("sonnet"), "Missing sonnet");
68
- assert.ok(ids.includes("haiku"), "Missing haiku");
69
- });
70
-
71
- test("ANTHROPIC_FALLBACK_MODELS does not contain OpenAI model IDs", () => {
72
- for (const m of ANTHROPIC_FALLBACK_MODELS) {
73
- assert.ok(
74
- !m.modelId.startsWith("gpt-"),
75
- `Anthropic model list must not contain OpenAI model: "${m.modelId}"`,
76
- );
77
- }
78
- });
79
-
80
- test("GEMINI_FALLBACK_MODELS contains only gemini- model IDs", () => {
81
- for (const m of GEMINI_FALLBACK_MODELS) {
82
- assert.ok(
83
- m.modelId.startsWith("gemini-"),
84
- `Expected modelId to start with "gemini-", got: "${m.modelId}"`,
85
- );
86
- }
87
- });
88
-
89
- test("GEMINI_FALLBACK_MODELS contains the expected models", () => {
90
- const ids = GEMINI_FALLBACK_MODELS.map((m) => m.modelId);
91
- assert.equal(ids[0], "gemini-3-flash-preview", "Default fast Gemini route should stay first");
92
- assert.ok(ids.includes("gemini-3.1-pro-preview"), "Missing gemini-3.1-pro-preview");
93
- assert.ok(ids.includes("gemini-3-flash-preview"), "Missing gemini-3-flash-preview");
94
- assert.ok(ids.includes("gemini-3.1-flash-lite-preview"), "Missing gemini-3.1-flash-lite-preview");
95
- assert.ok(ids.includes("gemini-2.5-pro"), "Missing gemini-2.5-pro");
96
- assert.ok(ids.includes("gemini-2.5-flash"), "Missing gemini-2.5-flash");
97
- assert.ok(ids.includes("gemini-2.5-flash-lite"), "Missing gemini-2.5-flash-lite");
98
- assert.ok(!ids.includes("gemini-3-flash"), "Gemini 3 Flash must not be offered; use preview ID");
99
- assert.ok(!ids.includes("gemini-3.1-pro"), "Gemini 3.1 Pro must not be offered without preview suffix");
100
- });
101
-
102
- test("GEMINI_FALLBACK_MODELS maps display names to exact CLI IDs with no reasoning support", () => {
103
- const expected = new Map([
104
- ["Gemini 3.1 Pro Preview", "gemini-3.1-pro-preview"],
105
- ["Gemini 3 Flash Preview", "gemini-3-flash-preview"],
106
- ["Gemini 3.1 Flash Lite Preview", "gemini-3.1-flash-lite-preview"],
107
- ["Gemini 2.5 Pro", "gemini-2.5-pro"],
108
- ["Gemini 2.5 Flash", "gemini-2.5-flash"],
109
- ["Gemini 2.5 Flash Lite", "gemini-2.5-flash-lite"],
110
- ]);
111
-
112
- assert.equal(GEMINI_FALLBACK_MODELS.length, expected.size);
113
- for (const model of GEMINI_FALLBACK_MODELS) {
114
- assert.equal(model.modelId, expected.get(model.label), `Unexpected CLI ID for ${model.label}`);
115
- assert.equal(model.id, model.modelId);
116
- assert.equal(model.supportedReasoningLevels, null);
117
- }
118
- });
119
-
120
- test("GEMINI_FALLBACK_MODELS does not contain OpenAI or Claude model IDs", () => {
121
- for (const m of GEMINI_FALLBACK_MODELS) {
122
- assert.ok(!m.modelId.startsWith("gpt-"), `Gemini list must not contain OpenAI model: "${m.modelId}"`);
123
- assert.ok(!m.modelId.startsWith("claude-"), `Gemini list must not contain Claude model: "${m.modelId}"`);
124
- }
125
- });
126
-
127
- // ---------------------------------------------------------------------------
128
- // providerModelsToCodexCapabilities conversion
129
- // ---------------------------------------------------------------------------
130
-
131
- test("providerModelsToCodexCapabilities converts Anthropic models to selectable capabilities", () => {
132
- const caps = providerModelsToCodexCapabilities(ANTHROPIC_FALLBACK_MODELS, "sonnet");
133
- const selectable = getSelectableModelCapabilities(caps);
134
-
135
- assert.ok(selectable.length > 0, "Should produce selectable models");
136
- const modelIds = selectable.map((m) => m.model);
137
- assert.ok(modelIds.includes("sonnet"), "Should include sonnet alias");
138
- assert.ok(modelIds.includes("opus"), "Should include opus alias");
139
- assert.ok(modelIds.includes("haiku"), "Should include haiku alias");
140
- for (const id of modelIds) {
141
- assert.ok(!id.startsWith("gpt-"), `Converted Anthropic capabilities must not contain OpenAI model: "${id}"`);
142
- }
143
- const sonnet = selectable.find((model) => model.model === "sonnet");
144
- assert.deepEqual(
145
- sonnet?.supportedReasoningLevels?.map((level) => level.id),
146
- ["low", "medium", "high", "max"],
147
- );
148
- });
149
-
150
- test("Claude reasoning options do not include OpenAI-only levels", () => {
151
- const caps = providerModelsToCodexCapabilities(ANTHROPIC_FALLBACK_MODELS, "sonnet");
152
- const sonnet = getSelectableModelCapabilities(caps).find((model) => model.model === "sonnet");
153
- const opus = getSelectableModelCapabilities(caps).find((model) => model.model === "opus");
154
- const ids = sonnet?.supportedReasoningLevels?.map((level) => level.id) ?? [];
155
-
156
- assert.deepEqual(ids, ["low", "medium", "high", "max"]);
157
- assert.deepEqual(opus?.supportedReasoningLevels?.map((level) => level.id), ["low", "medium", "high", "xhigh", "max"]);
158
- assert.ok(!ids.includes("xhigh"), "Sonnet fallback must not show xhigh unless verified");
159
- assert.ok(!ids.includes("none"), "Claude picker must not show OpenAI none reasoning");
160
- assert.ok(!ids.includes("minimal"), "Claude picker must not show OpenAI minimal reasoning");
161
- });
162
-
163
- test("providerModelsToCodexCapabilities converts Gemini models to selectable capabilities", () => {
164
- const caps = providerModelsToCodexCapabilities(GEMINI_FALLBACK_MODELS, "gemini-2.5-pro");
165
- const selectable = getSelectableModelCapabilities(caps);
166
-
167
- assert.ok(selectable.length > 0, "Should produce selectable models");
168
- const modelIds = selectable.map((m) => m.model);
169
- assert.ok(modelIds.includes("gemini-3.1-pro-preview"), "Should include gemini-3.1-pro-preview");
170
- assert.ok(modelIds.includes("gemini-3-flash-preview"), "Should include gemini-3-flash-preview");
171
- assert.ok(modelIds.includes("gemini-3.1-flash-lite-preview"), "Should include gemini-3.1-flash-lite-preview");
172
- assert.ok(modelIds.includes("gemini-2.5-pro"), "Should include gemini-2.5-pro");
173
- assert.ok(modelIds.includes("gemini-2.5-flash"), "Should include gemini-2.5-flash");
174
- assert.ok(modelIds.includes("gemini-2.5-flash-lite"), "Should include gemini-2.5-flash-lite");
175
- assert.ok(!modelIds.includes("gemini-3-flash"), "Should not include legacy non-preview Gemini 3 Flash");
176
- for (const id of modelIds) {
177
- assert.ok(!id.startsWith("gpt-"), `Converted Gemini capabilities must not contain OpenAI model: "${id}"`);
178
- assert.ok(!id.startsWith("claude-"), `Converted Gemini capabilities must not contain Claude model: "${id}"`);
179
- }
180
- for (const model of selectable) {
181
- assert.equal(model.supportedReasoningLevels, null);
182
- }
183
- });
184
-
185
- // ---------------------------------------------------------------------------
186
- // ProviderRuntime.modelPickerLabel
187
- // ---------------------------------------------------------------------------
188
-
189
- test("anthropicRuntime.modelPickerLabel is 'Claude'", () => {
190
- assert.equal(anthropicRuntime.modelPickerLabel, "Claude");
191
- });
192
-
193
- test("geminiRuntime.modelPickerLabel is 'Gemini'", () => {
194
- assert.equal(geminiRuntime.modelPickerLabel, "Gemini");
195
- });
196
-
197
- // ---------------------------------------------------------------------------
198
- // ModelPickerScreen renders correct copy per provider label
199
- // ---------------------------------------------------------------------------
200
-
201
- test("model picker renders 'Choose a Claude model' when activeProviderLabel is Claude", async () => {
202
- const stdin = new TestInput();
203
- const stdout = new TestOutput();
204
- let output = "";
205
- stdout.on("data", (chunk) => { output += chunk.toString(); });
206
-
207
- const claudeModels = getSelectableModelCapabilities(
208
- providerModelsToCodexCapabilities(ANTHROPIC_FALLBACK_MODELS, "sonnet"),
209
- );
210
-
211
- const { cleanup } = render(
212
- <ThemeProvider theme="mono">
213
- <ModelPickerScreen
214
- layout={createLayoutSnapshot(120, 40)}
215
- models={claudeModels}
216
- currentModel="sonnet"
217
- currentReasoning="high"
218
- activeProviderLabel="Claude"
219
- onSelect={() => {}}
220
- onCancel={() => {}}
221
- />
222
- </ThemeProvider>,
223
- { stdin: stdin as any, stdout: stdout as any, debug: true },
224
- );
225
-
226
- try {
227
- await sleep(100);
228
- const stripped = stripAnsi(output);
229
- assert.match(stripped, /Choose a Claude model to use inside Codexa/);
230
- // Must not say "OpenAI"
231
- assert.ok(!stripped.includes("OpenAI"), "Should not mention OpenAI when picking Claude models");
232
- } finally {
233
- cleanup();
234
- }
235
- });
236
-
237
- test("model picker renders 'Choose a Gemini model' when activeProviderLabel is Gemini", async () => {
238
- const stdin = new TestInput();
239
- const stdout = new TestOutput();
240
- let output = "";
241
- stdout.on("data", (chunk) => { output += chunk.toString(); });
242
-
243
- const geminiModels = getSelectableModelCapabilities(
244
- providerModelsToCodexCapabilities(GEMINI_FALLBACK_MODELS, "gemini-2.5-pro"),
245
- );
246
-
247
- const { cleanup } = render(
248
- <ThemeProvider theme="mono">
249
- <ModelPickerScreen
250
- layout={createLayoutSnapshot(120, 40)}
251
- models={geminiModels}
252
- currentModel="gemini-2.5-pro"
253
- currentReasoning="high"
254
- activeProviderLabel="Gemini"
255
- onSelect={() => {}}
256
- onCancel={() => {}}
257
- />
258
- </ThemeProvider>,
259
- { stdin: stdin as any, stdout: stdout as any, debug: true },
260
- );
261
-
262
- try {
263
- await sleep(100);
264
- const stripped = stripAnsi(output);
265
- assert.match(stripped, /Choose a Gemini model to use inside Codexa/);
266
- assert.ok(!stripped.includes("OpenAI"), "Should not mention OpenAI when picking Gemini models");
267
- } finally {
268
- cleanup();
269
- }
270
- });
271
-
272
- test("model picker renders 'Choose an OpenAI model' when activeProviderLabel is OpenAI", async () => {
273
- const stdin = new TestInput();
274
- const stdout = new TestOutput();
275
- let output = "";
276
- stdout.on("data", (chunk) => { output += chunk.toString(); });
277
-
278
- const { cleanup } = render(
279
- <ThemeProvider theme="mono">
280
- <ModelPickerScreen
281
- layout={createLayoutSnapshot(120, 40)}
282
- models={[]}
283
- currentModel="gpt-5.4"
284
- currentReasoning="high"
285
- activeProviderLabel="OpenAI"
286
- onSelect={() => {}}
287
- onCancel={() => {}}
288
- />
289
- </ThemeProvider>,
290
- { stdin: stdin as any, stdout: stdout as any, debug: true },
291
- );
292
-
293
- try {
294
- await sleep(100);
295
- const stripped = stripAnsi(output);
296
- assert.match(stripped, /Choose an OpenAI model to use inside Codexa/);
297
- } finally {
298
- cleanup();
299
- }
300
- });
301
-
302
- test("model picker Claude list does not contain OpenAI models", async () => {
303
- const stdin = new TestInput();
304
- const stdout = new TestOutput();
305
- let output = "";
306
- stdout.on("data", (chunk) => { output += chunk.toString(); });
307
-
308
- const claudeModels = getSelectableModelCapabilities(
309
- providerModelsToCodexCapabilities(ANTHROPIC_FALLBACK_MODELS, "sonnet"),
310
- );
311
-
312
- const { cleanup } = render(
313
- <ThemeProvider theme="mono">
314
- <ModelPickerScreen
315
- layout={createLayoutSnapshot(120, 40)}
316
- models={claudeModels}
317
- currentModel="sonnet"
318
- currentReasoning="high"
319
- activeProviderLabel="Claude"
320
- onSelect={() => {}}
321
- onCancel={() => {}}
322
- />
323
- </ThemeProvider>,
324
- { stdin: stdin as any, stdout: stdout as any, debug: true },
325
- );
326
-
327
- try {
328
- await sleep(100);
329
- const stripped = stripAnsi(output);
330
- // OpenAI model names must not appear in the rendered output
331
- assert.ok(!stripped.includes("gpt-5"), "Claude picker must not display OpenAI models");
332
- // Claude models must appear (new labels: "Sonnet 4.6", "Opus 4.7", "Haiku 4.5")
333
- assert.ok(
334
- stripped.includes("Sonnet 4.6") || stripped.includes("Opus 4.7") || stripped.includes("Haiku 4.5"),
335
- "Claude picker must display Claude model names",
336
- );
337
- } finally {
338
- cleanup();
339
- }
340
- });
341
-
342
- // ---------------------------------------------------------------------------
343
- // ProviderPicker initialProviderId — lands in actions panel for that provider
344
- // ---------------------------------------------------------------------------
345
-
346
- test("ProviderPicker with initialProviderId=anthropic starts in actions mode showing anthropic actions", async () => {
347
- const stdin = new TestInput();
348
- const stdout = new TestOutput();
349
- let output = "";
350
- stdout.on("data", (chunk) => { output += chunk.toString(); });
351
-
352
- const mockProviders: ProviderConfig[] = [
353
- {
354
- id: "openai",
355
- displayName: "OpenAI",
356
- currentModel: "gpt-5",
357
- backendType: "openai-api-key",
358
- routeMode: "in-codexa",
359
- enabled: true,
360
- statusLabel: "Enabled",
361
- launchCommand: null,
362
- isDefault: false,
363
- isActiveRoute: true,
364
- routeUnavailableReason: null,
365
- },
366
- {
367
- id: "anthropic",
368
- displayName: "Anthropic",
369
- currentModel: "sonnet",
370
- backendType: "claude-code-auth",
371
- routeMode: "in-codexa",
372
- enabled: true,
373
- statusLabel: "Enabled",
374
- launchCommand: null,
375
- isDefault: false,
376
- isActiveRoute: false,
377
- routeUnavailableReason: null,
378
- },
379
- ];
380
-
381
- const { cleanup } = render(
382
- <ThemeProvider theme="mono">
383
- <ProviderPicker
384
- layout={createLayoutSnapshot(120, 40)}
385
- providers={mockProviders}
386
- onAction={() => {}}
387
- onCancel={() => {}}
388
- initialProviderId="anthropic"
389
- />
390
- </ThemeProvider>,
391
- { stdin: stdin as any, stdout: stdout as any, debug: true },
392
- );
393
-
394
- try {
395
- await sleep(100);
396
- const stripped = stripAnsi(output);
397
- // Should be in actions mode for Anthropic — shows action items, not the provider list
398
- assert.ok(
399
- stripped.includes("Anthropic") || stripped.includes("anthropic"),
400
- "ProviderPicker should show Anthropic context when initialProviderId=anthropic",
401
- );
402
- // Should NOT be stuck on OpenAI's panel
403
- assert.ok(
404
- !stripped.includes("Select model") || stripped.includes("Anthropic"),
405
- "Actions panel should be scoped to Anthropic",
406
- );
407
- assert.ok(stripped.includes("Refresh Claude capabilities"), "Anthropic action should refresh Claude capabilities");
408
- } finally {
409
- cleanup();
410
- }
411
- });