@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,401 @@
1
+ import { homedir } from "os";
2
+ import { basename, join, parse, win32 } from "path";
3
+
4
+ function isWindowsStylePath(p: string): boolean {
5
+ return /^[A-Za-z]:[\\/]/.test(p) || /^\\\\/.test(p);
6
+ }
7
+
8
+ function smartJoin(base: string, ...parts: string[]): string {
9
+ return isWindowsStylePath(base) ? win32.join(base, ...parts) : join(base, ...parts);
10
+ }
11
+
12
+ export const APP_NAME = "Codexa";
13
+ export const APP_VERSION = "1.0.1";
14
+ export const DEFAULT_BACKEND = "codex-subprocess";
15
+ export const DEFAULT_MODEL = "gpt-5.4";
16
+ export const DEFAULT_MODE = "full-auto";
17
+ export const DEFAULT_REASONING_LEVEL = "high";
18
+ export const DEFAULT_LAYOUT_STYLE = "gemini-shell";
19
+ export const DEFAULT_THEME = "mono";
20
+ export const DEFAULT_WORKSPACE_DISPLAY_MODE = "dir";
21
+ export const DEFAULT_TERMINAL_TITLE_MODE = "dir";
22
+ export const DEFAULT_SHOW_BUSY_LOADER = true;
23
+ export const DEFAULT_AUTH_PREFERENCE = "chatgpt-login-goal";
24
+ export const CODEX_EXECUTABLE = process.env.CODEX_EXECUTABLE || "codex";
25
+ export const CLAUDE_EXECUTABLE = process.env.CLAUDE_EXECUTABLE || null;
26
+ export const MAX_CHAT_LINES = 2000;
27
+ export const MAX_VISIBLE_EVENTS = 8;
28
+
29
+ export function getCodexHome(): string {
30
+ return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
31
+ }
32
+
33
+ export function getCodexConfigFile(): string {
34
+ return smartJoin(getCodexHome(), "config.toml");
35
+ }
36
+
37
+ export function getCodexaTrustStoreFile(): string {
38
+ return smartJoin(getCodexHome(), "codexa-trust.json");
39
+ }
40
+
41
+ export const CODEX_HOME = getCodexHome();
42
+ export const CODEX_CONFIG_FILE = getCodexConfigFile();
43
+ export const CODEXA_TRUST_STORE_FILE = getCodexaTrustStoreFile();
44
+ export const SETTINGS_FILE = join(homedir(), ".codexa-settings.json");
45
+ export const MODEL_SPECS_FILE = join(homedir(), ".codexa-model-specs.json");
46
+
47
+ export const AVAILABLE_BACKENDS = [
48
+ {
49
+ id: "codex-subprocess",
50
+ label: "Codexa Core",
51
+ description: "Direct connection to the Codexa neural network.",
52
+ },
53
+ {
54
+ id: "openai-native",
55
+ label: "OpenAI Native",
56
+ description: "Future native provider. ChatGPT subscriptions do not automatically grant API access.",
57
+ },
58
+ ] as const;
59
+
60
+ export type AvailableBackend = (typeof AVAILABLE_BACKENDS)[number]["id"];
61
+
62
+ // Static model list used when runtime model discovery is unavailable.
63
+ // Named "legacy fallback" because dynamic discovery is the preferred source of truth,
64
+ // but this list is the live exported AVAILABLE_MODELS for now.
65
+ export const LEGACY_FALLBACK_MODELS = [
66
+ "gpt-5.5",
67
+ "gpt-5.4",
68
+ "gpt-5.4-mini",
69
+ "gpt-5.3-codex",
70
+ "gpt-5.2",
71
+ ] as const;
72
+
73
+ export const AVAILABLE_MODELS = LEGACY_FALLBACK_MODELS;
74
+
75
+ export type AvailableModel = string;
76
+
77
+ export const AVAILABLE_REASONING_LEVELS = [
78
+ { id: "none", label: "None" },
79
+ { id: "minimal", label: "Minimal" },
80
+ { id: "low", label: "Low" },
81
+ { id: "medium", label: "Medium" },
82
+ { id: "high", label: "High" },
83
+ { id: "xhigh", label: "XHigh" },
84
+ { id: "max", label: "Max" },
85
+ ] as const;
86
+
87
+ export type ReasoningLevel = string;
88
+
89
+ export const WORKSPACE_DISPLAY_MODES = ["dir", "name", "simple"] as const;
90
+ export const LEGACY_DIRECTORY_DISPLAY_MODES = ["normal", "simple"] as const;
91
+
92
+ export type WorkspaceDisplayMode = (typeof WORKSPACE_DISPLAY_MODES)[number];
93
+ export type LegacyDirectoryDisplayMode = (typeof LEGACY_DIRECTORY_DISPLAY_MODES)[number];
94
+ export type TerminalTitleMode = WorkspaceDisplayMode;
95
+
96
+ export const BUSY_LOADER_SETTING_VALUES = ["true", "false"] as const;
97
+
98
+ export type BusyLoaderSettingValue = (typeof BUSY_LOADER_SETTING_VALUES)[number];
99
+
100
+ export const TERMINAL_MOUSE_MODES = ["wheel", "selection"] as const;
101
+
102
+ export type TerminalMouseMode = (typeof TERMINAL_MOUSE_MODES)[number];
103
+
104
+ export const DEFAULT_TERMINAL_MOUSE_MODE: TerminalMouseMode = "selection";
105
+
106
+ export interface SettingOption<TValue extends string> {
107
+ value: TValue;
108
+ label: string;
109
+ }
110
+
111
+ export interface SettingDefinition<TKey extends string, TValue extends string> {
112
+ key: TKey;
113
+ label: string;
114
+ description?: string;
115
+ options: readonly SettingOption<TValue>[];
116
+ }
117
+
118
+ export interface UserSettingValues {
119
+ workspaceDisplayMode: WorkspaceDisplayMode;
120
+ terminalTitleMode: TerminalTitleMode;
121
+ showBusyLoader: BusyLoaderSettingValue;
122
+ terminalMouseMode: TerminalMouseMode;
123
+ }
124
+
125
+ export type UserSettingKey = keyof UserSettingValues;
126
+
127
+ export type UserSettingDefinition = {
128
+ [K in UserSettingKey]: SettingDefinition<K, UserSettingValues[K]>;
129
+ }[UserSettingKey];
130
+
131
+ export const USER_SETTING_DEFINITIONS: readonly UserSettingDefinition[] = [
132
+ {
133
+ key: "workspaceDisplayMode",
134
+ label: "Workspace display",
135
+ description: "Controls how the workspace label is displayed in the Codexa header.",
136
+ options: [
137
+ { value: "dir", label: "Dir" },
138
+ { value: "name", label: "Name" },
139
+ { value: "simple", label: "Simple" },
140
+ ],
141
+ },
142
+ {
143
+ key: "terminalTitleMode",
144
+ label: "Terminal title",
145
+ description: "Controls how the terminal tab/window title is displayed.",
146
+ options: [
147
+ { value: "dir", label: "Dir" },
148
+ { value: "name", label: "Name" },
149
+ { value: "simple", label: "Simple" },
150
+ ],
151
+ },
152
+ {
153
+ key: "showBusyLoader",
154
+ label: "Busy loader",
155
+ description: "Controls whether the footer shows a subtle loading animation while Codexa is busy.",
156
+ options: [
157
+ { value: "true", label: "True" },
158
+ { value: "false", label: "False" },
159
+ ],
160
+ },
161
+ {
162
+ key: "terminalMouseMode",
163
+ label: "Mouse mode",
164
+ description:
165
+ "Selection (default): no mouse tracking — native drag-select and native wheel scroll work unmodified. "
166
+ + "Scroll history via native terminal scrollback. "
167
+ + "Wheel: enables SGR mouse tracking so the Codexa timeline captures wheel events for in-app scroll. "
168
+ + "Native drag-select then requires Shift (Windows Terminal) or equivalent modifier. "
169
+ + "Run /mouse to toggle for the current session.",
170
+ options: [
171
+ { value: "selection", label: "Native selection" },
172
+ { value: "wheel", label: "Wheel scroll" },
173
+ ],
174
+ },
175
+ ] as const;
176
+
177
+ /** Rough token estimate: ~4 chars per token */
178
+ export function estimateTokens(chars: number): number {
179
+ return Math.ceil(chars / 4);
180
+ }
181
+
182
+ export const AVAILABLE_MODES = [
183
+ { key: "suggest", label: "Read-only" },
184
+ { key: "auto-edit", label: "Auto" },
185
+ { key: "full-auto", label: "Full Access" },
186
+ ] as const;
187
+
188
+ export type AvailableMode = (typeof AVAILABLE_MODES)[number]["key"];
189
+
190
+ export const MODE_COMMAND_ALIASES = {
191
+ default: DEFAULT_MODE,
192
+ ask: "suggest",
193
+ add: "auto-edit",
194
+ auto: "auto-edit",
195
+ plan: "suggest",
196
+ } as const;
197
+
198
+ export type ModeCommandAlias = keyof typeof MODE_COMMAND_ALIASES;
199
+
200
+ export const AUTH_PREFERENCES = [
201
+ {
202
+ id: "chatgpt-login-goal",
203
+ label: "ChatGPT login goal",
204
+ description: "Design toward account-style sign-in without claiming it works as a backend today.",
205
+ },
206
+ {
207
+ id: "api-key-first",
208
+ label: "API key first",
209
+ description: "Prefer official API credentials when native OpenAI support is added.",
210
+ },
211
+ {
212
+ id: "runner-managed",
213
+ label: "Codexa managed",
214
+ description: "Rely on the core neural bridge to manage authentication.",
215
+ },
216
+ ] as const;
217
+
218
+ export type AuthPreference = (typeof AUTH_PREFERENCES)[number]["id"];
219
+
220
+ export function formatModeLabel(mode: string): string {
221
+ const found = AVAILABLE_MODES.find((m) => m.key === mode);
222
+ return found?.label ?? mode.toUpperCase();
223
+ }
224
+
225
+ export function resolveModeCommand(mode: string): AvailableMode | null {
226
+ const normalized = mode.toLowerCase();
227
+ const canonical = AVAILABLE_MODES.find((item) => item.key === normalized);
228
+ if (canonical) {
229
+ return canonical.key;
230
+ }
231
+
232
+ return MODE_COMMAND_ALIASES[normalized as ModeCommandAlias] ?? null;
233
+ }
234
+
235
+ export function formatModeCommandHelp(): string {
236
+ return "suggest, auto-edit, full-auto; aliases: default, ask, add, auto, plan";
237
+ }
238
+
239
+ export function getNextMode(mode: AvailableMode): AvailableMode {
240
+ const currentIndex = AVAILABLE_MODES.findIndex((item) => item.key === mode);
241
+ if (currentIndex < 0) {
242
+ return AVAILABLE_MODES[0].key;
243
+ }
244
+
245
+ return AVAILABLE_MODES[(currentIndex + 1) % AVAILABLE_MODES.length].key;
246
+ }
247
+
248
+ export function formatBackendLabel(backend: string): string {
249
+ const found = AVAILABLE_BACKENDS.find((item) => item.id === backend);
250
+ return found?.label ?? backend;
251
+ }
252
+
253
+ export function formatReasoningLabel(reasoning: string): string {
254
+ const found = AVAILABLE_REASONING_LEVELS.find((item) => item.id === reasoning);
255
+ if (found) {
256
+ return found.label;
257
+ }
258
+
259
+ return reasoning
260
+ .split(/[-_\s]+/)
261
+ .filter(Boolean)
262
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
263
+ .join(" ") || reasoning;
264
+ }
265
+
266
+ export const AVAILABLE_THEMES = [
267
+ { id: "purple", label: "Midnight Purple" },
268
+ { id: "mono", label: "Black & White" },
269
+ { id: "dark", label: "Modern Dark" },
270
+ { id: "black", label: "Codex the Black" },
271
+ { id: "emerald", label: "Emerald Night" },
272
+ { id: "solar", label: "Solar Flare" },
273
+ { id: "cyber", label: "Cyberpunk Neon" },
274
+ { id: "ocean", label: "Deep Oceanic" },
275
+ { id: "nordic", label: "Nordic Frost" },
276
+ { id: "green", label: "Terminal Green" },
277
+ { id: "amber", label: "Terminal Amber" },
278
+ { id: "vaporwave", label: "Vaporwave Dream" },
279
+ { id: "dracula", label: "Dracula Night" },
280
+ { id: "gruvbox", label: "Gruvbox Hard" },
281
+ { id: "synthwave", label: "Synthwave '84" },
282
+ { id: "custom", label: "Customize..." },
283
+ ] as const;
284
+
285
+ export type AvailableTheme = (typeof AVAILABLE_THEMES)[number]["id"];
286
+
287
+ export function formatThemeLabel(themeId: string): string {
288
+ const found = AVAILABLE_THEMES.find((item) => item.id === themeId);
289
+ return found?.label ?? themeId;
290
+ }
291
+
292
+ export function formatWorkspaceDisplayModeLabel(mode: WorkspaceDisplayMode): string {
293
+ if (mode === "name") return "Name";
294
+ if (mode === "simple") return "Simple";
295
+ return "Dir";
296
+ }
297
+
298
+ export function formatTerminalTitleModeLabel(mode: TerminalTitleMode): string {
299
+ return formatWorkspaceDisplayModeLabel(mode);
300
+ }
301
+
302
+ // Maps the old "normal" value (pre-rename) to the current "dir" equivalent.
303
+ export function normalizeLegacyDirectoryDisplayMode(mode: LegacyDirectoryDisplayMode): WorkspaceDisplayMode {
304
+ return mode === "simple" ? "simple" : "dir";
305
+ }
306
+
307
+ export function formatDirectoryDisplayModeLabel(mode: WorkspaceDisplayMode | LegacyDirectoryDisplayMode): string {
308
+ if (mode === "normal") return "Dir";
309
+ return formatWorkspaceDisplayModeLabel(mode);
310
+ }
311
+
312
+ export function formatBusyLoaderSettingValue(enabled: boolean): BusyLoaderSettingValue {
313
+ return enabled ? "true" : "false";
314
+ }
315
+
316
+ export function parseBusyLoaderSettingValue(value: string): boolean {
317
+ return value === "true";
318
+ }
319
+
320
+ function formatWorkspaceLeaf(workspaceRoot: string): string {
321
+ const trimmed = workspaceRoot.trim();
322
+ if (!trimmed) {
323
+ return trimmed;
324
+ }
325
+
326
+ const api = isWindowsStylePath(trimmed) ? win32 : { parse, basename };
327
+ const { root } = api.parse(trimmed);
328
+ let normalized = trimmed;
329
+ // Stop at filesystem root — root.length > 0 prevents stripping the root itself
330
+ while (normalized.length > root.length && /[\\/]+$/.test(normalized)) {
331
+ normalized = normalized.slice(0, -1);
332
+ }
333
+
334
+ if (!normalized) {
335
+ return trimmed;
336
+ }
337
+
338
+ if (normalized === root) {
339
+ return root || trimmed;
340
+ }
341
+
342
+ return api.basename(normalized) || normalized;
343
+ }
344
+
345
+ export function formatWorkspaceDisplayPath(
346
+ workspaceRoot: string,
347
+ workspaceDisplayMode: WorkspaceDisplayMode,
348
+ ): string {
349
+ if (workspaceDisplayMode === "name") {
350
+ return APP_NAME;
351
+ }
352
+
353
+ return formatWorkspaceLeaf(workspaceRoot);
354
+ }
355
+
356
+ export function formatTerminalTitlePath(
357
+ workspaceRoot: string,
358
+ terminalTitleMode: TerminalTitleMode,
359
+ ): string {
360
+ if (terminalTitleMode === "name" || terminalTitleMode === "simple") {
361
+ return APP_NAME;
362
+ }
363
+
364
+ return formatWorkspaceLeaf(workspaceRoot);
365
+ }
366
+
367
+ export interface HeaderConfig {
368
+ showBrand: boolean;
369
+ showWorkspace: boolean;
370
+ showProvider: boolean;
371
+ showModel: boolean;
372
+ showReasoning: boolean;
373
+ showContext: boolean;
374
+ showAuthStatus: boolean;
375
+ }
376
+
377
+ export const HEADER_CONFIG_DEFAULTS: HeaderConfig = {
378
+ showBrand: true,
379
+ showWorkspace: true,
380
+ showProvider: false,
381
+ showModel: false,
382
+ showReasoning: false,
383
+ showContext: false,
384
+ showAuthStatus: false,
385
+ };
386
+
387
+ export function getRecommendedReasoningForModel(model: AvailableModel): ReasoningLevel {
388
+ return DEFAULT_REASONING_LEVEL;
389
+ }
390
+
391
+ export function normalizeReasoningForModel(
392
+ model: AvailableModel,
393
+ reasoningLevel: ReasoningLevel,
394
+ ): ReasoningLevel {
395
+ return reasoningLevel || getRecommendedReasoningForModel(model);
396
+ }
397
+
398
+ export function formatAuthPreferenceLabel(preference: string): string {
399
+ const found = AUTH_PREFERENCES.find((item) => item.id === preference);
400
+ return found?.label ?? preference;
401
+ }
@@ -0,0 +1,98 @@
1
+ export function isRecord(value: unknown): value is Record<string, unknown> {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+
5
+ function isPrimitive(value: unknown): value is string | number | boolean {
6
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
7
+ }
8
+
9
+ function formatTomlPrimitive(value: string | number | boolean): string {
10
+ if (typeof value === "string") {
11
+ return JSON.stringify(value);
12
+ }
13
+ if (typeof value === "boolean") {
14
+ return value ? "true" : "false";
15
+ }
16
+ return `${value}`;
17
+ }
18
+
19
+ function formatTomlArray(values: readonly unknown[]): string {
20
+ return `[${values.map((value) => {
21
+ if (isPrimitive(value)) {
22
+ return formatTomlPrimitive(value);
23
+ }
24
+
25
+ if (Array.isArray(value)) {
26
+ return formatTomlArray(value);
27
+ }
28
+
29
+ if (isRecord(value)) {
30
+ return `{ ${Object.entries(value).map(([key, item]) => `${key} = ${formatTomlValue(item)}`).join(", ")} }`;
31
+ }
32
+
33
+ // TOML has no null literal; fall back to JSON encoding for unknown types.
34
+ return JSON.stringify(value ?? null);
35
+ }).join(", ")}]`;
36
+ }
37
+
38
+ function formatTomlValue(value: unknown): string {
39
+ if (isPrimitive(value)) {
40
+ return formatTomlPrimitive(value);
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ return formatTomlArray(value);
45
+ }
46
+
47
+ if (isRecord(value)) {
48
+ return `{ ${Object.entries(value).map(([key, item]) => `${key} = ${formatTomlValue(item)}`).join(", ")} }`;
49
+ }
50
+
51
+ // TOML has no null literal; fall back to JSON encoding for unknown types.
52
+ return JSON.stringify(value ?? null);
53
+ }
54
+
55
+ function serializeTomlSection(
56
+ path: readonly string[],
57
+ value: Record<string, unknown>,
58
+ lines: string[],
59
+ ): void {
60
+ const scalarEntries = Object.entries(value).filter(([, item]) => !isRecord(item) && !Array.isArray(item));
61
+ const arrayEntries = Object.entries(value).filter(([, item]) => Array.isArray(item) && !(item as unknown[]).every(isRecord));
62
+ const tableEntries = Object.entries(value).filter(([, item]) => isRecord(item));
63
+ // An array whose every element is a record is a TOML array-of-tables ([[key]]).
64
+ const arrayTableEntries = Object.entries(value).filter(([, item]) => Array.isArray(item) && (item as unknown[]).every(isRecord));
65
+
66
+ if (path.length > 0) {
67
+ lines.push(`[${path.join(".")}]`);
68
+ }
69
+
70
+ for (const [key, item] of [...scalarEntries, ...arrayEntries]) {
71
+ lines.push(`${key} = ${formatTomlValue(item)}`);
72
+ }
73
+
74
+ for (const [key, item] of tableEntries) {
75
+ if (lines.length > 0 && lines[lines.length - 1] !== "") {
76
+ lines.push("");
77
+ }
78
+ serializeTomlSection([...path, key], item as Record<string, unknown>, lines);
79
+ }
80
+
81
+ for (const [key, item] of arrayTableEntries) {
82
+ for (const table of item as Record<string, unknown>[]) {
83
+ if (lines.length > 0 && lines[lines.length - 1] !== "") {
84
+ lines.push("");
85
+ }
86
+ lines.push(`[[${[...path, key].join(".")}]]`);
87
+ const tableLines: string[] = [];
88
+ serializeTomlSection([], table, tableLines);
89
+ lines.push(...tableLines);
90
+ }
91
+ }
92
+ }
93
+
94
+ export function serializeTomlDocument(data: Record<string, unknown>): string {
95
+ const lines: string[] = [];
96
+ serializeTomlSection([], data, lines);
97
+ return `${lines.join("\n").trim()}\n`;
98
+ }
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import test from "node:test";
6
+
7
+ test("persists trusted project roots", async () => {
8
+ const tempHome = mkdtempSync(join(tmpdir(), "codexa-trust-store-"));
9
+ const previousCodexHome = process.env.CODEX_HOME;
10
+ process.env.CODEX_HOME = tempHome;
11
+
12
+ try {
13
+ const module = await import(`./trustStore.js?trust=${Date.now()}`);
14
+ const projectRoot = "C:/Workspace/Repo";
15
+
16
+ assert.equal(module.isProjectTrusted(projectRoot), false);
17
+ module.setProjectTrust(projectRoot, true);
18
+ assert.equal(module.isProjectTrusted(projectRoot), true);
19
+ module.setProjectTrust(projectRoot, false);
20
+ assert.equal(module.isProjectTrusted(projectRoot), false);
21
+ } finally {
22
+ if (previousCodexHome === undefined) {
23
+ delete process.env.CODEX_HOME;
24
+ } else {
25
+ process.env.CODEX_HOME = previousCodexHome;
26
+ }
27
+ rmSync(tempHome, { recursive: true, force: true });
28
+ }
29
+ });
@@ -0,0 +1,68 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { getCodexaTrustStoreFile } from "./settings.js";
4
+ import { normalizeWorkspaceRoot } from "../core/workspaceRoot.js";
5
+
6
+ interface TrustStoreData {
7
+ trustedProjectRoots: string[];
8
+ }
9
+
10
+ function getDefaultTrustStore(): TrustStoreData {
11
+ return { trustedProjectRoots: [] };
12
+ }
13
+
14
+ function parseTrustStoreData(data: unknown): TrustStoreData {
15
+ if (!data || typeof data !== "object") {
16
+ return getDefaultTrustStore();
17
+ }
18
+
19
+ const record = data as Record<string, unknown>;
20
+ const trustedProjectRoots = Array.isArray(record.trustedProjectRoots)
21
+ ? record.trustedProjectRoots
22
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
23
+ .map((value) => normalizeWorkspaceRoot(value))
24
+ : [];
25
+
26
+ return {
27
+ trustedProjectRoots: Array.from(new Set(trustedProjectRoots)),
28
+ };
29
+ }
30
+
31
+ function loadTrustStore(): TrustStoreData {
32
+ try {
33
+ const trustStoreFile = getCodexaTrustStoreFile();
34
+ const text = readFileSync(trustStoreFile, "utf-8");
35
+ return parseTrustStoreData(JSON.parse(text));
36
+ } catch {
37
+ // Best-effort persistence; corrupt or missing file silently resets to empty.
38
+ return getDefaultTrustStore();
39
+ }
40
+ }
41
+
42
+ function saveTrustStore(data: TrustStoreData): void {
43
+ try {
44
+ const trustStoreFile = getCodexaTrustStoreFile();
45
+ mkdirSync(dirname(trustStoreFile), { recursive: true });
46
+ const tmpFile = `${trustStoreFile}.tmp`;
47
+ writeFileSync(tmpFile, JSON.stringify(data, null, 2), "utf-8");
48
+ renameSync(tmpFile, trustStoreFile);
49
+ } catch {
50
+ // Best-effort persistence only.
51
+ }
52
+ }
53
+
54
+ export function isProjectTrusted(projectRoot: string): boolean {
55
+ const normalizedRoot = normalizeWorkspaceRoot(projectRoot);
56
+ const store = loadTrustStore();
57
+ return store.trustedProjectRoots.includes(normalizedRoot);
58
+ }
59
+
60
+ export function setProjectTrust(projectRoot: string, trusted: boolean): void {
61
+ const normalizedRoot = normalizeWorkspaceRoot(projectRoot);
62
+ const store = loadTrustStore();
63
+ const nextRoots = trusted
64
+ ? Array.from(new Set([...store.trustedProjectRoots, normalizedRoot]))
65
+ : store.trustedProjectRoots.filter((value) => value !== normalizedRoot);
66
+
67
+ saveTrustStore({ trustedProjectRoots: nextRoots });
68
+ }