@vitest-agent/ui 4.0.1 → 5.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 (39) hide show
  1. package/README.md +3 -3
  2. package/dispatcher/cells/single-file-fail.js +1 -5
  3. package/dispatcher/cells/single-file-pass.js +1 -5
  4. package/dispatcher/cells/single-file-threshold.js +3 -8
  5. package/dispatcher/cells/single-project-fail.js +1 -5
  6. package/dispatcher/cells/single-project-pass.js +1 -5
  7. package/dispatcher/cells/single-project-threshold.js +4 -8
  8. package/dispatcher/cells/single-test-fail.js +1 -5
  9. package/dispatcher/cells/single-test-pass.js +1 -5
  10. package/dispatcher/cells/workspace-fail.js +1 -5
  11. package/dispatcher/cells/workspace-pass.js +1 -5
  12. package/dispatcher/cells/workspace-threshold.js +4 -8
  13. package/dispatcher/classify.js +6 -1
  14. package/dispatcher/dispatch.js +20 -25
  15. package/dispatcher/helpers.js +52 -3
  16. package/index.d.ts +148 -316
  17. package/index.js +5 -18
  18. package/{render-ink → ink}/CountColumns.js +2 -2
  19. package/{render-ink → ink}/CoverageBlock.js +2 -2
  20. package/{render-ink → ink}/FailureSection.js +2 -2
  21. package/{render-ink → ink}/FailuresSection.js +2 -2
  22. package/{render-ink → ink}/ModuleHeader.js +1 -1
  23. package/{render-ink → ink}/ProjectRow.js +1 -1
  24. package/{render-ink → ink}/StatusIcon.js +2 -2
  25. package/{render-ink → ink}/StreamApp.js +14 -5
  26. package/{render-ink → ink}/SuggestedActions.js +2 -2
  27. package/{render-ink → ink}/TagColumns.js +1 -1
  28. package/{render-ink → ink}/TestRow.js +1 -1
  29. package/{render-ink → ink}/TrendLine.js +2 -2
  30. package/ink/dispatch-ink.js +61 -0
  31. package/{dispatcher → ink}/ink-helpers.js +1 -1
  32. package/ink.d.ts +789 -0
  33. package/ink.js +15 -0
  34. package/package.json +12 -7
  35. package/reducer.js +4 -2
  36. package/render-agent.js +4 -1
  37. package/{render-ink/spinner.js → spinner.js} +1 -1
  38. package/synthesize.js +22 -7
  39. package/render-ink/index.js +0 -15
package/README.md CHANGED
@@ -11,8 +11,8 @@ Pure rendering primitives for vitest-agent. Owns the `RunEvent` reducer, the sha
11
11
  ## Features
12
12
 
13
13
  - **Reducer** — pure `(state, event) => state` fold over the `RunEvent` discriminated union; `reduceRenderStateAll` for one-shot replay
14
- - **Dispatcher matrix** — `dispatch` and `dispatchInk` route a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
15
- - **Ink components** — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames
14
+ - **Dispatcher matrix** — `dispatch` routes a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
15
+ - **Ink components** (`@vitest-agent/ui/ink`) — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames, plus `dispatchInk` / `inkDispatcherTable`, the Ink half of the matrix. The package root never loads React or Ink; only this subpath does
16
16
  - **Synthesizers** — `synthesizeRunEvents` from live Vitest modules; `synthesizeFromAgentReport` from a stored `AgentReport`
17
17
  - **Footer builder** — `buildFooter` assembles the L1 MCP-tool-pointer footer; `dominantClassification` picks the most actionable failure class
18
18
 
@@ -24,7 +24,7 @@ npm install @vitest-agent/ui
24
24
  pnpm add @vitest-agent/ui
25
25
  ```
26
26
 
27
- React and Ink are required peers:
27
+ React and Ink are optional peers, needed only for the `@vitest-agent/ui/ink` subpath:
28
28
 
29
29
  ```bash
30
30
  npm install react ink
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
3
  import { formatFailure, soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
5
4
 
6
5
  //#region src/dispatcher/cells/single-file-fail.ts
7
6
  const DEFAULT_WIDTH = 80;
@@ -20,10 +19,7 @@ const renderAgent = (inputs) => {
20
19
  }
21
20
  return `${sections.join("\n")}\n${buildFooter(inputs)}`;
22
21
  };
