@golba98/codexa 1.0.2 → 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 (150) hide show
  1. package/README.md +391 -94
  2. package/package.json +12 -7
  3. package/src/app.tsx +149 -117
  4. package/src/commands/handler.ts +4 -4
  5. package/src/config/buildInfo.ts +2 -2
  6. package/src/config/persistence.ts +10 -0
  7. package/src/config/settings.ts +7 -15
  8. package/src/config/updateCheckCache.ts +19 -1
  9. package/src/core/channel.ts +23 -0
  10. package/src/core/executables/codexExecutable.ts +1 -0
  11. package/src/core/executables/executableResolver.ts +61 -43
  12. package/src/core/launchContext.ts +33 -33
  13. package/src/core/process/processValidation.ts +9 -5
  14. package/src/core/providerLauncher/launcher.ts +59 -42
  15. package/src/core/updateCheck.ts +111 -95
  16. package/src/session/types.ts +2 -1
  17. package/src/ui/ActionRequiredBlock.tsx +5 -5
  18. package/src/ui/ActivityBars.tsx +3 -3
  19. package/src/ui/ActivityIndicator.tsx +6 -6
  20. package/src/ui/AgentBlock.tsx +6 -6
  21. package/src/ui/AnimatedStatusText.tsx +1 -1
  22. package/src/ui/AppShell.tsx +9 -24
  23. package/src/ui/AttachmentImportPanel.tsx +8 -8
  24. package/src/ui/AuthPanel.tsx +20 -20
  25. package/src/ui/BottomComposer.tsx +117 -78
  26. package/src/ui/DashCard.tsx +3 -3
  27. package/src/ui/Markdown.tsx +17 -17
  28. package/src/ui/ModelPickerScreen.tsx +10 -10
  29. package/src/ui/ModelReasoningPicker.tsx +14 -14
  30. package/src/ui/Panel.tsx +3 -3
  31. package/src/ui/PlanActionPicker.tsx +6 -6
  32. package/src/ui/PlanReviewPanel.tsx +8 -8
  33. package/src/ui/ProviderPicker.tsx +18 -18
  34. package/src/ui/RunFooter.tsx +3 -3
  35. package/src/ui/SelectionPanel.tsx +4 -4
  36. package/src/ui/SettingsPanel.tsx +8 -8
  37. package/src/ui/Spinner.tsx +1 -1
  38. package/src/ui/TextEntryPanel.tsx +10 -10
  39. package/src/ui/ThinkingBlock.tsx +8 -8
  40. package/src/ui/Timeline.tsx +21 -17
  41. package/src/ui/TopHeader.tsx +421 -293
  42. package/src/ui/TurnGroup.tsx +31 -31
  43. package/src/ui/UpdateAvailableCard.tsx +41 -0
  44. package/src/ui/UpdatePromptPanel.tsx +203 -0
  45. package/src/ui/focus.ts +3 -0
  46. package/src/ui/layout.ts +1 -1
  47. package/src/ui/logoVariants.ts +107 -0
  48. package/src/ui/modeDisplay.ts +12 -12
  49. package/src/ui/runtimeDisplay.ts +100 -0
  50. package/src/ui/theme.tsx +274 -395
  51. package/src/ui/timelineMeasure.ts +29 -19
  52. package/scripts/audit-codexa-capabilities.mjs +0 -466
  53. package/scripts/gen-build-info.mjs +0 -33
  54. package/scripts/smoke-terminal-bench.mjs +0 -35
  55. package/src/appRenderStability.test.ts +0 -131
  56. package/src/commands/handler.test.ts +0 -655
  57. package/src/config/launchArgs.test.ts +0 -189
  58. package/src/config/layeredConfig.test.ts +0 -143
  59. package/src/config/persistence.test.ts +0 -114
  60. package/src/config/runtimeConfig.test.ts +0 -218
  61. package/src/config/settings.test.ts +0 -155
  62. package/src/config/trustStore.test.ts +0 -29
  63. package/src/core/attachments.test.ts +0 -155
  64. package/src/core/auth/codexAuth.test.ts +0 -68
  65. package/src/core/cleanupFastFail.test.ts +0 -76
  66. package/src/core/codexExecArgs.test.ts +0 -195
  67. package/src/core/codexLaunch.test.ts +0 -205
  68. package/src/core/codexPrompt.test.ts +0 -252
  69. package/src/core/executables/codexExecutable.test.ts +0 -212
  70. package/src/core/executables/executableResolver.test.ts +0 -129
  71. package/src/core/executables/geminiExecutable.test.ts +0 -116
  72. package/src/core/executables/pathSanityScan.test.ts +0 -47
  73. package/src/core/githubDiagnostics.test.ts +0 -92
  74. package/src/core/hollowResponseFormat.test.ts +0 -58
  75. package/src/core/launchContext.test.ts +0 -157
  76. package/src/core/models/codexCapabilities.test.ts +0 -45
  77. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  78. package/src/core/models/modelSpecs.test.ts +0 -283
  79. package/src/core/perf/renderDebug.test.ts +0 -230
  80. package/src/core/planStorage.test.ts +0 -143
  81. package/src/core/process/CommandRunner.test.ts +0 -105
  82. package/src/core/projectInstructions.test.ts +0 -50
  83. package/src/core/providerLauncher/launcher.test.ts +0 -238
  84. package/src/core/providerLauncher/registry.test.ts +0 -324
  85. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  86. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  87. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  88. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  89. package/src/core/providerRuntime/gemini.test.ts +0 -437
  90. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  91. package/src/core/providerRuntime/local.test.ts +0 -787
  92. package/src/core/providerRuntime/registry.test.ts +0 -233
  93. package/src/core/providers/codexJsonStream.test.ts +0 -148
  94. package/src/core/providers/codexSubprocess.test.ts +0 -68
  95. package/src/core/providers/codexTranscript.test.ts +0 -284
  96. package/src/core/terminal/startupClear.test.ts +0 -55
  97. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  98. package/src/core/terminal/terminalControl.test.ts +0 -75
  99. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  100. package/src/core/terminal/terminalSelection.test.ts +0 -42
  101. package/src/core/terminal/terminalTitle.test.ts +0 -328
  102. package/src/core/updateCheck.test.ts +0 -194
  103. package/src/core/workspaceActivity.test.ts +0 -163
  104. package/src/core/workspaceGuard.test.ts +0 -151
  105. package/src/core/workspaceRoot.test.ts +0 -23
  106. package/src/exec.test.ts +0 -13
  107. package/src/headless/execArgs.test.ts +0 -147
  108. package/src/headless/execRunner.test.ts +0 -436
  109. package/src/index.test.tsx +0 -620
  110. package/src/session/appSession.test.ts +0 -897
  111. package/src/session/chatLifecycle.test.ts +0 -64
  112. package/src/session/liveRenderScheduler.test.ts +0 -201
  113. package/src/session/planFlow.test.ts +0 -103
  114. package/src/session/planTranscript.test.ts +0 -65
  115. package/src/session/promptRunSchedule.test.ts +0 -36
  116. package/src/ui/ActivityIndicator.test.tsx +0 -58
  117. package/src/ui/AgentBlock.test.ts +0 -6
  118. package/src/ui/AnimatedStatusText.test.ts +0 -16
  119. package/src/ui/AppShell.test.tsx +0 -1776
  120. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  121. package/src/ui/BottomComposer.test.ts +0 -674
  122. package/src/ui/CodexLogo.tsx +0 -55
  123. package/src/ui/Markdown.test.ts +0 -157
  124. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  125. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  126. package/src/ui/ModelPickerState.test.tsx +0 -151
  127. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  128. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  129. package/src/ui/PromptCardBorder.test.tsx +0 -161
  130. package/src/ui/ProviderPicker.test.tsx +0 -289
  131. package/src/ui/ProviderShortcut.test.tsx +0 -143
  132. package/src/ui/SettingsPanel.test.tsx +0 -233
  133. package/src/ui/Timeline.test.ts +0 -2067
  134. package/src/ui/TimelineNavigation.test.tsx +0 -201
  135. package/src/ui/TopHeader.test.tsx +0 -254
  136. package/src/ui/TurnGroup.test.tsx +0 -365
  137. package/src/ui/busyStatusAnimation.test.ts +0 -30
  138. package/src/ui/commandNormalize.test.ts +0 -142
  139. package/src/ui/diffRenderer.test.ts +0 -102
  140. package/src/ui/focusFlow.test.tsx +0 -1098
  141. package/src/ui/inputBuffer.test.ts +0 -151
  142. package/src/ui/layout.test.ts +0 -146
  143. package/src/ui/modeDisplay.test.ts +0 -42
  144. package/src/ui/runActivityView.test.ts +0 -89
  145. package/src/ui/runLifecycleView.test.tsx +0 -237
  146. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  147. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  148. package/src/ui/textLayout.test.ts +0 -18
  149. package/src/ui/themeFlow.test.ts +0 -53
  150. 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
- });