@golba98/codexa 1.0.2 → 1.0.4

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 (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -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
- });