@golba98/codexa 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,365 @@
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
+ });