@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,142 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { normalizeCommand, getFriendlyActionLabel } from "./commandNormalize.js";
4
+
5
+ // ─── normalizeCommand ─────────────────────────────────────────────────────────
6
+
7
+ test("normalizeCommand: strips full PowerShell path with single-quoted command", () => {
8
+ const raw = `"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'Get-ChildItem -Force | Select-Object Name,Mode,Length'`;
9
+ assert.equal(normalizeCommand(raw), "Get-ChildItem -Force | Select-Object Name,Mode,Length");
10
+ });
11
+
12
+ test("normalizeCommand: strips full PowerShell path with double-quoted command", () => {
13
+ const raw = `"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content package.json"`;
14
+ assert.equal(normalizeCommand(raw), "Get-Content package.json");
15
+ });
16
+
17
+ test("normalizeCommand: strips bare pwsh.exe -Command", () => {
18
+ assert.equal(normalizeCommand(`pwsh.exe -Command 'git status'`), "git status");
19
+ assert.equal(normalizeCommand(`pwsh -Command "npm install"`), "npm install");
20
+ });
21
+
22
+ test("normalizeCommand: strips bare powershell.exe -Command", () => {
23
+ assert.equal(normalizeCommand(`powershell.exe -Command 'dir'`), "dir");
24
+ assert.equal(normalizeCommand(`powershell -Command "Get-Content readme.md"`), "Get-Content readme.md");
25
+ });
26
+
27
+ test("normalizeCommand: strips cmd.exe /c wrapper", () => {
28
+ assert.equal(normalizeCommand(`cmd.exe /c "dir /b"`), "dir /b");
29
+ assert.equal(normalizeCommand(`cmd /C "npm test"`), "npm test");
30
+ });
31
+
32
+ test("normalizeCommand: strips bash -lc wrapper", () => {
33
+ assert.equal(normalizeCommand(`bash -lc 'bun install'`), "bun install");
34
+ assert.equal(normalizeCommand(`bash -lc "git diff"`), "git diff");
35
+ });
36
+
37
+ test("normalizeCommand: returns command unchanged when no wrapper is detected", () => {
38
+ assert.equal(normalizeCommand("git status"), "git status");
39
+ assert.equal(normalizeCommand("bun test"), "bun test");
40
+ assert.equal(normalizeCommand("python -m pytest"), "python -m pytest");
41
+ assert.equal(normalizeCommand("tsc --noEmit"), "tsc --noEmit");
42
+ });
43
+
44
+ test("normalizeCommand: collapses local file paths inside commands", () => {
45
+ assert.equal(
46
+ normalizeCommand(`pwsh.exe -Command 'Get-Content C:\\Users\\Example\\Project\\README.md'`),
47
+ "Get-Content README.md",
48
+ );
49
+ assert.equal(
50
+ normalizeCommand(`Get-Content C:/Users/Example/Project/src/App.tsx`),
51
+ "Get-Content src/App.tsx",
52
+ );
53
+ });
54
+
55
+ test("normalizeCommand: is case-insensitive for -Command flag", () => {
56
+ assert.equal(normalizeCommand(`pwsh.exe -command 'ls'`), "ls");
57
+ assert.equal(normalizeCommand(`pwsh.exe -COMMAND 'ls'`), "ls");
58
+ });
59
+
60
+ // ─── getFriendlyActionLabel ───────────────────────────────────────────────────
61
+
62
+ test("getFriendlyActionLabel: Get-ChildItem → List files", () => {
63
+ assert.equal(getFriendlyActionLabel("Get-ChildItem -Force | Select-Object Name,Mode,Length"), "List files");
64
+ assert.equal(getFriendlyActionLabel("Get-ChildItem"), "List files");
65
+ assert.equal(getFriendlyActionLabel("dir /b"), "List files");
66
+ assert.equal(getFriendlyActionLabel("ls -la"), "List files");
67
+ assert.equal(getFriendlyActionLabel("rg --files"), "List files");
68
+ });
69
+
70
+ test("getFriendlyActionLabel: Get-Content / cat → Read file", () => {
71
+ assert.equal(getFriendlyActionLabel("Get-Content package.json"), "Read file");
72
+ assert.equal(getFriendlyActionLabel("cat src/index.ts"), "Read file");
73
+ });
74
+
75
+ test("getFriendlyActionLabel: git status → Check git status", () => {
76
+ assert.equal(getFriendlyActionLabel("git status"), "Check git status");
77
+ assert.equal(getFriendlyActionLabel("git status --short"), "Check git status");
78
+ });
79
+
80
+ test("getFriendlyActionLabel: git diff → Inspect changes", () => {
81
+ assert.equal(getFriendlyActionLabel("git diff"), "Inspect changes");
82
+ assert.equal(getFriendlyActionLabel("git diff HEAD~1"), "Inspect changes");
83
+ });
84
+
85
+ test("getFriendlyActionLabel: install commands → Install dependencies", () => {
86
+ assert.equal(getFriendlyActionLabel("bun install"), "Install dependencies");
87
+ assert.equal(getFriendlyActionLabel("npm install"), "Install dependencies");
88
+ assert.equal(getFriendlyActionLabel("npm install --save-dev"), "Install dependencies");
89
+ });
90
+
91
+ test("getFriendlyActionLabel: test commands → Run tests", () => {
92
+ assert.equal(getFriendlyActionLabel("bun test"), "Run tests");
93
+ assert.equal(getFriendlyActionLabel("npm test"), "Run tests");
94
+ });
95
+
96
+ test("getFriendlyActionLabel: typecheck commands → Run typecheck", () => {
97
+ assert.equal(getFriendlyActionLabel("tsc --noEmit"), "Run typecheck");
98
+ assert.equal(getFriendlyActionLabel("bun run typecheck"), "Run typecheck");
99
+ });
100
+
101
+ test("getFriendlyActionLabel: returns null for unrecognized commands", () => {
102
+ assert.equal(getFriendlyActionLabel("python -m pytest"), null);
103
+ assert.equal(getFriendlyActionLabel("echo hello"), null);
104
+ assert.equal(getFriendlyActionLabel("node scripts/build.js"), null);
105
+ });
106
+
107
+ test("normalizeCommand: decodes JSON-escaped quotes in PowerShell commands", () => {
108
+ const raw = `pwsh.exe -Command 'Get-ChildItem | Where-Object { $_.Name -match \\"requirements\\" }'`;
109
+ assert.equal(
110
+ normalizeCommand(raw),
111
+ 'Get-ChildItem | Where-Object { $_.Name -match "requirements" }',
112
+ );
113
+ });
114
+
115
+ test("normalizeCommand: decodes escaped quotes in bare commands", () => {
116
+ assert.equal(normalizeCommand(`grep -r \\"TODO\\" ./src`), 'grep -r "TODO" ./src');
117
+ });
118
+
119
+ test("normalizeCommand: decodes escaped quotes in cmd.exe wrapper", () => {
120
+ assert.equal(normalizeCommand(`cmd.exe /c "grep \\"error\\" log.txt"`), 'grep "error" log.txt');
121
+ });
122
+
123
+ test("normalizeCommand: passes through MCP tool call format unchanged", () => {
124
+ assert.equal(normalizeCommand("filesystem:read_file"), "filesystem:read_file");
125
+ assert.equal(normalizeCommand("github:create_issue"), "github:create_issue");
126
+ });
127
+
128
+ test("normalizeCommand: passes through web search format unchanged", () => {
129
+ assert.equal(
130
+ normalizeCommand("web search: TypeScript union types"),
131
+ "web search: TypeScript union types",
132
+ );
133
+ });
134
+
135
+ test("getFriendlyActionLabel: returns null for MCP tool call format", () => {
136
+ assert.equal(getFriendlyActionLabel("filesystem:read_file"), null);
137
+ assert.equal(getFriendlyActionLabel("github:list_issues"), null);
138
+ });
139
+
140
+ test("getFriendlyActionLabel: returns null for web search commands", () => {
141
+ assert.equal(getFriendlyActionLabel("web search: TypeScript generics tutorial"), null);
142
+ });
@@ -0,0 +1,66 @@
1
+ import { formatTerminalAnswerInline } from "./terminalAnswerFormat.js";
2
+
3
+ function stripOuterQuotes(s: string): string {
4
+ if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
5
+ return s.slice(1, -1);
6
+ }
7
+ return s;
8
+ }
9
+
10
+ function decodeEscapedQuotes(s: string): string {
11
+ return s.replace(/\\"/g, '"');
12
+ }
13
+
14
+ function cleanCommand(command: string): string {
15
+ return formatTerminalAnswerInline(decodeEscapedQuotes(command.trim()));
16
+ }
17
+
18
+ /**
19
+ * Strips shell-wrapper invocations from a command string and returns the inner
20
+ * command that is actually being executed.
21
+ *
22
+ * Handles:
23
+ * "C:\Program Files\PowerShell\7\pwsh.exe" -Command '...'
24
+ * pwsh.exe -Command "..."
25
+ * powershell.exe -Command '...'
26
+ * cmd.exe /c "..."
27
+ * bash -lc '...'
28
+ */
29
+ export function normalizeCommand(command: string): string {
30
+ // Full quoted path ending with pwsh.exe / powershell.exe
31
+ let match = command.match(/^"[^"]*(?:pwsh|powershell)(?:\.exe)?"\s+-Command\s+(.+)$/si);
32
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
33
+
34
+ // Bare pwsh.exe / powershell.exe on PATH
35
+ match = command.match(/^(?:pwsh|powershell)(?:\.exe)?\s+-Command\s+(.+)$/si);
36
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
37
+
38
+ // cmd.exe /c "..."
39
+ match = command.match(/^cmd(?:\.exe)?\s+\/[cC]\s+(.+)$/s);
40
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
41
+
42
+ // bash -lc "..." or bash -lc '...'
43
+ match = command.match(/^bash\s+-lc\s+(.+)$/s);
44
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
45
+
46
+ return cleanCommand(command);
47
+ }
48
+
49
+ /**
50
+ * Maps a normalized command to a short human-readable label.
51
+ * Returns null when no label applies and the command itself should be shown.
52
+ */
53
+ export function getFriendlyActionLabel(normalizedCommand: string): string | null {
54
+ const cmd = normalizedCommand.trim();
55
+
56
+ if (/^(?:Get-ChildItem|dir|ls)\b/i.test(cmd)) return "List files";
57
+ if (/^rg\s+--files\b/i.test(cmd)) return "List files";
58
+ if (/^(?:Get-Content|cat)\b/i.test(cmd)) return "Read file";
59
+ if (/^git\s+status\b/.test(cmd)) return "Check git status";
60
+ if (/^git\s+diff\b/.test(cmd)) return "Inspect changes";
61
+ if (/^(?:bun|npm)\s+install\b/.test(cmd)) return "Install dependencies";
62
+ if (/^(?:bun|npm)\s+test\b/.test(cmd)) return "Run tests";
63
+ if (/^(?:tsc\b|bun\s+run\s+typecheck\b)/.test(cmd)) return "Run typecheck";
64
+
65
+ return null;
66
+ }
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { isUnifiedDiff, maybeRenderDiff, renderUnifiedDiff } from "./diffRenderer.js";
4
+
5
+ const GIT_DIFF = [
6
+ "diff --git a/src/example.ts b/src/example.ts",
7
+ "index 1111111..2222222 100644",
8
+ "--- a/src/example.ts",
9
+ "+++ b/src/example.ts",
10
+ "@@ -1,3 +1,4 @@",
11
+ " const name = \"Codexa\";",
12
+ "-console.log(\"old\");",
13
+ "+console.log(\"new\");",
14
+ "+console.log(\"added\");",
15
+ " export default name;",
16
+ ].join("\n");
17
+
18
+ test("detects a git unified diff", () => {
19
+ assert.equal(isUnifiedDiff(GIT_DIFF), true);
20
+ });
21
+
22
+ test("detects a simple file-header and hunk diff", () => {
23
+ const diff = [
24
+ "--- old.txt",
25
+ "+++ new.txt",
26
+ "@@ -1 +1 @@",
27
+ "-old",
28
+ "+new",
29
+ ].join("\n");
30
+
31
+ assert.equal(isUnifiedDiff(diff), true);
32
+ });
33
+
34
+ test("classifies file, hunk, added, removed, context, and meta lines", () => {
35
+ const rendered = renderUnifiedDiff(GIT_DIFF);
36
+ assert.deepEqual(rendered.map((line) => line.type), [
37
+ "file",
38
+ "meta",
39
+ "file",
40
+ "file",
41
+ "hunk",
42
+ "context",
43
+ "remove",
44
+ "add",
45
+ "add",
46
+ "context",
47
+ ]);
48
+ });
49
+
50
+ test("handles malformed diff-like text without throwing", () => {
51
+ assert.doesNotThrow(() => renderUnifiedDiff("diff --git a/a b/a\nnot enough here"));
52
+ assert.equal(maybeRenderDiff("diff --git a/a b/a\nnot enough here"), null);
53
+ });
54
+
55
+ test("does not classify normal text as a diff", () => {
56
+ const text = [
57
+ "Here are options:",
58
+ "- use --help",
59
+ "+ consider reading docs",
60
+ ].join("\n");
61
+
62
+ assert.equal(isUnifiedDiff(text), false);
63
+ assert.equal(maybeRenderDiff(text), null);
64
+ });
65
+
66
+ test("keeps Windows paths readable in file headers", () => {
67
+ const diff = [
68
+ "--- a\\src\\example.ts",
69
+ "+++ b\\src\\example.ts",
70
+ "@@ -1 +1 @@",
71
+ "-old",
72
+ "+new",
73
+ ].join("\n");
74
+
75
+ const rendered = renderUnifiedDiff(diff);
76
+ assert.equal(rendered[0]?.text, "--- a\\src\\example.ts");
77
+ assert.equal(rendered[1]?.text, "+++ b\\src\\example.ts");
78
+ });
79
+
80
+ test("strips ANSI and unsafe controls before classification", () => {
81
+ const diff = [
82
+ "\u001b[31m--- a/file.ts\u001b[0m",
83
+ "\u001b[32m+++ b/file.ts\u001b[0m",
84
+ "@@ -1 +1 @@",
85
+ "\u001b[31m-old\u001b[0m",
86
+ "\u001b[32m+new\u001b[0m\u0007",
87
+ ].join("\n");
88
+
89
+ const rendered = renderUnifiedDiff(diff);
90
+ assert.deepEqual(rendered.map((line) => line.text), [
91
+ "--- a/file.ts",
92
+ "+++ b/file.ts",
93
+ "@@ -1 +1 @@",
94
+ "-old",
95
+ "+new",
96
+ ]);
97
+ });
98
+
99
+ test("forced rendering accepts explicit diff fences with change lines", () => {
100
+ assert.equal(isUnifiedDiff("-old\n+new", { force: true }), true);
101
+ assert.equal(isUnifiedDiff("-old\n+new"), false);
102
+ });
@@ -0,0 +1,116 @@
1
+ import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
2
+
3
+ export type DiffRenderLineType = "file" | "hunk" | "add" | "remove" | "context" | "meta";
4
+
5
+ export interface DiffRenderLine {
6
+ type: DiffRenderLineType;
7
+ text: string;
8
+ }
9
+
10
+ export interface DiffRenderOptions {
11
+ force?: boolean;
12
+ }
13
+
14
+ const DIFF_GIT_HEADER_PATTERN = /^diff --git\s+/;
15
+ const HUNK_HEADER_PATTERN = /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s*@@/;
16
+ const INDEX_HEADER_PATTERN = /^index\s+\S+\.\.\S+/;
17
+ const OLD_FILE_HEADER_PATTERN = /^---\s+\S+/;
18
+ const NEW_FILE_HEADER_PATTERN = /^\+\+\+\s+\S+/;
19
+ const ADD_LINE_PATTERN = /^\+(?!\+\+)/;
20
+ const REMOVE_LINE_PATTERN = /^-(?!--)/;
21
+
22
+ function normalizeDiffText(text: string): string {
23
+ return sanitizeTerminalOutput(text, { preserveTabs: false, tabSize: 2 })
24
+ .replace(/\r\n/g, "\n")
25
+ .replace(/\r/g, "\n");
26
+ }
27
+
28
+ function getDiffLines(text: string): string[] {
29
+ return normalizeDiffText(text)
30
+ .split("\n")
31
+ .map((line) => line.trimEnd());
32
+ }
33
+
34
+ function trimBlankEdges(lines: string[]): string[] {
35
+ let start = 0;
36
+ let end = lines.length;
37
+
38
+ while (start < end && lines[start]?.trim() === "") start += 1;
39
+ while (end > start && lines[end - 1]?.trim() === "") end -= 1;
40
+
41
+ return lines.slice(start, end);
42
+ }
43
+
44
+ function hasPairedFileHeaders(lines: readonly string[]): boolean {
45
+ return lines.some((line) => OLD_FILE_HEADER_PATTERN.test(line))
46
+ && lines.some((line) => NEW_FILE_HEADER_PATTERN.test(line));
47
+ }
48
+
49
+ function hasStrongDiffSignal(lines: readonly string[]): boolean {
50
+ return lines.some((line) =>
51
+ DIFF_GIT_HEADER_PATTERN.test(line)
52
+ || HUNK_HEADER_PATTERN.test(line)
53
+ || INDEX_HEADER_PATTERN.test(line)
54
+ ) || hasPairedFileHeaders(lines);
55
+ }
56
+
57
+ function hasRealChangeLine(lines: readonly string[]): boolean {
58
+ return lines.some((line) => ADD_LINE_PATTERN.test(line) || REMOVE_LINE_PATTERN.test(line));
59
+ }
60
+
61
+ function shouldRenderUnifiedDiff(lines: readonly string[], options: DiffRenderOptions = {}): boolean {
62
+ if (lines.length < 2) return false;
63
+ if (options.force) {
64
+ return hasStrongDiffSignal(lines) || hasRealChangeLine(lines);
65
+ }
66
+ return hasStrongDiffSignal(lines) && hasRealChangeLine(lines);
67
+ }
68
+
69
+ export function classifyDiffLine(line: string): DiffRenderLineType {
70
+ if (DIFF_GIT_HEADER_PATTERN.test(line) || OLD_FILE_HEADER_PATTERN.test(line) || NEW_FILE_HEADER_PATTERN.test(line)) {
71
+ return "file";
72
+ }
73
+
74
+ if (HUNK_HEADER_PATTERN.test(line) || line.startsWith("@@")) {
75
+ return "hunk";
76
+ }
77
+
78
+ if (ADD_LINE_PATTERN.test(line)) {
79
+ return "add";
80
+ }
81
+
82
+ if (REMOVE_LINE_PATTERN.test(line)) {
83
+ return "remove";
84
+ }
85
+
86
+ if (INDEX_HEADER_PATTERN.test(line) || line.startsWith("\\ No newline")) {
87
+ return "meta";
88
+ }
89
+
90
+ return "context";
91
+ }
92
+
93
+ export function isUnifiedDiff(text: string, options: DiffRenderOptions = {}): boolean {
94
+ return shouldRenderUnifiedDiff(trimBlankEdges(getDiffLines(text)), options);
95
+ }
96
+
97
+ export function renderUnifiedDiff(text: string, options: DiffRenderOptions = {}): DiffRenderLine[] {
98
+ const lines = trimBlankEdges(getDiffLines(text));
99
+ if (lines.length === 0) {
100
+ return [];
101
+ }
102
+
103
+ if (!shouldRenderUnifiedDiff(lines, options)) {
104
+ return [];
105
+ }
106
+
107
+ return lines.map((line) => ({
108
+ type: classifyDiffLine(line),
109
+ text: line,
110
+ }));
111
+ }
112
+
113
+ export function maybeRenderDiff(text: string, options: DiffRenderOptions = {}): DiffRenderLine[] | null {
114
+ const rendered = renderUnifiedDiff(text, options);
115
+ return rendered.length > 0 ? rendered : null;
116
+ }
@@ -0,0 +1,61 @@
1
+ import type { Screen } from "../session/types.js";
2
+
3
+ export const FOCUS_IDS = {
4
+ composer: "composer",
5
+ planReviewPanel: "plan-review-panel",
6
+ backendPicker: "backend-picker",
7
+ providerPicker: "provider-picker",
8
+ modelPicker: "model-picker",
9
+ modePicker: "mode-picker",
10
+ reasoningPicker: "reasoning-picker",
11
+ themePicker: "theme-picker",
12
+ settingsPanel: "settings-panel",
13
+ authPanel: "auth-panel",
14
+ permissionsPanel: "permissions-panel",
15
+ permissionsApprovalPicker: "permissions-approval-picker",
16
+ permissionsSandboxPicker: "permissions-sandbox-picker",
17
+ permissionsNetworkPicker: "permissions-network-picker",
18
+ permissionsAddWritableRoot: "permissions-add-writable-root",
19
+ permissionsRemoveWritableRoot: "permissions-remove-writable-root",
20
+ importConfirmationPanel: "import-confirmation",
21
+ } as const;
22
+
23
+ export type FocusTargetId = (typeof FOCUS_IDS)[keyof typeof FOCUS_IDS];
24
+
25
+ export function getFocusTargetForScreen(screen: Screen): FocusTargetId {
26
+ switch (screen) {
27
+ case "backend-picker":
28
+ return FOCUS_IDS.backendPicker;
29
+ case "provider-picker":
30
+ return FOCUS_IDS.providerPicker;
31
+ case "model-picker":
32
+ return FOCUS_IDS.modelPicker;
33
+ case "mode-picker":
34
+ return FOCUS_IDS.modePicker;
35
+ case "reasoning-picker":
36
+ return FOCUS_IDS.reasoningPicker;
37
+ case "theme-picker":
38
+ return FOCUS_IDS.themePicker;
39
+ case "settings-panel":
40
+ return FOCUS_IDS.settingsPanel;
41
+ case "auth-panel":
42
+ return FOCUS_IDS.authPanel;
43
+ case "permissions-panel":
44
+ return FOCUS_IDS.permissionsPanel;
45
+ case "permissions-approval-picker":
46
+ return FOCUS_IDS.permissionsApprovalPicker;
47
+ case "permissions-sandbox-picker":
48
+ return FOCUS_IDS.permissionsSandboxPicker;
49
+ case "permissions-network-picker":
50
+ return FOCUS_IDS.permissionsNetworkPicker;
51
+ case "permissions-add-writable-root":
52
+ return FOCUS_IDS.permissionsAddWritableRoot;
53
+ case "permissions-remove-writable-root":
54
+ return FOCUS_IDS.permissionsRemoveWritableRoot;
55
+ case "import-confirmation":
56
+ return FOCUS_IDS.importConfirmationPanel;
57
+ case "main":
58
+ default:
59
+ return FOCUS_IDS.composer;
60
+ }
61
+ }