@vitest-agent/ui 1.0.5 → 1.1.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.
package/index.d.ts CHANGED
@@ -1476,9 +1476,19 @@ interface CountColumnsProps {
1476
1476
  /** Number of timed-out tests. */
1477
1477
  readonly timeoutCount: number;
1478
1478
  }
1479
+ /**
1480
+ * Fixed width of the duration cell that follows the count columns on an
1481
+ * aggregate row — sized for the formatter's widest common output
1482
+ * (`999.9ms`). Longer values overflow their row rather than truncate.
1483
+ *
1484
+ * @public
1485
+ */
1486
+ declare const DURATION_CELL_WIDTH = 7;
1479
1487
  /**
1480
1488
  * Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
1481
- * for an aggregate row. Zeros are dimmed; all four columns always appear.
1489
+ * for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
1490
+ * columns align across rows; counts of 10,000+ overflow their row without
1491
+ * truncation. Zeros are dimmed; all four columns always appear.
1482
1492
  *
1483
1493
  * @public
1484
1494
  */
@@ -1591,8 +1601,13 @@ interface ProjectRowProps {
1591
1601
  readonly frame: string;
1592
1602
  /** Name-column width so the count columns align across rows. */
1593
1603
  readonly nameWidth: number;
1594
- /** Per-tag test counts for the project, merged across its modules. */
1595
- readonly tagCounts?: Record<string, number>;
1604
+ /** Per-tag test counts for the project, merged across its modules; feeds the TagColumns cells. */
1605
+ readonly tagCounts?: Record<string, number> | undefined;
1606
+ /**
1607
+ * The view-level tag union. Every row renders every tag in it so the
1608
+ * tag columns align; empty (the default) renders no tag cells.
1609
+ */
1610
+ readonly tagUnion?: ReadonlyArray<string>;
1596
1611
  }
1597
1612
  /**
1598
1613
  * Renders one per-project row in the `workspace`-shape live view: status
@@ -1712,6 +1727,35 @@ declare const spinnerFrame: (index: number) => string;
1712
1727
  */
1713
1728
  declare const spinnerFrameForTime: (nowMs: number) => number;
1714
1729
  //#endregion
1730
+ //#region src/render-ink/TagColumns.d.ts
1731
+ /**
1732
+ * Compute the view-level tag union: the alphabetically sorted set of tag
1733
+ * names across every row's tag counts. A union of a single tag carries no
1734
+ * signal (the whole view is one kind), so it collapses to empty and the
1735
+ * view renders no tag columns at all.
1736
+ *
1737
+ * @public
1738
+ */
1739
+ declare const tagUnion: (rows: ReadonlyArray<Record<string, number> | undefined>) => ReadonlyArray<string>;
1740
+ /**
1741
+ * Props for the `TagColumns` component.
1742
+ *
1743
+ * @public
1744
+ */
1745
+ interface TagColumnsProps {
1746
+ /** The view-level tag union — every row renders every tag in it. */
1747
+ readonly tags: ReadonlyArray<string>;
1748
+ /** This row's per-tag test counts; tags absent here render a dimmed 0. */
1749
+ readonly counts?: Record<string, number> | undefined;
1750
+ }
1751
+ /**
1752
+ * Renders one row's fixed-width tag cells for the given tag union.
1753
+ * Renders nothing when the union is empty.
1754
+ *
1755
+ * @public
1756
+ */
1757
+ declare const TagColumns: FC<TagColumnsProps>;
1758
+ //#endregion
1715
1759
  //#region src/render-ink/TestRow.d.ts
1716
1760
  /**
1717
1761
  * Props for the `TestRow` component.
@@ -1845,5 +1889,5 @@ declare const synthesizeFromAgentReport: (report: AgentReport, options?: Synthes
1845
1889
  */
1846
1890
  declare const CURRENT_UI_VERSION: string;
1847
1891
  //#endregion
