@vitest-agent/ui 1.0.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dispatcher/cells/single-file-fail.js +28 -0
  4. package/dispatcher/cells/single-file-pass.js +19 -0
  5. package/dispatcher/cells/single-file-threshold.js +22 -0
  6. package/dispatcher/cells/single-project-fail.js +34 -0
  7. package/dispatcher/cells/single-project-pass.js +17 -0
  8. package/dispatcher/cells/single-project-threshold.js +25 -0
  9. package/dispatcher/cells/single-test-fail.js +22 -0
  10. package/dispatcher/cells/single-test-pass.js +19 -0
  11. package/dispatcher/cells/single-test-threshold.js +12 -0
  12. package/dispatcher/cells/workspace-fail.js +27 -0
  13. package/dispatcher/cells/workspace-pass.js +25 -0
  14. package/dispatcher/cells/workspace-threshold.js +28 -0
  15. package/dispatcher/classify.js +48 -0
  16. package/dispatcher/dispatch.js +73 -0
  17. package/dispatcher/footer.js +55 -0
  18. package/dispatcher/helpers.js +186 -0
  19. package/dispatcher/ink-helpers.js +79 -0
  20. package/format-duration.js +34 -0
  21. package/index.d.ts +1843 -0
  22. package/index.js +40 -0
  23. package/package.json +59 -0
  24. package/pubsub/Channel.js +30 -0
  25. package/pubsub/Publisher.js +37 -0
  26. package/pubsub/Subscriber.js +77 -0
  27. package/pubsub/index.js +5 -0
  28. package/reducer.js +260 -0
  29. package/render-agent.js +137 -0
  30. package/render-ink/CountColumns.js +34 -0
  31. package/render-ink/CoverageBlock.js +80 -0
  32. package/render-ink/FailureSection.js +69 -0
  33. package/render-ink/FailuresSection.js +85 -0
  34. package/render-ink/ModuleHeader.js +43 -0
  35. package/render-ink/ProjectRow.js +56 -0
  36. package/render-ink/StatusIcon.js +44 -0
  37. package/render-ink/StreamApp.js +453 -0
  38. package/render-ink/SuggestedActions.js +49 -0
  39. package/render-ink/TestRow.js +33 -0
  40. package/render-ink/TrendLine.js +42 -0
  41. package/render-ink/index.js +14 -0
  42. package/render-ink/spinner.js +62 -0
  43. package/render-ink/tag-suffix.js +18 -0
  44. package/synthesize.js +335 -0
  45. package/tsdoc-metadata.json +11 -0
