@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,365 +0,0 @@
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 { render } from "ink";
6
- import type { AssistantEvent, RunEvent, UIState, UserPromptEvent } from "../session/types.js";
7
- import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
8
- import { ThemeProvider } from "./theme.js";
9
- import { TurnGroup, resolveTurnRunPhase } from "./TurnGroup.js";
10
-
11
- class TestInput extends PassThrough {
12
- readonly isTTY = true;
13
-
14
- setRawMode(): this {
15
- return this;
16
- }
17
-
18
- override resume(): this {
19
- return this;
20
- }
21
-
22
- override pause(): this {
23
- return this;
24
- }
25
-
26
- ref(): this {
27
- return this;
28
- }
29
-
30
- unref(): this {
31
- return this;
32
- }
33
- }
34
-
35
- class TestOutput extends PassThrough {
36
- readonly isTTY = true;
37
- columns = 120;
38
- rows = 40;
39
- }
40
-
41
- function stripAnsi(value: string): string {
42
- return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
43
- }
44
-
45
- function sleep(ms = 50): Promise<void> {
46
- return new Promise((resolve) => setTimeout(resolve, ms));
47
- }
48
-
49
- function makeRunningRun(turnId: number): RunEvent {
50
- return {
51
- id: 2,
52
- type: "run",
53
- createdAt: 2,
54
- startedAt: 2,
55
- durationMs: null,
56
- backendId: "codex-subprocess",
57
- backendLabel: "Codexa",
58
- runtime: TEST_RUNTIME,
59
- prompt: "Do work",
60
- progressEntries: [],
61
- status: "running",
62
- summary: "Running",
63
- truncatedOutput: false,
64
- toolActivities: [],
65
- activity: [],
66
- touchedFileCount: 0,
67
- errorMessage: null,
68
- turnId,
69
- };
70
- }
71
-
72
- function makeUser(turnId: number): UserPromptEvent {
73
- return {
74
- id: 1,
75
- type: "user",
76
- createdAt: 1,
77
- prompt: "Do work",
78
- turnId,
79
- };
80
- }
81
-
82
- function makeAssistant(turnId: number, content: string): AssistantEvent {
83
- return {
84
- id: 3,
85
- type: "assistant",
86
- createdAt: 3,
87
- content,
88
- contentChunks: [],
89
- turnId,
90
- };
91
- }
92
-
93
- function renderTurnGroup(node: React.ReactElement) {
94
- const stdin = new TestInput();
95
- const stdout = new TestOutput();
96
- let output = "";
97
- stdout.on("data", (chunk) => {
98
- output += chunk.toString();
99
- });
100
- const instance = render(node, {
101
- stdin: stdin as unknown as NodeJS.ReadStream,
102
- stdout: stdout as unknown as NodeJS.WriteStream,
103
- stderr: stdout as unknown as NodeJS.WriteStream,
104
- debug: true,
105
- exitOnCtrlC: false,
106
- });
107
-
108
- return {
109
- instance,
110
- readOutput: () => stripAnsi(output),
111
- resetOutput: () => {
112
- output = "";
113
- },
114
- };
115
- }
116
-
117
- test("does not render duplicate transcript working placeholder before streaming appears", async () => {
118
- const turnId = 10;
119
- const user = makeUser(turnId);
120
- const run = makeRunningRun(turnId);
121
- const assistant = makeAssistant(turnId, "Streaming line");
122
-
123
- const harness = renderTurnGroup(
124
- <ThemeProvider theme="purple">
125
- <TurnGroup
126
- cols={120}
127
- turnIndex={1}
128
- user={user}
129
- run={run}
130
- assistant={null}
131
- opacity="active"
132
- question={null}
133
- runPhase="thinking"
134
- streamPreviewRows={8}
135
- streamMode="assistant-first"
136
- />
137
- </ThemeProvider>,
138
- );
139
-
140
- await sleep();
141
- let frame = harness.readOutput();
142
- assert.doesNotMatch(frame, /Codex is working/i);
143
-
144
- harness.resetOutput();
145
- harness.instance.rerender(
146
- <ThemeProvider theme="purple">
147
- <TurnGroup
148
- cols={120}
149
- turnIndex={1}
150
- user={user}
151
- run={run}
152
- assistant={assistant}
153
- opacity="active"
154
- question={null}
155
- runPhase="streaming"
156
- streamPreviewRows={8}
157
- streamMode="assistant-first"
158
- />
159
- </ThemeProvider>,
160
- );
161
-
162
- await sleep();
163
- frame = harness.readOutput();
164
- assert.match(frame, /Codexa/);
165
- assert.match(frame, /Streaming line/i);
166
-
167
- harness.instance.unmount();
168
- });
169
-
170
- test("renders progressive content updates during streaming", async () => {
171
- const turnId = 12;
172
- const user = makeUser(turnId);
173
- const run = makeRunningRun(turnId);
174
-
175
- const harness = renderTurnGroup(
176
- <ThemeProvider theme="purple">
177
- <TurnGroup
178
- cols={120}
179
- turnIndex={1}
180
- user={user}
181
- run={run}
182
- assistant={makeAssistant(turnId, "First chunk")}
183
- opacity="active"
184
- question={null}
185
- runPhase="streaming"
186
- streamPreviewRows={8}
187
- streamMode="assistant-first"
188
- />
189
- </ThemeProvider>,
190
- );
191
-
192
- await sleep();
193
- let frame = harness.readOutput();
194
- assert.match(frame, /first chunk/i);
195
-
196
- // Update with more content — should render incrementally
197
- harness.resetOutput();
198
- harness.instance.rerender(
199
- <ThemeProvider theme="purple">
200
- <TurnGroup
201
- cols={120}
202
- turnIndex={1}
203
- user={user}
204
- run={run}
205
- assistant={makeAssistant(turnId, "First chunk\nSecond chunk")}
206
- opacity="active"
207
- question={null}
208
- runPhase="streaming"
209
- streamPreviewRows={8}
210
- streamMode="assistant-first"
211
- />
212
- </ThemeProvider>,
213
- );
214
-
215
- await sleep();
216
- frame = harness.readOutput();
217
- assert.match(frame, /second chunk/i);
218
-
219
- harness.instance.unmount();
220
- });
221
-
222
- test("finalization with same content does not cause visual flash", async () => {
223
- const turnId = 13;
224
- const user = makeUser(turnId);
225
- const run = makeRunningRun(turnId);
226
- const content = "The response content stays the same";
227
- const assistant = makeAssistant(turnId, content);
228
-
229
- const harness = renderTurnGroup(
230
- <ThemeProvider theme="purple">
231
- <TurnGroup
232
- cols={120}
233
- turnIndex={1}
234
- user={user}
235
- run={run}
236
- assistant={assistant}
237
- opacity="active"
238
- question={null}
239
- runPhase="streaming"
240
- streamPreviewRows={8}
241
- streamMode="assistant-first"
242
- />
243
- </ThemeProvider>,
244
- );
245
-
246
- await sleep();
247
- let streamingFrame = harness.readOutput();
248
- assert.match(streamingFrame, /response content stays/i);
249
-
250
- // Transition to final with same content
251
- harness.resetOutput();
252
- harness.instance.rerender(
253
- <ThemeProvider theme="purple">
254
- <TurnGroup
255
- cols={120}
256
- turnIndex={1}
257
- user={user}
258
- run={{ ...run, status: "completed", durationMs: 800 }}
259
- assistant={assistant}
260
- opacity="active"
261
- question={null}
262
- runPhase="final"
263
- streamPreviewRows={8}
264
- streamMode="assistant-first"
265
- />
266
- </ThemeProvider>,
267
- );
268
-
269
- await sleep();
270
- let finalFrame = harness.readOutput();
271
- // Content should still be present — no flash/disappearance
272
- assert.match(finalFrame, /response content stays/i);
273
- assert.match(finalFrame, /Codexa/);
274
-
275
- harness.instance.unmount();
276
- });
277
-
278
- test("assistant output 'Codexa' label is indented by transcriptContentIndent", async () => {
279
- const turnId = 100;
280
- const user = makeUser(turnId);
281
- const run = makeRunningRun(turnId);
282
- const assistant = makeAssistant(turnId, "Indented response");
283
-
284
- const harness = renderTurnGroup(
285
- <ThemeProvider theme="purple">
286
- <TurnGroup
287
- cols={120}
288
- turnIndex={1}
289
- user={user}
290
- run={run}
291
- assistant={assistant}
292
- opacity="active"
293
- question={null}
294
- runPhase="streaming"
295
- streamPreviewRows={8}
296
- streamMode="assistant-first"
297
- />
298
- </ThemeProvider>,
299
- );
300
-
301
- await sleep();
302
- const frame = harness.readOutput();
303
- const lines = frame.split("\n");
304
- const codexaLine = lines.find((l) => l.includes("Codexa"));
305
- assert.ok(codexaLine, "expected Codexa label");
306
- // Check that the line starts with exactly 4 spaces (transcriptContentIndent = 4)
307
- assert.match(codexaLine, /^ Codexa/);
308
-
309
- harness.instance.unmount();
310
- });
311
-
312
-
313
- test("snaps cleanly from streaming cursor view to completion view", async () => {
314
- const turnId = 11;
315
- const user = makeUser(turnId);
316
- const running = makeRunningRun(turnId);
317
- const assistant = makeAssistant(turnId, "Final response text");
318
-
319
- const harness = renderTurnGroup(
320
- <ThemeProvider theme="purple">
321
- <TurnGroup
322
- cols={120}
323
- turnIndex={2}
324
- user={user}
325
- run={running}
326
- assistant={assistant}
327
- opacity="active"
328
- question={null}
329
- runPhase="streaming"
330
- streamPreviewRows={6}
331
- streamMode="assistant-first"
332
- />
333
- </ThemeProvider>,
334
- );
335
-
336
- await sleep();
337
- let frame = harness.readOutput();
338
- assert.match(frame, /final response text/i);
339
- assert.match(frame, /▌/);
340
-
341
- harness.resetOutput();
342
- harness.instance.rerender(
343
- <ThemeProvider theme="purple">
344
- <TurnGroup
345
- cols={120}
346
- turnIndex={2}
347
- user={user}
348
- run={{ ...running, status: "completed", durationMs: 1200 }}
349
- assistant={assistant}
350
- opacity="active"
351
- question={null}
352
- runPhase="final"
353
- streamPreviewRows={6}
354
- streamMode="assistant-first"
355
- />
356
- </ThemeProvider>,
357
- );
358
-
359
- await sleep();
360
- frame = harness.readOutput();
361
- assert.match(frame, /final response text/i);
362
- assert.doesNotMatch(frame, /▌/);
363
-
364
- harness.instance.unmount();
365
- });
@@ -1,30 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- BUSY_STATUS_FRAME_MS,
5
- BUSY_STATUS_FRAMES,
6
- getBusyStatusFrame,
7
- isAnimatedBusyState,
8
- } from "./busyStatusAnimation.js";
9
-
10
- test("busy status frames advance in a fixed-width dot slot", () => {
11
- assert.deepEqual(
12
- [0, 1, 2, 3, 4].map(getBusyStatusFrame),
13
- [" . ", " .. ", " ...", " . ", " .. "],
14
- );
15
- assert.ok(BUSY_STATUS_FRAMES.every((frame) => frame.length === BUSY_STATUS_FRAMES[0]!.length));
16
- });
17
-
18
- test("busy status animation uses a low redraw cadence", () => {
19
- assert.equal(BUSY_STATUS_FRAME_MS, 900);
20
- });
21
-
22
- test("busy animation runs only for active work states", () => {
23
- assert.equal(isAnimatedBusyState("THINKING"), true);
24
- assert.equal(isAnimatedBusyState("RESPONDING"), true);
25
- assert.equal(isAnimatedBusyState("SHELL_RUNNING"), true);
26
- assert.equal(isAnimatedBusyState("IDLE"), false);
27
- assert.equal(isAnimatedBusyState("ANSWER_VISIBLE"), false);
28
- assert.equal(isAnimatedBusyState("ERROR"), false);
29
- assert.equal(isAnimatedBusyState("AWAITING_USER_ACTION"), false);
30
- });
@@ -1,142 +0,0 @@
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
- });
@@ -1,102 +0,0 @@
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
- });