@golba98/codexa 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,204 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import React from "react";
4
+ import { PassThrough } from "node:stream";
5
+ import { Box, Text, render } from "ink";
6
+ import { AttachmentImportPanel, type PendingImportFile } from "./AttachmentImportPanel.js";
7
+ import { ThemeProvider } from "./theme.js";
8
+
9
+ class TestInput extends PassThrough {
10
+ readonly isTTY = true;
11
+
12
+ setRawMode(): this {
13
+ return this;
14
+ }
15
+
16
+ override resume(): this {
17
+ return this;
18
+ }
19
+
20
+ override pause(): this {
21
+ return this;
22
+ }
23
+
24
+ ref(): this {
25
+ return this;
26
+ }
27
+
28
+ unref(): this {
29
+ return this;
30
+ }
31
+ }
32
+
33
+ class TestOutput extends PassThrough {
34
+ readonly isTTY = true;
35
+ columns = 120;
36
+ rows = 40;
37
+ }
38
+
39
+ function stripAnsi(value: string): string {
40
+ return value.replace(/\[[0-?]*[ -/]*[@-~]/g, "");
41
+ }
42
+
43
+ function sleep(ms = 50): Promise<void> {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ function createInkHarness(node: React.ReactElement) {
48
+ const stdin = new TestInput();
49
+ const stdout = new TestOutput();
50
+ let output = "";
51
+
52
+ stdout.on("data", (chunk) => {
53
+ output += chunk.toString();
54
+ });
55
+
56
+ const instance = render(node, {
57
+ stdin: stdin as unknown as NodeJS.ReadStream,
58
+ stdout: stdout as unknown as NodeJS.WriteStream,
59
+ stderr: stdout as unknown as NodeJS.WriteStream,
60
+ debug: true,
61
+ exitOnCtrlC: false,
62
+ patchConsole: false,
63
+ });
64
+
65
+ return {
66
+ stdin,
67
+ instance,
68
+ getOutput(): string {
69
+ return stripAnsi(output);
70
+ },
71
+ async cleanup() {
72
+ instance.cleanup();
73
+ await sleep(20);
74
+ },
75
+ };
76
+ }
77
+
78
+ const TEST_FILE: PendingImportFile = {
79
+ srcPath: "C:\\Users\\jorda\\OneDrive\\Screenshots\\Screenshot 2026-05-18.png",
80
+ rawPath: "C:\\Users\\jorda\\OneDrive\\Screenshots\\Screenshot 2026-05-18.png",
81
+ destFilename: "Screenshot 2026-05-18.png",
82
+ isImage: true,
83
+ };
84
+
85
+ const ATTACHMENTS_DIR = "C:\\Development\\1-JavaScript\\13-Custom-CLI-Normal\\.codexa\\attachments";
86
+ const WORKSPACE_ROOT = "C:\\Development\\1-JavaScript\\13-Custom-CLI-Normal";
87
+
88
+ interface HarnessState {
89
+ confirmed: number;
90
+ cancelled: number;
91
+ }
92
+
93
+ function AttachmentImportPanelHarness({
94
+ files = [TEST_FILE],
95
+ modelSupportsVision = null,
96
+ }: {
97
+ files?: PendingImportFile[];
98
+ modelSupportsVision?: boolean | null;
99
+ }) {
100
+ const [state, setState] = React.useState<HarnessState>({ confirmed: 0, cancelled: 0 });
101
+
102
+ return (
103
+ <ThemeProvider theme="purple">
104
+ <Box flexDirection="column">
105
+ <AttachmentImportPanel
106
+ focusId="import-confirmation"
107
+ files={files}
108
+ attachmentsDir={ATTACHMENTS_DIR}
109
+ workspaceRoot={WORKSPACE_ROOT}
110
+ modelSupportsVision={modelSupportsVision}
111
+ onConfirm={() => setState((s) => ({ ...s, confirmed: s.confirmed + 1 }))}
112
+ onCancel={() => setState((s) => ({ ...s, cancelled: s.cancelled + 1 }))}
113
+ />
114
+ <Text>{`confirmed:${state.confirmed}`}</Text>
115
+ <Text>{`cancelled:${state.cancelled}`}</Text>
116
+ </Box>
117
+ </ThemeProvider>
118
+ );
119
+ }
120
+
121
+ test("AttachmentImportPanel renders filename and destination path", async () => {
122
+ const harness = createInkHarness(<AttachmentImportPanelHarness />);
123
+ try {
124
+ await sleep();
125
+ const output = harness.getOutput();
126
+ assert.match(output, /Screenshot 2026-05-18\.png/);
127
+ assert.match(output, /\.codexa\/attachments/);
128
+ } finally {
129
+ await harness.cleanup();
130
+ }
131
+ });
132
+
133
+ test("AttachmentImportPanel Enter key calls onConfirm", async () => {
134
+ const harness = createInkHarness(<AttachmentImportPanelHarness />);
135
+ try {
136
+ await sleep();
137
+ harness.stdin.write("\r");
138
+ await sleep(80);
139
+ const output = harness.getOutput();
140
+ assert.match(output, /confirmed:1/);
141
+ assert.match(output, /cancelled:0/);
142
+ } finally {
143
+ await harness.cleanup();
144
+ }
145
+ });
146
+
147
+ test("AttachmentImportPanel Esc key calls onCancel", async () => {
148
+ const harness = createInkHarness(<AttachmentImportPanelHarness />);
149
+ try {
150
+ await sleep();
151
+ harness.stdin.write("");
152
+ await sleep(80);
153
+ const output = harness.getOutput();
154
+ assert.match(output, /confirmed:0/);
155
+ assert.match(output, /cancelled:1/);
156
+ } finally {
157
+ await harness.cleanup();
158
+ }
159
+ });
160
+
161
+ test("AttachmentImportPanel shows vision warning when modelSupportsVision is false and file is image", async () => {
162
+ const harness = createInkHarness(
163
+ <AttachmentImportPanelHarness modelSupportsVision={false} />,
164
+ );
165
+ try {
166
+ await sleep();
167
+ const output = harness.getOutput();
168
+ assert.match(output, /active model may not support images/i);
169
+ } finally {
170
+ await harness.cleanup();
171
+ }
172
+ });
173
+
174
+ test("AttachmentImportPanel does NOT show vision warning when modelSupportsVision is null", async () => {
175
+ const harness = createInkHarness(
176
+ <AttachmentImportPanelHarness modelSupportsVision={null} />,
177
+ );
178
+ try {
179
+ await sleep();
180
+ const output = harness.getOutput();
181
+ assert.doesNotMatch(output, /active model may not support images/i);
182
+ } finally {
183
+ await harness.cleanup();
184
+ }
185
+ });
186
+
187
+ test("AttachmentImportPanel does NOT show vision warning for non-image file even when modelSupportsVision is false", async () => {
188
+ const textFile: PendingImportFile = {
189
+ srcPath: "C:\\Users\\jorda\\Documents\\notes.txt",
190
+ rawPath: "C:\\Users\\jorda\\Documents\\notes.txt",
191
+ destFilename: "notes.txt",
192
+ isImage: false,
193
+ };
194
+ const harness = createInkHarness(
195
+ <AttachmentImportPanelHarness files={[textFile]} modelSupportsVision={false} />,
196
+ );
197
+ try {
198
+ await sleep();
199
+ const output = harness.getOutput();
200
+ assert.doesNotMatch(output, /active model may not support images/i);
201
+ } finally {
202
+ await harness.cleanup();
203
+ }
204
+ });
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+ import path from "node:path";
4
+ import { useTheme } from "./theme.js";
5
+
6
+ export interface PendingImportFile {
7
+ srcPath: string;
8
+ rawPath: string;
9
+ destFilename: string;
10
+ isImage: boolean;
11
+ }
12
+
13
+ interface AttachmentImportPanelProps {
14
+ focusId: string;
15
+ files: PendingImportFile[];
16
+ attachmentsDir: string;
17
+ workspaceRoot: string;
18
+ modelSupportsVision: boolean | null;
19
+ onConfirm: () => void;
20
+ onCancel: () => void;
21
+ }
22
+
23
+ export function AttachmentImportPanel({
24
+ focusId,
25
+ files,
26
+ attachmentsDir,
27
+ workspaceRoot,
28
+ modelSupportsVision,
29
+ onConfirm,
30
+ onCancel,
31
+ }: AttachmentImportPanelProps) {
32
+ const theme = useTheme();
33
+ const { isFocused } = useFocus({ id: focusId, autoFocus: true });
34
+
35
+ useInput((_, key) => {
36
+ if (key.escape) {
37
+ onCancel();
38
+ return;
39
+ }
40
+ if (key.return) {
41
+ onConfirm();
42
+ return;
43
+ }
44
+ }, { isActive: isFocused });
45
+
46
+ const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
47
+ const hasImages = files.some((f) => f.isImage);
48
+ const showVisionWarning = hasImages && modelSupportsVision === false;
49
+ const fileLabel = files.length === 1 ? "file" : "files";
50
+
51
+ return (
52
+ <Box flexDirection="column" width="100%" marginTop={1}>
53
+ <Box
54
+ borderStyle="round"
55
+ borderColor={theme.BORDER_SUBTLE}
56
+ paddingX={2}
57
+ paddingY={1}
58
+ width="100%"
59
+ >
60
+ <Text color={theme.ACCENT} bold>IMPORT FILE </Text>
61
+ <Text color={theme.MUTED}>
62
+ Copy {files.length} outside-workspace {fileLabel} into .codexa/attachments?
63
+ </Text>
64
+ </Box>
65
+
66
+ <Box
67
+ borderStyle="round"
68
+ borderColor={theme.BORDER_ACTIVE}
69
+ paddingX={2}
70
+ paddingY={1}
71
+ marginTop={1}
72
+ width="100%"
73
+ flexDirection="column"
74
+ >
75
+ {files.map((file, i) => (
76
+ <Box key={i} flexDirection="column" marginBottom={i < files.length - 1 ? 1 : 0}>
77
+ <Text color={theme.TEXT}>{path.basename(file.srcPath)}</Text>
78
+ <Text color={theme.DIM}>
79
+ {"→ "}{relativeAttachmentsDir}/{file.destFilename}
80
+ </Text>
81
+ </Box>
82
+ ))}
83
+
84
+ {showVisionWarning && (
85
+ <Box marginTop={1}>
86
+ <Text color={theme.WARNING}>
87
+ Note: active model may not support images.
88
+ </Text>
89
+ </Box>
90
+ )}
91
+
92
+ <Box marginTop={1}>
93
+ <Text color={theme.DIM}>Enter copy and continue Esc cancel</Text>
94
+ </Box>
95
+ </Box>
96
+ </Box>
97
+ );
98
+ }
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+ import { AUTH_PREFERENCES, formatAuthPreferenceLabel } from "../config/settings.js";
4
+ import type { CodexAuthProbeResult } from "../core/auth/codexAuth.js";
5
+ import { getAuthStateLabel } from "../core/auth/codexAuth.js";
6
+ import type { BackendProvider } from "../core/providers/types.js";
7
+ import { useTheme } from "./theme.js";
8
+
9
+ interface AuthPanelProps {
10
+ focusId: string;
11
+ provider: BackendProvider;
12
+ authPreference: string;
13
+ authStatus: CodexAuthProbeResult;
14
+ authStatusBusy: boolean;
15
+ onSetPreference: (value: string) => void;
16
+ onRefreshAuthStatus: () => void;
17
+ onClose: () => void;
18
+ }
19
+
20
+ export function AuthPanel({
21
+ focusId,
22
+ provider,
23
+ authPreference,
24
+ authStatus,
25
+ authStatusBusy,
26
+ onSetPreference,
27
+ onRefreshAuthStatus,
28
+ onClose,
29
+ }: AuthPanelProps) {
30
+ const theme = useTheme();
31
+ const { isFocused } = useFocus({ id: focusId, autoFocus: true });
32
+
33
+ useInput((input, key) => {
34
+ if (key.escape || input.toLowerCase() === "q") {
35
+ onClose();
36
+ return;
37
+ }
38
+
39
+ const numeric = Number.parseInt(input, 10);
40
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= AUTH_PREFERENCES.length) {
41
+ onSetPreference(AUTH_PREFERENCES[numeric - 1]!.id);
42
+ }
43
+
44
+ if (input.toLowerCase() === "r") {
45
+ onRefreshAuthStatus();
46
+ }
47
+ }, { isActive: isFocused });
48
+
49
+ const authStateLabel = getAuthStateLabel(authStatus.state);
50
+ const authStateColor =
51
+ authStatus.state === "authenticated"
52
+ ? theme.SUCCESS
53
+ : authStatus.state === "unauthenticated"
54
+ ? theme.ERROR
55
+ : theme.WARNING;
56
+ const checkedAtLabel = authStatus.checkedAt > 0
57
+ ? new Date(authStatus.checkedAt).toLocaleTimeString()
58
+ : "not checked yet";
59
+
60
+ return (
61
+ <Box
62
+ borderStyle="round"
63
+ borderColor={theme.BORDER_ACTIVE}
64
+ flexDirection="column"
65
+ paddingX={2}
66
+ paddingY={1}
67
+ marginTop={1}
68
+ width="100%"
69
+ >
70
+ <Text color={theme.ACCENT} bold>
71
+ Auth and subscription guidance
72
+ </Text>
73
+ <Text color={theme.MUTED}>Current backend: {provider.label}</Text>
74
+ <Text color={theme.MUTED}>Current preference: {formatAuthPreferenceLabel(authPreference)}</Text>
75
+ <Text color={theme.INFO}>Backend auth: {provider.authLabel}</Text>
76
+ <Text color={authStateColor}>Runtime auth state: {authStateLabel}</Text>
77
+ <Text color={theme.DIM}>Last checked: {checkedAtLabel}</Text>
78
+ <Text color={theme.DIM}>Probe summary: {authStatus.rawSummary || "No probe output yet"}</Text>
79
+ <Text color={theme.TEXT}>
80
+ This UI securely bridges to the Codexa neural network. It does not collect or store your ChatGPT credentials.
81
+ </Text>
82
+ <Text color={theme.TEXT}>{provider.statusMessage}</Text>
83
+ <Box flexDirection="column" marginTop={1}>
84
+ <Text color={theme.INFO}>Commands:</Text>
85
+ <Text color={theme.TEXT}> /login guided ChatGPT sign-in steps</Text>
86
+ <Text color={theme.TEXT}> /logout guided sign-out steps</Text>
87
+ <Text color={theme.TEXT}> /auth status refresh Codexa authentication</Text>
88
+ </Box>
89
+ <Box flexDirection="column" marginTop={1}>
90
+ {AUTH_PREFERENCES.map((item, index) => (
91
+ <Text key={item.id} color={item.id === authPreference ? theme.SUCCESS : theme.TEXT}>
92
+ {index + 1}. {item.label} {item.id === authPreference ? "✓" : ""}
93
+ </Text>
94
+ ))}
95
+ </Box>
96
+ <Box marginTop={1}>
97
+ <Text color={theme.DIM}>
98
+ Press 1-{AUTH_PREFERENCES.length} to change preference, R to refresh status, Esc to close.
99
+ </Text>
100
+ </Box>
101
+ {authStatusBusy && (
102
+ <Box marginTop={1}>
103
+ <Text color={theme.WARNING}>Checking auth status...</Text>
104
+ </Box>
105
+ )}
106
+ {authStatus.recommendedAction && (
107
+ <Box marginTop={1}>
108
+ <Text color={theme.DIM}>Next step: {authStatus.recommendedAction}</Text>
109
+ </Box>
110
+ )}
111
+ </Box>
112
+ );
113
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { AVAILABLE_BACKENDS } from "../config/settings.js";
3
+ import { FOCUS_IDS } from "./focus.js";
4
+ import { SelectionPanel } from "./SelectionPanel.js";
5
+
6
+ interface BackendPickerProps {
7
+ currentBackend: string;
8
+ onSelect: (backend: string) => void;
9
+ onCancel: () => void;
10
+ }
11
+
12
+ export function BackendPicker({ currentBackend, onSelect, onCancel }: BackendPickerProps) {
13
+ const items = AVAILABLE_BACKENDS.map((backend) => ({
14
+ label: backend.id === currentBackend ? `${backend.label} ✓` : `${backend.label} ${backend.id}`,
15
+ value: backend.id,
16
+ }));
17
+
18
+ return (
19
+ <SelectionPanel
20
+ focusId={FOCUS_IDS.backendPicker}
21
+ title="Select backend"
22
+ subtitle="Codexa is connected and ready. Native OpenAI is reserved for a later implementation pass."
23
+ items={items}
24
+ onSelect={onSelect}
25
+ onCancel={onCancel}
26
+ />
27
+ );
28
+ }