@vitest-agent/ui 2.5.3 → 4.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.
package/render-agent.js CHANGED
@@ -1,6 +1,18 @@
1
- import { formatDisplayDuration } from "./format-duration.js";
1
+ import { formatTotalsLine } from "./counts.js";
2
+ import { formatFailure as formatFailure$1 } from "./dispatcher/helpers.js";
3
+ import { Fmt } from "@effected/cli";
2
4
 
3
5
  //#region src/render-agent.ts
6
+ /**
7
+ * Agent-mode renderer: produces a token-efficient final-frame string
8
+ * from a fully-reduced `RenderState`.
9
+ *
10
+ * Output is intentionally separate from the Ink renderer — agents
11
+ * benefit from a different layout than humans (no ANSI, no live
12
+ * progress, no decoration overhead). The output is stable for stable
13
+ * inputs: no timestamps, no derived "time ago" phrasing, no randomized
14
+ * ordering.
15
+ */
4
16
  const DEFAULT_WIDTH = 80;
5
17
  const DEFAULT_MAX_GAPS = 3;
6
18
  const severityLabel = {
@@ -8,18 +20,11 @@ const severityLabel = {
8
20
  warn: "warn",
9
21
  blocker: "blocker"
10
22
  };
11
- const formatHeader = (state) => {
12
- const { passCount, failCount, skipCount, timeoutCount, durationMs } = state.totals;
13
- const parts = [`${passCount}/${passCount + failCount + skipCount + timeoutCount} passed`];
14
- if (failCount > 0) parts.push(`${failCount} failed`);
15
- if (timeoutCount > 0) parts.push(`${timeoutCount} timed out`);
16
- if (skipCount > 0) parts.push(`${skipCount} skipped`);
17
- if (state.unhandledErrors.length > 0) {
18
- const noun = state.unhandledErrors.length === 1 ? "unhandled error" : "unhandled errors";
19
- parts.push(`${state.unhandledErrors.length} ${noun}`);
20
- }
21
- return `Tests: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
22
- };
23
+ const formatHeader = (state) => formatTotalsLine({
24
+ label: "Tests",
25
+ ...state.totals,
26
+ unhandledErrors: state.unhandledErrors.length
27
+ });
23
28
  const formatModulesSection = (state) => {
24
29
  const modules = state.moduleOrder.map((path) => state.modules[path]).filter((m) => m !== void 0);
25
30
  const { passCount, failCount, skipCount, timeoutCount } = state.totals;
@@ -27,9 +32,9 @@ const formatModulesSection = (state) => {
27
32
  const collected = state.collectedModules ?? modules.length;
28
33
  if (modules.length === 0) {
29
34
  if (collected === 0) return null;
30
- return `${collected} ${collected === 1 ? "module" : "modules"} all-passed.`;
35
+ return `${Fmt.plural(collected, "module")} all-passed.`;
31
36
  }
32
- if (modules.filter((m) => m.failCount > 0 || m.skipCount > 0).length === 0) return `${collected} ${collected === 1 ? "module" : "modules"} all-passed.`;
37
+ if (modules.filter((m) => m.failCount > 0 || m.skipCount > 0).length === 0) return `${Fmt.plural(collected, "module")} all-passed.`;
33
38
  const lines = ["Modules:"];
34
39
  for (const m of modules) {
35
40
  const parts = [];
@@ -41,21 +46,9 @@ const formatModulesSection = (state) => {
41
46
  }
42
47
  return lines.join("\n");
43
48
  };
44
- const truncate = (line, max) => {
45
- if (line.length <= max) return line;
46
- const slice = max - 1;
47
- if (slice <= 0) return "…";
48
- return `${line.slice(0, slice)}…`;
49
- };
49
+ const truncate = (line, max) => Fmt.truncate(line, max);
50
50
  const formatFailure = (f, width, includeStack) => {
51
- const suite = f.suitePath.length > 0 ? `${f.suitePath.join(" > ")} > ` : "";
52
- const classification = f.classification !== null ? ` [${f.classification}]` : "";
53
- const lines = [`- ${f.modulePath} > ${suite}${f.testName}${classification}`];
54
- if (f.error?.message !== void 0) {
55
- const firstLine = f.error.message.split("\n", 1)[0] ?? "";
56
- lines.push(` ${truncate(firstLine, Math.max(20, width - 2))}`);
57
- }
58
- if (f.error?.diff !== void 0) for (const diffLine of f.error.diff.split("\n")) lines.push(` ${truncate(diffLine, Math.max(20, width - 2))}`);
51
+ const lines = [...formatFailure$1(f, width)];
59
52
  if (includeStack && f.error?.stack !== void 0) for (const stackLine of f.error.stack.split("\n")) lines.push(` ${truncate(stackLine, Math.max(20, width - 2))}`);
60
53
  return lines.join("\n");
61
54
  };
@@ -77,10 +70,8 @@ const formatUnhandledErrorsSection = (state, width, includeStack) => {
77
70
  for (const err of state.unhandledErrors) lines.push(formatUnhandledError(err, width, includeStack));
78
71
  return lines.join("\n");
79
72
  };
80
- const formatPercent = (n) => {
81
- const rounded = Math.round(n * 10) / 10;
82
- return Number.isInteger(rounded) ? `${rounded}%` : `${rounded}%`;
83
- };
73
+ /** Coverage metrics arrive on istanbul's 0–100 scale. */
74
+ const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
84
75
  const formatCoverageSection = (state, maxGaps) => {
85
76
  const cov = state.coverage;
86
77
  if (cov === null) return null;
@@ -108,10 +99,7 @@ const formatCoverageSection = (state, maxGaps) => {
108
99
  lines.push(`- ${g.file}${detail}`);
109
100
  }
110
101
  const omitted = cov.gaps.length - topN.length;
111
- if (omitted > 0) {
112
- const noun = omitted === 1 ? "gap" : "gaps";
113
- lines.push(`- (+${omitted} more ${noun})`);
114
- }
102
+ if (omitted > 0) lines.push(`- (+${Fmt.plural(omitted, "more gap", "more gaps")})`);
115
103
  }
116
104
  return lines.join("\n");
117
105
  };
@@ -1,41 +1,42 @@
1
+ import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
1
2
  import { Text } from "ink";
3
+ import { useGlyphs } from "@effected/cli/ui";
2
4
  import { jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/CountColumns.tsx
5
7
  /**
6
- * The four-outcome count columns for an aggregate row (a project or a
7
- * module): pass, fail, skip, timeout. All four always render — a zero
8
- * is dimmed, never omitted — so the columns line up across rows.
9
- */
10
- /**
11
8
  * Fixed width of the duration cell that follows the count columns on an
12
9
  * aggregate row — sized for the formatter's widest common output
13
- * (`999.9ms`). Longer values overflow their row rather than truncate.
10
+ * (`59m 59s`). Longer values overflow their row rather than truncate.
14
11
  *
15
12
  * @public
16
13
  */
17
14
  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]
15
+ const cell = (count, status, glyphs) => /* @__PURE__ */ jsxs(Text, {
16
+ ...count > 0 ? statusInkStyle(status) : inkStyle(VitestAgentTokens.zero),
17
+ children: [String(count).padStart(4), statusGlyph(status, glyphs)]
21
18
  });
22
19
  /**
23
20
  * Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
24
21
  * for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
25
22
  * columns align across rows; counts of 10,000+ overflow their row without
26
- * truncation. Zeros are dimmed; all four columns always appear.
23
+ * truncation. Zeros are greyed; all four columns always appear. Glyphs and
24
+ * colours come from `VitestAgentStatus` (skip is `muted`).
27
25
  *
28
26
  * @public
29
27
  */
30
- const CountColumns = ({ passCount, failCount, skipCount, timeoutCount }) => /* @__PURE__ */ jsxs(Text, { children: [
31
- cell(passCount, "✓", "green"),
32
- " ",
33
- cell(failCount, "✗", "red"),
34
- " ",
35
- cell(skipCount, "↷", "yellow"),
36
- " ",
37
- cell(timeoutCount, "⧖", "#e09a4e")
38
- ] });
28
+ const CountColumns = ({ passCount, failCount, skipCount, timeoutCount }) => {
29
+ const glyphs = useGlyphs();
30
+ return /* @__PURE__ */ jsxs(Text, { children: [
31
+ cell(passCount, "success", glyphs),
32
+ " ",
33
+ cell(failCount, "failure", glyphs),
34
+ " ",
35
+ cell(skipCount, "skip", glyphs),
36
+ " ",
37
+ cell(timeoutCount, "timeout", glyphs)
38
+ ] });
39
+ };
39
40
 
40
41
  //#endregion
41
42
  export { CountColumns, DURATION_CELL_WIDTH };
@@ -1,16 +1,22 @@
1
+ import { inkStyle } from "../theme.js";
1
2
  import { Box, Text } from "ink";
3
+ import { Fmt } from "@effected/cli";
2
4
  import { jsx, jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/CoverageBlock.tsx
7
+ /**
8
+ * Coverage section: per-metric percentage, threshold violations, top-N gaps.
9
+ */
5
10
  const METRIC_ORDER = [
6
11
  "lines",
7
12
  "branches",
8
13
  "functions",
9
14
  "statements"
10
15
  ];
11
- const formatPercent = (n) => {
12
- return `${Math.round(n * 10) / 10}%`;
13
- };
16
+ /** Coverage metrics arrive on istanbul's 0–100 scale. */
17
+ const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
18
+ /** A metric below its threshold is a coverage threshold failure: the kit's `failure` token. */
19
+ const FAILURE = inkStyle("failure");
14
20
  /**
15
21
  * Renders the coverage section: per-metric percentages, threshold
16
22
  * violations, and top-N gap entries sorted by missing line count.
@@ -35,7 +41,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
35
41
  return /* @__PURE__ */ jsxs(Box, { children: [
36
42
  /* @__PURE__ */ jsx(Text, { children: ` ${metric}: ` }),
37
43
  failing ? /* @__PURE__ */ jsx(Text, {
38
- color: "red",
44
+ ...FAILURE,
39
45
  children: formatPercent(actual)
40
46
  }) : /* @__PURE__ */ jsx(Text, { children: formatPercent(actual) }),
41
47
  threshold !== void 0 ? /* @__PURE__ */ jsxs(Text, {
@@ -51,10 +57,10 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
51
57
  coverage.violations.length > 0 ? /* @__PURE__ */ jsxs(Box, {
52
58
  flexDirection: "column",
53
59
  children: [/* @__PURE__ */ jsx(Text, {
54
- color: "red",
60
+ ...FAILURE,
55
61
  children: "Violations"
56
62
  }), coverage.violations.map((v) => /* @__PURE__ */ jsx(Text, {
57
- color: "red",
63
+ ...FAILURE,
58
64
  children: ` ${v.metric}: ${formatPercent(v.actual)} < ${formatPercent(v.expected)}`
59
65
  }, v.metric))]
60
66
  }) : null,
@@ -68,7 +74,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
68
74
  topGaps.map((g) => /* @__PURE__ */ jsxs(Text, { children: [` ${g.file}`, g.uncoveredLines !== void 0 ? `: ${g.uncoveredLines}` : ""] }, g.file)),
69
75
  elidedGaps > 0 ? /* @__PURE__ */ jsx(Text, {
70
76
  dimColor: true,
71
- children: ` (+${elidedGaps} more ${elidedGaps === 1 ? "gap" : "gaps"})`
77
+ children: ` (+${Fmt.plural(elidedGaps, "more gap", "more gaps")})`
72
78
  }) : null
73
79
  ]
74
80
  }) : null
@@ -1,17 +1,24 @@
1
+ import { inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
1
2
  import { Box, Text } from "ink";
3
+ import { useGlyphs } from "@effected/cli/ui";
2
4
  import { jsx, jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/FailureSection.tsx
7
+ /**
8
+ * Failure list: one block per failed test with classification, message,
9
+ * diff, and optionally stack trace.
10
+ */
5
11
  const FailureRow = ({ failure, includeStack }) => {
6
12
  const suite = failure.suitePath.length > 0 ? `${failure.suitePath.join(" > ")} > ` : "";
7
13
  const classification = failure.classification !== null ? ` [${failure.classification}]` : "";
14
+ const glyphs = useGlyphs();
8
15
  return /* @__PURE__ */ jsxs(Box, {
9
16
  flexDirection: "column",
10
17
  children: [
11
18
  /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { children: [
12
19
  /* @__PURE__ */ jsx(Text, {
13
- color: "red",
14
- children: "✗"
20
+ ...statusInkStyle("failure"),
21
+ children: statusGlyph("failure", glyphs)
15
22
  }),
16
23
  ` ${failure.modulePath} > ${suite}${failure.testName}`,
17
24
  failure.classification !== null ? /* @__PURE__ */ jsx(Text, {
@@ -20,17 +27,17 @@ const FailureRow = ({ failure, includeStack }) => {
20
27
  }) : null
21
28
  ] }) }),
22
29
  failure.error?.message !== void 0 ? /* @__PURE__ */ jsx(Text, {
23
- color: "red",
30
+ ...inkStyle("failure"),
24
31
  children: ` ${failure.error.message.split("\n", 1)[0] ?? ""}`
25
32
  }) : null,
26
33
  failure.error?.diff !== void 0 ? failure.error.diff.split("\n").map((line, idx) => {
27
34
  const key = `diff-${idx}`;
28
35
  if (line.startsWith("-")) return /* @__PURE__ */ jsx(Text, {
29
- color: "red",
36
+ ...inkStyle("failure"),
30
37
  children: ` ${line}`
31
38
  }, key);
32
39
  if (line.startsWith("+")) return /* @__PURE__ */ jsx(Text, {
33
- color: "green",
40
+ ...inkStyle("success"),
34
41
  children: ` ${line}`
35
42
  }, key);
36
43
  return /* @__PURE__ */ jsx(Text, { children: ` ${line}` }, key);
@@ -1,7 +1,18 @@
1
+ import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
1
2
  import { Box, Text } from "ink";
3
+ import { useGlyphs } from "@effected/cli/ui";
2
4
  import { jsx, jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/FailuresSection.tsx
7
+ /**
8
+ * The capped failures block for aggregate-row shapes (workspace,
9
+ * single-project) — the rows there are projects or modules and cannot
10
+ * expand a per-test error inline, so the failures collect here.
11
+ *
12
+ * Each entry: a glyph (⧖ for a timed-out failure, ✗ otherwise), the
13
+ * `module › suite › test` path, the classification tag when known, and
14
+ * one indented line — the first line of the error message.
15
+ */
5
16
  const pathOf = (f) => {
6
17
  return [
7
18
  f.modulePath,
@@ -9,6 +20,7 @@ const pathOf = (f) => {
9
20
  f.testName
10
21
  ].join(" › ");
11
22
  };
23
+ const failureStatus = (f) => f.timedOut === true ? "timeout" : "failure";
12
24
  const firstLine = (message) => message.split("\n", 1)[0] ?? "";
13
25
  const FAILURE_VALUE_LIMIT = 200;
14
26
  /**
@@ -20,6 +32,7 @@ const FAILURE_VALUE_LIMIT = 200;
20
32
  const FailuresSection = ({ failures, limit }) => {
21
33
  const shown = failures.slice(0, limit);
22
34
  const overflow = failures.length - shown.length;
35
+ const glyphs = useGlyphs();
23
36
  return /* @__PURE__ */ jsxs(Box, {
24
37
  flexDirection: "column",
25
38
  children: [
@@ -37,13 +50,13 @@ const FailuresSection = ({ failures, limit }) => {
37
50
  /* @__PURE__ */ jsxs(Text, { children: [
38
51
  " ",
39
52
  /* @__PURE__ */ jsx(Text, {
40
- color: f.timedOut === true ? "#e09a4e" : "red",
41
- children: f.timedOut === true ? "⧖" : "✗"
53
+ ...statusInkStyle(failureStatus(f)),
54
+ children: statusGlyph(failureStatus(f), glyphs)
42
55
  }),
43
56
  " ",
44
57
  pathOf(f),
45
58
  f.classification !== null ? /* @__PURE__ */ jsxs(Text, {
46
- color: "#c98ae0",
59
+ ...inkStyle(VitestAgentTokens.classification),
47
60
  children: [
48
61
  " [",
49
62
  f.classification,
@@ -1,3 +1,4 @@
1
+ import { statusInkStyle } from "../theme.js";
1
2
  import { formatDisplayDuration } from "../format-duration.js";
2
3
  import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
3
4
  import { StatusIcon } from "./StatusIcon.js";
@@ -13,7 +14,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  */
14
15
  const projectGlyph = (counts, running, timedOut, frame) => {
15
16
  if (running && !timedOut) return /* @__PURE__ */ jsx(Text, {
16
- color: "yellow",
17
+ ...statusInkStyle("running"),
17
18
  children: frame
18
19
  });
19
20
  if (counts.failCount > 0) return /* @__PURE__ */ jsx(StatusIcon, { status: "failed" });
@@ -1,4 +1,6 @@
1
+ import { statusGlyph, statusInkStyle } from "../theme.js";
1
2
  import { Text } from "ink";
3
+ import { useGlyphs } from "@effected/cli/ui";
2
4
  import { jsx } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/StatusIcon.tsx
@@ -8,37 +10,34 @@ import { jsx } from "react/jsx-runtime";
8
10
  * Ink-only primitive — no DOM-isms. Renders one colored character so
9
11
  * the surrounding row can keep its width budget predictable.
10
12
  */
11
- const GLYPH = {
12
- passed: "✓",
13
- failed: "✗",
14
- skipped: "↷",
15
- pending: "◯",
16
- running: "…",
17
- queued: "·",
18
- finished: "✓",
19
- threshold: "⚠",
20
- "timed-out": "⧖"
21
- };
22
- const COLOR = {
23
- passed: "green",
24
- failed: "red",
25
- skipped: "gray",
26
- pending: "cyan",
27
- running: "yellow",
28
- queued: "gray",
29
- finished: "green",
30
- threshold: "yellow",
31
- "timed-out": "#e09a4e"
13
+ /**
14
+ * Each icon kind's status in the shared vocabulary. `finished` is a pass;
15
+ * `threshold` is a coverage threshold failure, which the kit ranks as a
16
+ * `failure`, not a warning.
17
+ */
18
+ const STATUS = {
19
+ passed: "success",
20
+ failed: "failure",
21
+ skipped: "skip",
22
+ pending: "pending",
23
+ running: "running",
24
+ queued: "queued",
25
+ finished: "success",
26
+ threshold: "failure",
27
+ "timed-out": "timeout"
32
28
  };
33
29
  /**
34
30
  * Renders a single colored status glyph for a test, module, or run.
35
31
  *
36
32
  * @public
37
33
  */
38
- const StatusIcon = ({ status }) => /* @__PURE__ */ jsx(Text, {
39
- color: COLOR[status],
40
- children: GLYPH[status]
41
- });
34
+ const StatusIcon = ({ status }) => {
35
+ const glyphs = useGlyphs();
36
+ return /* @__PURE__ */ jsx(Text, {
37
+ ...statusInkStyle(STATUS[status]),
38
+ children: statusGlyph(STATUS[status], glyphs)
39
+ });
40
+ };
42
41
 
43
42
  //#endregion
44
43
  export { StatusIcon };
@@ -1,4 +1,5 @@
1
1
  import { classifyRunShape } from "../dispatcher/classify.js";
2
+ import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
2
3
  import { formatDisplayDuration } from "../format-duration.js";
3
4
  import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
4
5
  import { StatusIcon } from "./StatusIcon.js";
@@ -8,6 +9,8 @@ import { spinnerFrame } from "./spinner.js";
8
9
  import { TestRow } from "./TestRow.js";
9
10
  import { TrendLine } from "./TrendLine.js";
10
11
  import { Box, Text } from "ink";
12
+ import { Fmt } from "@effected/cli";
13
+ import { useGlyphs } from "@effected/cli/ui";
11
14
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
15
 
13
16
  //#region src/render-ink/StreamApp.tsx
@@ -118,7 +121,7 @@ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut, tagUnion:
118
121
  });
119
122
  } else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "timed-out" });
120
123
  } else if (running) glyph = /* @__PURE__ */ jsx(Text, {
121
- color: "yellow",
124
+ ...statusInkStyle("running"),
122
125
  children: frame
123
126
  });
124
127
  else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: moduleIcon(module) });
@@ -182,47 +185,54 @@ const InlineError = ({ failure }) => {
182
185
  };
183
186
  const failureKey = (f) => `failure:${f.modulePath}::${f.suitePath.join("/")}::${f.testName}`;
184
187
  /** A process-level unhandled error, rendered in the aggregate Unhandled errors block. */
185
- const UnhandledErrorItem = ({ error }) => /* @__PURE__ */ jsxs(Box, {
186
- flexDirection: "column",
187
- children: [/* @__PURE__ */ jsxs(Text, { children: [
188
- " ",
189
- /* @__PURE__ */ jsx(Text, {
190
- color: "red",
191
- children: "✗"
192
- }),
193
- " unhandled error"
194
- ] }), /* @__PURE__ */ jsxs(Text, {
195
- dimColor: true,
196
- children: [" ", error.message.split("\n", 1)[0] ?? ""]
197
- })]
198
- });
188
+ const UnhandledErrorItem = ({ error }) => {
189
+ const glyphs = useGlyphs();
190
+ return /* @__PURE__ */ jsxs(Box, {
191
+ flexDirection: "column",
192
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
193
+ " ",
194
+ /* @__PURE__ */ jsx(Text, {
195
+ ...statusInkStyle("failure"),
196
+ children: statusGlyph("failure", glyphs)
197
+ }),
198
+ " unhandled error"
199
+ ] }), /* @__PURE__ */ jsxs(Text, {
200
+ dimColor: true,
201
+ children: [" ", error.message.split("\n", 1)[0] ?? ""]
202
+ })]
203
+ });
204
+ };
199
205
  const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
200
206
  const failurePath = (f) => [
201
207
  f.modulePath,
202
208
  ...f.suitePath,
203
209
  f.testName
204
210
  ].join(" › ");
211
+ const failureStatus = (f) => f.timedOut === true ? "timeout" : "failure";
205
212
  /** A failure entry rendered in the Live region. */
206
- const FailureItem = ({ failure }) => /* @__PURE__ */ jsxs(Box, {
207
- flexDirection: "column",
208
- children: [/* @__PURE__ */ jsxs(Text, { children: [
209
- " ",
210
- /* @__PURE__ */ jsx(Text, {
211
- color: failure.timedOut === true ? "#e09a4e" : "red",
212
- children: failure.timedOut === true ? "⧖" : "✗"
213
- }),
214
- " ",
215
- failurePath(failure),
216
- failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
217
- color: "#c98ae0",
218
- children: [
219
- " [",
220
- failure.classification,
221
- "]"
222
- ]
223
- }) : null
224
- ] }), /* @__PURE__ */ jsx(InlineError, { failure })]
225
- });
213
+ const FailureItem = ({ failure }) => {
214
+ const glyphs = useGlyphs();
215
+ return /* @__PURE__ */ jsxs(Box, {
216
+ flexDirection: "column",
217
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
218
+ " ",
219
+ /* @__PURE__ */ jsx(Text, {
220
+ ...statusInkStyle(failureStatus(failure)),
221
+ children: statusGlyph(failureStatus(failure), glyphs)
222
+ }),
223
+ " ",
224
+ failurePath(failure),
225
+ failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
226
+ ...inkStyle(VitestAgentTokens.classification),
227
+ children: [
228
+ " [",
229
+ failure.classification,
230
+ "]"
231
+ ]
232
+ }) : null
233
+ ] }), /* @__PURE__ */ jsx(InlineError, { failure })]
234
+ });
235
+ };
226
236
  /**
227
237
  * The bottom `Total:` rollup line. In the workspace view `labelWidth`
228
238
  * pads the label to the project-row prefix width so the count columns
@@ -245,9 +255,15 @@ const TotalsLine = ({ totals, labelWidth }) => /* @__PURE__ */ jsxs(Text, { chil
245
255
  children: [" ", formatDisplayDuration(totals.durationMs).padStart(7)]
246
256
  })
247
257
  ] });
258
+ /**
259
+ * The coverage judgment. A threshold violation is a `failure` (the kit's
260
+ * decision: threshold failure is ✗, only a target shortfall is ⚠).
261
+ */
248
262
  const CoverageItem = ({ state }) => {
249
263
  if (state.coverage === null) return null;
250
- const clean = state.coverage.violations.length === 0;
264
+ const violations = state.coverage.violations.length;
265
+ const status = violations === 0 ? "success" : "failure";
266
+ const glyphs = useGlyphs();
251
267
  return /* @__PURE__ */ jsxs(Text, { children: [
252
268
  /* @__PURE__ */ jsx(Text, {
253
269
  bold: true,
@@ -255,11 +271,11 @@ const CoverageItem = ({ state }) => {
255
271
  }),
256
272
  " ",
257
273
  /* @__PURE__ */ jsx(Text, {
258
- color: clean ? "green" : "yellow",
259
- children: clean ? "✓" : "⚠"
274
+ ...statusInkStyle(status),
275
+ children: statusGlyph(status, glyphs)
260
276
  }),
261
277
  " ",
262
- clean ? "all metrics meet thresholds" : `${state.coverage.violations.length} threshold violation(s)`
278
+ violations === 0 ? "all metrics meet thresholds" : Fmt.plural(violations, "threshold violation")
263
279
  ] });
264
280
  };
265
281
  const samePath = (a, b) => {
@@ -273,7 +289,7 @@ const samePath = (a, b) => {
273
289
  * run picture — all rows in discovery order, with running rows capped
274
290
  * at MAX_LIVE_RUNNING_ROWS and finished rows always shown.
275
291
  */
276
- const liveRegion = (state, shape, nowMs, frame) => {
292
+ const liveRegion = (state, shape, nowMs, frame, glyphs) => {
277
293
  const groups = groupByProject(state);
278
294
  const timedOut = state.phase === "timed-out";
279
295
  const finished = state.phase === "finished" || state.phase === "timed-out";
@@ -287,14 +303,14 @@ const liveRegion = (state, shape, nowMs, frame) => {
287
303
  }) : null;
288
304
  if (shape === "single-test") {
289
305
  const sole = ordered[0];
290
- if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsx(Text, {
306
+ if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsxs(Text, {
291
307
  dimColor: true,
292
- children: "discovering tests…"
308
+ children: ["discovering tests", glyphs.ellipsis]
293
309
  });
294
310
  const only = sole.tests[0];
295
- if (only === void 0) return /* @__PURE__ */ jsx(Text, {
311
+ if (only === void 0) return /* @__PURE__ */ jsxs(Text, {
296
312
  dimColor: true,
297
- children: "discovering tests…"
313
+ children: ["discovering tests", glyphs.ellipsis]
298
314
  });
299
315
  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;
300
316
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -306,9 +322,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
306
322
  unhandledErrorsSection
307
323
  ] });
308
324
  }
309
- if (groups.length === 0) return /* @__PURE__ */ jsx(Text, {
325
+ if (groups.length === 0) return /* @__PURE__ */ jsxs(Text, {
310
326
  dimColor: true,
311
- children: "discovering tests…"
327
+ children: ["discovering tests", glyphs.ellipsis]
312
328
  });
313
329
  const failuresSection = state.failures.length > 0 ? /* @__PURE__ */ jsx(Box, {
314
330
  flexDirection: "column",
@@ -318,10 +334,10 @@ const liveRegion = (state, shape, nowMs, frame) => {
318
334
  const trendItem = state.trend !== null ? /* @__PURE__ */ jsx(TrendLine, { trend: state.trend }) : null;
319
335
  const totalsItem = (labelWidth) => /* @__PURE__ */ jsxs(Box, {
320
336
  flexDirection: "column",
321
- children: [finished && timedOut ? /* @__PURE__ */ jsx(Text, {
322
- color: "#e09a4e",
337
+ children: [finished && timedOut ? /* @__PURE__ */ jsxs(Text, {
338
+ ...statusInkStyle("timeout"),
323
339
  bold: true,
324
- children: "⧖ Run timed out"
340
+ children: [statusGlyph("timeout", glyphs), " Run timed out"]
325
341
  }) : null, /* @__PURE__ */ jsx(TotalsLine, {
326
342
  totals: state.totals,
327
343
  labelWidth
@@ -379,7 +395,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
379
395
  runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
380
396
  dimColor: true,
381
397
  children: [
382
- " … and ",
398
+ " ",
399
+ glyphs.ellipsis,
400
+ " and ",
383
401
  runningOverflow,
384
402
  " more running"
385
403
  ]
@@ -430,7 +448,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
430
448
  runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
431
449
  dimColor: true,
432
450
  children: [
433
- " … and ",
451
+ " ",
452
+ glyphs.ellipsis,
453
+ " and ",
434
454
  runningOverflow,
435
455
  " more running"
436
456
  ]
@@ -443,9 +463,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
443
463
  ] });
444
464
  }
445
465
  const sole = ordered[0];
446
- if (sole === void 0) return /* @__PURE__ */ jsx(Text, {
466
+ if (sole === void 0) return /* @__PURE__ */ jsxs(Text, {
447
467
  dimColor: true,
448
- children: "discovering tests…"
468
+ children: ["discovering tests", glyphs.ellipsis]
449
469
  });
450
470
  return /* @__PURE__ */ jsxs(Fragment, { children: [
451
471
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
@@ -483,10 +503,11 @@ const liveRegion = (state, shape, nowMs, frame) => {
483
503
  */
484
504
  const StreamApp = ({ state, frameIndex, nowMs }) => {
485
505
  const now = nowMs ?? Date.now();
486
- const frame = spinnerFrame(frameIndex);
506
+ const glyphs = useGlyphs();
507
+ const frame = spinnerFrame(frameIndex, glyphs);
487
508
  const projects = groupByProject(state).map((g) => buildProjectSummary(g.name, sumCounts(g.modules)));
488
509
  const shape = classifyRunShape(state, projects);
489
- const liveContent = liveRegion(state, shape, now, frame);
510
+ const liveContent = liveRegion(state, shape, now, frame, glyphs);
490
511
  return /* @__PURE__ */ jsx(Box, {
491
512
  flexDirection: "column",
492
513
  children: liveContent