@golba98/codexa 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,157 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import test from "node:test";
6
- import {
7
- buildWorkspaceStatusMessage,
8
- createWorkspaceRelaunchPlan,
9
- guardWorkspaceRelaunch,
10
- resolveLaunchContext,
11
- } from "./launchContext.js";
12
- import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
13
-
14
- function createTempWorkspace(): string {
15
- return mkdtempSync(join(tmpdir(), "codexa-launch-context-"));
16
- }
17
-
18
- test("uses installed launcher metadata when available", () => {
19
- const context = resolveLaunchContext({
20
- workspaceRoot: "C:/target",
21
- packageRoot: "C:/repo",
22
- execPath: "C:/Program Files/nodejs/node.exe",
23
- env: {
24
- CODEXA_LAUNCH_KIND: "installed-bin",
25
- CODEXA_PACKAGE_ROOT: "C:/repo",
26
- CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
27
- CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
28
- CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
29
- },
30
- });
31
-
32
- assert.equal(context.launchKind, "installed-bin");
33
- assert.equal(context.packageRoot, "C:\\repo");
34
- assert.equal(context.launcherScriptPath, "C:/repo/bin/codexa.js");
35
- assert.equal(context.relaunchExecutable, "C:/Program Files/nodejs/node.exe");
36
- assert.deepEqual(context.relaunchArgs, ["C:/repo/bin/codexa.js", "--profile", "review"]);
37
- });
38
-
39
- test("falls back to bun repo launch metadata when no installed launcher env exists", () => {
40
- const context = resolveLaunchContext({
41
- workspaceRoot: "C:/repo",
42
- packageRoot: "C:/repo",
43
- execPath: "C:/tools/bun.exe",
44
- hasBunRuntime: true,
45
- forwardArgs: ["--profile", "review", "--config", "model=\"gpt-5.4\""],
46
- env: {},
47
- });
48
-
49
- assert.equal(context.launchKind, "dev-run");
50
- assert.equal(context.relaunchExecutable, "C:/tools/bun.exe");
51
- assert.deepEqual(context.relaunchArgs, [
52
- "run",
53
- "--silent",
54
- join("C:\\repo", "src", "index.tsx"),
55
- "--",
56
- "--profile",
57
- "review",
58
- "--config",
59
- "model=\"gpt-5.4\"",
60
- ]);
61
- });
62
-
63
- test("creates an installed-bin relaunch plan with normalized target cwd and env", () => {
64
- const root = createTempWorkspace();
65
- try {
66
- const nextWorkspace = join(root, "next");
67
- mkdirSync(nextWorkspace);
68
-
69
- const context = resolveLaunchContext({
70
- workspaceRoot: root,
71
- packageRoot: "C:/repo",
72
- execPath: "C:/Program Files/nodejs/node.exe",
73
- env: {
74
- CODEXA_LAUNCH_KIND: "installed-bin",
75
- CODEXA_PACKAGE_ROOT: "C:/repo",
76
- CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
77
- CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
78
- CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
79
- },
80
- });
81
-
82
- const result = createWorkspaceRelaunchPlan(".\\next", context, {});
83
- assert.equal(result.ok, true);
84
- if (!result.ok) return;
85
-
86
- assert.equal(result.plan.executable, "C:/Program Files/nodejs/node.exe");
87
- assert.deepEqual(result.plan.args, ["C:/repo/bin/codexa.js", "--profile", "review"]);
88
- assert.equal(result.plan.cwd, normalizeWorkspaceRoot(nextWorkspace));
89
- assert.equal(result.plan.env.CODEX_WORKSPACE_ROOT, normalizeWorkspaceRoot(nextWorkspace));
90
- assert.equal(result.plan.env.CODEXA_LAUNCH_KIND, "installed-bin");
91
- } finally {
92
- rmSync(root, { recursive: true, force: true });
93
- }
94
- });
95
-
96
- test("rejects relaunch targets that do not exist", () => {
97
- const root = createTempWorkspace();
98
- try {
99
- const context = resolveLaunchContext({
100
- workspaceRoot: root,
101
- packageRoot: root,
102
- execPath: "C:/tools/bun.exe",
103
- hasBunRuntime: true,
104
- env: {},
105
- });
106
-
107
- const result = createWorkspaceRelaunchPlan(".\\missing", context, {});
108
- assert.equal(result.ok, false);
109
- if (result.ok) return;
110
- assert.match(result.message, /does not exist/i);
111
- } finally {
112
- rmSync(root, { recursive: true, force: true });
113
- }
114
- });
115
-
116
- test("rejects relaunch targets that are files instead of directories", () => {
117
- const root = createTempWorkspace();
118
- try {
119
- const fileTarget = join(root, "notes.txt");
120
- writeFileSync(fileTarget, "hello", "utf8");
121
- const context = resolveLaunchContext({
122
- workspaceRoot: root,
123
- packageRoot: root,
124
- execPath: "C:/tools/bun.exe",
125
- hasBunRuntime: true,
126
- env: {},
127
- });
128
-
129
- const result = createWorkspaceRelaunchPlan(".\\notes.txt", context, {});
130
- assert.equal(result.ok, false);
131
- if (result.ok) return;
132
- assert.match(result.message, /not a directory/i);
133
- } finally {
134
- rmSync(root, { recursive: true, force: true });
135
- }
136
- });
137
-
138
- test("blocks workspace relaunch while busy", () => {
139
- assert.deepEqual(guardWorkspaceRelaunch(false), { allowed: true });
140
- assert.equal(guardWorkspaceRelaunch(true).allowed, false);
141
- assert.match(guardWorkspaceRelaunch(true).message ?? "", /finish the current run/i);
142
- });
143
-
144
- test("describes dev launch mode with install guidance", () => {
145
- const message = buildWorkspaceStatusMessage(resolveLaunchContext({
146
- workspaceRoot: "C:/repo",
147
- packageRoot: "C:/repo",
148
- execPath: "C:/tools/bun.exe",
149
- hasBunRuntime: true,
150
- env: {},
151
- }));
152
-
153
- assert.match(message, /Launch mode: dev\/repo launch/i);
154
- assert.match(message, /npm link/i);
155
- assert.match(message, /where codexa/i);
156
- assert.match(message, /\/workspace relaunch <path>/i);
157
- });
@@ -1,45 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { parseCodexCliCapabilities } from "./codexCapabilities.js";
4
-
5
- test("detects exec help flags and strips ANSI noise", () => {
6
- const capabilities = parseCodexCliCapabilities(
7
- "\u001B[32mOptions:\u001B[0m\n --ask-for-approval <policy>\n --sandbox <mode>\n -c, --config <key=value>\n",
8
- "Global options:\n --ask-for-approval <policy>\n",
9
- );
10
-
11
- assert.deepEqual(capabilities, {
12
- askForApproval: true,
13
- sandbox: true,
14
- config: true,
15
- fullAuto: false,
16
- });
17
- });
18
-
19
- test("does not promote top-level approval flags into exec capabilities", () => {
20
- const capabilities = parseCodexCliCapabilities(
21
- "Usage: codex exec [OPTIONS]\n --sandbox <mode>\n --full-auto\n",
22
- "Global options:\n --ask-for-approval <policy>\n --sandbox <mode>\n --full-auto\n",
23
- );
24
-
25
- assert.deepEqual(capabilities, {
26
- askForApproval: false,
27
- sandbox: true,
28
- config: false,
29
- fullAuto: true,
30
- });
31
- });
32
-
33
- test("requires exact option tokens instead of prose guesses", () => {
34
- const capabilities = parseCodexCliCapabilities(
35
- "This build documents ask-for-approval compatibility in prose only.\nIt also mentions sandbox_mode in config docs.\n",
36
- "Use full auto mode for trusted environments.\n",
37
- );
38
-
39
- assert.deepEqual(capabilities, {
40
- askForApproval: false,
41
- sandbox: false,
42
- config: false,
43
- fullAuto: false,
44
- });
45
- });
@@ -1,246 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- clearCodexModelCapabilityCache,
5
- createFallbackModelCapabilities,
6
- findModelCapability,
7
- formatModelCapabilitiesList,
8
- getCodexModelCapabilities,
9
- getPreferredModelFromCapabilities,
10
- normalizeCodexModelListResponses,
11
- normalizeReasoningForModelCapabilities,
12
- } from "./codexModelCapabilities.js";
13
-
14
- const SAMPLE_RESPONSE = {
15
- data: [
16
- {
17
- id: "gpt-5.4",
18
- model: "gpt-5.4",
19
- displayName: "gpt-5.4",
20
- description: "Frontier model",
21
- hidden: false,
22
- isDefault: true,
23
- defaultReasoningEffort: "medium",
24
- supportedReasoningEfforts: [
25
- { reasoningEffort: "low", description: "Fast" },
26
- { reasoningEffort: "medium", description: "Balanced" },
27
- { reasoningEffort: "high", description: "Deep" },
28
- { reasoningEffort: "xhigh", description: "Deepest" },
29
- ],
30
- },
31
- {
32
- id: "gpt-5.1-codex-mini",
33
- model: "gpt-5.1-codex-mini",
34
- displayName: "Codex Mini",
35
- description: "Small model",
36
- hidden: false,
37
- isDefault: false,
38
- defaultReasoningEffort: "medium",
39
- supportedReasoningEfforts: [
40
- { reasoningEffort: "medium", description: "Dynamic" },
41
- { reasoningEffort: "high", description: "Deep" },
42
- ],
43
- },
44
- ],
45
- nextCursor: null,
46
- };
47
-
48
- test("normalizes model/list responses with per-model reasoning metadata", () => {
49
- const capabilities = normalizeCodexModelListResponses([SAMPLE_RESPONSE], {
50
- discoveredAt: 123,
51
- executable: "codex.cmd",
52
- });
53
-
54
- assert.equal(capabilities.status, "ready");
55
- assert.equal(capabilities.source, "runtime");
56
- assert.equal(capabilities.executable, "codex.cmd");
57
- assert.equal(capabilities.models.length, 2);
58
- assert.equal(capabilities.models[0]?.model, "gpt-5.4");
59
- assert.equal(capabilities.models[0]?.reasoningLevelCount, 4);
60
- assert.deepEqual(capabilities.models[0]?.supportedReasoningLevels?.map((item) => item.id), [
61
- "low",
62
- "medium",
63
- "high",
64
- "xhigh",
65
- ]);
66
- assert.equal(capabilities.models[1]?.label, "Codex Mini");
67
- assert.equal(capabilities.models[1]?.reasoningLevelCount, 2);
68
- });
69
-
70
- test("normalization rejects empty or malformed discovery output", () => {
71
- assert.throws(
72
- () => normalizeCodexModelListResponses([{ data: [] }]),
73
- /no usable models/i,
74
- );
75
- assert.throws(
76
- () => normalizeCodexModelListResponses([{ data: [{ id: "", model: "" }] }]),
77
- /no usable models/i,
78
- );
79
- });
80
-
81
- test("fallback keeps models but does not invent reasoning support", () => {
82
- const capabilities = createFallbackModelCapabilities(new Error("offline"), {
83
- discoveredAt: 456,
84
- executable: "codex.cmd",
85
- });
86
-
87
- assert.equal(capabilities.status, "fallback");
88
- assert.equal(capabilities.error, "offline");
89
- assert.ok(capabilities.models.length > 0);
90
- assert.equal(capabilities.models[0]?.available, false);
91
- assert.equal(capabilities.models[0]?.supportedReasoningLevels, null);
92
- assert.equal(capabilities.models[0]?.reasoningLevelCount, null);
93
- });
94
-
95
- test("normalizes reasoning by keeping valid values and clamping invalid ones", () => {
96
- const capabilities = normalizeCodexModelListResponses([SAMPLE_RESPONSE]);
97
-
98
- assert.equal(
99
- normalizeReasoningForModelCapabilities("gpt-5.4", "high", capabilities),
100
- "high",
101
- );
102
- assert.equal(
103
- normalizeReasoningForModelCapabilities("gpt-5.1-codex-mini", "xhigh", capabilities),
104
- "medium",
105
- );
106
- });
107
-
108
- test("handles unavailable selected model with runtime default", () => {
109
- const capabilities = normalizeCodexModelListResponses([SAMPLE_RESPONSE]);
110
-
111
- assert.equal(getPreferredModelFromCapabilities(capabilities, "missing-model"), "gpt-5.4");
112
- assert.equal(findModelCapability(capabilities, "Codex Mini"), null);
113
- assert.equal(findModelCapability(capabilities, "gpt-5.1-codex-mini")?.model, "gpt-5.1-codex-mini");
114
- });
115
-
116
- test("formats model list with dynamic reasoning counts", () => {
117
- const capabilities = normalizeCodexModelListResponses([SAMPLE_RESPONSE]);
118
- const message = formatModelCapabilitiesList(capabilities, "gpt-5.1-codex-mini");
119
-
120
- assert.match(message, /Detected from Codex runtime/i);
121
- assert.match(message, /gpt-5\.4.*4 reasoning levels/i);
122
- assert.match(message, /gpt-5\.1-codex-mini.*2 reasoning levels.*\*/i);
123
- });
124
-
125
- test("caches successful discovery and refreshes when forced", async () => {
126
- clearCodexModelCapabilityCache();
127
- let calls = 0;
128
-
129
- const discover = async () => {
130
- calls += 1;
131
- return normalizeCodexModelListResponses([
132
- {
133
- data: [
134
- {
135
- id: `model-${calls}`,
136
- model: `model-${calls}`,
137
- displayName: `Model ${calls}`,
138
- hidden: false,
139
- isDefault: true,
140
- defaultReasoningEffort: "medium",
141
- supportedReasoningEfforts: [{ reasoningEffort: "medium", description: "Default" }],
142
- },
143
- ],
144
- },
145
- ], { discoveredAt: calls, executable: "codex" });
146
- };
147
-
148
- const first = await getCodexModelCapabilities({
149
- executable: "codex",
150
- discover,
151
- now: () => 100,
152
- ttlMs: 1000,
153
- });
154
- const cached = await getCodexModelCapabilities({
155
- executable: "codex",
156
- discover,
157
- now: () => 200,
158
- ttlMs: 1000,
159
- });
160
- const refreshed = await getCodexModelCapabilities({
161
- executable: "codex",
162
- discover,
163
- forceRefresh: true,
164
- now: () => 300,
165
- ttlMs: 1000,
166
- });
167
-
168
- assert.equal(calls, 2);
169
- assert.equal(first.models[0]?.model, "model-1");
170
- assert.equal(cached.models[0]?.model, "model-1");
171
- assert.equal(refreshed.models[0]?.model, "model-2");
172
- clearCodexModelCapabilityCache();
173
- });
174
-
175
- test("concurrent callers share a single in-flight discovery promise", async () => {
176
- clearCodexModelCapabilityCache();
177
- let calls = 0;
178
- let resolveDiscover: (value: unknown) => void = () => {};
179
- const gate = new Promise<unknown>((resolve) => {
180
- resolveDiscover = resolve;
181
- });
182
-
183
- const discover = async () => {
184
- calls += 1;
185
- await gate;
186
- return normalizeCodexModelListResponses([
187
- {
188
- data: [
189
- {
190
- id: "model-a",
191
- model: "model-a",
192
- displayName: "Model A",
193
- hidden: false,
194
- isDefault: true,
195
- defaultReasoningEffort: "medium",
196
- supportedReasoningEfforts: [{ reasoningEffort: "medium", description: "Default" }],
197
- },
198
- ],
199
- },
200
- ], { discoveredAt: 1, executable: "codex" });
201
- };
202
-
203
- const firstPromise = getCodexModelCapabilities({
204
- executable: "codex",
205
- discover,
206
- now: () => 100,
207
- ttlMs: 1000,
208
- });
209
- const secondPromise = getCodexModelCapabilities({
210
- executable: "codex",
211
- discover,
212
- now: () => 100,
213
- ttlMs: 1000,
214
- });
215
- const thirdPromise = getCodexModelCapabilities({
216
- executable: "codex",
217
- discover,
218
- now: () => 100,
219
- ttlMs: 1000,
220
- });
221
-
222
- resolveDiscover(undefined);
223
- const [first, second, third] = await Promise.all([firstPromise, secondPromise, thirdPromise]);
224
-
225
- assert.equal(calls, 1, "discovery should run exactly once when called concurrently");
226
- assert.equal(first.models[0]?.model, "model-a");
227
- assert.equal(second.models[0]?.model, "model-a");
228
- assert.equal(third.models[0]?.model, "model-a");
229
- clearCodexModelCapabilityCache();
230
- });
231
-
232
- test("falls back safely when discovery fails", async () => {
233
- clearCodexModelCapabilityCache();
234
- const capabilities = await getCodexModelCapabilities({
235
- executable: "codex",
236
- discover: async () => {
237
- throw new Error("broken json");
238
- },
239
- now: () => 100,
240
- });
241
-
242
- assert.equal(capabilities.status, "fallback");
243
- assert.match(capabilities.error ?? "", /broken json/);
244
- assert.equal(capabilities.models[0]?.supportedReasoningLevels, null);
245
- clearCodexModelCapabilityCache();
246
- });