23
- const renderSingleFileFail = {
24
- agent: renderAgent,
25
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
26
- };
22
+ const renderSingleFileFail = { agent: renderAgent };
27
23
 
28
24
  //#endregion
29
25
  export { renderSingleFileFail };
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
3
  import { soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
5
4
  import { Fmt } from "@effected/cli";
6
5
 
7
6
  //#region src/dispatcher/cells/single-file-pass.ts
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
11
10
  const { passCount, durationMs } = inputs.state.totals;
12
11
  return `${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})\n${buildFooter(inputs)}`;
13
12
  };
14
- const renderSingleFilePass = {
15
- agent: renderAgent,
16
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
17
- };
13
+ const renderSingleFilePass = { agent: renderAgent };
18
14
 
19
15
  //#endregion
20
16
  export { renderSingleFilePass };
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
- import { formatCoverageJudgmentLine, soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
3
+ import { formatCoverageSummaryLines, soleModulePath } from "../helpers.js";
5
4
  import { Fmt } from "@effected/cli";
6
5
 
7
6
  //#region src/dispatcher/cells/single-file-threshold.ts
@@ -10,14 +9,10 @@ const renderAgent = (inputs) => {
10
9
  if (modulePath === void 0) return "";
11
10
  const { passCount, durationMs } = inputs.state.totals;
12
11
  const lines = [`${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})`];
13
- const coverage = formatCoverageJudgmentLine(inputs.state);
14
- if (coverage !== null) lines.push(coverage);
12
+ lines.push(...formatCoverageSummaryLines(inputs.state));
15
13
  return `${lines.join("\n")}\n${buildFooter(inputs)}`;
16
14
  };
17
- const renderSingleFileThreshold = {
18
- agent: renderAgent,
19
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
20
- };
15
+ const renderSingleFileThreshold = { agent: renderAgent };
21
16
 
22
17
  //#endregion
23
18
  export { renderSingleFileThreshold };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatFailure, formatTotals } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/single-project-fail.ts
6
5
  const DEFAULT_WIDTH = 80;
@@ -25,10 +24,7 @@ const renderAgent = (inputs) => {
25
24
  if (moduleLines.length > 1) sections.push(moduleLines);
26
25
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
27
26
  };
28
- const renderSingleProjectFail = {
29
- agent: renderAgent,
30
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
31
- };
27
+ const renderSingleProjectFail = { agent: renderAgent };
32
28
 
33
29
  //#endregion
34
30
  export { renderSingleProjectFail };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatTotals } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
  import { Fmt } from "@effected/cli";
5
4
 
6
5
  //#region src/dispatcher/cells/single-project-pass.ts
@@ -10,10 +9,7 @@ const renderAgent = (inputs) => {
10
9
  const collected = inputs.state.collectedModules ?? inputs.state.moduleOrder.length;
11
10
  return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${Fmt.plural(collected, "module")} all-passed.`]).join("\n\n")}\n${buildFooter(inputs)}`;
12
11
  };
13
- const renderSingleProjectPass = {
14
- agent: renderAgent,
15
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
16
- };
12
+ const renderSingleProjectPass = { agent: renderAgent };
17
13
 
18
14
  //#endregion
19
15
  export { renderSingleProjectPass };
@@ -1,25 +1,21 @@
1
1
  import { buildFooter } from "../footer.js";
2
- import { formatBelowTargetTable, formatCoverageJudgmentLine, formatTotals, formatTrendLine } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
2
+ import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, formatTrendLine } from "../helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/single-project-threshold.ts
6
5
  const BELOW_TARGET_LIMIT = 5;
7
6
  const renderAgent = (inputs) => {
8
7
  const sections = [[formatTotals(inputs.state)]];
9
- const coverage = formatCoverageJudgmentLine(inputs.state);
8
+ const coverage = formatCoverageSummaryLines(inputs.state);
10
9
  const trend = formatTrendLine(inputs.trend);
11
10
  const summaryLines = [];
12
- if (coverage !== null) summaryLines.push(coverage);
11
+ summaryLines.push(...coverage);
13
12
  if (trend !== null) summaryLines.push(trend);
14
13
  if (summaryLines.length > 0) sections.push(summaryLines);
15
14
  const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
16
15
  if (below.length > 0) sections.push([...below]);
17
16
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
18
17
  };
19
- const renderSingleProjectThreshold = {
20
- agent: renderAgent,
21
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
22
- };
18
+ const renderSingleProjectThreshold = { agent: renderAgent };
23
19
 
