@vitest-agent/reporter 5.0.9 → 6.0.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/README.md CHANGED
@@ -11,7 +11,7 @@ The default reporter package and the reference for custom-reporter authors. Ship
11
11
  ## Features
12
12
 
13
13
  - **`DefaultVitestAgentReporter`** — the production default `VitestAgentReporterFactory` the plugin wires automatically; classifies runs into one of 12 shape × outcome cells, owns the Ink live-mount lifecycle, and emits a GitHub Actions Step Summary in CI
14
- - **Dispatch helpers** — `buildDispatchInputs`, `resolveCellOptions`, `renderAgentStringForReport`, `renderHumanStringForReport`
14
+ - **Dispatch helpers** — `buildDispatchInputs`, `renderAgentStringForReport`, `renderHumanStringForReport`
15
15
  - **Contract re-exports** — `VitestAgentReporterFactory`, `ReporterKit`, `ReporterRenderInput`, `RenderedOutput`, `VitestAgentReporter`, `ResolvedReporterConfig` all re-exported from `@vitest-agent/sdk` so you only need one import
16
16
  - **Worked example** — `DefaultVitestAgentReporter` source in this package is the canonical reference for the `VitestAgentReporterFactory` contract
17
17
 
@@ -1,45 +1,13 @@
1
- import { renderGithubLog, toDisplayPath } from "./githubLog.js";
1
+ import { resolveFileLink } from "./fileLinks.js";
2
+ import { summarizeProject } from "./projectSummary.js";
3
+ import { renderGithubLog, strayOutputOf, toDisplayPath } from "./githubLog.js";
2
4
  import { LiveViewEnv, startLiveView } from "./liveView.js";
3
- import { Doc, Render, Status } from "@effected/cli";
4
- import { RUN_REPORT_FILE_SCHEMA_URL, countSuiteFailures, isTimeoutError } from "@vitest-agent/sdk";
5
+ import { Doc, Fmt, Render, Status } from "@effected/cli";
6
+ import { RUN_REPORT_FILE_SCHEMA_URL, formatStrayOutputNote } from "@vitest-agent/sdk";
5
7
  import { classifyOutcome, classifyRunShape, dispatch, reduceRenderStateAll, synthesizeFromAgentReport } from "@vitest-agent/ui";
8
+ import { Effect } from "effect";
6
9
 
7
10
  // src/defaultReporter.ts
