@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,897 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import type { BackendProgressUpdate } from "../core/providers/types.js";
4
- import type { AssistantEvent, RunEvent, TimelineEvent, UserPromptEvent } from "./types.js";
5
- import { getRunPlanText, isBusy } from "./types.js";
6
- import { createInitialSessionState, reduceSessionState, type SessionState } from "./appSession.js";
7
- import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
8
- import { isAnimatedBusyState } from "../ui/busyStatusAnimation.js";
9
-
10
- function makeUserEvent(turnId: number): UserPromptEvent {
11
- return { id: 1, type: "user", createdAt: 1, prompt: "Do work", turnId };
12
- }
13
-
14
- function makeRunEvent(turnId: number): RunEvent {
15
- return {
16
- id: 2,
17
- type: "run",
18
- createdAt: 2,
19
- startedAt: 2,
20
- durationMs: null,
21
- backendId: "codex-subprocess",
22
- backendLabel: "Codexa",
23
- runtime: TEST_RUNTIME,
24
- prompt: "Do work",
25
- progressEntries: [],
26
- status: "running",
27
- summary: "Running",
28
- truncatedOutput: false,
29
- toolActivities: [],
30
- activity: [],
31
- touchedFileCount: 0,
32
- errorMessage: null,
33
- turnId,
34
- };
35
- }
36
-
37
- function makeAssistantEvent(turnId: number, content: string): AssistantEvent {
38
- return { id: 3, type: "assistant", createdAt: 3, content, contentChunks: [], turnId };
39
- }
40
-
41
- function makeProgressUpdate(
42
- id: string,
43
- text: string,
44
- source: BackendProgressUpdate["source"] = "reasoning",
45
- ): BackendProgressUpdate {
46
- return {
47
- id,
48
- source,
49
- text,
50
- };
51
- }
52
-
53
- function stateWithActiveRun(turnId: number): SessionState {
54
- const state = createInitialSessionState();
55
- return {
56
- ...state,
57
- activeEvents: [
58
- makeUserEvent(turnId),
59
- makeRunEvent(turnId),
60
- ],
61
- };
62
- }
63
-
64
- test("SUBMIT_PROMPT_RUN atomically clears composer, records history, appends one turn, and enters thinking", () => {
65
- const turnId = 50;
66
- const runId = 2;
67
- const initial: SessionState = {
68
- ...createInitialSessionState(),
69
- inputValue: "hello",
70
- cursor: 5,
71
- history: ["older"],
72
- };
73
-
74
- const state = reduceSessionState(initial, {
75
- type: "SUBMIT_PROMPT_RUN",
76
- historyValue: "hello",
77
- turnId,
78
- runId,
79
- events: [makeUserEvent(turnId), makeRunEvent(turnId)],
80
- });
81
-
82
- assert.equal(state.inputValue, "");
83
- assert.equal(state.cursor, 0);
84
- assert.deepEqual(state.history, ["hello", "older"]);
85
- assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run"]);
86
- assert.equal(state.activeEvents.filter((event) => event.type === "user").length, 1);
87
- assert.equal(state.activeEvents.filter((event) => event.type === "run").length, 1);
88
- assert.deepEqual(state.uiState, { kind: "THINKING", turnId });
89
- assert.deepEqual(state.staticEvents, []);
90
- });
91
-
92
- test("busy lifecycle preserves one canonical active turn until finalization", () => {
93
- const turnId = 51;
94
- let state = reduceSessionState(createInitialSessionState(), {
95
- type: "SUBMIT_PROMPT_RUN",
96
- historyValue: "hello",
97
- turnId,
98
- runId: 2,
99
- events: [makeUserEvent(turnId), makeRunEvent(turnId)],
100
- });
101
-
102
- state = reduceSessionState(state, {
103
- type: "RUN_APPEND_ASSISTANT_DELTA",
104
- turnId,
105
- runId: 2,
106
- chunk: "Hello",
107
- eventFactory: () => makeAssistantEvent(turnId, "Hello"),
108
- });
109
- state = reduceSessionState(state, {
110
- type: "RUN_MARK_FINAL_ANSWER_OBSERVED",
111
- runId: 2,
112
- turnId,
113
- response: "Hello",
114
- });
115
-
116
- assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run", "assistant"]);
117
- assert.equal(state.activeEvents.filter((event) => event.type === "user").length, 1);
118
- assert.equal(state.activeEvents.filter((event) => event.type === "assistant").length, 1);
119
- assert.deepEqual(state.staticEvents, []);
120
-
121
- state = reduceSessionState(state, {
122
- type: "FINALIZE_RUN",
123
- runId: 2,
124
- turnId,
125
- status: "completed",
126
- response: undefined,
127
- assistantFactory: () => makeAssistantEvent(turnId, ""),
128
- });
129
-
130
- assert.deepEqual(state.activeEvents, []);
131
- assert.deepEqual(state.staticEvents.map((event) => event.type), ["user", "run", "assistant"]);
132
- assert.equal(state.staticEvents.filter((event) => event.type === "user").length, 1);
133
- assert.equal(state.staticEvents.filter((event) => event.type === "assistant").length, 1);
134
- assert.equal(state.uiState.kind, "IDLE");
135
- });
136
-
137
- test("CLEAR_TRANSCRIPT removes all rendered transcript event state and preserves prompt history", () => {
138
- const turnId = 7;
139
- const shellEvent: TimelineEvent = {
140
- id: 10,
141
- type: "shell",
142
- createdAt: 10,
143
- command: "echo stale",
144
- lines: ["stale shell output"],
145
- stderrLines: [],
146
- summary: "Executed shell",
147
- status: "completed",
148
- exitCode: 0,
149
- durationMs: 20,
150
- };
151
- const state: SessionState = {
152
- ...createInitialSessionState(),
153
- staticEvents: [
154
- makeUserEvent(turnId),
155
- { ...makeRunEvent(turnId), status: "completed", durationMs: 100 },
156
- makeAssistantEvent(turnId, "stale assistant"),
157
- shellEvent,
158
- { id: 11, type: "system", createdAt: 11, title: "Mode", content: "Updated" },
159
- ],
160
- activeEvents: [
161
- makeUserEvent(turnId + 1),
162
- makeRunEvent(turnId + 1),
163
- makeAssistantEvent(turnId + 1, "streaming"),
164
- ],
165
- uiState: { kind: "RESPONDING", turnId: turnId + 1 },
166
- inputValue: "/clear",
167
- cursor: 6,
168
- history: ["Hello"],
169
- };
170
-
171
- const cleared = reduceSessionState(state, { type: "CLEAR_TRANSCRIPT" });
172
-
173
- assert.deepEqual(cleared.staticEvents, []);
174
- assert.deepEqual(cleared.activeEvents, []);
175
- assert.deepEqual(cleared.uiState, { kind: "IDLE" });
176
- assert.equal(cleared.clearCount, state.clearCount + 1);
177
- assert.equal(cleared.clearEpoch, state.clearEpoch + 1);
178
- assert.deepEqual(cleared.history, ["Hello"]);
179
- });
180
-
181
- test("FINALIZE_RUN preserves streamed content when response is undefined", () => {
182
- const turnId = 1;
183
- let state = stateWithActiveRun(turnId);
184
-
185
- // Simulate streaming: append assistant delta
186
- state = reduceSessionState(state, {
187
- type: "RUN_APPEND_ASSISTANT_DELTA",
188
- turnId,
189
- runId: 2,
190
- chunk: "Streamed response text",
191
- eventFactory: () => makeAssistantEvent(turnId, "Streamed response text"),
192
- });
193
-
194
- // Finalize with undefined response — should preserve streamed content
195
- state = reduceSessionState(state, {
196
- type: "FINALIZE_RUN",
197
- runId: 2,
198
- turnId,
199
- status: "completed",
200
- response: undefined,
201
- assistantFactory: () => makeAssistantEvent(turnId, ""),
202
- });
203
-
204
- const assistantEvent = state.staticEvents.find(
205
- (e): e is AssistantEvent => e.type === "assistant",
206
- );
207
- assert.ok(assistantEvent, "Assistant event should exist in static events");
208
- assert.equal(assistantEvent.content, "Streamed response text");
209
- });
210
-
211
- test("FINALIZE_RUN stores the fixed elapsed duration supplied by the app", () => {
212
- const turnId = 11;
213
- const state = reduceSessionState(stateWithActiveRun(turnId), {
214
- type: "FINALIZE_RUN",
215
- runId: 2,
216
- turnId,
217
- status: "completed",
218
- durationMs: 9876,
219
- response: "Done",
220
- assistantFactory: () => makeAssistantEvent(turnId, "Done"),
221
- });
222
-
223
- const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
224
- assert.ok(runEvent);
225
- assert.equal(runEvent.durationMs, 9876);
226
- });
227
-
228
- test("FINALIZE_RUN replaces streamed content when response differs", () => {
229
- const turnId = 2;
230
- let state = stateWithActiveRun(turnId);
231
-
232
- // Simulate streaming
233
- state = reduceSessionState(state, {
234
- type: "RUN_APPEND_ASSISTANT_DELTA",
235
- turnId,
236
- runId: 2,
237
- chunk: "Partial streamed text",
238
- eventFactory: () => makeAssistantEvent(turnId, "Partial streamed text"),
239
- });
240
-
241
- // Finalize with different response — should replace
242
- state = reduceSessionState(state, {
243
- type: "FINALIZE_RUN",
244
- runId: 2,
245
- turnId,
246
- status: "completed",
247
- response: "Full sanitized response with additional content",
248
- assistantFactory: () => makeAssistantEvent(turnId, "Full sanitized response with additional content"),
249
- });
250
-
251
- const assistantEvent = state.staticEvents.find(
252
- (e): e is AssistantEvent => e.type === "assistant",
253
- );
254
- assert.ok(assistantEvent, "Assistant event should exist in static events");
255
- assert.equal(assistantEvent.content, "Full sanitized response with additional content");
256
- });
257
-
258
- test("RUN_APPEND_ASSISTANT_DELTA transitions UI state to RESPONDING", () => {
259
- const turnId = 3;
260
- let state = stateWithActiveRun(turnId);
261
- state = reduceSessionState(state, {
262
- type: "UI_ACTION",
263
- action: { type: "PROMPT_RUN_STARTED", turnId },
264
- });
265
- assert.equal(state.uiState.kind, "THINKING");
266
-
267
- state = reduceSessionState(state, {
268
- type: "RUN_APPEND_ASSISTANT_DELTA",
269
- turnId,
270
- runId: 2,
271
- chunk: "First chunk",
272
- eventFactory: () => makeAssistantEvent(turnId, "First chunk"),
273
- });
274
-
275
- assert.equal(state.uiState.kind, "RESPONDING");
276
- });
277
-
278
- test("FINALIZE_RUN after assistant delta transitions UI state to IDLE", () => {
279
- const turnId = 35;
280
- let state = stateWithActiveRun(turnId);
281
- state = reduceSessionState(state, {
282
- type: "UI_ACTION",
283
- action: { type: "PROMPT_RUN_STARTED", turnId },
284
- });
285
- state = reduceSessionState(state, {
286
- type: "RUN_APPEND_ASSISTANT_DELTA",
287
- turnId,
288
- runId: 2,
289
- chunk: "Done.",
290
- eventFactory: () => makeAssistantEvent(turnId, "Done."),
291
- });
292
-
293
- state = reduceSessionState(state, {
294
- type: "FINALIZE_RUN",
295
- runId: 2,
296
- turnId,
297
- status: "completed",
298
- response: undefined,
299
- assistantFactory: () => makeAssistantEvent(turnId, ""),
300
- });
301
-
302
- assert.equal(state.uiState.kind, "IDLE");
303
- assert.equal(isAnimatedBusyState(state.uiState.kind), false);
304
- });
305
-
306
- test("RUN_MARK_FINAL_ANSWER_OBSERVED completes visible answer without thinking animation", () => {
307
- const turnId = 36;
308
- let state = stateWithActiveRun(turnId);
309
- state = reduceSessionState(state, {
310
- type: "UI_ACTION",
311
- action: { type: "PROMPT_RUN_STARTED", turnId },
312
- });
313
- state = reduceSessionState(state, {
314
- type: "RUN_APPEND_ASSISTANT_DELTA",
315
- turnId,
316
- runId: 2,
317
- chunk: "READY",
318
- eventFactory: () => makeAssistantEvent(turnId, "READY"),
319
- });
320
-
321
- state = reduceSessionState(state, {
322
- type: "RUN_MARK_FINAL_ANSWER_OBSERVED",
323
- runId: 2,
324
- turnId,
325
- response: "READY",
326
- });
327
-
328
- assert.equal(state.uiState.kind, "ANSWER_VISIBLE");
329
- assert.equal(isAnimatedBusyState(state.uiState.kind), false);
330
- assert.equal(isBusy(state.uiState), true);
331
- const run = state.activeEvents.find((event): event is RunEvent => event.type === "run");
332
- assert.ok(run);
333
- assert.equal(run.responseSegments?.[0]?.status, "completed");
334
- });
335
-
336
- test("plan deltas update one plan block and FINALIZE_RUN does not create assistant response", () => {
337
- const turnId = 37;
338
- let state = createInitialSessionState();
339
- const run: RunEvent = {
340
- ...makeRunEvent(turnId),
341
- plan: {
342
- id: "plan-2",
343
- streamSeq: 1,
344
- chunks: [],
345
- status: "active",
346
- startedAt: 2,
347
- },
348
- streamItems: [{ streamSeq: 1, kind: "plan", refId: "plan-2" }],
349
- responseSegments: [],
350
- lastStreamSeq: 1,
351
- activeResponseSegmentId: null,
352
- };
353
- state = {
354
- ...state,
355
- activeEvents: [makeUserEvent(turnId), run],
356
- };
357
- state = reduceSessionState(state, {
358
- type: "UI_ACTION",
359
- action: { type: "PROMPT_RUN_STARTED", turnId },
360
- });
361
-
362
- state = reduceSessionState(state, {
363
- type: "RUN_APPEND_PLAN_DELTA",
364
- turnId,
365
- runId: 2,
366
- chunk: "1. Inspect\n",
367
- });
368
- state = reduceSessionState(state, {
369
- type: "RUN_APPEND_PLAN_DELTA",
370
- turnId,
371
- runId: 2,
372
- chunk: "2. Render panel",
373
- });
374
- state = reduceSessionState(state, {
375
- type: "RUN_UPSERT_TOOL_ACTIVITY",
376
- runId: 2,
377
- activity: {
378
- id: "tool-1",
379
- command: "Get-Content src/app.tsx",
380
- status: "completed",
381
- startedAt: 10,
382
- completedAt: 20,
383
- },
384
- });
385
-
386
- const activeRun = state.activeEvents.find((event): event is RunEvent => event.type === "run");
387
- assert.ok(activeRun);
388
- assert.deepEqual(activeRun.streamItems?.map((item) => item.kind), ["plan", "action"]);
389
- assert.equal(getRunPlanText(activeRun.plan), "1. Inspect\n2. Render panel");
390
-
391
- state = reduceSessionState(state, {
392
- type: "FINALIZE_RUN",
393
- runId: 2,
394
- turnId,
395
- status: "completed",
396
- response: "1. Inspect\n2. Render panel",
397
- responsePresentation: "plan",
398
- assistantFactory: () => makeAssistantEvent(turnId, "should not render"),
399
- });
400
-
401
- const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
402
- assert.ok(finalizedRun);
403
- assert.equal(finalizedRun.plan?.status, "completed");
404
- assert.equal(getRunPlanText(finalizedRun.plan), "1. Inspect\n2. Render panel");
405
- assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["action", "plan"]);
406
- assert.deepEqual(finalizedRun.streamItems?.map((item) => item.streamSeq), [2, 3]);
407
- assert.equal(state.staticEvents.some((event) => event.type === "assistant"), false);
408
- });
409
-
410
- test("FINALIZE_RUN with plan presentation creates visible plan from final response without deltas", () => {
411
- const turnId = 38;
412
- let state = stateWithActiveRun(turnId);
413
- state = reduceSessionState(state, {
414
- type: "RUN_UPSERT_TOOL_ACTIVITY",
415
- runId: 2,
416
- activity: {
417
- id: "tool-1",
418
- command: "rg --files",
419
- status: "completed",
420
- startedAt: 10,
421
- completedAt: 20,
422
- },
423
- });
424
-
425
- state = reduceSessionState(state, {
426
- type: "FINALIZE_RUN",
427
- runId: 2,
428
- turnId,
429
- status: "completed",
430
- response: "1. Inspect files\n2. Update the timeline cache",
431
- responsePresentation: "plan",
432
- assistantFactory: () => makeAssistantEvent(turnId, "should not render"),
433
- });
434
-
435
- const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
436
- assert.ok(finalizedRun);
437
- assert.equal(finalizedRun.plan?.status, "completed");
438
- assert.equal(getRunPlanText(finalizedRun.plan), "1. Inspect files\n2. Update the timeline cache");
439
- assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["action", "plan"]);
440
- assert.equal(state.staticEvents.some((event) => event.type === "assistant"), false);
441
- });
442
-
443
- test("FINALIZE_RUN for approved plan execution keeps approved plan and records assistant response", () => {
444
- const turnId = 39;
445
- let state = createInitialSessionState();
446
- const approvedPlan = "1. Inspect files\n2. Render panel";
447
- state = {
448
- ...state,
449
- activeEvents: [
450
- makeUserEvent(turnId),
451
- {
452
- ...makeRunEvent(turnId),
453
- plan: {
454
- id: "plan-2",
455
- streamSeq: 1,
456
- chunks: [approvedPlan],
457
- status: "completed",
458
- startedAt: 2,
459
- },
460
- approvedPlan,
461
- streamItems: [{ streamSeq: 1, kind: "plan", refId: "plan-2" }],
462
- responseSegments: [],
463
- lastStreamSeq: 1,
464
- activeResponseSegmentId: null,
465
- },
466
- ],
467
- };
468
-
469
- state = reduceSessionState(state, {
470
- type: "FINALIZE_RUN",
471
- runId: 2,
472
- turnId,
473
- status: "completed",
474
- response: "Implemented the approved plan.",
475
- assistantFactory: () => makeAssistantEvent(turnId, "Implemented the approved plan."),
476
- });
477
-
478
- const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
479
- const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
480
- assert.ok(finalizedRun);
481
- assert.ok(finalizedAssistant);
482
- assert.equal(getRunPlanText(finalizedRun.plan), approvedPlan);
483
- assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["plan", "response"]);
484
- assert.equal(finalizedAssistant.content, "Implemented the approved plan.");
485
- });
486
-
487
- test("FINALIZE_RUN for canceled thinking-only run transitions UI state to IDLE", () => {
488
- const turnId = 36;
489
- let state = stateWithActiveRun(turnId);
490
- state = reduceSessionState(state, {
491
- type: "UI_ACTION",
492
- action: { type: "PROMPT_RUN_STARTED", turnId },
493
- });
494
-
495
- state = reduceSessionState(state, {
496
- type: "FINALIZE_RUN",
497
- runId: 2,
498
- turnId,
499
- status: "canceled",
500
- assistantFactory: () => makeAssistantEvent(turnId, ""),
501
- });
502
-
503
- assert.equal(state.uiState.kind, "IDLE");
504
- assert.equal(isAnimatedBusyState(state.uiState.kind), false);
505
- });
506
-
507
- test("FINALIZE_RUN for failed run records error and exits busy animation", () => {
508
- const turnId = 37;
509
- let state = stateWithActiveRun(turnId);
510
- state = reduceSessionState(state, {
511
- type: "UI_ACTION",
512
- action: { type: "PROMPT_RUN_STARTED", turnId },
513
- });
514
-
515
- state = reduceSessionState(state, {
516
- type: "FINALIZE_RUN",
517
- runId: 2,
518
- turnId,
519
- status: "failed",
520
- message: "Provider exploded",
521
- assistantFactory: () => makeAssistantEvent(turnId, ""),
522
- });
523
-
524
- const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
525
- assert.ok(runEvent);
526
- assert.equal(runEvent.status, "failed");
527
- assert.equal(runEvent.errorMessage, "Provider exploded");
528
- assert.deepEqual(state.uiState, { kind: "ERROR", turnId, message: "Provider exploded" });
529
- assert.equal(isAnimatedBusyState(state.uiState.kind), false);
530
- });
531
-
532
- test("stale RUN_APPEND_ASSISTANT_DELTA does not mutate current UI state", () => {
533
- const currentTurnId = 38;
534
- let state = stateWithActiveRun(currentTurnId);
535
- state = reduceSessionState(state, {
536
- type: "UI_ACTION",
537
- action: { type: "PROMPT_RUN_STARTED", turnId: currentTurnId },
538
- });
539
-
540
- const afterStaleDelta = reduceSessionState(state, {
541
- type: "RUN_APPEND_ASSISTANT_DELTA",
542
- turnId: currentTurnId - 1,
543
- runId: 2,
544
- chunk: "late chunk",
545
- eventFactory: () => makeAssistantEvent(currentTurnId - 1, "late chunk"),
546
- });
547
-
548
- assert.strictEqual(afterStaleDelta, state);
549
- assert.deepEqual(afterStaleDelta.uiState, { kind: "THINKING", turnId: currentTurnId });
550
- });
551
-
552
- test("RUN_APPLY_PROGRESS_UPDATES preserves separate entries and updates by id", () => {
553
- const turnId = 30;
554
- let state = stateWithActiveRun(turnId);
555
-
556
- state = reduceSessionState(state, {
557
- type: "RUN_APPLY_PROGRESS_UPDATES",
558
- runId: 2,
559
- updates: [
560
- makeProgressUpdate("progress-1", "Checking files"),
561
- makeProgressUpdate("progress-2", "Comparing snapshots"),
562
- ],
563
- });
564
-
565
- state = reduceSessionState(state, {
566
- type: "RUN_APPLY_PROGRESS_UPDATES",
567
- runId: 2,
568
- updates: [
569
- makeProgressUpdate("progress-1", "Checking files\n\nFound two candidates"),
570
- ],
571
- });
572
-
573
- const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
574
- assert.ok(runEvent);
575
- assert.equal(runEvent.progressEntries.length, 2);
576
- assert.equal(runEvent.progressEntries[0]?.text, "Checking files\n\nFound two candidates");
577
- assert.equal(runEvent.progressEntries[0]?.blocks.length, 2);
578
- assert.equal(runEvent.progressEntries[0]?.blocks[0]?.text, "Checking files");
579
- assert.equal(runEvent.progressEntries[0]?.blocks[0]?.status, "completed");
580
- assert.equal(runEvent.progressEntries[0]?.blocks[1]?.text, "Found two candidates");
581
- assert.equal(runEvent.progressEntries[1]?.text, "Comparing snapshots");
582
- assert.equal(runEvent.progressEntries[1]?.blocks[0]?.text, "Comparing snapshots");
583
- });
584
-
585
- test("RUN_APPLY_PROGRESS_UPDATES keeps completed block identities stable while the active block grows", () => {
586
- const turnId = 31;
587
- let state = stateWithActiveRun(turnId);
588
-
589
- state = reduceSessionState(state, {
590
- type: "RUN_APPLY_PROGRESS_UPDATES",
591
- runId: 2,
592
- updates: [
593
- makeProgressUpdate("progress-1", "Inspecting workspace\n\nSwitching to scratch files"),
594
- ],
595
- });
596
-
597
- const afterFirstUpdate = state.activeEvents.find((event): event is RunEvent => event.type === "run");
598
- assert.ok(afterFirstUpdate);
599
- const completedBlock = afterFirstUpdate.progressEntries[0]?.blocks[0];
600
- const activeBlock = afterFirstUpdate.progressEntries[0]?.blocks[1];
601
-
602
- state = reduceSessionState(state, {
603
- type: "RUN_APPLY_PROGRESS_UPDATES",
604
- runId: 2,
605
- updates: [
606
- makeProgressUpdate("progress-1", "Inspecting workspace\n\nSwitching to scratch files in repo root"),
607
- ],
608
- });
609
-
610
- const afterSecondUpdate = state.activeEvents.find((event): event is RunEvent => event.type === "run");
611
- assert.ok(afterSecondUpdate);
612
- assert.strictEqual(afterSecondUpdate.progressEntries[0]?.blocks[0], completedBlock);
613
- assert.notStrictEqual(afterSecondUpdate.progressEntries[0]?.blocks[1], activeBlock);
614
- assert.equal(afterSecondUpdate.progressEntries[0]?.blocks[1]?.text, "Switching to scratch files in repo root");
615
- });
616
-
617
- test("reducer preserves response action response ordering across dispatched callbacks", () => {
618
- const turnId = 32;
619
- let state = stateWithActiveRun(turnId);
620
-
621
- state = reduceSessionState(state, {
622
- type: "RUN_APPEND_ASSISTANT_DELTA",
623
- turnId,
624
- runId: 2,
625
- chunk: "First segment.",
626
- eventFactory: () => makeAssistantEvent(turnId, "First segment."),
627
- });
628
- state = reduceSessionState(state, {
629
- type: "RUN_UPSERT_TOOL_ACTIVITY",
630
- runId: 2,
631
- activity: {
632
- id: "tool-1",
633
- command: "Get-Content README.md",
634
- status: "completed",
635
- startedAt: 10,
636
- completedAt: 20,
637
- },
638
- });
639
- state = reduceSessionState(state, {
640
- type: "RUN_APPEND_ASSISTANT_DELTA",
641
- turnId,
642
- runId: 2,
643
- chunk: "Second segment.",
644
- eventFactory: () => makeAssistantEvent(turnId, "Second segment."),
645
- });
646
-
647
- const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
648
- assert.ok(runEvent);
649
- assert.deepEqual(runEvent.streamItems?.map((item) => item.kind), ["response", "action", "response"]);
650
- assert.equal(runEvent.responseSegments?.[0]?.chunks.join(""), "First segment.");
651
- assert.equal(runEvent.responseSegments?.[1]?.chunks.join(""), "Second segment.");
652
- });
653
-
654
- test("RUN_APPLY_LIVE_UPDATES applies ordered busy updates in one reducer action", () => {
655
- const turnId = 34;
656
- let state = stateWithActiveRun(turnId);
657
-
658
- state = reduceSessionState(state, {
659
- type: "UI_ACTION",
660
- action: { type: "PROMPT_RUN_STARTED", turnId },
661
- });
662
- state = reduceSessionState(state, {
663
- type: "RUN_APPLY_LIVE_UPDATES",
664
- turnId,
665
- runId: 2,
666
- updates: [
667
- { type: "progress", update: makeProgressUpdate("think-1", "Inspecting files") },
668
- {
669
- type: "tool",
670
- activity: {
671
- id: "tool-1",
672
- command: "Get-Content README.md",
673
- status: "running",
674
- startedAt: 10,
675
- },
676
- },
677
- { type: "assistant", chunk: "First segment." },
678
- {
679
- type: "tool",
680
- activity: {
681
- id: "tool-1",
682
- command: "Get-Content README.md",
683
- status: "completed",
684
- startedAt: 10,
685
- completedAt: 20,
686
- },
687
- },
688
- { type: "assistant", chunk: "Second segment." },
689
- ],
690
- assistantEventFactory: (chunk) => ({
691
- ...makeAssistantEvent(turnId, ""),
692
- contentChunks: [chunk],
693
- }),
694
- });
695
-
696
- const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
697
- const assistantEvent = state.activeEvents.find((event): event is AssistantEvent => event.type === "assistant");
698
- assert.ok(runEvent);
699
- assert.ok(assistantEvent);
700
- assert.equal(state.uiState.kind, "RESPONDING");
701
- assert.deepEqual(
702
- runEvent.streamItems?.map((item) => item.kind),
703
- ["thinking", "action", "response"],
704
- );
705
- assert.equal(assistantEvent.id, 3);
706
- assert.deepEqual(assistantEvent.contentChunks, ["First segment.", "Second segment."]);
707
- assert.equal(runEvent.toolActivities[0]?.status, "completed");
708
- });
709
-
710
- test("first prompt lifecycle exposes progress before finalization and preserves chronological stream order", () => {
711
- const turnId = 33;
712
- const userEvent = makeUserEvent(turnId);
713
- const runEvent = { ...makeRunEvent(turnId), summary: "Codex is starting..." };
714
- let state = createInitialSessionState();
715
-
716
- state = reduceSessionState(state, {
717
- type: "UI_ACTION",
718
- action: { type: "PROMPT_RUN_STARTED", turnId },
719
- });
720
- state = reduceSessionState(state, {
721
- type: "SET_ACTIVE_EVENTS",
722
- events: [userEvent, runEvent],
723
- });
724
-
725
- assert.equal(state.uiState.kind, "THINKING");
726
- assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run"]);
727
- assert.equal((state.activeEvents[1] as RunEvent).summary, "Codex is starting...");
728
-
729
- state = reduceSessionState(state, {
730
- type: "RUN_APPLY_PROGRESS_UPDATES",
731
- runId: 2,
732
- updates: [makeProgressUpdate("think-1", "Codex is checking project structure.")],
733
- });
734
- state = reduceSessionState(state, {
735
- type: "RUN_UPSERT_TOOL_ACTIVITY",
736
- runId: 2,
737
- activity: {
738
- id: "tool-1",
739
- command: "Get-ChildItem",
740
- status: "running",
741
- startedAt: 10,
742
- },
743
- });
744
- state = reduceSessionState(state, {
745
- type: "RUN_APPLY_PROGRESS_UPDATES",
746
- runId: 2,
747
- updates: [makeProgressUpdate("think-2", "Codex is validating the startup flow.")],
748
- });
749
- state = reduceSessionState(state, {
750
- type: "RUN_APPEND_ASSISTANT_DELTA",
751
- turnId,
752
- runId: 2,
753
- chunk: "Final answer.",
754
- eventFactory: () => makeAssistantEvent(turnId, "Final answer."),
755
- });
756
-
757
- const activeRun = state.activeEvents.find((event): event is RunEvent => event.type === "run");
758
- assert.ok(activeRun);
759
- assert.deepEqual(
760
- activeRun.streamItems?.map((item) => item.kind),
761
- ["thinking", "action", "thinking", "response"],
762
- );
763
-
764
- state = reduceSessionState(state, {
765
- type: "FINALIZE_RUN",
766
- runId: 2,
767
- turnId,
768
- status: "completed",
769
- response: undefined,
770
- assistantFactory: () => makeAssistantEvent(turnId, ""),
771
- });
772
-
773
- const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
774
- const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
775
- assert.ok(finalizedRun);
776
- assert.ok(finalizedAssistant);
777
- assert.deepEqual(
778
- finalizedRun.streamItems?.map((item) => item.kind),
779
- ["thinking", "action", "thinking", "response"],
780
- );
781
- assert.equal(finalizedAssistant.content, "Final answer.");
782
- });
783
-
784
- test("FINALIZE_RUN preserves construction trail and appends final response after actions", () => {
785
- const turnId = 34;
786
- const userEvent = makeUserEvent(turnId);
787
- const runEvent = { ...makeRunEvent(turnId), summary: "Codex is starting..." };
788
- let state = createInitialSessionState();
789
-
790
- state = reduceSessionState(state, {
791
- type: "UI_ACTION",
792
- action: { type: "PROMPT_RUN_STARTED", turnId },
793
- });
794
- state = reduceSessionState(state, {
795
- type: "SET_ACTIVE_EVENTS",
796
- events: [userEvent, runEvent],
797
- });
798
- state = reduceSessionState(state, {
799
- type: "RUN_APPLY_PROGRESS_UPDATES",
800
- runId: 2,
801
- updates: [makeProgressUpdate("think-1", "Inspecting 5-Date Verification.")],
802
- });
803
- state = reduceSessionState(state, {
804
- type: "RUN_UPSERT_TOOL_ACTIVITY",
805
- runId: 2,
806
- activity: {
807
- id: "tool-1",
808
- command: "Get-Content 5-Date-Verification.md",
809
- status: "completed",
810
- startedAt: 10,
811
- completedAt: 20,
812
- summary: "Read 42 lines",
813
- },
814
- });
815
- state = reduceSessionState(state, {
816
- type: "FINALIZE_RUN",
817
- runId: 2,
818
- turnId,
819
- status: "completed",
820
- response: "Final answer.",
821
- assistantFactory: () => makeAssistantEvent(turnId, "Final answer."),
822
- });
823
-
824
- const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
825
- const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
826
- assert.ok(finalizedRun);
827
- assert.ok(finalizedAssistant);
828
- assert.deepEqual(
829
- finalizedRun.streamItems?.map((item) => item.kind),
830
- ["thinking", "action", "response"],
831
- );
832
- assert.equal(finalizedRun.toolActivities[0]?.status, "completed");
833
- assert.equal(finalizedRun.responseSegments?.[0]?.chunks.join(""), "Final answer.");
834
- assert.equal(finalizedAssistant.content, "Final answer.");
835
- });
836
-
837
- test("finalized runs retain their runtime snapshot after unrelated state changes", () => {
838
- const turnId = 4;
839
- let state = stateWithActiveRun(turnId);
840
-
841
- state = reduceSessionState(state, {
842
- type: "FINALIZE_RUN",
843
- runId: 2,
844
- turnId,
845
- status: "completed",
846
- response: "Done",
847
- assistantFactory: () => makeAssistantEvent(turnId, "Done"),
848
- });
849
-
850
- const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
851
- assert.ok(runEvent);
852
- assert.equal(runEvent.runtime.model, TEST_RUNTIME.model);
853
- assert.equal(runEvent.runtime.policy.sandboxMode, TEST_RUNTIME.policy.sandboxMode);
854
- });
855
-
856
- // ─── externalCliStatus state machine ─────────────────────────────────────────
857
-
858
- test("initial session state has externalCliStatus 'idle'", () => {
859
- const state = createInitialSessionState();
860
- assert.equal(state.externalCliStatus, "idle");
861
- });
862
-
863
- test("SET_EXTERNAL_CLI_STATUS transitions idle → starting", () => {
864
- const state = createInitialSessionState();
865
- const next = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "starting" });
866
- assert.equal(next.externalCliStatus, "starting");
867
- });
868
-
869
- test("SET_EXTERNAL_CLI_STATUS transitions starting → ready", () => {
870
- let state = createInitialSessionState();
871
- state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "starting" });
872
- const next = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
873
- assert.equal(next.externalCliStatus, "ready");
874
- });
875
-
876
- test("SET_EXTERNAL_CLI_STATUS is a no-op when status is already the same (returns same object)", () => {
877
- let state = createInitialSessionState();
878
- state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
879
- const again = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
880
- assert.strictEqual(again, state, "should return the exact same state reference");
881
- });
882
-
883
- test("SUBMIT_PROMPT_RUN does not change externalCliStatus when provider is already ready", () => {
884
- let state = createInitialSessionState();
885
- state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
886
-
887
- const userEvent = makeUserEvent(99);
888
- const runEvent = makeRunEvent(99);
889
- const next = reduceSessionState(state, {
890
- type: "SUBMIT_PROMPT_RUN",
891
- turnId: 99,
892
- runId: 2,
893
- events: [userEvent, runEvent],
894
- });
895
-
896
- assert.equal(next.externalCliStatus, "ready", "externalCliStatus must remain 'ready' across prompts");
897
- });