1848
- export { ActionSeverity, type AgentCellFn, type AgentReport, CURRENT_UI_VERSION, type Cell, type CellOptions, CountColumns, type CountColumnsProps, CoverageBlock, type CoverageBlockProps, FileCoverageReport as CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, type CoverageTotals, type DispatchInputs, FailureRecord, FailureSection, type FailureSectionProps, FailuresSection, type FailuresSectionProps, type InkCellFn, type MetricThresholds, ModuleHeader, type ModuleHeaderProps, ModuleRecord, ProjectRow, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, RenderState, RenderTotals, RunEvent, type RunEventByTag, RunEventChannel, RunEventChannelLive, type RunOutcome, type RunShape, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, type StatusIconKind, type StatusIconProps, StreamApp, type StreamAppProps, SuggestedActionRecord, SuggestedActions, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, type TestClassification, TestRecord, TestRow, type TestRowProps, TrendLine, type TrendLineProps, type TrendSummary, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite, accumulateUntilFinished, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, forEachRenderState, formatDisplayDuration, initialRenderState, publish, publishAll, reduceRenderState, reduceRenderStateAll, renderAgent, renderStateStream, spinnerFrame, spinnerFrameForTime, subscribeRaw, synthesizeFromAgentReport, synthesizeRunEvents };
1892
+ export { ActionSeverity, type AgentCellFn, type AgentReport, CURRENT_UI_VERSION, type Cell, type CellOptions, CountColumns, type CountColumnsProps, CoverageBlock, type CoverageBlockProps, FileCoverageReport as CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, type CoverageTotals, DURATION_CELL_WIDTH, type DispatchInputs, FailureRecord, FailureSection, type FailureSectionProps, FailuresSection, type FailuresSectionProps, type InkCellFn, type MetricThresholds, ModuleHeader, type ModuleHeaderProps, ModuleRecord, ProjectRow, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, RenderState, RenderTotals, RunEvent, type RunEventByTag, RunEventChannel, RunEventChannelLive, type RunOutcome, type RunShape, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, type StatusIconKind, type StatusIconProps, StreamApp, type StreamAppProps, SuggestedActionRecord, SuggestedActions, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, TagColumns, type TagColumnsProps, type TestClassification, TestRecord, TestRow, type TestRowProps, TrendLine, type TrendLineProps, type TrendSummary, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite, accumulateUntilFinished, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, forEachRenderState, formatDisplayDuration, initialRenderState, publish, publishAll, reduceRenderState, reduceRenderStateAll, renderAgent, renderStateStream, spinnerFrame, spinnerFrameForTime, subscribeRaw, synthesizeFromAgentReport, synthesizeRunEvents, tagUnion };
1849
1893
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -8,12 +8,13 @@ import { reduceRenderState, reduceRenderStateAll } from "./reducer.js";
8
8
  import { accumulateUntilFinished, forEachRenderState, renderStateStream, subscribeRaw } from "./pubsub/Subscriber.js";
9
9
  import "./pubsub/index.js";
10
10
  import { renderAgent } from "./render-agent.js";
11
- import { CountColumns } from "./render-ink/CountColumns.js";
11
+ import { CountColumns, DURATION_CELL_WIDTH } from "./render-ink/CountColumns.js";
12
12
  import { CoverageBlock } from "./render-ink/CoverageBlock.js";
13
13
  import { FailureSection } from "./render-ink/FailureSection.js";
14
14
  import { FailuresSection } from "./render-ink/FailuresSection.js";
15
15
  import { StatusIcon } from "./render-ink/StatusIcon.js";
16
16
  import { ModuleHeader } from "./render-ink/ModuleHeader.js";
17
+ import { TagColumns, tagUnion } from "./render-ink/TagColumns.js";
17
18
  import { ProjectRow } from "./render-ink/ProjectRow.js";
18
19
  import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime } from "./render-ink/spinner.js";
19
20
  import { TestRow } from "./render-ink/TestRow.js";