8
- const countTimeouts = (report) => {
9
- let timeoutCount = 0;
10
- for (const mod of report.failed) for (const test of mod.tests) {
11
- const firstError = test.errors?.[0];
12
- if (test.state === "failed" && firstError !== void 0 && isTimeoutError({ message: firstError.message })) timeoutCount++;
13
- }
14
- return timeoutCount;
15
- };
16
- const summarizeProject = (report) => {
17
- const collapsedTagCounts = report.tagCounts !== void 0 ? collapseTagCounts(report.tagCounts) : void 0;
18
- const tagCountsHasEntries = collapsedTagCounts !== void 0 && Object.keys(collapsedTagCounts).length > 0;
19
- const belowTargetCount = report.coverage?.belowTargetFiles?.length;
20
- const violationsCount = report.coverage !== void 0 && report.coverage.lowCoverage.length > 0 ? report.coverage.lowCoverage.length : void 0;
21
- const timeoutCount = countTimeouts(report);
22
- return {
23
- name: report.project ?? "default",
24
- passCount: report.summary.passed,
25
- failCount: report.summary.failed + countSuiteFailures(report) - timeoutCount,
26
- skipCount: report.summary.skipped,
27
- durationMs: report.summary.duration,
28
- ...timeoutCount > 0 ? { timeoutCount } : {},
29
- ...tagCountsHasEntries && collapsedTagCounts !== void 0 ? { tagCounts: collapsedTagCounts } : {},
30
- ...belowTargetCount !== void 0 ? { belowTarget: belowTargetCount } : {},
31
- ...violationsCount !== void 0 ? { violations: violationsCount } : {}
32
- };
33
- };
34
- const collapseTagCounts = (entries) => {
35
- const out = {};
36
- if (entries === void 0) return out;
37
- for (const [tag, entry] of Object.entries(entries)) {
38
- const total = (entry.passed ?? 0) + (entry.failed ?? 0) + (entry.skipped ?? 0);
39
- if (total > 0) out[tag] = total;
40
- }
41
- return out;
42
- };
43
11
  const collectBelowTarget = (reports) => {
44
12
  const out = [];
45
13
  for (const r of reports) {
@@ -78,28 +46,20 @@ const buildDispatchInputs = (state, input, overrides = {}) => {
78
46
  runCommand: overrides.runCommand ?? null
79
47
  };
80
48
  };
81
- /**
82
- * Build `CellOptions` from a {@link ReporterKit}. Picks the kit's
83
- * resolved `noColor` value and the pre-bound OSC-8 hyperlink helper.
84
- *
85
- * @public
86
- */
87
- const resolveCellOptions = (kit) => ({
88
- noColor: kit.config.noColor,
89
- osc8: kit.stdOsc8
90
- });
91
49
  const shouldRenderForMode = (mode) => mode === "agent";
50
+ const synthesizeOptionsFrom = (options) => ({ ...options.suggestedActions !== void 0 ? { suggestedActions: options.suggestedActions } : {} });
92
51
  /**
93
52
  * Convenience helper for one-shot consumers (e.g. a CLI command
94
53
  * replaying a stored run). Synthesizes a minimal `ReporterRenderInput`
95
54
  * from a single `AgentReport`, classifies the shape and outcome, and
96
55
  * returns the dispatched agent-string for the matching cell. Equivalent
97
- * to the pre-2.0 `renderRun(events, "agent")` shortcut.
56
+ * to the pre-2.0 `renderRun(events, "agent")` shortcut. Pass
57
+ * `options.suggestedActions` to append the run's `Actions:` list.
98
58
  *
99
59
  * @public
100
60
  */
101
- const renderAgentStringForReport = (report) => {
102
- const events = synthesizeFromAgentReport(report);
61
+ const renderAgentStringForReport = (report, options = {}) => {
62
+ const events = synthesizeFromAgentReport(report, synthesizeOptionsFrom(options));
103
63
  const state = reduceRenderStateAll(events);
104
64
  const projects = [summarizeProject(report)];
105
65
  const inputs = {
@@ -111,10 +71,7 @@ const renderAgentStringForReport = (report) => {
111
71
  belowTarget: collectBelowTarget([report]),
112
72
  runCommand: null
113
73
  };
114
- return dispatch(inputs, {
115
- noColor: true,
116
- osc8: (_url, label) => label
117
- });
74
+ return dispatch(inputs, {});
118
75
  };
119
76
  /**
120
77
  * Same as {@link renderAgentStringForReport} but returns the Ink-half
@@ -140,7 +97,7 @@ const renderHumanStringForReport = (report, options = {}) => renderHumanString(r
140
97
  * @internal
141
98
  */
142
99
  const renderHumanString = async (report, options, env) => {
143
- const events = synthesizeFromAgentReport(report);
100
+ const events = synthesizeFromAgentReport(report, synthesizeOptionsFrom(options));
144
101
  const state = reduceRenderStateAll(events);
145
102
  const projects = [summarizeProject(report)];
146
103
  const inputs = {
@@ -152,12 +109,11 @@ const renderHumanString = async (report, options, env) => {
152
109
  belowTarget: collectBelowTarget([report]),
153
110
  runCommand: null
154
111
  };
155
- const opts = {
156
- noColor: false,
157
- osc8: (_url, label) => label
158
- };
159
112
  const { renderInkReport } = await import("./humanReport.js");
160
- return await renderInkReport(inputs, opts, env, options.width) ?? dispatch(inputs, opts);
113
+ const rendered = await renderInkReport(inputs, env, options.width);
114
+ if (rendered !== null) return rendered;
115
+ const link = await Effect.runPromise(resolveFileLink.pipe(Effect.provide(env)));
116
+ return dispatch(inputs, { link });
161
117
  };
162
118
  const NON_STABLE_SUMMARY_CLASSIFICATIONS = [
163
119
  "new-failure",
@@ -166,6 +122,8 @@ const NON_STABLE_SUMMARY_CLASSIFICATIONS = [
166
122
  "recovered"
167
123
  ];
168
124
  const MAX_SUMMARY_COVERAGE_ROWS = 10;
125
+ /** Sample lines shown in the step summary's stray-output section; the same cap the human note uses. */
126
+ const MAX_SUMMARY_STRAY_SAMPLES = 3;
169
127
  /**
170
128
  * The step summary and `summary.md` are files, not log lines, so their
171
129
  * markdown context opts out of the `ci` default that neutralizes workflow
@@ -212,6 +170,42 @@ const renderTrendSection = (trendSummary) => {
212
170
  return Render.markdown([Doc.heading(3, "Trend"), Doc.lines(lines)], summaryRenderContext);
213
171
  };
214
172
  /**
173
+ * Split a plain-text note on its backtick spans into inlines, so a span the
174
+ * shared formatter wrote as `` `x` `` renders as a code span in markdown
175
+ * rather than as the sanitizer's escaped literal backticks. An unmatched
176
+ * trailing backtick is kept as text.
177
+ */
178
+ const inlineWithCodeSpans = (text) => {
179
+ const parts = text.split("`");
180
+ const inlines = [];
181
+ parts.forEach((part, index) => {
182
+ if (part.length === 0) return;
183
+ const insideSpan = index % 2 === 1 && index < parts.length - 1;
184
+ inlines.push(insideSpan ? Doc.code(part) : Doc.text(part));
185
+ });
186
+ return inlines;
187
+ };
188
+ /**
189
+ * What the run wrote straight to the terminal, past Vitest's console capture
190
+ * (issue #578): the same headline the human and agent notes print, the byte
191
+ * count, then the sample lines as code spans (a sample is untrusted text, so
192
+ * it is never read as markdown). `null` when the run wrote nothing stray.
193
+ */
194
+ const renderStrayOutputSection = (reports) => {
195
+ const stray = strayOutputOf(reports);
196
+ if (stray === void 0) return null;
197
+ const [headline = ""] = formatStrayOutputNote(stray);
198
+ const shown = stray.samples.slice(0, MAX_SUMMARY_STRAY_SAMPLES);
199
+ const lines = shown.map((sample) => [Doc.text(`${sample.stream}: `), Doc.code(sample.text)]);
200
+ const more = stray.total - shown.length;
201
+ if (more > 0 && shown.length > 0) lines.push([Doc.text(`(+${more} more)`)]);
202
+ return Render.markdown([
203
+ Doc.heading(3, "Stray output"),
204
+ Doc.paragraph(...inlineWithCodeSpans(`${headline} ${Fmt.plural(stray.bytes, "byte")} recorded.`)),
205
+ ...lines.length > 0 ? [Doc.lines(lines)] : []
206
+ ], summaryRenderContext);
207
+ };
208
+ /**
215
209
  * Per-project pass/fail/timeout/skip/duration table — the unconditional
216
210
  * half of the summary body.
217
211
  *
@@ -265,7 +259,7 @@ const renderTotalsSection = (reports) => {
265
259
  * Assemble the vitest-agent markdown body shared by the GitHub step
266
260
  * summary and the `summary.md` report file: a `## vitest-agent` heading,
267
261
  * the always-present per-project totals table, then whichever of the
268
- * classification, coverage, and trend sections have content.
262
+ * classification, coverage, trend, and stray-output sections have content.
269
263
  *
270
264
  * Always returns markdown. An all-green run still gets a body — with
271
265
  * Vitest's own job summary disabled, returning `null` here left the step
@@ -278,7 +272,8 @@ const buildSummaryMarkdown = (input) => {
278
272
  const sections = [
279
273
  renderClassificationsSection(input.classifications),
280
274
  renderCoverageSection(input.reports),
281
- renderTrendSection(input.trendSummary)
275
+ renderTrendSection(input.trendSummary),
276
+ renderStrayOutputSection(input.reports)
282
277
  ].filter((section) => section !== null);
283
278
  return [
284
279
  "## vitest-agent",
@@ -341,11 +336,10 @@ const DefaultVitestAgentReporter = (kit) => {
341
336
  render(input, renderKit) {
342
337
  const out = [];
343
338
  if (shouldRenderForMode(renderKit.config.consoleMode)) {
344
- const events = input.reports.flatMap((r) => synthesizeFromAgentReport(r));
339
+ const events = input.reports.flatMap((r) => synthesizeFromAgentReport(r, { ...input.suggestedActions !== void 0 ? { suggestedActions: input.suggestedActions } : {} }));
345
340
  const state = reduceRenderStateAll(events);
346
341
  const inputs = buildDispatchInputs(state, input, { runCommand: renderKit.config.runCommand ?? null });
347
- const opts = resolveCellOptions(renderKit);
348
- const content = dispatch(inputs, opts);
342
+ const content = dispatch(inputs, {});
349
343
  if (content.length > 0) out.push({
350
344
  target: "stdout",
351
345
  content,
@@ -378,4 +372,4 @@ const DefaultVitestAgentReporter = (kit) => {
378
372
  };
379
373
  };
380
374
 
381
- export { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanString, renderHumanStringForReport, resolveCellOptions };
375
+ export { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanString, renderHumanStringForReport };
package/fileLinks.js ADDED
@@ -0,0 +1,43 @@
1
+ import { CliLinks } from "@effected/cli";
2
+ import { Effect, Option } from "effect";
3
+ import { Audience, TerminalEnv } from "@effected/env";
4
+
5
+ // src/fileLinks.ts
6
+ /**
7
+ * The file-link writer the reporter's human renders draw paths through
8
+ * (issue #541): `@effected/cli`'s `CliLinks.linker` over the services the
9
+ * reporter's environment carries.
10
+ *
11
+ * The kit decides everything about a link. `CliLinks` picks where a file
12
+ * opens (`vscode://file/<path>:<line>:<col>` when `TERM_PROGRAM` is VS Code
13
+ * or a `.vscode/` directory sits at the project root, `file://` otherwise,
14
+ * or nothing under `editorLinks: "off"`); `TerminalEnv` says whether stdout
15
+ * renders OSC 8 at all (a pipe, tmux, GNU screen, `NO_COLOR` and
16
+ * `NO_HYPERLINK` all say no); and the `Audience` rule keeps every escape
17
+ * away from an agent. The writer is pure, so it is resolved once and handed
18
+ * to the cells (`CellOptions.link`) and to the Ink tree (`FileLinkProvider`).
19
+ *
20
+ * @internal
21
+ */
22
+ /** The writer an agent, a pipe or an environment without the kit's services gets: the label alone. */
23
+ const plainFileLink = (_target, label) => label;
24
+ /**
25
+ * Resolve the writer from the environment, when it carries the kit's
26
+ * `CliLinks`, `TerminalEnv` and `Audience`; the label alone when any is
27
+ * missing (a test environment fixed to a theme only, say). It never fails
28
+ * and never requires: a reporter rendering without `CliEnv` keeps working,
29
+ * it just writes no link.
30
+ */
31
+ const resolveFileLink = Effect.gen(function* () {
32
+ const links = yield* Effect.serviceOption(CliLinks);
33
+ const terminal = yield* Effect.serviceOption(TerminalEnv);
34
+ const audience = yield* Effect.serviceOption(Audience);
35
+ if (Option.isNone(links) || Option.isNone(terminal) || Option.isNone(audience)) return plainFileLink;
36
+ return CliLinks.linker({
37
+ links: links.value,
38
+ hyperlinks: terminal.value.stdout.hyperlinks,
39
+ audience: audience.value.kind
40
+ });
41
+ });
42
+
43
+ export { plainFileLink, resolveFileLink };
package/githubLog.js CHANGED
@@ -1,4 +1,6 @@
1
- import { Doc, Render, Status } from "@effected/cli";
1
+ import { summarizeProject } from "./projectSummary.js";
2
+ import { Doc, Fmt, Render, Status } from "@effected/cli";
3
+ import { formatStrayOutputNote } from "@vitest-agent/sdk";
2
4
  import { relative } from "node:path";
3
5
 
4
6
  // src/githubLog.ts
@@ -17,14 +19,21 @@ import { relative } from "node:path";
17
19
  * `::group::`) and rendered with `Render.githubLog`, which also neutralizes
18
20
  * any workflow command a project name or path could otherwise inject.
19
21
  *
20
- * Known inconsistency, kept deliberately for now: the per-project line counts
21
- * a timed-out test as `failed` (it reads `report.summary.failed`), while the
22
- * step-summary totals table splits timeouts into their own column.
22
+ * The per-project counts come from `summarizeProject`, the same projection
23
+ * the step-summary totals table renders, so a timed-out test reads
24
+ * `timed out` here as it does there instead of folding into `failed`.
23
25
  *
24
26
  * @internal
25
27
  */
26
28
  const MAX_NAMED_FILES = 3;
27
29
  /**
30
+ * The run's stray output. It is a run-level signal (the bytes arrive on one
31
+ * stream shared by every project), so every project report of a run carries
32
+ * the same value and the first one found is the run's; `undefined` when the
33
+ * run wrote nothing past Vitest's console capture.
34
+ */
35
+ const strayOutputOf = (reports) => reports.find((report) => report.strayOutput !== void 0)?.strayOutput;
36
+ /**
28
37
  * Renders a coverage-target file path relative to `process.cwd()` for
29
38
  * display. Falls back to the original string unchanged when the relative
30
39
  * path is empty or escapes the root (starts with `..`) rather than
@@ -41,30 +50,39 @@ const NON_STABLE_CLASSIFICATIONS = [
41
50
  "flaky",
42
51
  "recovered"
43
52
  ];
44
- /** `<project>: <passed>/<total> passed, <failed> failed, <skipped> skipped` — every counter shown, zeros included. */
53
+ /**
54
+ * `<project>: <passed>/<total> passed, <failed> failed, <timed out> timed out, <skipped> skipped` — every counter
55
+ * shown, zeros included, read from the same `ProjectSummary` the totals table renders.
56
+ */
45
57
  const projectCounts = (report) => {
46
- const { passed, failed, skipped, total } = report.summary;
58
+ const { passCount, failCount, skipCount, timeoutCount = 0 } = summarizeProject(report);
47
59
  return Doc.counts({
48
60
  label: report.project ?? "default",
49
61
  layout: "inline",
50
- total: () => total,
62
+ total: () => report.summary.total,
51
63
  counters: [
52
64
  Doc.counter(Status.core, "success", {
53
65
  key: "passed",
54
66
  label: "passed",
55
- n: passed,
67
+ n: passCount,
56
68
  showZero: true
57
69
  }),
58
70
  Doc.counter(Status.core, "failure", {
59
71
  key: "failed",
60
72
  label: "failed",
61
- n: failed,
73
+ n: failCount,
74
+ showZero: true
75
+ }),
76
+ Doc.counter(Status.core, "warning", {
77
+ key: "timedOut",
78
+ label: "timed out",
79
+ n: timeoutCount,
62
80
  showZero: true
63
81
  }),
64
82
  Doc.counter(Status.core, "skip", {
65
83
  key: "skipped",
66
84
  label: "skipped",
67
- n: skipped,
85
+ n: skipCount,
68
86
  showZero: true
69
87
  })
70
88
  ]
@@ -82,6 +100,12 @@ function renderGithubLog(input, kit) {
82
100
  for (const classification of input.classifications.values()) classificationCounts.set(classification, (classificationCounts.get(classification) ?? 0) + 1);
83
101
  const classificationParts = NON_STABLE_CLASSIFICATIONS.filter((kind) => (classificationCounts.get(kind) ?? 0) > 0).map((kind) => `${kind}: ${classificationCounts.get(kind)}`);
84
102
  if (classificationParts.length > 0) body.push(Doc.paragraph(`classifications: ${classificationParts.join(", ")}`));
103
+ const stray = strayOutputOf(input.reports);
104
+ if (stray !== void 0) {
105
+ const [headline, ...samples] = formatStrayOutputNote(stray);
106
+ body.push(Doc.paragraph(`${headline} ${Fmt.plural(stray.bytes, "byte")} recorded.`));
107
+ if (samples.length > 0) body.push(Doc.verbatim(samples.join("\n")));
108
+ }
85
109
  if (kit.config.dbPath !== void 0) body.push(Doc.paragraph(`db: ${kit.config.dbPath}`));
86
110
  const ctx = Render.contextOf({
87
111
  audience: "ci",
@@ -94,4 +118,4 @@ function renderGithubLog(input, kit) {
94
118
  };
95
119
  }
96
120
 
97
- export { renderGithubLog, toDisplayPath };
121
+ export { renderGithubLog, strayOutputOf, toDisplayPath };
package/humanReport.js CHANGED
@@ -1,7 +1,8 @@
1
- import { CliUi, UiProvider } from "@effected/cli/ui";
1
+ import { resolveFileLink } from "./fileLinks.js";
2
2
  import { Effect } from "effect";
3
3
  import { TerminalEnv } from "@effected/env";
4
- import { dispatchInk } from "@vitest-agent/ui/ink";
4
+ import { CliUi, UiProvider } from "@effected/cli/ui";
5
+ import { FileLinkProvider, dispatchInk } from "@vitest-agent/ui/ink";
5
6
  import { renderToString } from "ink";
6
7
  import { createElement } from "react";
7
8
 
@@ -14,22 +15,26 @@ const FALLBACK_COLUMNS = 80;
14
15
  *
15
16
  * The layout width is `width` when given, else the terminal's
16
17
  * (`TerminalEnv.width`: stdout's columns, else `COLUMNS`, else 80 — so a
17
- * non-TTY run lays out at 80).
18
+ * non-TTY run lays out at 80). File paths are drawn through the link writer
19
+ * the environment decides (`resolveFileLink`): the cell's own string gets it
20
+ * as `CellOptions.link`, and the tree through `FileLinkProvider`.
18
21
  *
19
22
  * @internal
20
23
  */
21
- const renderInkReport = async (inputs, opts, env, width) => {
22
- const element = dispatchInk(inputs, opts);
23
- if (element === null) return null;
24
- const { context, columns } = await Effect.runPromise(Effect.gen(function* () {
24
+ const renderInkReport = async (inputs, env, width) => {
25
+ const { context, columns, link } = await Effect.runPromise(Effect.gen(function* () {
25
26
  const context = yield* CliUi.context;
26
27
  const terminal = yield* TerminalEnv;
28
+ const link = yield* resolveFileLink;
27
29
  return {
28
30
  context,
29
- columns: width ?? terminal.width(FALLBACK_COLUMNS)
31
+ columns: width ?? terminal.width(FALLBACK_COLUMNS),
32
+ link
30
33
  };
31
34
  }).pipe(Effect.provide(env)));
32
- return renderToString(createElement(UiProvider, { value: context }, element), { columns });
35
+ const element = dispatchInk(inputs, { link });
36
+ if (element === null) return null;
37
+ return renderToString(createElement(UiProvider, { value: context }, createElement(FileLinkProvider, { link }, element)), { columns });
33
38
  };
34
39
 
35
40
  export { renderInkReport };
package/index.d.ts CHANGED
@@ -8,15 +8,27 @@ import "@effected/env";
8
8
  declare const FileCoverageReport: Schema.Struct<{
9
9
  readonly file: Schema.String;
10
10
  readonly summary: Schema.Struct<{
11
- readonly statements: Schema.Number;
12
- readonly branches: Schema.Number;
13
- readonly functions: Schema.Number;
14
- readonly lines: Schema.Number;
11
+ readonly statements: Schema.Finite;
12
+ readonly branches: Schema.Finite;
13
+ readonly functions: Schema.Finite;
14
+ readonly lines: Schema.Finite;
15
15
  }>;
16
16
  readonly uncoveredLines: Schema.String;
17
17
  }>;
18
18
  /** @public */
19
19
  type FileCoverageReport = typeof FileCoverageReport.Type;
20
+ /**
21
+ * A `SuggestedAction` event, denormalized into the queue the renderer reads.
22
+ * @public
23
+ */
24
+ declare const SuggestedActionRecord: Schema.Struct<{
25
+ readonly severity: Schema.Literals<readonly ["info", "warn", "blocker"]>;
26
+ readonly title: Schema.String;
27
+ readonly detail: Schema.String;
28
+ readonly targetTool: Schema.optional<Schema.String>;
29
+ }>;
30
+ /** @public */
31
+ type SuggestedActionRecord = typeof SuggestedActionRecord.Type;
20
32
  /**
21
33
  * The renderer state. Both the human (Ink) and agent (string)
22
34
  * renderers read this shape; the reducer is the only producer.
@@ -80,24 +92,24 @@ declare const RenderState: Schema.Struct<{
80
92
  }>;
81
93
  readonly coverage: Schema.NullOr<Schema.Struct<{
82
94
  readonly metrics: Schema.Struct<{
83
- readonly statements: Schema.Number;
84
- readonly branches: Schema.Number;
85
- readonly functions: Schema.Number;
86
- readonly lines: Schema.Number;
95
+ readonly statements: Schema.Finite;
96
+ readonly branches: Schema.Finite;
97
+ readonly functions: Schema.Finite;
98
+ readonly lines: Schema.Finite;
87
99
  }>;
88
100
  readonly thresholds: Schema.Struct<{
89
- readonly lines: Schema.optional<Schema.Number>;
90
- readonly functions: Schema.optional<Schema.Number>;
91
- readonly branches: Schema.optional<Schema.Number>;
92
- readonly statements: Schema.optional<Schema.Number>;
101
+ readonly lines: Schema.optional<Schema.Finite>;
102
+ readonly functions: Schema.optional<Schema.Finite>;
103
+ readonly branches: Schema.optional<Schema.Finite>;
104
+ readonly statements: Schema.optional<Schema.Finite>;
93
105
  }>;
94
106
  readonly gaps: Schema.$Array<Schema.Struct<{
95
107
  readonly file: Schema.String;
96
108
  readonly missing: Schema.Struct<{
97
- readonly statements: Schema.Number;
98
- readonly branches: Schema.Number;
99
- readonly functions: Schema.Number;
100
- readonly lines: Schema.Number;
109
+ readonly statements: Schema.Finite;
110
+ readonly branches: Schema.Finite;
111
+ readonly functions: Schema.Finite;
112
+ readonly lines: Schema.Finite;
101
113
  }>;
102
114
  readonly uncoveredLines: Schema.optional<Schema.String>;
103
115
  }>>;
@@ -112,16 +124,16 @@ declare const RenderState: Schema.Struct<{
112
124
  readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
113
125
  readonly pattern: Schema.String;
114
126
  readonly summary: Schema.Struct<{
115
- readonly statements: Schema.Number;
116
- readonly branches: Schema.Number;
117
- readonly functions: Schema.Number;
118
- readonly lines: Schema.Number;
127
+ readonly statements: Schema.Finite;
128
+ readonly branches: Schema.Finite;
129
+ readonly functions: Schema.Finite;
130
+ readonly lines: Schema.Finite;
119
131
  }>;
120
132
  readonly thresholds: Schema.Struct<{
121
- readonly lines: Schema.optional<Schema.Number>;
122
- readonly functions: Schema.optional<Schema.Number>;
123
- readonly branches: Schema.optional<Schema.Number>;
124
- readonly statements: Schema.optional<Schema.Number>;
133
+ readonly lines: Schema.optional<Schema.Finite>;
134
+ readonly functions: Schema.optional<Schema.Finite>;
135
+ readonly branches: Schema.optional<Schema.Finite>;
136
+ readonly statements: Schema.optional<Schema.Finite>;
125
137
  }>;
126
138
  }>>>;
127
139
  }>>;
@@ -178,15 +190,35 @@ declare const RenderState: Schema.Struct<{
178
190
  * wrote nothing stray.
179
191
  */
180
192
  readonly strayOutput: Schema.optional<Schema.Struct<{
181
- readonly total: Schema.Number;
182
- readonly stdout: Schema.Number;
183
- readonly stderr: Schema.Number;
184
- readonly bytes: Schema.Number;
193
+ readonly total: Schema.Finite;
194
+ readonly stdout: Schema.Finite;
195
+ readonly stderr: Schema.Finite;
196
+ readonly bytes: Schema.Finite;
185
197
  readonly samples: Schema.$Array<Schema.Struct<{
186
198
  readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
187
199
  readonly text: Schema.String;
188
200
  }>>;
189
201
  }>>;
202
+ /**
203
+ * `console.*` output Vitest captured from tests that did not fail, folded
204
+ * from `RunFinished.consoleLeaks`. Absent when the run logged nothing
205
+ * (issue #544).
206
+ */
207
+ readonly consoleLeaks: Schema.optional<Schema.Struct<{
208
+ readonly total: Schema.Finite;
209
+ readonly byFile: Schema.$Array<Schema.Struct<{
210
+ readonly file: Schema.String;
211
+ readonly stdout: Schema.Finite;
212
+ readonly stderr: Schema.Finite;
213
+ readonly tests: Schema.optional<Schema.$Array<Schema.String>>;
214
+ readonly sample: Schema.optional<Schema.String>;
215
+ }>>;
216
+ readonly truncated: Schema.optional<Schema.Boolean>;
217
+ readonly fromFailingTests: Schema.optional<Schema.Struct<{
218
+ readonly total: Schema.Finite;
219
+ readonly files: Schema.Finite;
220
+ }>>;
221
+ }>>;
190
222
  }>;
191
223
  /** @public */
192
224
  type RenderState = typeof RenderState.Type;
@@ -289,27 +321,41 @@ interface DispatchInputs {
289
321
  readonly runCommand: string | null;
290
322
  }
291
323
  /**
292
- * Render-time options threaded into each cell from the resolved
293
- * `ReporterKit`. These are concerns the cell needs to format its
294
- * output but that do not belong on the state-derived
295
- * {@link DispatchInputs} struct.
324
+ * Where a file link points: a path, with an optional line and column.
296
325
  *
297
- * The plugin builds this once per run from the kit and passes it into
326
+ * The file half of `@effected/cli`'s `LinkTarget`, spelled structurally so
327
+ * this platform-free contract names no kit type. A relative path resolves
328
+ * against the working directory the kit's `CliLinks` layer was built with.
329
+ * @public
330
+ */
331
+ interface FileLinkTarget {
332
+ readonly file: string;
333
+ readonly line?: number;
334
+ readonly col?: number;
335
+ }
336
+ /**
337
+ * Render-time options threaded into each cell. These are concerns the
338
+ * cell needs to format its output but that do not belong on the
339
+ * state-derived {@link DispatchInputs} struct.
340
+ *
341
+ * The caller builds this once per render and passes it into
298
342
  * `dispatch(inputs, opts)`. Cells destructure only what they consume.
299
343
  * @public
300
344
  */
301
345
  interface CellOptions {
302
346
  /**
303
- * Resolved value of the `NO_COLOR` env var. Cells gate ANSI escape
304
- * sequences and OSC-8 hyperlinks on this.
305
- */
306
- readonly noColor: boolean;
307
- /**
308
- * Pre-bound OSC-8 hyperlink helper. The plugin has already decided
309
- * whether OSC-8 should be enabled (`target=stdout`, `!noColor`) so
310
- * cells can call this directly without re-consulting the environment.
347
+ * Writes a file path as a link for the audience and terminal the output
348
+ * is for: the label wrapped in an OSC 8 hyperlink when the reader can
349
+ * follow it, the label unchanged otherwise. The kit decides both halves
350
+ * (`@effected/cli`'s `CliLinks.linker`, `Render.context("stdout").link`):
351
+ * `vscode://file/<path>:<line>:<col>` under VS Code, `file://` otherwise,
352
+ * never for an agent audience, and nothing on a terminal that cannot
353
+ * render hyperlinks (a pipe, tmux, GNU screen, `NO_COLOR`).
354
+ *
355
+ * Omitted, every path is its label alone, which is what an agent reads;
356
+ * a cell therefore never writes an escape sequence of its own.
311
357
  */
312
- readonly osc8: (url: string, label: string) => string;
358
+ readonly link?: (target: FileLinkTarget, label: string) => string;
313
359
  }
314
360
  /**
315
361
  * One unit of rendered reporter output, tagged by where it goes.
@@ -343,11 +389,11 @@ declare const AgentReport: Schema.Struct<{
343
389
  readonly project: Schema.optional<Schema.String>;
344
390
  readonly reason: Schema.Literals<readonly ["passed", "failed", "interrupted"]>;
345
391
  readonly summary: Schema.Struct<{
346
- readonly total: Schema.Number;
347
- readonly passed: Schema.Number;
348
- readonly failed: Schema.Number;
349
- readonly skipped: Schema.Number;
350
- readonly duration: Schema.Number;
392
+ readonly total: Schema.Finite;
393
+ readonly passed: Schema.Finite;
394
+ readonly failed: Schema.Finite;
395
+ readonly skipped: Schema.Finite;
396
+ readonly duration: Schema.Finite;
351
397
  /**
352
398
  * Count of every collected module, passing modules included.
353
399
  * `AgentReport.failed` only carries failing modules, so a green run
@@ -355,12 +401,12 @@ declare const AgentReport: Schema.Struct<{
355
401
  * this a fully-passing replay reads as "0 modules all-passed"
356
402
  * (issue #204). Optional to keep existing fixtures/reports valid.
357
403
  */
358
- readonly modules: Schema.optional<Schema.Number>;
404
+ readonly modules: Schema.optional<Schema.Finite>;
359
405
  }>;
360
406
  readonly failed: Schema.$Array<Schema.Struct<{
361
407
  readonly file: Schema.String;
362
408
  readonly state: Schema.Literals<readonly ["passed", "failed", "skipped", "pending"]>;
363
- readonly duration: Schema.optional<Schema.Number>;
409
+ readonly duration: Schema.optional<Schema.Finite>;
364
410
  readonly errors: Schema.optional<Schema.$Array<Schema.Struct<{
365
411
  readonly message: Schema.String;
366
412
  readonly stack: Schema.optional<Schema.String>;
@@ -372,7 +418,7 @@ declare const AgentReport: Schema.Struct<{
372
418
  readonly name: Schema.String;
373
419
  readonly fullName: Schema.String;
374
420
  readonly state: Schema.Literals<readonly ["passed", "failed", "skipped", "pending"]>;
375
- readonly duration: Schema.optional<Schema.Number>;
421
+ readonly duration: Schema.optional<Schema.Finite>;
376
422
  readonly flaky: Schema.optional<Schema.Boolean>;
377
423
  readonly slow: Schema.optional<Schema.Boolean>;
378
424
  readonly errors: Schema.optional<Schema.$Array<Schema.Struct<{
@@ -395,81 +441,81 @@ declare const AgentReport: Schema.Struct<{
395
441
  readonly failedFiles: Schema.$Array<Schema.String>;
396
442
  readonly coverage: Schema.optional<Schema.Struct<{
397
443
  readonly totals: Schema.Struct<{
398
- readonly statements: Schema.Number;
399
- readonly branches: Schema.Number;
400
- readonly functions: Schema.Number;
401
- readonly lines: Schema.Number;
444
+ readonly statements: Schema.Finite;
445
+ readonly branches: Schema.Finite;
446
+ readonly functions: Schema.Finite;
447
+ readonly lines: Schema.Finite;
402
448
  }>;
403
449
  readonly thresholds: Schema.Struct<{
404
450
  readonly global: Schema.Struct<{
405
- readonly lines: Schema.optional<Schema.Number>;
406
- readonly functions: Schema.optional<Schema.Number>;
407
- readonly branches: Schema.optional<Schema.Number>;
408
- readonly statements: Schema.optional<Schema.Number>;
451
+ readonly lines: Schema.optional<Schema.Finite>;
452
+ readonly functions: Schema.optional<Schema.Finite>;
453
+ readonly branches: Schema.optional<Schema.Finite>;
454
+ readonly statements: Schema.optional<Schema.Finite>;
409
455
  }>;
410
456
  readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
411
- readonly lines: Schema.optional<Schema.Number>;
412
- readonly functions: Schema.optional<Schema.Number>;
413
- readonly branches: Schema.optional<Schema.Number>;
414
- readonly statements: Schema.optional<Schema.Number>;
457
+ readonly lines: Schema.optional<Schema.Finite>;
458
+ readonly functions: Schema.optional<Schema.Finite>;
459
+ readonly branches: Schema.optional<Schema.Finite>;
460
+ readonly statements: Schema.optional<Schema.Finite>;
415
461
  readonly perFile: Schema.optionalKey<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
416
- readonly lines: Schema.optional<Schema.Number>;
417
- readonly functions: Schema.optional<Schema.Number>;
418
- readonly branches: Schema.optional<Schema.Number>;
419
- readonly statements: Schema.optional<Schema.Number>;
462
+ readonly lines: Schema.optional<Schema.Finite>;
463
+ readonly functions: Schema.optional<Schema.Finite>;
464
+ readonly branches: Schema.optional<Schema.Finite>;
465
+ readonly statements: Schema.optional<Schema.Finite>;
420
466
  }>]>>;
421
467
  }>]>>, never>;
422
468
  }>;
423
469
  readonly targets: Schema.optional<Schema.Struct<{
424
470
  readonly global: Schema.Struct<{
425
- readonly lines: Schema.optional<Schema.Number>;
426
- readonly functions: Schema.optional<Schema.Number>;
427
- readonly branches: Schema.optional<Schema.Number>;
428
- readonly statements: Schema.optional<Schema.Number>;
471
+ readonly lines: Schema.optional<Schema.Finite>;
472
+ readonly functions: Schema.optional<Schema.Finite>;
473
+ readonly branches: Schema.optional<Schema.Finite>;
474
+ readonly statements: Schema.optional<Schema.Finite>;
429
475
  }>;
430
476
  readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
431
- readonly lines: Schema.optional<Schema.Number>;
432
- readonly functions: Schema.optional<Schema.Number>;
433
- readonly branches: Schema.optional<Schema.Number>;
434
- readonly statements: Schema.optional<Schema.Number>;
477
+ readonly lines: Schema.optional<Schema.Finite>;
478
+ readonly functions: Schema.optional<Schema.Finite>;
479
+ readonly branches: Schema.optional<Schema.Finite>;
480
+ readonly statements: Schema.optional<Schema.Finite>;
435
481
  readonly perFile: Schema.optionalKey<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
436
- readonly lines: Schema.optional<Schema.Number>;
437
- readonly functions: Schema.optional<Schema.Number>;
438
- readonly branches: Schema.optional<Schema.Number>;
439
- readonly statements: Schema.optional<Schema.Number>;
482
+ readonly lines: Schema.optional<Schema.Finite>;
483
+ readonly functions: Schema.optional<Schema.Finite>;
484
+ readonly branches: Schema.optional<Schema.Finite>;
485
+ readonly statements: Schema.optional<Schema.Finite>;
440
486
  }>]>>;
441
487
  }>]>>, never>;
442
488
  }>>;
443
489
  readonly baselines: Schema.optional<Schema.Struct<{
444
490
  readonly global: Schema.Struct<{
445
- readonly lines: Schema.optional<Schema.Number>;
446
- readonly functions: Schema.optional<Schema.Number>;
447
- readonly branches: Schema.optional<Schema.Number>;
448
- readonly statements: Schema.optional<Schema.Number>;
491
+ readonly lines: Schema.optional<Schema.Finite>;
492
+ readonly functions: Schema.optional<Schema.Finite>;
493
+ readonly branches: Schema.optional<Schema.Finite>;
494
+ readonly statements: Schema.optional<Schema.Finite>;
449
495
  }>;
450
496
  readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
451
- readonly lines: Schema.optional<Schema.Number>;
452
- readonly functions: Schema.optional<Schema.Number>;
453
- readonly branches: Schema.optional<Schema.Number>;
454
- readonly statements: Schema.optional<Schema.Number>;
497
+ readonly lines: Schema.optional<Schema.Finite>;
498
+ readonly functions: Schema.optional<Schema.Finite>;
499
+ readonly branches: Schema.optional<Schema.Finite>;
500
+ readonly statements: Schema.optional<Schema.Finite>;
455
501
  readonly perFile: Schema.optionalKey<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
456
- readonly lines: Schema.optional<Schema.Number>;
457
- readonly functions: Schema.optional<Schema.Number>;
458
- readonly branches: Schema.optional<Schema.Number>;
459
- readonly statements: Schema.optional<Schema.Number>;
502
+ readonly lines: Schema.optional<Schema.Finite>;
503
+ readonly functions: Schema.optional<Schema.Finite>;
504
+ readonly branches: Schema.optional<Schema.Finite>;
505
+ readonly statements: Schema.optional<Schema.Finite>;
460
506
  }>]>>;
461
507
  }>]>>, never>;
462
508
  }>>;
463
509
  readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
464
510
  readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
465
- readonly totalFiles: Schema.optional<Schema.Number>;
511
+ readonly totalFiles: Schema.optional<Schema.Finite>;
466
512
  readonly lowCoverage: Schema.$Array<Schema.Struct<{
467
513
  readonly file: Schema.String;
468
514
  readonly summary: Schema.Struct<{
469
- readonly statements: Schema.Number;
470
- readonly branches: Schema.Number;
471
- readonly functions: Schema.Number;
472
- readonly lines: Schema.Number;
515
+ readonly statements: Schema.Finite;
516
+ readonly branches: Schema.Finite;
517
+ readonly functions: Schema.Finite;
518
+ readonly lines: Schema.Finite;
473
519
  }>;
474
520
  readonly uncoveredLines: Schema.String;
475
521
  }>>;
@@ -477,10 +523,10 @@ declare const AgentReport: Schema.Struct<{
477
523
  readonly belowTarget: Schema.optional<Schema.$Array<Schema.Struct<{
478
524
  readonly file: Schema.String;
479
525
  readonly summary: Schema.Struct<{
480
- readonly statements: Schema.Number;
481
- readonly branches: Schema.Number;
482
- readonly functions: Schema.Number;
483
- readonly lines: Schema.Number;
526
+ readonly statements: Schema.Finite;
527
+ readonly branches: Schema.Finite;
528
+ readonly functions: Schema.Finite;
529
+ readonly lines: Schema.Finite;
484
530
  }>;
485
531
  readonly uncoveredLines: Schema.String;
486
532
  }>>>;
@@ -488,44 +534,44 @@ declare const AgentReport: Schema.Struct<{
488
534
  readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
489
535
  readonly pattern: Schema.String;
490
536
  readonly summary: Schema.Struct<{
491
- readonly statements: Schema.Number;
492
- readonly branches: Schema.Number;
493
- readonly functions: Schema.Number;
494
- readonly lines: Schema.Number;
537
+ readonly statements: Schema.Finite;
538
+ readonly branches: Schema.Finite;
539
+ readonly functions: Schema.Finite;
540
+ readonly lines: Schema.Finite;
495
541
  }>;
496
542
  readonly thresholds: Schema.Struct<{
497
- readonly lines: Schema.optional<Schema.Number>;
498
- readonly functions: Schema.optional<Schema.Number>;
499
- readonly branches: Schema.optional<Schema.Number>;
500
- readonly statements: Schema.optional<Schema.Number>;
543
+ readonly lines: Schema.optional<Schema.Finite>;
544
+ readonly functions: Schema.optional<Schema.Finite>;
545
+ readonly branches: Schema.optional<Schema.Finite>;
546
+ readonly statements: Schema.optional<Schema.Finite>;
501
547
  }>;
502
548
  }>>>;
503
549
  }>>;
504
550
  readonly tagCounts: Schema.optional<Schema.$Record<Schema.String, Schema.Struct<{
505
- readonly passed: Schema.optional<Schema.Number>;
506
- readonly failed: Schema.optional<Schema.Number>;
507
- readonly skipped: Schema.optional<Schema.Number>;
551
+ readonly passed: Schema.optional<Schema.Finite>;
552
+ readonly failed: Schema.optional<Schema.Finite>;
553
+ readonly skipped: Schema.optional<Schema.Finite>;
508
554
  }>>>;
509
555
  readonly consoleLeaks: Schema.optional<Schema.Struct<{
510
- readonly total: Schema.Number;
556
+ readonly total: Schema.Finite;
511
557
  readonly byFile: Schema.$Array<Schema.Struct<{
512
558
  readonly file: Schema.String;
513
- readonly stdout: Schema.Number;
514
- readonly stderr: Schema.Number;
559
+ readonly stdout: Schema.Finite;
560
+ readonly stderr: Schema.Finite;
515
561
  readonly tests: Schema.optional<Schema.$Array<Schema.String>>;
516
562
  readonly sample: Schema.optional<Schema.String>;
517
563
  }>>;
518
564
  readonly truncated: Schema.optional<Schema.Boolean>;
519
565
  readonly fromFailingTests: Schema.optional<Schema.Struct<{
520
- readonly total: Schema.Number;
521
- readonly files: Schema.Number;
566
+ readonly total: Schema.Finite;
567
+ readonly files: Schema.Finite;
522
568
  }>>;
523
569
  }>>;
524
570
  readonly strayOutput: Schema.optional<Schema.Struct<{
525
- readonly total: Schema.Number;
526
- readonly stdout: Schema.Number;
527
- readonly stderr: Schema.Number;
528
- readonly bytes: Schema.Number;
571
+ readonly total: Schema.Finite;
572
+ readonly stdout: Schema.Finite;
573
+ readonly stderr: Schema.Finite;
574
+ readonly bytes: Schema.Finite;
529
575
  readonly samples: Schema.$Array<Schema.Struct<{
530
576
  readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
531
577
  readonly text: Schema.String;
@@ -701,24 +747,24 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
701
747
  readonly runCount: Schema.Number;
702
748
  }>, Schema.TaggedStruct<"CoverageReady", {
703
749
  readonly metrics: Schema.Struct<{
704
- readonly statements: Schema.Number;
705
- readonly branches: Schema.Number;
706
- readonly functions: Schema.Number;
707
- readonly lines: Schema.Number;
750
+ readonly statements: Schema.Finite;
751
+ readonly branches: Schema.Finite;
752
+ readonly functions: Schema.Finite;
753
+ readonly lines: Schema.Finite;
708
754
  }>;
709
755
  readonly thresholds: Schema.Struct<{
710
- readonly lines: Schema.optional<Schema.Number>;
711
- readonly functions: Schema.optional<Schema.Number>;
712
- readonly branches: Schema.optional<Schema.Number>;
713
- readonly statements: Schema.optional<Schema.Number>;
756
+ readonly lines: Schema.optional<Schema.Finite>;
757
+ readonly functions: Schema.optional<Schema.Finite>;
758
+ readonly branches: Schema.optional<Schema.Finite>;
759
+ readonly statements: Schema.optional<Schema.Finite>;
714
760
  }>;
715
761
  readonly gaps: Schema.$Array<Schema.Struct<{
716
762
  readonly file: Schema.String;
717
763
  readonly missing: Schema.Struct<{
718
- readonly statements: Schema.Number;
719
- readonly branches: Schema.Number;
720
- readonly functions: Schema.Number;
721
- readonly lines: Schema.Number;
764
+ readonly statements: Schema.Finite;
765
+ readonly branches: Schema.Finite;
766
+ readonly functions: Schema.Finite;
767
+ readonly lines: Schema.Finite;
722
768
  }>;
723
769
  readonly uncoveredLines: Schema.optional<Schema.String>;
724
770
  }>>;
@@ -728,16 +774,16 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
728
774
  readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
729
775
  readonly pattern: Schema.String;
730
776
  readonly summary: Schema.Struct<{
731
- readonly statements: Schema.Number;
732
- readonly branches: Schema.Number;
733
- readonly functions: Schema.Number;
734
- readonly lines: Schema.Number;
777
+ readonly statements: Schema.Finite;
778
+ readonly branches: Schema.Finite;
779
+ readonly functions: Schema.Finite;
780
+ readonly lines: Schema.Finite;
735
781
  }>;
736
782
  readonly thresholds: Schema.Struct<{
737
- readonly lines: Schema.optional<Schema.Number>;
738
- readonly functions: Schema.optional<Schema.Number>;
739
- readonly branches: Schema.optional<Schema.Number>;
740
- readonly statements: Schema.optional<Schema.Number>;
783
+ readonly lines: Schema.optional<Schema.Finite>;
784
+ readonly functions: Schema.optional<Schema.Finite>;
785
+ readonly branches: Schema.optional<Schema.Finite>;
786
+ readonly statements: Schema.optional<Schema.Finite>;
741
787
  }>;
742
788
  }>>>;
743
789
  }>, Schema.TaggedStruct<"ThresholdViolation", {
@@ -788,15 +834,35 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
788
834
  * wrote nothing stray, and in older replay data.
789
835
  */
790
836
  readonly strayOutput: Schema.optional<Schema.Struct<{
791
- readonly total: Schema.Number;
792
- readonly stdout: Schema.Number;
793
- readonly stderr: Schema.Number;
794
- readonly bytes: Schema.Number;
837
+ readonly total: Schema.Finite;
838
+ readonly stdout: Schema.Finite;
839
+ readonly stderr: Schema.Finite;
840
+ readonly bytes: Schema.Finite;
795
841
  readonly samples: Schema.$Array<Schema.Struct<{
796
842
  readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
797
843
  readonly text: Schema.String;
798
844
  }>>;
799
845
  }>>;
846
+ /**
847
+ * `console.*` output Vitest captured from tests that did not fail
848
+ * (`AgentReport.consoleLeaks`). Optional: absent when the run logged
849
+ * nothing, and in older replay data (issue #544).
850
+ */
851
+ readonly consoleLeaks: Schema.optional<Schema.Struct<{
852
+ readonly total: Schema.Finite;
853
+ readonly byFile: Schema.$Array<Schema.Struct<{
854
+ readonly file: Schema.String;
855
+ readonly stdout: Schema.Finite;
856
+ readonly stderr: Schema.Finite;
857
+ readonly tests: Schema.optional<Schema.$Array<Schema.String>>;
858
+ readonly sample: Schema.optional<Schema.String>;
859
+ }>>;
860
+ readonly truncated: Schema.optional<Schema.Boolean>;
861
+ readonly fromFailingTests: Schema.optional<Schema.Struct<{
862
+ readonly total: Schema.Finite;
863
+ readonly files: Schema.Finite;
864
+ }>>;
865
+ }>>;
800
866
  }>]>;
801
867
  /** @public */
802
868
  type RunEvent = typeof RunEvent.Type;
@@ -806,27 +872,27 @@ type RunEvent = typeof RunEvent.Type;
806
872
  */
807
873
  declare const ResolvedThresholds: Schema.Struct<{
808
874
  readonly global: Schema.Struct<{
809
- readonly lines: Schema.optional<Schema.Number>;
810
- readonly functions: Schema.optional<Schema.Number>;
811
- readonly branches: Schema.optional<Schema.Number>;
812
- readonly statements: Schema.optional<Schema.Number>;
875
+ readonly lines: Schema.optional<Schema.Finite>;
876
+ readonly functions: Schema.optional<Schema.Finite>;
877
+ readonly branches: Schema.optional<Schema.Finite>;
878
+ readonly statements: Schema.optional<Schema.Finite>;
813
879
  }>;
814
880
  readonly perFile: Schema.withDecodingDefaultKey<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
815
- readonly lines: Schema.optional<Schema.Number>;
816
- readonly functions: Schema.optional<Schema.Number>;
817
- readonly branches: Schema.optional<Schema.Number>;
818
- readonly statements: Schema.optional<Schema.Number>;
881
+ readonly lines: Schema.optional<Schema.Finite>;
882
+ readonly functions: Schema.optional<Schema.Finite>;
883
+ readonly branches: Schema.optional<Schema.Finite>;
884
+ readonly statements: Schema.optional<Schema.Finite>;
819
885
  }>]>, never>;
820
886
  readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
821
- readonly lines: Schema.optional<Schema.Number>;
822
- readonly functions: Schema.optional<Schema.Number>;
823
- readonly branches: Schema.optional<Schema.Number>;
824
- readonly statements: Schema.optional<Schema.Number>;
887
+ readonly lines: Schema.optional<Schema.Finite>;
888
+ readonly functions: Schema.optional<Schema.Finite>;
889
+ readonly branches: Schema.optional<Schema.Finite>;
890
+ readonly statements: Schema.optional<Schema.Finite>;
825
891
  readonly perFile: Schema.optionalKey<Schema.Union<readonly [Schema.Boolean, Schema.Struct<{
826
- readonly lines: Schema.optional<Schema.Number>;
827
- readonly functions: Schema.optional<Schema.Number>;
828
- readonly branches: Schema.optional<Schema.Number>;
829
- readonly statements: Schema.optional<Schema.Number>;
892
+ readonly lines: Schema.optional<Schema.Finite>;
893
+ readonly functions: Schema.optional<Schema.Finite>;
894
+ readonly branches: Schema.optional<Schema.Finite>;
895
+ readonly statements: Schema.optional<Schema.Finite>;
830
896
  }>]>>;
831
897
  }>]>>, never>;