@@ -0,0 +1,453 @@
1
+ import { classifyRunShape } from "../dispatcher/classify.js";
2
+ import { formatDisplayDuration } from "../format-duration.js";
3
+ import { CountColumns } from "./CountColumns.js";
4
+ import { StatusIcon } from "./StatusIcon.js";
5
+ import { formatTagSuffix } from "./tag-suffix.js";
6
+ import { ProjectRow } from "./ProjectRow.js";
7
+ import { spinnerFrame } from "./spinner.js";
8
+ import { TestRow } from "./TestRow.js";
9
+ import { TrendLine } from "./TrendLine.js";
10
+ import { Box, Text } from "ink";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+
13
+ //#region src/render-ink/StreamApp.tsx
14
+ const ANONYMOUS_PROJECT = "default";
15
+ /** Maximum number of running rows displayed in the Live region. Beyond this,
16
+ * excess running units collapse into a single "… and N more running" line. */
17
+ const MAX_LIVE_RUNNING_ROWS = 4;
18
+ const groupByProject = (state) => {
19
+ const order = [];
20
+ const byName = /* @__PURE__ */ new Map();
21
+ for (const path of state.moduleOrder) {
22
+ const mod = state.modules[path];
23
+ if (mod === void 0) continue;
24
+ const name = mod.projectName ?? ANONYMOUS_PROJECT;
25
+ const existing = byName.get(name);
26
+ if (existing === void 0) {
27
+ byName.set(name, [mod]);
28
+ order.push(name);
29
+ } else existing.push(mod);
30
+ }
31
+ return order.map((name) => ({
32
+ name,
33
+ modules: byName.get(name) ?? []
34
+ }));
35
+ };
36
+ const moduleRunning = (m) => m.status !== "finished";
37
+ const moduleElapsedMs = (m, nowMs) => {
38
+ if (m.status === "finished") return m.durationMs;
39
+ if (m.startedAt === void 0) return 0;
40
+ const started = Date.parse(m.startedAt);
41
+ return Number.isNaN(started) ? 0 : Math.max(0, nowMs - started);
42
+ };
43
+ const sumCounts = (modules) => {
44
+ let passCount = 0;
45
+ let failCount = 0;
46
+ let skipCount = 0;
47
+ let timeoutCount = 0;
48
+ for (const m of modules) {
49
+ passCount += m.passCount;
50
+ failCount += m.failCount;
51
+ skipCount += m.skipCount;
52
+ timeoutCount += m.timeoutCount;
53
+ }
54
+ return {
55
+ passCount,
56
+ failCount,
57
+ skipCount,
58
+ timeoutCount
59
+ };
60
+ };
61
+ const mergeTagCounts = (modules) => {
62
+ const out = {};
63
+ for (const m of modules) for (const [tag, count] of Object.entries(m.tagCounts ?? {})) out[tag] = (out[tag] ?? 0) + count;
64
+ return out;
65
+ };
66
+ const projectElapsedMs = (group, nowMs) => {
67
+ if (!group.modules.some(moduleRunning)) {
68
+ let total = 0;
69
+ for (const m of group.modules) total += m.durationMs;
70
+ return total;
71
+ }
72
+ let earliest = Number.POSITIVE_INFINITY;
73
+ for (const m of group.modules) {
74
+ if (m.startedAt === void 0) continue;
75
+ const started = Date.parse(m.startedAt);
76
+ if (!Number.isNaN(started) && started < earliest) earliest = started;
77
+ }
78
+ return earliest === Number.POSITIVE_INFINITY ? 0 : Math.max(0, nowMs - earliest);
79
+ };
80
+ const moduleIcon = (m) => {
81
+ if (m.failCount > 0) return "failed";
82
+ if (m.timeoutCount > 0) return "timed-out";
83
+ if (m.skipCount > 0 && m.passCount === 0) return "skipped";
84
+ return "passed";
85
+ };
86
+ /**
87
+ * One `single-project`-shape row: a module with the four count columns.
88
+ *
89
+ * Used in the Live region for both running rows (spinner, `running=true`)
90
+ * and finished rows (resolved glyph, `running=false`).
91
+ */
92
+ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut }) => {
93
+ const running = moduleRunning(module);
94
+ const queued = module.status === "queued";
95
+ let glyph;
96
+ let notStartedSuffix = null;
97
+ if (timedOut && running) if (queued) {
98
+ glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "queued" });
99
+ notStartedSuffix = /* @__PURE__ */ jsx(Text, {
100
+ dimColor: true,
101
+ children: " not started — killed before reaching this module"
102
+ });
103
+ } else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "timed-out" });
104
+ else if (running) glyph = /* @__PURE__ */ jsx(Text, {
105
+ color: "yellow",
106
+ children: frame
107
+ });
108
+ else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: moduleIcon(module) });
109
+ return /* @__PURE__ */ jsxs(Box, { children: [
110
+ /* @__PURE__ */ jsx(Text, { children: " " }),
111
+ glyph,
112
+ /* @__PURE__ */ jsxs(Text, { children: [
113
+ " ",
114
+ module.modulePath.padEnd(nameWidth),
115
+ " "
116
+ ] }),
117
+ /* @__PURE__ */ jsx(CountColumns, {
118
+ passCount: module.passCount,
119
+ failCount: module.failCount,
120
+ skipCount: module.skipCount,
121
+ timeoutCount: module.timeoutCount
122
+ }),
123
+ /* @__PURE__ */ jsxs(Text, {
124
+ dimColor: true,
125
+ children: [" ", formatDisplayDuration(moduleElapsedMs(module, nowMs))]
126
+ }),
127
+ formatTagSuffix(module.tagCounts).length > 0 ? /* @__PURE__ */ jsxs(Text, {
128
+ color: "cyan",
129
+ children: [" ", formatTagSuffix(module.tagCounts)]
130
+ }) : null,
131
+ notStartedSuffix
132
+ ] });
133
+ };
134
+ const INLINE_VALUE_LIMIT = 200;
135
+ /** A failing test's error, expanded inline beneath a leaf row or in a failure block. */
136
+ const InlineError = ({ failure }) => {
137
+ if (failure.error?.message === void 0) return null;
138
+ const first = failure.error.message.split("\n", 1)[0] ?? "";
139
+ const expected = failure.error.expected;
140
+ const received = failure.error.received;
141
+ return /* @__PURE__ */ jsxs(Box, {
142
+ flexDirection: "column",
143
+ children: [
144
+ /* @__PURE__ */ jsxs(Text, {
145
+ dimColor: true,
146
+ children: [" ", first]
147
+ }),
148
+ expected !== void 0 ? /* @__PURE__ */ jsxs(Text, {
149
+ dimColor: true,
150
+ children: [
151
+ " ",
152
+ "expected: ",
153
+ expected.slice(0, INLINE_VALUE_LIMIT)
154
+ ]
155
+ }) : null,
156
+ received !== void 0 ? /* @__PURE__ */ jsxs(Text, {
157
+ dimColor: true,
158
+ children: [
159
+ " ",
160
+ "received: ",
161
+ received.slice(0, INLINE_VALUE_LIMIT)
162
+ ]
163
+ }) : null
164
+ ]
165
+ });
166
+ };
167
+ const failureKey = (f) => `failure:${f.modulePath}::${f.suitePath.join("/")}::${f.testName}`;
168
+ const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
169
+ const failurePath = (f) => [
170
+ f.modulePath,
171
+ ...f.suitePath,
172
+ f.testName
173
+ ].join(" › ");
174
+ /** A failure entry rendered in the Live region. */
175
+ const FailureItem = ({ failure }) => /* @__PURE__ */ jsxs(Box, {
176
+ flexDirection: "column",
177
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
178
+ " ",
179
+ /* @__PURE__ */ jsx(Text, {
180
+ color: failure.timedOut === true ? "#e09a4e" : "red",
181
+ children: failure.timedOut === true ? "⧖" : "✗"
182
+ }),
183
+ " ",
184
+ failurePath(failure),
185
+ failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
186
+ color: "#c98ae0",
187
+ children: [
188
+ " [",
189
+ failure.classification,
190
+ "]"
191
+ ]
192
+ }) : null
193
+ ] }), /* @__PURE__ */ jsx(InlineError, { failure })]
194
+ });
195
+ const TotalsLine = ({ totals }) => /* @__PURE__ */ jsxs(Text, { children: [
196
+ /* @__PURE__ */ jsx(Text, {
197
+ bold: true,
198
+ children: "Total:"
199
+ }),
200
+ " ",
201
+ /* @__PURE__ */ jsx(CountColumns, {
202
+ passCount: totals.passCount,
203
+ failCount: totals.failCount,
204
+ skipCount: totals.skipCount,
205
+ timeoutCount: totals.timeoutCount
206
+ }),
207
+ /* @__PURE__ */ jsxs(Text, {
208
+ dimColor: true,
209
+ children: [" ", formatDisplayDuration(totals.durationMs)]
210
+ })
211
+ ] });
212
+ const CoverageItem = ({ state }) => {
213
+ if (state.coverage === null) return null;
214
+ const clean = state.coverage.violations.length === 0;
215
+ return /* @__PURE__ */ jsxs(Text, { children: [
216
+ /* @__PURE__ */ jsx(Text, {
217
+ bold: true,
218
+ children: "Coverage:"
219
+ }),
220
+ " ",
221
+ /* @__PURE__ */ jsx(Text, {
222
+ color: clean ? "green" : "yellow",
223
+ children: clean ? "✓" : "⚠"
224
+ }),
225
+ " ",
226
+ clean ? "all metrics meet thresholds" : `${state.coverage.violations.length} threshold violation(s)`
227
+ ] });
228
+ };
229
+ const samePath = (a, b) => {
230
+ if (a.length !== b.length) return false;
231
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
232
+ return true;
233
+ };
234
+ /**
235
+ * Compose the Live region content for the given state + shape. Pure;
236
+ * recomputed every render and every clock tick. Renders the ENTIRE
237
+ * run picture — all rows in discovery order, with running rows capped
238
+ * at MAX_LIVE_RUNNING_ROWS and finished rows always shown.
239
+ */
240
+ const liveRegion = (state, shape, nowMs, frame) => {
241
+ const groups = groupByProject(state);
242
+ const timedOut = state.phase === "timed-out";
243
+ const finished = state.phase === "finished" || state.phase === "timed-out";
244
+ const ordered = state.moduleOrder.map((p) => state.modules[p]).filter((m) => m !== void 0);
245
+ if (shape === "single-test") {
246
+ const sole = ordered[0];
247
+ if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsx(Text, {
248
+ dimColor: true,
249
+ children: "discovering tests…"
250
+ });
251
+ const only = sole.tests[0];
252
+ if (only === void 0) return /* @__PURE__ */ jsx(Text, {
253
+ dimColor: true,
254
+ children: "discovering tests…"
255
+ });
256
+ const failure = only.status === "failed" || only.status === "timed-out" ? state.failures.find((f) => f.testName === only.testName && f.modulePath === sole.modulePath && samePath(f.suitePath, only.suitePath)) : void 0;
257
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TestRow, {
258
+ test: only,
259
+ indent: 0
260
+ }), failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null] });
261
+ }
262
+ if (groups.length === 0) return /* @__PURE__ */ jsx(Text, {
263
+ dimColor: true,
264
+ children: "discovering tests…"
265
+ });
266
+ const failuresSection = state.failures.length > 0 ? /* @__PURE__ */ jsx(Box, {
267
+ flexDirection: "column",
268
+ children: state.failures.map((f) => /* @__PURE__ */ jsx(FailureItem, { failure: f }, failureKey(f)))
269
+ }) : null;
270
+ const coverageItem = /* @__PURE__ */ jsx(CoverageItem, { state });
271
+ const trendItem = state.trend !== null ? /* @__PURE__ */ jsx(TrendLine, { trend: state.trend }) : null;
272
+ const totalsItem = /* @__PURE__ */ jsxs(Box, {
273
+ flexDirection: "column",
274
+ children: [finished && timedOut ? /* @__PURE__ */ jsx(Text, {
275
+ color: "#e09a4e",
276
+ bold: true,
277
+ children: "⧖ Run timed out"
278
+ }) : null, /* @__PURE__ */ jsx(TotalsLine, { totals: state.totals })]
279
+ });
280
+ if (shape === "workspace") {
281
+ const nameWidth = Math.max(0, ...groups.map((g) => g.name.length));
282
+ let runningVisible = 0;
283
+ let runningOverflow = 0;
284
+ const rows = [];
285
+ for (const g of groups) if (g.modules.some(moduleRunning)) if (runningVisible < MAX_LIVE_RUNNING_ROWS) {
286
+ runningVisible++;
287
+ const counts = sumCounts(g.modules);
288
+ const summary = {
289
+ name: g.name,
290
+ passCount: counts.passCount,
291
+ failCount: counts.failCount,
292
+ skipCount: counts.skipCount,
293
+ durationMs: 0
294
+ };
295
+ rows.push(/* @__PURE__ */ jsx(ProjectRow, {
296
+ project: summary,
297
+ counts,
298
+ running: true,
299
+ timedOut,
300
+ elapsedMs: projectElapsedMs(g, nowMs),
301
+ frame,
302
+ nameWidth,
303
+ tagCounts: mergeTagCounts(g.modules)
304
+ }, g.name));
305
+ } else runningOverflow++;
306
+ else {
307
+ const counts = sumCounts(g.modules);
308
+ const summary = {
309
+ name: g.name,
310
+ passCount: counts.passCount,
311
+ failCount: counts.failCount,
312
+ skipCount: counts.skipCount,
313
+ durationMs: 0
314
+ };
315
+ rows.push(/* @__PURE__ */ jsx(ProjectRow, {
316
+ project: summary,
317
+ counts,
318
+ running: false,
319
+ timedOut,
320
+ elapsedMs: projectElapsedMs(g, nowMs),
321
+ frame,
322
+ nameWidth,
323
+ tagCounts: mergeTagCounts(g.modules)
324
+ }, g.name));
325
+ }
326
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
327
+ /* @__PURE__ */ jsxs(Text, {
328
+ bold: true,
329
+ children: [
330
+ "Projects (",
331
+ groups.length,
332
+ "):"
333
+ ]
334
+ }),
335
+ rows,
336
+ runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
337
+ dimColor: true,
338
+ children: [
339
+ " … and ",
340
+ runningOverflow,
341
+ " more running"
342
+ ]
343
+ }) : null,
344
+ failuresSection,
345
+ coverageItem,
346
+ trendItem,
347
+ totalsItem
348
+ ] });
349
+ }
350
+ if (shape === "single-project") {
351
+ const nameWidth = Math.max(0, ...ordered.map((m) => m.modulePath.length));
352
+ let runningVisible = 0;
353
+ let runningOverflow = 0;
354
+ const rows = [];
355
+ for (const m of ordered) if (moduleRunning(m)) if (runningVisible < MAX_LIVE_RUNNING_ROWS) {
356
+ runningVisible++;
357
+ rows.push(/* @__PURE__ */ jsx(ModuleStreamRow, {
358
+ module: m,
359
+ nowMs,
360
+ frame,
361
+ nameWidth,
362
+ timedOut
363
+ }, m.modulePath));
364
+ } else runningOverflow++;
365
+ else rows.push(/* @__PURE__ */ jsx(ModuleStreamRow, {
366
+ module: m,
367
+ nowMs,
368
+ frame,
369
+ nameWidth,
370
+ timedOut
371
+ }, m.modulePath));
372
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
373
+ /* @__PURE__ */ jsxs(Text, {
374
+ bold: true,
375
+ children: [
376
+ "Modules (",
377
+ ordered.length,
378
+ "):"
379
+ ]
380
+ }),
381
+ rows,
382
+ runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
383
+ dimColor: true,
384
+ children: [
385
+ " … and ",
386
+ runningOverflow,
387
+ " more running"
388
+ ]
389
+ }) : null,
390
+ failuresSection,
391
+ coverageItem,
392
+ trendItem,
393
+ totalsItem
394
+ ] });
395
+ }
396
+ const sole = ordered[0];
397
+ if (sole === void 0) return /* @__PURE__ */ jsx(Text, {
398
+ dimColor: true,
399
+ children: "discovering tests…"
400
+ });
401
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
402
+ /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
403
+ bold: true,
404
+ children: sole.modulePath
405
+ }), /* @__PURE__ */ jsxs(Text, {
406
+ dimColor: true,
407
+ children: [
408
+ " — ",
409
+ sole.tests.length,
410
+ " tests"
411
+ ]
412
+ })] }),
413
+ sole.tests.map((t) => {
414
+ const failure = t.status === "failed" || t.status === "timed-out" ? state.failures.find((f) => f.testName === t.testName && f.modulePath === sole.modulePath && samePath(f.suitePath, t.suitePath)) : void 0;
415
+ return /* @__PURE__ */ jsxs(Box, {
416
+ flexDirection: "column",
417
+ children: [/* @__PURE__ */ jsx(TestRow, {
418
+ test: t,
419
+ indent: 2
420
+ }), failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null]
421
+ }, testKey(sole.modulePath, t));
422
+ }),
423
+ coverageItem,
424
+ trendItem,
425
+ totalsItem
426
+ ] });
427
+ };
428
+ /**
429
+ * Root Ink component for the `stream` live renderer. Classifies the run
430
+ * shape and renders the appropriate per-shape live region.
431
+ *
432
+ * @public
433
+ */
434
+ const StreamApp = ({ state, frameIndex, nowMs }) => {
435
+ const now = nowMs ?? Date.now();
436
+ const frame = spinnerFrame(frameIndex);
437
+ return /* @__PURE__ */ jsx(Box, {
438
+ flexDirection: "column",
439
+ children: liveRegion(state, classifyRunShape(state, groupByProject(state).map((g) => {
440
+ const c = sumCounts(g.modules);
441
+ return {
442
+ name: g.name,
443
+ passCount: c.passCount,
444
+ failCount: c.failCount,
445
+ skipCount: c.skipCount,
446
+ durationMs: 0
447
+ };
448
+ })), now, frame)
449
+ });
450
+ };
451
+
452
+ //#endregion
453
+ export { StreamApp };
@@ -0,0 +1,49 @@
1
+ import { Box, Text } from "ink";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+
4
+ //#region src/render-ink/SuggestedActions.tsx
5
+ const SEVERITY_COLOR = {
6
+ info: "blue",
7
+ warn: "yellow",
8
+ blocker: "red"
9
+ };
10
+ /**
11
+ * Renders the suggested-actions queue as severity-prefixed rows with
12
+ * optional tool hints. Returns `null` when the actions list is empty.
13
+ *
14
+ * @public
15
+ */
16
+ const SuggestedActions = ({ actions }) => {
17
+ if (actions.length === 0) return null;
18
+ return /* @__PURE__ */ jsxs(Box, {
19
+ flexDirection: "column",
20
+ children: [/* @__PURE__ */ jsx(Text, {
21
+ bold: true,
22
+ children: "Actions"
23
+ }), actions.map((action, idx) => /* @__PURE__ */ jsxs(Box, {
24
+ flexDirection: "column",
25
+ children: [/* @__PURE__ */ jsxs(Box, { children: [
26
+ /* @__PURE__ */ jsx(Text, {
27
+ color: SEVERITY_COLOR[action.severity],
28
+ bold: true,
29
+ children: ` ${action.severity}: `
30
+ }),
31
+ /* @__PURE__ */ jsx(Text, { children: action.title }),
32
+ action.targetTool !== void 0 ? /* @__PURE__ */ jsxs(Text, {
33
+ dimColor: true,
34
+ children: [
35
+ " (tool: ",
36
+ action.targetTool,
37
+ ")"
38
+ ]
39
+ }) : null
40
+ ] }), /* @__PURE__ */ jsx(Text, {
41
+ dimColor: true,
42
+ children: ` ${action.detail}`
43
+ })]
44
+ }, `${action.severity}-${idx}-${action.title}`))]
45
+ });
46
+ };
47
+
48
+ //#endregion
49
+ export { SuggestedActions };
@@ -0,0 +1,33 @@
1
+ import { formatDisplayDuration } from "../format-duration.js";
2
+ import { StatusIcon } from "./StatusIcon.js";
3
+ import { Box, Text } from "ink";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/render-ink/TestRow.tsx
7
+ const testGlyph = (status) => status;
8
+ /**
9
+ * Renders one test row: status glyph, optional suite prefix, test name, and duration.
10
+ *
11
+ * @public
12
+ */
13
+ const TestRow = ({ test, indent = 2 }) => {
14
+ const pad = " ".repeat(indent);
15
+ const suite = test.suitePath.length > 0 ? `${test.suitePath.join(" > ")} > ` : "";
16
+ const duration = test.durationMs !== null ? ` (${formatDisplayDuration(test.durationMs)})` : "";
17
+ return /* @__PURE__ */ jsxs(Box, { children: [
18
+ /* @__PURE__ */ jsx(Text, { children: pad }),
19
+ /* @__PURE__ */ jsx(StatusIcon, { status: testGlyph(test.status) }),
20
+ /* @__PURE__ */ jsxs(Text, { children: [
21
+ " ",
22
+ suite,
23
+ test.testName
24
+ ] }),
25
+ /* @__PURE__ */ jsx(Text, {
26
+ dimColor: true,
27
+ children: duration
28
+ })
29
+ ] });
30
+ };
31
+
32
+ //#endregion
33
+ export { TestRow };
@@ -0,0 +1,42 @@
1
+ import { Text } from "ink";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+
4
+ //#region src/render-ink/TrendLine.tsx
5
+ /**
6
+ * The one-line trend signal — `Trend: <direction> (<N> runs)`.
7
+ */
8
+ const COLOR = {
9
+ improving: "green",
10
+ regressing: "red",
11
+ stable: "gray"
12
+ };
13
+ /**
14
+ * Renders the one-line trend signal: direction (colored) and run count.
15
+ *
16
+ * @public
17
+ */
18
+ const TrendLine = ({ trend }) => {
19
+ const runs = trend.runCount === 1 ? "1 run" : `${trend.runCount} runs`;
20
+ return /* @__PURE__ */ jsxs(Text, { children: [
21
+ /* @__PURE__ */ jsx(Text, {
22
+ bold: true,
23
+ children: "Trend:"
24
+ }),
25
+ " ",
26
+ /* @__PURE__ */ jsx(Text, {
27
+ color: COLOR[trend.direction],
28
+ children: trend.direction
29
+ }),
30
+ /* @__PURE__ */ jsxs(Text, {
31
+ dimColor: true,
32
+ children: [
33
+ " (",
34
+ runs,
35
+ ")"
36
+ ]
37
+ })
38
+ ] });
39
+ };
40
+
41
+ //#endregion
42
+ export { TrendLine };
@@ -0,0 +1,14 @@
1
+ import { CountColumns } from "./CountColumns.js";
2
+ import { CoverageBlock } from "./CoverageBlock.js";
3
+ import { FailureSection } from "./FailureSection.js";
4
+ import { FailuresSection } from "./FailuresSection.js";
5
+ import { StatusIcon } from "./StatusIcon.js";
6
+ import { ModuleHeader } from "./ModuleHeader.js";
7
+ import { ProjectRow } from "./ProjectRow.js";
8
+ import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime } from "./spinner.js";
9
+ import { TestRow } from "./TestRow.js";
10
+ import { TrendLine } from "./TrendLine.js";
11
+ import { StreamApp } from "./StreamApp.js";
12
+ import { SuggestedActions } from "./SuggestedActions.js";
13
+
14
+ export { };
@@ -0,0 +1,62 @@
1
+ //#region src/render-ink/spinner.ts
2
+ /**
3
+ * Hand-rolled Braille spinner for the `stream` live renderer.
4
+ *
5
+ * No `ink-spinner` dependency — that package is a thin wrapper over the
6
+ * same ten Braille characters plus a timer. The `stream` renderer
7
+ * already needs a frame clock for the ticking elapsed column, so the
8
+ * timer is shared and only the frame array lives here.
9
+ *
10
+ * The frame index is presentation state: it is derived from wall-clock
11
+ * time by `createLiveInk` and passed to `StreamApp` as a prop. It never
12
+ * enters the event-sourced `RenderState`.
13
+ */
14
+ /**
15
+ * The ten Braille spinner frames, in animation order.
16
+ *
17
+ * @public
18
+ */
19
+ const SPINNER_FRAMES = [
20
+ "⠋",
21
+ "⠙",
22
+ "⠹",
23
+ "⠸",
24
+ "⠼",
25
+ "⠴",
26
+ "⠦",
27
+ "⠧",
28
+ "⠇",
29
+ "⠏"
30
+ ];
31
+ /**
32
+ * How long each spinner frame is held, in milliseconds.
33
+ *
34
+ * @public
35
+ */
36
+ const SPINNER_FRAME_MS = 80;
37
+ /**
38
+ * Resolve the spinner glyph for a frame index. The index wraps modulo
39
+ * the frame count and tolerates negative values, so a wall-clock-derived
40
+ * index is always valid.
41
+ *
42
+ * @param index - the frame index (wraps modulo frame count)
43
+ * @returns the Braille glyph for the given frame
44
+ * @public
45
+ */
46
+ const spinnerFrame = (index) => {
47
+ const count = SPINNER_FRAMES.length;
48
+ return SPINNER_FRAMES[(Math.trunc(index) % count + count) % count];
49
+ };
50
+ /**
51
+ * Derive the spinner frame index from a wall-clock timestamp. Using the
52
+ * clock — rather than a monotonic counter — keeps the animation correct
53
+ * across watch-mode remounts with no extra closure state to reset.
54
+ *
55
+ * @param nowMs - current wall-clock time in milliseconds
56
+ * @returns the frame index for the given timestamp
57
+ * @public
58
+ */
59
+ const spinnerFrameForTime = (nowMs) => Math.floor(nowMs / 80);
60
+
61
+ //#endregion
62
+ export { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime };
@@ -0,0 +1,18 @@
1
+ //#region src/render-ink/tag-suffix.ts
2
+ /**
3
+ * Format a per-row tag-count suffix — sorted `tag:count` pairs joined
4
+ * by two spaces. A single tag is suppressed: a project that is all one
5
+ * kind carries no signal worth the column, matching the agent
6
+ * renderer's behaviour.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ const formatTagSuffix = (tagCounts) => {
11
+ if (tagCounts === void 0) return "";
12
+ const entries = Object.entries(tagCounts);
13
+ if (entries.length <= 1) return "";
14
+ return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([tag, count]) => `${tag}:${count}`).join(" ");
15
+ };
16
+
17
+ //#endregion
18
+ export { formatTagSuffix };