@@ -32,7 +33,7 @@ import { ActionSeverity, CoverageFile, CoverageGap, CoverageMetric, CoverageRend
32
33
  *
33
34
  * @public
34
35
  */
35
- const CURRENT_UI_VERSION = "1.0.5";
36
+ const CURRENT_UI_VERSION = "1.1.0";
36
37
 
37
38
  //#endregion
38
- export { ActionSeverity, CURRENT_UI_VERSION, CountColumns, CoverageBlock, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, FailureRecord, FailureSection, FailuresSection, ModuleHeader, ModuleRecord, ProjectRow, RenderState, RenderTotals, RunEvent, RunEventChannel, RunEventChannelLive, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActionRecord, SuggestedActions, TestRecord, TestRow, TrendLine, accumulateUntilFinished, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, forEachRenderState, formatDisplayDuration, initialRenderState, publish, publishAll, reduceRenderState, reduceRenderStateAll, renderAgent, renderStateStream, spinnerFrame, spinnerFrameForTime, subscribeRaw, synthesizeFromAgentReport, synthesizeRunEvents };
39
+ export { ActionSeverity, CURRENT_UI_VERSION, CountColumns, CoverageBlock, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, DURATION_CELL_WIDTH, FailureRecord, FailureSection, FailuresSection, ModuleHeader, ModuleRecord, ProjectRow, RenderState, RenderTotals, RunEvent, RunEventChannel, RunEventChannelLive, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActionRecord, SuggestedActions, TagColumns, TestRecord, TestRow, TrendLine, accumulateUntilFinished, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, forEachRenderState, formatDisplayDuration, initialRenderState, publish, publishAll, reduceRenderState, reduceRenderStateAll, renderAgent, renderStateStream, spinnerFrame, spinnerFrameForTime, subscribeRaw, synthesizeFromAgentReport, synthesizeRunEvents, tagUnion };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/ui",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Shared event-sourced renderer for vitest-agent. React Ink view for humans, plain-string view for agents, both fed by the same reducer over a RunEvent stream.",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "dependencies": {
41
- "@vitest-agent/sdk": "1.3.1",
41
+ "@vitest-agent/sdk": "1.3.2",
42
42
  "effect": "^3.21.4"
43
43
  },
44
44
  "peerDependencies": {
@@ -7,16 +7,23 @@ import { jsxs } from "react/jsx-runtime";
7
7
  * module): pass, fail, skip, timeout. All four always render — a zero
8
8
  * is dimmed, never omitted — so the columns line up across rows.
9
9
  */
10
- const cell = (count, glyph, color) => count > 0 ? /* @__PURE__ */ jsxs(Text, {
11
- color,
12
- children: [count, glyph]
13
- }) : /* @__PURE__ */ jsxs(Text, {
14
- color: "gray",
15
- children: [count, glyph]
10
+ /**
11
+ * Fixed width of the duration cell that follows the count columns on an
12
+ * aggregate row — sized for the formatter's widest common output
13
+ * (`999.9ms`). Longer values overflow their row rather than truncate.
14
+ *
15
+ * @public
16
+ */
17
+ const DURATION_CELL_WIDTH = 7;
18
+ const cell = (count, glyph, color) => /* @__PURE__ */ jsxs(Text, {
19
+ color: count > 0 ? color : "gray",
20
+ children: [String(count).padStart(4), glyph]
16
21
  });
17
22
  /**
18
23
  * Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
19
- * for an aggregate row. Zeros are dimmed; all four columns always appear.
24
+ * for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
25
+ * columns align across rows; counts of 10,000+ overflow their row without
26
+ * truncation. Zeros are dimmed; all four columns always appear.
20
27
  *
21
28
  * @public
22
29
  */
@@ -31,4 +38,4 @@ const CountColumns = ({ passCount, failCount, skipCount, timeoutCount }) => /* @
31
38
  ] });
32
39
 
33
40
  //#endregion
34
- export { CountColumns };
41
+ export { CountColumns, DURATION_CELL_WIDTH };
@@ -1,9 +1,9 @@
1
1
  import { formatDisplayDuration } from "../format-duration.js";
2
- import { CountColumns } from "./CountColumns.js";
2
+ import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
3
3
  import { StatusIcon } from "./StatusIcon.js";
4
- import { formatTagSuffix } from "./tag-suffix.js";
4
+ import { TagColumns } from "./TagColumns.js";
5
5
  import { Box, Text } from "ink";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
 
8
8
  //#region src/render-ink/ProjectRow.tsx
9
9
  /**
@@ -28,7 +28,7 @@ const projectGlyph = (counts, running, timedOut, frame) => {
28
28
  *
29
29
  * @public
30
30
  */
31
- const ProjectRow = ({ project, counts, running, timedOut = false, elapsedMs, frame, nameWidth, tagCounts }) => /* @__PURE__ */ jsxs(Box, { children: [
31
+ const ProjectRow = ({ project, counts, running, timedOut = false, elapsedMs, frame, nameWidth, tagCounts, tagUnion = [] }) => /* @__PURE__ */ jsxs(Box, { children: [
32
32
  /* @__PURE__ */ jsx(Text, { children: " " }),
33
33
  projectGlyph(counts, running, timedOut, frame),
34
34
  /* @__PURE__ */ jsxs(Text, { children: [
@@ -44,12 +44,12 @@ const ProjectRow = ({ project, counts, running, timedOut = false, elapsedMs, fra
44
44
  }),
45
45
  /* @__PURE__ */ jsxs(Text, {
46
46
  dimColor: true,
47
- children: [" ", formatDisplayDuration(elapsedMs)]
47
+ children: [" ", formatDisplayDuration(elapsedMs).padStart(7)]
48
48
  }),
49
- formatTagSuffix(tagCounts).length > 0 ? /* @__PURE__ */ jsxs(Text, {
50
- color: "cyan",
51
- children: [" ", formatTagSuffix(tagCounts)]
52
- }) : null
49
+ tagUnion.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(TagColumns, {
50
+ tags: tagUnion,
51
+ counts: tagCounts
52
+ })] }) : null
53
53
  ] });
54
54
 
55
55
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import { classifyRunShape } from "../dispatcher/classify.js";
2
2
  import { formatDisplayDuration } from "../format-duration.js";
3
- import { CountColumns } from "./CountColumns.js";
3
+ import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
4
4
  import { StatusIcon } from "./StatusIcon.js";
5
- import { formatTagSuffix } from "./tag-suffix.js";
5
+ import { TagColumns, tagUnion } from "./TagColumns.js";
6
6
  import { ProjectRow } from "./ProjectRow.js";
7
7
  import { spinnerFrame } from "./spinner.js";
8
8
  import { TestRow } from "./TestRow.js";
@@ -89,7 +89,7 @@ const moduleIcon = (m) => {
89
89
  * Used in the Live region for both running rows (spinner, `running=true`)
90
90
  * and finished rows (resolved glyph, `running=false`).
91
91
  */
92
- const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut }) => {
92
+ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut, tagUnion: rowTagUnion }) => {
93
93
  const running = moduleRunning(module);
94
94
  const queued = module.status === "queued";
95
95
  let glyph;
@@ -122,12 +122,12 @@ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut }) => {
122
122
  }),
123
123
  /* @__PURE__ */ jsxs(Text, {
124
124
  dimColor: true,
125
- children: [" ", formatDisplayDuration(moduleElapsedMs(module, nowMs))]
125
+ children: [" ", formatDisplayDuration(moduleElapsedMs(module, nowMs)).padStart(7)]
126
126
  }),
127
- formatTagSuffix(module.tagCounts).length > 0 ? /* @__PURE__ */ jsxs(Text, {
128
- color: "cyan",
129
- children: [" ", formatTagSuffix(module.tagCounts)]
130
- }) : null,
127
+ rowTagUnion.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, { children: " " }), /* @__PURE__ */ jsx(TagColumns, {
128
+ tags: rowTagUnion,
129
+ counts: module.tagCounts
130
+ })] }) : null,
131
131
  notStartedSuffix