832
898
  }>;
@@ -857,8 +923,8 @@ type Transport = typeof Transport.Type;
857
923
  *
858
924
  * `detail` is pre-resolved by the plugin (via `DetailResolver`) but
859
925
  * reporters that want to override can ignore it. `noColor` is the resolved
860
- * value of the `NO_COLOR` env var; reporters use it to gate ANSI escapes and
861
- * OSC-8 hyperlinks.
926
+ * value of the `NO_COLOR` env var; reporters use it to gate ANSI escapes
927
+ * (hyperlinks are the kit's decision: `@effected/cli`'s `CliLinks`).
862
928
  * @public
863
929
  */
864
930
  interface ResolvedReporterConfig {
@@ -943,8 +1009,9 @@ interface ResolvedReporterConfig {
943
1009
  * Plugin-provided primitives passed to the reporter factory at construction
944
1010
  * time. The `std*` prefix marks these as "the plugin gives you these — do
945
1011
  * not import equivalents yourself"; they are pre-resolved with full context
946
- * (environment, executor, NO_COLOR, target=stdout) so the reporter doesn't
947
- * have to re-derive that state.
1012
+ * (environment, executor, NO_COLOR) so the reporter doesn't have to
1013
+ * re-derive that state. File hyperlinks are not among them: the reporter
1014
+ * reads `@effected/cli`'s `CliLinks` from its own environment.
948
1015
  *
949
1016
  * The shape is open to additions: future fields (e.g. `stdLogger`,
950
1017
  * `stdRuntime`) won't break existing reporters because the parameter is a
@@ -954,12 +1021,6 @@ interface ResolvedReporterConfig {
954
1021
  interface ReporterKit {
955
1022
  readonly config: ResolvedReporterConfig;
956
1023
  readonly stdEnv: Environment;
957
- /**
958
- * Pre-bound OSC-8 hyperlink helper. The plugin has already decided
959
- * whether OSC-8 should be enabled (target=stdout, !noColor) so the
960
- * reporter can call this directly without consulting environment.
961
- */
962
- readonly stdOsc8: (url: string, label: string) => string;
963
1024
  /**
964
1025
  * Live run-event channel. The plugin publishes one `RunEvent`
965
1026
  * per Vitest streaming callback (`onTestRunStart`, `onTestModuleStart`,
@@ -990,6 +1051,13 @@ interface ReporterKit {
990
1051
  interface ReporterRenderInput {
991
1052
  readonly reports: ReadonlyArray<AgentReport>;
992
1053
  readonly classifications: ReadonlyMap<string, TestClassification>;
1054
+ /**
1055
+ * Run-level suggested actions for the agent (a `SuggestedAction` event
1056
+ * each), rendered as a compact `Actions:` list after the cell output. The
1057
+ * default reporter feeds them to the synthesizer for every report; absent
1058
+ * when the plugin has nothing to suggest (issue #544).
1059
+ */
1060
+ readonly suggestedActions?: ReadonlyArray<SuggestedActionRecord>;
993
1061
  readonly trendSummary?: {
994
1062
  readonly direction: "improving" | "regressing" | "stable";
995
1063
  readonly runCount: number;
@@ -1087,22 +1155,34 @@ export declare const buildDispatchInputs: (state: RenderState, input: ReporterRe
1087
1155
  readonly runCommand?: string | null;
1088
1156
  }) => DispatchInputs;
1089
1157
  /**
1090
- * Build `CellOptions` from a {@link ReporterKit}. Picks the kit's
1091
- * resolved `noColor` value and the pre-bound OSC-8 hyperlink helper.
1158
+ * Options for the one-shot report renderers.
1092
1159
  *
1093
1160
  * @public
1094
1161
  */
1095
- export declare const resolveCellOptions: (kit: ReporterKit) => CellOptions;
1162
+ interface ReportRenderOptions {
1163
+ /** Run-level suggested actions to append as the `Actions:` list (issue #544). */
1164
+ readonly suggestedActions?: ReadonlyArray<SuggestedActionRecord>;
1165
+ }
1166
+ /**
1167
+ * Options for the Ink one-shot renderer: the agent options plus a layout width.
1168
+ *
1169
+ * @public
1170
+ */
1171
+ interface HumanReportRenderOptions extends ReportRenderOptions {
1172
+ /** The column width to lay out at; the terminal's (else 80) when omitted. */
1173
+ readonly width?: number;
1174
+ }
1096
1175
  /**
1097
1176
  * Convenience helper for one-shot consumers (e.g. a CLI command
1098
1177
  * replaying a stored run). Synthesizes a minimal `ReporterRenderInput`
1099
1178
  * from a single `AgentReport`, classifies the shape and outcome, and
1100
1179
  * returns the dispatched agent-string for the matching cell. Equivalent
1101
- * to the pre-2.0 `renderRun(events, "agent")` shortcut.
1180
+ * to the pre-2.0 `renderRun(events, "agent")` shortcut. Pass
1181
+ * `options.suggestedActions` to append the run's `Actions:` list.
1102
1182
  *
1103
1183
  * @public
1104
1184
  */
1105
- export declare const renderAgentStringForReport: (report: AgentReport) => string;
1185
+ export declare const renderAgentStringForReport: (report: AgentReport, options?: ReportRenderOptions) => string;
1106
1186
  /**
1107
1187
  * Same as {@link renderAgentStringForReport} but returns the Ink-half
1108
1188
  * rendered to a string via Ink's `renderToString`, inside the kit's
@@ -1119,9 +1199,7 @@ export declare const renderAgentStringForReport: (report: AgentReport) => string
1119
1199
  *
1120
1200
  * @public
1121
1201
  */
1122
- export declare const renderHumanStringForReport: (report: AgentReport, options?: {
1123
- readonly width?: number;
1124
- }) => Promise<string>;
1202
+ export declare const renderHumanStringForReport: (report: AgentReport, options?: HumanReportRenderOptions) => Promise<string>;
1125
1203
  /**
1126
1204
  * The default reporter factory.
1127
1205
  *
@@ -1155,5 +1233,5 @@ export declare const DefaultVitestAgentReporter: VitestAgentReporterFactory;
1155
1233
  * @public
1156
1234
  */
1157
1235
  export declare const CURRENT_REPORTER_VERSION: string;
1158
- export type { AgentReport, CellOptions, ConsoleMode, DetailLevel, DispatchInputs, Environment, Executor, FileCoverageReport, ProjectSummary, RenderState, RenderedOutput, ReporterKit, ReporterRenderInput, ResolvedReporterConfig, ResolvedThresholds, RunEvent, RunOutcome, RunShape, TestClassification, Transport, TrendSummary, VitestAgentReporter, VitestAgentReporterFactory };
1236
+ export type { AgentReport, CellOptions, ConsoleMode, DetailLevel, DispatchInputs, Environment, Executor, FileCoverageReport, FileLinkTarget, HumanReportRenderOptions, ProjectSummary, RenderState, RenderedOutput, ReportRenderOptions, ReporterKit, ReporterRenderInput, ResolvedReporterConfig, ResolvedThresholds, RunEvent, RunOutcome, RunShape, SuggestedActionRecord, TestClassification, Transport, TrendSummary, VitestAgentReporter, VitestAgentReporterFactory };
1159
1237
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport, resolveCellOptions } from "./defaultReporter.js";
1
+ import { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport } from "./defaultReporter.js";
2
2
 
3
3
  // src/index.ts
4
4
  /**
@@ -8,6 +8,6 @@ import { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForRe
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_REPORTER_VERSION = "5.0.9";
11
+ const CURRENT_REPORTER_VERSION = "6.0.1";
12
12
 
13
- export { CURRENT_REPORTER_VERSION, DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport, resolveCellOptions };
13
+ export { CURRENT_REPORTER_VERSION, DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport };
package/liveView.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { plainFileLink, resolveFileLink } from "./fileLinks.js";
1
2
  import { CliEnv, Doc } from "@effected/cli";
2
3
  import { initialRenderState } from "@vitest-agent/sdk";
3
4
  import { SPINNER_FRAME_MS, reduceRenderState, renderAgent } from "@vitest-agent/ui";
5
+ import { Cause, Deferred, Effect, Exit, Layer, PubSub, Scope } from "effect";
4
6
  import * as NodeServices from "@effect/platform-node/NodeServices";
5
7
  import { CliUi } from "@effected/cli/ui";
6
- import { Cause, Deferred, Effect, Exit, Layer, PubSub, Scope } from "effect";
7
8
 
8
9
  // src/liveView.ts
9
10
  /**
@@ -41,29 +42,40 @@ import { Cause, Deferred, Effect, Exit, Layer, PubSub, Scope } from "effect";
41
42
  * watch rerun.
42
43
  */
43
44
  /**
44
- * The live view's options, everything but `events`: what the reporter hands
45
- * `CliUi.live`, and what the tests hand `CliUiTest.live`, so both run the
46
- * same fold, drawing and run predicates.
45
+ * The live view's options, everything but `events`, for a view whose paths
46
+ * are drawn through `link`: what the reporter hands `CliUi.live` once it has
47
+ * resolved the writer from its environment (`resolveFileLink`).
47
48
  *
48
49
  * @internal
49
50
  */
50
- const liveViewOptions = {
51
+ const liveViewOptionsWith = (link) => ({
51
52
  initial: initialRenderState,
52
53
  reduce: reduceRenderState,
53
- render: CliUi.lazyView(() => import("./streamView.js")),
54
+ render: CliUi.lazyView(() => import("./streamView.js").then((view) => view.makeStreamView(link))),
54
55
  isStart: (event) => event._tag === "RunStarted",
55
56
  isTerminal: (event) => event._tag === "RunFinished" || event._tag === "RunTimedOut",
56
57
  begins: (event, before, after) => event._tag === "RunStarted" || before.phase === "idle" && after.phase !== "idle",
57
58
  mode: "owned",
58
59
  tickMillis: SPINNER_FRAME_MS,
59
60
  final: (state) => renderAgent(state).trimEnd().split("\n").map((line) => Doc.line(line, { wrap: false }))
60
- };
61
+ });
62
+ /**
63
+ * The live view's options with no link writer (every path its label alone):
64
+ * what the tests hand `CliUiTest.live`, so they run the same fold, drawing
65
+ * and run predicates `startLiveView` does.
66
+ *
67
+ * @internal
68
+ */
69
+ const liveViewOptions = liveViewOptionsWith(plainFileLink);
61
70
  /**
62
71
  * The environment a Vitest-hosted live view reads: the kit's `CliEnv` over
63
72
  * Node's stdio and terminal. It builds `CliTheme` (colour from the terminal,
64
- * glyphs `auto`, so `TERM=dumb` draws ASCII) and `TerminalEnv` (the width
65
- * the report-time human render lays out at), and sets `CliInteractive` (a
66
- * human audience with a TTY on stdin and stdout).
73
+ * glyphs `auto`, so `TERM=dumb` draws ASCII), `TerminalEnv` (the width the
74
+ * report-time human render lays out at, and whether stdout renders OSC 8),
75
+ * `Audience` and `CliLinks` (where a file link opens; the platform is
76
+ * provided outside `CliEnv.layer`, so a `.vscode/` directory is looked for
77
+ * and a relative path resolved against the working directory), and sets
78
+ * `CliInteractive` (a human audience with a TTY on stdin and stdout).
67
79
  *
68
80
  * @internal
69
81
  */
@@ -95,10 +107,10 @@ const startLiveView = (channel, env = LiveViewEnv) => {
95
107
  const scope = Effect.runSync(Scope.make());
96
108
  const subscription = Effect.runSync(PubSub.subscribe(channel).pipe(Scope.provide(scope)));
97
109
  const mounted = Effect.runSync(Deferred.make());
98
- const program = CliUi.live({
99
- ...liveViewOptions,
110
+ const program = resolveFileLink.pipe(Effect.flatMap((link) => CliUi.live({
111
+ ...liveViewOptionsWith(link),
100
112
  events: subscription
101
- }).pipe(Effect.onExit((exit) => Deferred.done(mounted, exit)), Effect.flatMap((handle) => handle.done), Scope.provide(scope), Effect.provide(env));
113
+ })), Effect.onExit((exit) => Deferred.done(mounted, exit)), Effect.flatMap((handle) => handle.done), Scope.provide(scope), Effect.provide(env));
102
114
  Effect.runFork(program);
103
115
  let closing;
104
116
  let print;
@@ -117,4 +129,4 @@ const startLiveView = (channel, env = LiveViewEnv) => {
117
129
  };
118
130
  };
119
131
 
120
- export { LiveViewEnv, liveViewOptions, printAbove, startLiveView };
132
+ export { LiveViewEnv, liveViewOptionsWith, printAbove, startLiveView };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/reporter",
3
- "version": "5.0.9",
3
+ "version": "6.0.1",
4
4
  "private": false,
5
5
  "description": "Named VitestAgentReporterFactory implementations for the vitest-agent ecosystem.",
6
6
  "keywords": [
@@ -40,17 +40,17 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@effect/platform-node": "^4.0.6",
43
- "@vitest-agent/sdk": "7.1.0",
44
- "@vitest-agent/ui": "5.0.9",
43
+ "@vitest-agent/sdk": "8.0.0",
44
+ "@vitest-agent/ui": "5.1.1",
45
45
  "effect": "^4.0.6",
46
46
  "ink": "^8.0.0",
47
47
  "react": "^19.3.1"
48
48
  },
49
49
  "peerDependencies": {
50
- "@effected/cli": "^0.22.0",
50
+ "@effected/cli": "^0.22.1",
51
51
  "@effected/env": "^0.5.0",
52
52
  "@effected/glob": "^0.14.0",
53
- "@effected/walker": "^0.19.0",
53
+ "@effected/walker": "^0.20.0",
54
54
  "@vitest/coverage-istanbul": "^5.0.0",
55
55
  "@vitest/coverage-v8": "^5.0.0",
56
56
  "vitest": "^5.0.0"
@@ -0,0 +1,41 @@
1
+ import { countSuiteFailures, isTimeoutError } from "@vitest-agent/sdk";
2
+
3
+ // src/projectSummary.ts
4
+ /** Timed-out tests in `report.failed[].tests[]`, identified per test by `isTimeoutError`. */
5
+ const countTimeouts = (report) => {
6
+ let timeoutCount = 0;
7
+ for (const mod of report.failed) for (const test of mod.tests) {
8
+ const firstError = test.errors?.[0];
9
+ if (test.state === "failed" && firstError !== void 0 && isTimeoutError({ message: firstError.message })) timeoutCount++;
10
+ }
11
+ return timeoutCount;
12
+ };
13
+ const collapseTagCounts = (entries) => {
14
+ const out = {};
15
+ if (entries === void 0) return out;
16
+ for (const [tag, entry] of Object.entries(entries)) {
17
+ const total = (entry.passed ?? 0) + (entry.failed ?? 0) + (entry.skipped ?? 0);
18
+ if (total > 0) out[tag] = total;
19
+ }
20
+ return out;
21
+ };
22
+ const summarizeProject = (report) => {
23
+ const collapsedTagCounts = report.tagCounts !== void 0 ? collapseTagCounts(report.tagCounts) : void 0;
24
+ const tagCountsHasEntries = collapsedTagCounts !== void 0 && Object.keys(collapsedTagCounts).length > 0;
25
+ const belowTargetCount = report.coverage?.belowTargetFiles?.length;
26
+ const violationsCount = report.coverage !== void 0 && report.coverage.lowCoverage.length > 0 ? report.coverage.lowCoverage.length : void 0;
27
+ const timeoutCount = countTimeouts(report);
28
+ return {
29
+ name: report.project ?? "default",
30
+ passCount: report.summary.passed,
31
+ failCount: report.summary.failed + countSuiteFailures(report) - timeoutCount,
32
+ skipCount: report.summary.skipped,
33
+ durationMs: report.summary.duration,
34
+ ...timeoutCount > 0 ? { timeoutCount } : {},
35
+ ...tagCountsHasEntries && collapsedTagCounts !== void 0 ? { tagCounts: collapsedTagCounts } : {},
36
+ ...belowTargetCount !== void 0 ? { belowTarget: belowTargetCount } : {},
37
+ ...violationsCount !== void 0 ? { violations: violationsCount } : {}
38
+ };
39
+ };
40
+
41
+ export { countTimeouts, summarizeProject };
package/streamView.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import { SPINNER_FRAME_MS } from "@vitest-agent/ui";
2
- import { StreamApp } from "@vitest-agent/ui/ink";
2
+ import { FileLinkProvider, StreamApp } from "@vitest-agent/ui/ink";
3
3
  import { createElement } from "react";
4
4
 
5
5
  // src/streamView.ts
6
- const streamView = (state, frame) => createElement(StreamApp, {
6
+ /**
7
+ * The view for one live view, closed over its link writer.
8
+ *
9
+ * @internal
10
+ */
11
+ const makeStreamView = (link) => (state, frame) => createElement(FileLinkProvider, { link }, createElement(StreamApp, {
7
12
  state,
8
13
  frameIndex: frame,
9
14
  nowMs: frame * SPINNER_FRAME_MS
10
- });
15
+ }));
11
16
 
12
- export { streamView as default };
17
+ export { makeStreamView };
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.59.4"
9
9
  }
10
10
  ]
11
11
  }