@vitest-agent/ui 1.0.5 → 1.1.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.
package/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { Context, Effect, Layer, PubSub, Queue, Schema, Scope, Stream } from "effect";
2
+ import "@effect/platform";
3
+ import "config-file-effect";
4
+ import "@effect/sql/SqlClient";
5
+ import "workspaces-effect";
6
+ import "xdg-effect";
2
7
  import { FC, ReactElement } from "react";
3
-
4
8
  //#region ../sdk/dist/dev/pkg/index.d.ts
5
9
  //#region src/schemas/Coverage.d.ts
6
10
  /**
@@ -31,10 +35,6 @@ declare const FileCoverageReport: Schema.Struct<{
31
35
  }>;
32
36
  /** @public */
33
37
  type FileCoverageReport = typeof FileCoverageReport.Type;
34
- /**
35
- * Complete coverage report attached to an AgentReport.
36
- * @public
37
- */
38
38
  //#endregion
39
39
  //#region src/schemas/RenderState.d.ts
40
40
  /**
@@ -306,7 +306,8 @@ type RenderState = typeof RenderState.Type;
306
306
  * code share one definition.
307
307
  * @public
308
308
  */
309
- declare const initialRenderState: RenderState; //#endregion
309
+ declare const initialRenderState: RenderState;
310
+ //#endregion
310
311
  //#region src/contracts/dispatcher.d.ts
311
312
  /**
312
313
  * The four run shapes the dispatcher distinguishes.
@@ -427,12 +428,7 @@ interface CellOptions {
427
428
  * cells can call this directly without re-consulting the environment.
428
429
  */
429
430
  readonly osc8: (url: string, label: string) => string;
430
- } //#endregion
431
- //#region src/schemas/AgentReport.d.ts
432
- /**
433
- * Aggregate test run statistics.
434
- * @public
435
- */
431
+ }
436
432
  /**
437
433
  * Complete per-project test report written to disk as JSON.
438
434
  * @public
@@ -584,12 +580,7 @@ declare const AgentReport: Schema.Struct<{
584
580
  }>>;
585
581
  }>;
586
582
  /** @public */
587
- type AgentReport = typeof AgentReport.Type; //#endregion
588
- //#region src/schemas/Common.d.ts
589
- /**
590
- * Possible states for an individual test case.
591
- * @public
592
- */
583
+ type AgentReport = typeof AgentReport.Type;
593
584
  /**
594
585
  * Classification of a test's failure history across runs.
595
586
  * @public
@@ -597,10 +588,6 @@ type AgentReport = typeof AgentReport.Type; //#endregion
597
588
  declare const TestClassification: Schema.Literal<["stable", "new-failure", "persistent", "flaky", "recovered"]>;
598
589
  /** @public */
599
590
  type TestClassification = typeof TestClassification.Type;
600
- /**
601
- * Console output verbosity mode for AgentReporter.
602
- * @public
603
- */
604
591
  //#endregion
605
592
  //#region src/schemas/RunEvent.d.ts
606
593
  /**
@@ -617,10 +604,6 @@ type CoverageMetric = typeof CoverageMetric.Type;
617
604
  declare const ActionSeverity: Schema.Literal<["info", "warn", "blocker"]>;
618
605
  /** @public */
619
606
  type ActionSeverity = typeof ActionSeverity.Type;
620
- /**
621
- * Lifecycle scope of a Vitest setup/teardown hook.
622
- * @public
623
- */
624
607
  /**
625
608
  * A coverage gap surfaced to the renderer — a per-file shortfall that
626
609
  * a renderer may want to call out individually.
@@ -796,7 +779,8 @@ type RunEvent = typeof RunEvent.Type;
796
779
  * pulling the whole union shape into scope.
797
780
  * @public
798
781
  */
799
- type RunEventByTag = { [E in RunEvent as E["_tag"]]: E }; //#endregion
782
+ type RunEventByTag = { [E in RunEvent as E["_tag"]]: E; };
783
+ //#endregion
800
784
  //#region src/schemas/Thresholds.d.ts
801
785
  /**
802
786
  * Per-metric threshold values. All optional -- only set metrics are enforced.
@@ -810,10 +794,6 @@ declare const MetricThresholds: Schema.Struct<{
810
794
  }>;
811
795
  /** @public */
812
796
  type MetricThresholds = typeof MetricThresholds.Type;
813
- /**
814
- * A glob pattern paired with its metric thresholds.
815
- * @public
816
- */
817
797
  //#endregion
818
798
  //#region src/utils/build-report.d.ts