24
20
  //#endregion
25
21
  export { renderSingleProjectThreshold };
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
2
2
  import { statusGlyph } from "../../theme.js";
3
3
  import { formatDisplayDuration } from "../../format-duration.js";
4
4
  import { formatFailure, formatTestName, soleTest } from "../helpers.js";
5
- import { renderAgentStringAsInk } from "../ink-helpers.js";
6
5
 
7
6
  //#region src/dispatcher/cells/single-test-fail.ts
8
7
  const DEFAULT_WIDTH = 80;
@@ -14,10 +13,7 @@ const renderAgent = (inputs) => {
14
13
  const failure = inputs.state.failures[0];
15
14
  return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
16
15
  };
17
- const renderSingleTestFail = {
18
- agent: renderAgent,
19
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
20
- };
16
+ const renderSingleTestFail = { agent: renderAgent };
21
17
 
22
18
  //#endregion
23
19
  export { renderSingleTestFail };
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
2
2
  import { statusGlyph } from "../../theme.js";
3
3
  import { formatDisplayDuration } from "../../format-duration.js";
4
4
  import { formatTestName, soleTest } from "../helpers.js";
5
- import { renderAgentStringAsInk } from "../ink-helpers.js";
6
5
 
7
6
  //#region src/dispatcher/cells/single-test-pass.ts
8
7
  const renderAgent = (inputs) => {
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
11
10
  const duration = test.durationMs ?? 0;
12
11
  return `${statusGlyph("success")} ${formatTestName(test)} (${formatDisplayDuration(duration)})\n${buildFooter(inputs)}`;
13
12
  };
14
- const renderSingleTestPass = {
15
- agent: renderAgent,
16
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
17
- };
13
+ const renderSingleTestPass = { agent: renderAgent };
18
14
 
19
15
  //#endregion
20
16
  export { renderSingleTestPass };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatFailure, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-fail.ts
6
5
  const DEFAULT_WIDTH = 80;
@@ -18,10 +17,7 @@ const renderAgent = (inputs) => {
18
17
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
19
18
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
20
19
  };
21
- const renderWorkspaceFail = {
22
- agent: renderAgent,
23
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
24
- };
20
+ const renderWorkspaceFail = { agent: renderAgent };
25
21
 
26
22
  //#endregion
27
23
  export { renderWorkspaceFail };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatCoverageJudgmentLine, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-pass.ts