132
132
  ] });
133
133
  };
@@ -192,10 +192,15 @@ const FailureItem = ({ failure }) => /* @__PURE__ */ jsxs(Box, {
192
192
  }) : null
193
193
  ] }), /* @__PURE__ */ jsx(InlineError, { failure })]
194
194
  });
195
- const TotalsLine = ({ totals }) => /* @__PURE__ */ jsxs(Text, { children: [
195
+ /**
196
+ * The bottom `Total:` rollup line. In the workspace view `labelWidth`
197
+ * pads the label to the project-row prefix width so the count columns
198
+ * align with the rows above; other shapes omit it.
199
+ */
200
+ const TotalsLine = ({ totals, labelWidth }) => /* @__PURE__ */ jsxs(Text, { children: [
196
201
  /* @__PURE__ */ jsx(Text, {
197
202
  bold: true,
198
- children: "Total:"
203
+ children: labelWidth !== void 0 ? "Total:".padEnd(labelWidth) : "Total:"
199
204
  }),
200
205
  " ",
201
206
  /* @__PURE__ */ jsx(CountColumns, {
@@ -206,7 +211,7 @@ const TotalsLine = ({ totals }) => /* @__PURE__ */ jsxs(Text, { children: [
206
211
  }),
207
212
  /* @__PURE__ */ jsxs(Text, {
208
213
  dimColor: true,
209
- children: [" ", formatDisplayDuration(totals.durationMs)]
214
+ children: [" ", formatDisplayDuration(totals.durationMs).padStart(7)]
210
215
  })
211
216
  ] });
212
217
  const CoverageItem = ({ state }) => {
@@ -269,16 +274,21 @@ const liveRegion = (state, shape, nowMs, frame) => {
269
274
  }) : null;
270
275
  const coverageItem = /* @__PURE__ */ jsx(CoverageItem, { state });
271
276
  const trendItem = state.trend !== null ? /* @__PURE__ */ jsx(TrendLine, { trend: state.trend }) : null;
272
- const totalsItem = /* @__PURE__ */ jsxs(Box, {
277
+ const totalsItem = (labelWidth) => /* @__PURE__ */ jsxs(Box, {
273
278
  flexDirection: "column",
274
279
  children: [finished && timedOut ? /* @__PURE__ */ jsx(Text, {
275
280
  color: "#e09a4e",
276
281
  bold: true,
277
282
  children: "⧖ Run timed out"
278
- }) : null, /* @__PURE__ */ jsx(TotalsLine, { totals: state.totals })]
283
+ }) : null, /* @__PURE__ */ jsx(TotalsLine, {
284
+ totals: state.totals,
285
+ labelWidth
286
+ })]
279
287
  });
280
288
  if (shape === "workspace") {
281
289
  const nameWidth = Math.max(0, ...groups.map((g) => g.name.length));
290
+ const groupTagCounts = new Map(groups.map((g) => [g.name, mergeTagCounts(g.modules)]));
291
+ const workspaceTags = tagUnion(groups.map((g) => groupTagCounts.get(g.name)));
282
292
  let runningVisible = 0;
283
293
  let runningOverflow = 0;
284
294
  const rows = [];
@@ -300,7 +310,8 @@ const liveRegion = (state, shape, nowMs, frame) => {
300
310
  elapsedMs: projectElapsedMs(g, nowMs),
301
311
  frame,
302
312
  nameWidth,
303
- tagCounts: mergeTagCounts(g.modules)
313
+ tagCounts: groupTagCounts.get(g.name),
314
+ tagUnion: workspaceTags
304
315
  }, g.name));
305
316
  } else runningOverflow++;
306
317
  else {
@@ -320,7 +331,8 @@ const liveRegion = (state, shape, nowMs, frame) => {
320
331
  elapsedMs: projectElapsedMs(g, nowMs),
321
332
  frame,
322
333
  nameWidth,
323
- tagCounts: mergeTagCounts(g.modules)
334
+ tagCounts: groupTagCounts.get(g.name),
335
+ tagUnion: workspaceTags
324
336
  }, g.name));
325
337
  }
326
338
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -344,11 +356,12 @@ const liveRegion = (state, shape, nowMs, frame) => {
344
356
  failuresSection,
345
357
  coverageItem,
346
358
  trendItem,
347
- totalsItem
359
+ totalsItem(nameWidth + 4)
348
360
  ] });
349
361
  }
350
362
  if (shape === "single-project") {
351
363
  const nameWidth = Math.max(0, ...ordered.map((m) => m.modulePath.length));
364
+ const moduleTags = tagUnion(ordered.map((m) => m.tagCounts));
352
365
  let runningVisible = 0;
353
366
  let runningOverflow = 0;
354
367
  const rows = [];
@@ -359,7 +372,8 @@ const liveRegion = (state, shape, nowMs, frame) => {
359
372
  nowMs,
360
373
  frame,
361
374
  nameWidth,
362
- timedOut
375
+ timedOut,
376
+ tagUnion: moduleTags
363
377
  }, m.modulePath));
364
378
  } else runningOverflow++;
365
379
  else rows.push(/* @__PURE__ */ jsx(ModuleStreamRow, {
@@ -367,7 +381,8 @@ const liveRegion = (state, shape, nowMs, frame) => {
367
381
  nowMs,
368
382
  frame,
369
383
  nameWidth,
370
- timedOut
384
+ timedOut,
385
+ tagUnion: moduleTags
371
386
  }, m.modulePath));
372
387
  return /* @__PURE__ */ jsxs(Fragment, { children: [
373
388
  /* @__PURE__ */ jsxs(Text, {
@@ -390,7 +405,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
390
405
  failuresSection,
391
406
  coverageItem,
392
407
  trendItem,
393
- totalsItem
408
+ totalsItem()
394
409
  ] });
395
410
  }
396
411
  const sole = ordered[0];
@@ -422,7 +437,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
422
437
  }),
423
438
  coverageItem,
424
439
  trendItem,
425
- totalsItem
440
+ totalsItem()
426
441
  ] });
427
442
  };
428
443
  /**
@@ -0,0 +1,49 @@
1
+ import { Text } from "ink";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+
4
+ //#region src/render-ink/TagColumns.tsx
5
+ /**
6
+ * Fixed-width tag-count columns for aggregate rows in the `stream` view.
7
+ * Every row renders every tag in the view's union so the columns align
8
+ * across rows; a zero count is dimmed gray, a nonzero count cyan. Each
9
+ * cell is the tag label, a colon, and a 4-digit right-aligned count;
10
+ * cells sit two spaces apart.
11
+ */
12
+ /**
13
+ * Compute the view-level tag union: the alphabetically sorted set of tag
14
+ * names across every row's tag counts. A union of a single tag carries no
15
+ * signal (the whole view is one kind), so it collapses to empty and the
16
+ * view renders no tag columns at all.
17
+ *
18
+ * @public
19
+ */
20
+ const tagUnion = (rows) => {
21
+ const names = /* @__PURE__ */ new Set();
22
+ for (const row of rows) for (const tag of Object.keys(row ?? {})) names.add(tag);
23
+ if (names.size <= 1) return [];
24
+ return [...names].sort((a, b) => a.localeCompare(b));
25
+ };
26
+ /**
27
+ * Renders one row's fixed-width tag cells for the given tag union.
28
+ * Renders nothing when the union is empty.
29
+ *
30
+ * @public
31
+ */
32
+ const TagColumns = ({ tags, counts }) => {
33
+ if (tags.length === 0) return null;
34
+ return /* @__PURE__ */ jsx(Text, { children: tags.map((tag, i) => {
35
+ const count = counts?.[tag] ?? 0;
36
+ return /* @__PURE__ */ jsxs(Text, {
37
+ color: count > 0 ? "cyan" : "gray",
38
+ children: [
39
+ i > 0 ? " " : "",
40
+ tag,
41
+ ":",
42
+ String(count).padStart(4)
43
+ ]
44
+ }, tag);
45
+ }) });
46
+ };
47
+
48
+ //#endregion
49
+ export { TagColumns, tagUnion };
@@ -1,9 +1,10 @@
1
- import { CountColumns } from "./CountColumns.js";
1
+ import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
2
2
  import { CoverageBlock } from "./CoverageBlock.js";
3
3
  import { FailureSection } from "./FailureSection.js";
4
4
  import { FailuresSection } from "./FailuresSection.js";
5
5
  import { StatusIcon } from "./StatusIcon.js";
6
6
  import { ModuleHeader } from "./ModuleHeader.js";
7
+ import { TagColumns, tagUnion } from "./TagColumns.js";
7
8
  import { ProjectRow } from "./ProjectRow.js";
8
9
  import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime } from "./spinner.js";
9
10
  import { TestRow } from "./TestRow.js";
@@ -11,4 +12,4 @@ import { TrendLine } from "./TrendLine.js";
11
12
  import { StreamApp } from "./StreamApp.js";
12
13
  import { SuggestedActions } from "./SuggestedActions.js";
13
14
 
14
- export { CountColumns, CoverageBlock, FailureSection, FailuresSection, ModuleHeader, ProjectRow, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActions, TestRow, TrendLine, spinnerFrame, spinnerFrameForTime };
15
+ export { CountColumns, CoverageBlock, DURATION_CELL_WIDTH, FailureSection, FailuresSection, ModuleHeader, ProjectRow, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActions, TagColumns, TestRow, TrendLine, spinnerFrame, spinnerFrameForTime, tagUnion };
@@ -1,18 +0,0 @@
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 };