819
799
  /**
@@ -952,24 +932,6 @@ interface VitestTestModule {
952
932
  diagnostic(): VitestModuleDiagnostic | undefined;
953
933
  errors(): Array<VitestModuleError>;
954
934
  }
955
- /**
956
- * Convert Vitest TestModule/TestCase objects into an `AgentReport`.
957
- *
958
- * @remarks
959
- * This is a pure data transformation function with no I/O. It tallies
960
- * pass/fail/skip counts, extracts error details with diffs, and builds
961
- * the complete report structure. Only modules with at least one failing
962
- * test are included in the `failed` array to keep reports compact.
963
- *
964
- * @param testModules - All test modules from the Vitest run
965
- * @param unhandledErrors - Unhandled errors from the run
966
- * @param reason - Overall run outcome (`"passed"`, `"failed"`, or `"interrupted"`)
967
- * @param options - Reporter options; only `omitPassingTests` is used
968
- * @param projectName - Optional project name for monorepo grouping
969
- * @returns Structured report ready for JSON serialization
970
- *
971
- * @public
972
- */
973
935
  //#endregion
974
936
  //#region src/dispatcher/cell-types.d.ts
975
937
  /**
@@ -1476,9 +1438,19 @@ interface CountColumnsProps {
1476
1438
  /** Number of timed-out tests. */
1477
1439
  readonly timeoutCount: number;
1478
1440
  }
1441
+ /**
1442
+ * Fixed width of the duration cell that follows the count columns on an
1443
+ * aggregate row — sized for the formatter's widest common output
1444
+ * (`999.9ms`). Longer values overflow their row rather than truncate.
1445
+ *
1446
+ * @public
1447
+ */
1448
+ declare const DURATION_CELL_WIDTH = 7;
1479
1449
  /**
1480
1450
  * Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
1481
- * for an aggregate row. Zeros are dimmed; all four columns always appear.
1451
+ * for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
1452
+ * columns align across rows; counts of 10,000+ overflow their row without
1453
+ * truncation. Zeros are dimmed; all four columns always appear.
1482
1454
  *
1483
1455
  * @public
1484
1456
  */
@@ -1591,8 +1563,13 @@ interface ProjectRowProps {
1591
1563
  readonly frame: string;
1592
1564
  /** Name-column width so the count columns align across rows. */
1593
1565
  readonly nameWidth: number;
1594
- /** Per-tag test counts for the project, merged across its modules. */
1595
- readonly tagCounts?: Record<string, number>;
1566
+ /** Per-tag test counts for the project, merged across its modules; feeds the TagColumns cells. */
1567
+ readonly tagCounts?: Record<string, number> | undefined;
1568
+ /**
1569
+ * The view-level tag union. Every row renders every tag in it so the
1570
+ * tag columns align; empty (the default) renders no tag cells.
1571
+ */
1572
+ readonly tagUnion?: ReadonlyArray<string>;
1596
1573
  }
1597
1574
  /**
1598
1575
  * Renders one per-project row in the `workspace`-shape live view: status
@@ -1712,6 +1689,35 @@ declare const spinnerFrame: (index: number) => string;
1712
1689
  */
1713
1690
  declare const spinnerFrameForTime: (nowMs: number) => number;
1714
1691
  //#endregion
1692
+ //#region src/render-ink/TagColumns.d.ts
1693
+ /**
1694
+ * Compute the view-level tag union: the alphabetically sorted set of tag
1695
+ * names across every row's tag counts. A union of a single tag carries no
1696
+ * signal (the whole view is one kind), so it collapses to empty and the
1697
+ * view renders no tag columns at all.
1698
+ *
1699
+ * @public
1700
+ */
1701
+ declare const tagUnion: (rows: ReadonlyArray<Record<string, number> | undefined>) => ReadonlyArray<string>;
1702
+ /**
1703
+ * Props for the `TagColumns` component.
1704
+ *
1705
+ * @public
1706
+ */
1707
+ interface TagColumnsProps {
1708
+ /** The view-level tag union — every row renders every tag in it. */
1709
+ readonly tags: ReadonlyArray<string>;
1710
+ /** This row's per-tag test counts; tags absent here render a dimmed 0. */
1711
+ readonly counts?: Record<string, number> | undefined;
1712
+ }
1713
+ /**
1714
+ * Renders one row's fixed-width tag cells for the given tag union.
1715
+ * Renders nothing when the union is empty.
1716
+ *
1717
+ * @public
1718
+ */
1719
+ declare const TagColumns: FC<TagColumnsProps>;
1720
+ //#endregion
1715
1721
  //#region src/render-ink/TestRow.d.ts
1716
1722
  /**
1717
1723
  * Props for the `TestRow` component.
@@ -1845,5 +1851,5 @@ declare const synthesizeFromAgentReport: (report: AgentReport, options?: Synthes
1845
1851
  */
1846
1852
  declare const CURRENT_UI_VERSION: string;
1847
1853
  //#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 };
1854
+ 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
1855
  //# 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.1";
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.1",
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.3",
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 };