6
5
  const renderAgent = (inputs) => {
@@ -16,10 +15,7 @@ const renderAgent = (inputs) => {
16
15
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
17
16
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
18
17
  };
19
- const renderWorkspacePass = {
20
- agent: renderAgent,
21
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
22
- };
18
+ const renderWorkspacePass = { agent: renderAgent };
23
19
 
24
20
  //#endregion
25
21
  export { renderWorkspacePass };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
- import { formatBelowTargetTable, formatCoverageJudgmentLine, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
2
+ import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-threshold.ts
6
5
  const BELOW_TARGET_LIMIT = 10;
@@ -9,8 +8,8 @@ const renderAgent = (inputs) => {
9
8
  const projects = formatProjectsTable(inputs.projects);
10
9
  if (projects.length > 0) sections.push([...projects]);
11
10
  const summaryLines = [];
12
- const coverage = formatCoverageJudgmentLine(inputs.state);
13
- if (coverage !== null) summaryLines.push(coverage);
11
+ const coverage = formatCoverageSummaryLines(inputs.state);
12
+ summaryLines.push(...coverage);
14
13
  const trend = formatTrendLine(inputs.trend);
15
14
  if (trend !== null) summaryLines.push(trend);
16
15
  if (summaryLines.length > 0) sections.push(summaryLines);
@@ -19,10 +18,7 @@ const renderAgent = (inputs) => {
19
18
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
20
19
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
21
20
  };
22
- const renderWorkspaceThreshold = {
23
- agent: renderAgent,
24
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
25
- };
21
+ const renderWorkspaceThreshold = { agent: renderAgent };
26
22
 
27
23
  //#endregion
28
24
  export { renderWorkspaceThreshold };
@@ -7,6 +7,8 @@
7
7
  * UI rewrite spec §7 open question 1:
8
8
  *
9
9
  * 1. More than one project → `workspace`.
10
+ * 1b. `collectedModules` greater than one → `single-project` (the reduced
11
+ * `modules` map may hold only the failing modules of a larger run).
10
12
  * 2. One module with exactly one test → `single-test`.
11
13
  * 3. One module with more than one test → `single-file`.
12
14
  * 4. Otherwise → `single-project`.
@@ -18,6 +20,7 @@
18
20
  */
19
21
  const classifyRunShape = (state, projects) => {
20
22
  if (projects.length > 1) return "workspace";
23
+ if (state.collectedModules !== void 0 && state.collectedModules > 1) return "single-project";
21
24
  const moduleEntries = Object.values(state.modules);
22
25
  if (moduleEntries.length === 1) {
23
26
  const sole = moduleEntries[0];
@@ -50,7 +53,9 @@ const classifyOutcome = (state) => {
50
53
  if (state.totals.failCount > 0) return "some-fail";
51
54
  if (state.unhandledErrors.length > 0) return "some-fail";
52
55
  if (state.totals.timeoutCount > 0) return "some-fail";
53
- if (state.coverage !== null && state.coverage.violations.length > 0) return "threshold-violation";
56
+ if (state.coverage !== null) {
57
+ if (state.coverage.violations.length > 0 || (state.coverage.globShortfalls?.length ?? 0) > 0) return "threshold-violation";
58
+ }
54
59
  return "all-pass";
55
60
  };
56
61
 
@@ -10,9 +10,7 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
10
10
  import { renderWorkspaceFail } from "./cells/workspace-fail.js";
11
11
  import { renderWorkspacePass } from "./cells/workspace-pass.js";
12
12
  import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
13
- import { formatScopedCoverageNote } from "@vitest-agent/sdk";
14
- import { Box, Text } from "ink";
15
- import { createElement } from "react";
13
+ import { formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
16
14
 
17
15
  //#region src/dispatcher/dispatch.ts
18
16
  /**
@@ -56,11 +54,27 @@ const dispatcherTable = {
56
54
  const dispatch = (inputs, opts) => {
57
55
  const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
58
56
  const note = scopedCoverageNoteFor(inputs);
59
- return note !== null ? `${body}\n${note}` : body;
57
+ const withNote = note !== null ? `${body}\n${note}` : body;
58
+ const stray = strayOutputNoteFor(inputs);
59
+ return stray !== null ? `${withNote}\n${stray}\n` : withNote;
60
+ };
61
+ /**
62
+ * The stray-output note for a run that wrote past Vitest's console capture,
63
+ * or `null` when it wrote nothing stray. `dispatch` and `dispatchInk` append
64
+ * it after the cell's output (and the scoped-coverage note); cells never emit
65
+ * it themselves.
66
+ *
67
+ * @param inputs - the classified dispatch inputs
68
+ * @returns the note's lines joined, or `null`
69
+ * @internal
70
+ */
71
+ const strayOutputNoteFor = (inputs) => {
72
+ const stray = inputs.state.strayOutput;
73
+ return stray === void 0 ? null : formatStrayOutputNote(stray).join("\n");
60
74
  };
61
75
  /**
62
76
  * Build the scoped-coverage note for this run, or `null` on a full
63
- * (non-scoped) run. Shared by {@link dispatch} and {@link dispatchInk} so
77
+ * (non-scoped) run. Shared by {@link dispatch} and the Ink entry's `dispatchInk` so
64
78
  * both render paths surface the same information (issue #160 gap 1) —
65
79
  * Vitest's coverage thresholds are meaningless against a subset of the
66
80
  * project's test files, so every cell's own threshold-flavored coverage
@@ -73,25 +87,6 @@ const scopedCoverageNoteFor = (inputs) => {
73
87
  if (cov === null || cov.scoped !== true) return null;
74
88
  return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
75
89
  };
76
- /**
77
- * Dispatch to the cell that matches `(inputs.shape, inputs.outcome)`
78
- * and return its Ink-half React tree. Returns `null` when the matched
79
- * cell does not expose an `ink` half — callers should fall back to
80
- * the agent string in that case.
81
- *
82
- * @param inputs - the classified dispatch inputs
83
- * @param opts - cell rendering options
84
- * @returns the Ink React element, or `null` when the cell has no Ink half
85
- * @public
86
- */
87
- const dispatchInk = (inputs, opts) => {
88
- const cell = dispatcherTable[inputs.shape][inputs.outcome];
89
- if (cell.ink === void 0) return null;
90
- const element = cell.ink(inputs, opts);
91
- const note = scopedCoverageNoteFor(inputs);
92
- if (note === null) return element;
93
- return createElement(Box, { flexDirection: "column" }, element, createElement(Text, null, note));
94
- };
95
90
 
96
91
  //#endregion
97
- export { dispatch, dispatchInk, dispatcherTable };
92
+ export { dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor };
@@ -5,6 +5,12 @@ import { Doc, Fmt, Render } from "@effected/cli";
5
5
 
6
6
  //#region src/dispatcher/helpers.ts
7
7
  /**
8
+ * Format a coverage percentage given on istanbul's 0–100 scale. One
9
+ * decimal place for non-integer values, none for integers, with a
10
+ * trailing percent sign (`Fmt.percent` with `scale: 100`).
11
+ */
12
+ const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
13
+ /**
8
14
  * Format the `Tests:` header line —
9
15
  * `<pass>/<total> passed[, <fail> failed][, <timeout> timed out][, <skip> skipped] (Xms)`.
10
16
  * `total` folds in `timeoutCount` — a timed-out test is a real collected
@@ -70,18 +76,61 @@ const formatFailure = (f, width) => {
70
76
  })).split("\n");
71
77
  };
72
78
  /**
79
+ * The `Glob aggregates below threshold:` block — one
80
+ * `- <pattern>: lines 60% < 90%, branches 55% < 80%` line per shortfall,
81
+ * listing only the metrics that miss their minimum. Returns an empty array
82
+ * when there are no shortfalls. Shared by the dispatcher cells and
83
+ * `renderAgent` so both agent surfaces use one line format.
84
+ */
85
+ const formatGlobShortfallLines = (shortfalls) => {
86
+ if (shortfalls === void 0 || shortfalls.length === 0) return [];
87
+ const metricOrder = [
88
+ "lines",
89
+ "branches",
90
+ "functions",
91
+ "statements"
92
+ ];
93
+ const lines = ["Glob aggregates below threshold:"];
94
+ for (const g of shortfalls) {
95
+ const parts = [];
96
+ for (const m of metricOrder) {
97
+ const min = g.thresholds[m];
98
+ const actual = g.summary[m];
99
+ if (min !== void 0 && actual < min) parts.push(`${m} ${formatPercent(actual)} < ${formatPercent(min)}`);
100
+ }
101
+ lines.push(`- ${g.pattern}: ${parts.join(", ")}`);
102
+ }
103
+ return lines;
104
+ };
105
+ /**
73
106
  * One coverage judgment line — `Coverage: ✓ all metrics meet thresholds`
74
107
  * for a clean run, `Coverage: ✗ <N> files below minimum thresholds (...)`
75
- * for a violation. Returns `null` when the run carries no coverage block.
108
+ * for a per-file violation, `Coverage: ✗ <N> glob aggregates below
109
+ * threshold` when only glob aggregate shortfalls exist. Returns `null`
110
+ * when the run carries no coverage block.
76
111
  */
77
112
  const formatCoverageJudgmentLine = (state) => {
78
113
  const cov = state.coverage;
79
114
  if (cov === null) return null;
80
- if (cov.violations.length === 0) return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
115
+ const shortfallCount = cov.globShortfalls?.length ?? 0;
116
+ if (cov.violations.length === 0) {
117
+ if (shortfallCount > 0) return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(shortfallCount, "glob aggregate")} below threshold`;
118
+ return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
119
+ }
81
120
  const metrics = cov.violations.map((v) => v.metric).join(", ");
82
121
  const fileCount = countLowCoverageFiles(cov.gaps);
83
122
  return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(fileCount, "file")} below minimum thresholds (${metrics})`;
84
123
  };
124
+ /**
125
+ * The coverage judgment line followed by the glob aggregate shortfall
126
+ * block, as the lines a threshold cell appends to its summary. Empty when
127
+ * the run carries no coverage block.
128
+ */
129
+ const formatCoverageSummaryLines = (state) => {
130
+ const judgment = formatCoverageJudgmentLine(state);
131
+ if (judgment === null) return [];
132
+ return [judgment, ...formatGlobShortfallLines(state.coverage?.globShortfalls)];
133
+ };
85
134
  const countLowCoverageFiles = (gaps) => {
86
135
  const seen = /* @__PURE__ */ new Set();
87
136
  for (const g of gaps) seen.add(g.file);
@@ -191,4 +240,4 @@ const pctCell = (n) => {
191
240
  };
192
241
 
193
242
  //#endregion
194
- export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
243
+ export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };