@vitest-agent/ui 5.0.8 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -17,6 +17,6 @@ import { ActionSeverity, CoverageFile, CoverageGap, CoverageMetric, CoverageRend
17
17
  *
18
18
  * @public
19
19
  */
20
- const CURRENT_UI_VERSION = "5.0.8";
20
+ const CURRENT_UI_VERSION = "5.1.0";
21
21
 
22
22
  export { ActionSeverity, CURRENT_UI_VERSION, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, FailureRecord, ModuleRecord, RenderState, RenderTotals, RunEvent, SPINNER_FRAMES, SPINNER_FRAME_MS, SUITE_FAILURE_LABEL, SUITE_LOAD_FAILURE_LABEL, SuggestedActionRecord, TestRecord, VitestAgentStatus, VitestAgentTokens, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatcherTable, dominantClassification, formatDisplayDuration, initialRenderState, inkStyle, reduceRenderState, reduceRenderStateAll, renderAgent, spinnerFrame, statusGlyph, statusInkStyle, synthesizeFromAgentReport, synthesizeRunEvents };
@@ -1,4 +1,5 @@
1
1
  import { inkStyle } from "../theme.js";
2
+ import { useFileLink } from "./FileLink.js";
2
3
  import { Fmt } from "@effected/cli";
3
4
  import { Box, Text } from "ink";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -27,6 +28,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
27
28
  const sortedGaps = [...coverage.gaps].sort((a, b) => b.missing.lines - a.missing.lines);
28
29
  const topGaps = maxGaps > 0 ? sortedGaps.slice(0, maxGaps) : [];
29
30
  const elidedGaps = maxGaps > 0 ? coverage.gaps.length - topGaps.length : 0;
31
+ const link = useFileLink();
30
32
  return /* @__PURE__ */ jsxs(Box, {
31
33
  flexDirection: "column",
32
34
  children: [
@@ -71,7 +73,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
71
73
  bold: true,
72
74
  children: "Gaps"
73
75
  }),
74
- topGaps.map((g) => /* @__PURE__ */ jsxs(Text, { children: [` ${g.file}`, g.uncoveredLines !== void 0 ? `: ${g.uncoveredLines}` : ""] }, g.file)),
76
+ topGaps.map((g) => /* @__PURE__ */ jsxs(Text, { children: [` ${link({ file: g.file }, g.file)}`, g.uncoveredLines !== void 0 ? `: ${g.uncoveredLines}` : ""] }, g.file)),
75
77
  elidedGaps > 0 ? /* @__PURE__ */ jsx(Text, {
76
78
  dimColor: true,
77
79
  children: ` (+${Fmt.plural(elidedGaps, "more gap", "more gaps")})`
@@ -1,4 +1,5 @@
1
1
  import { inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
2
+ import { useFileLink } from "./FileLink.js";
2
3
  import { useGlyphs } from "@effected/cli/ui";
3
4
  import { Box, Text } from "ink";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -12,6 +13,7 @@ const FailureRow = ({ failure, includeStack }) => {
12
13
  const suite = failure.suitePath.length > 0 ? `${failure.suitePath.join(" > ")} > ` : "";
13
14
  const classification = failure.classification !== null ? ` [${failure.classification}]` : "";
14
15
  const glyphs = useGlyphs();
16
+ const link = useFileLink();
15
17
  return /* @__PURE__ */ jsxs(Box, {
16
18
  flexDirection: "column",
17
19
  children: [
@@ -20,7 +22,7 @@ const FailureRow = ({ failure, includeStack }) => {
20
22
  ...statusInkStyle("failure"),
21
23
  children: statusGlyph("failure", glyphs)
22
24
  }),
23
- ` ${failure.modulePath} > ${suite}${failure.testName}`,
25
+ ` ${link({ file: failure.modulePath }, failure.modulePath)} > ${suite}${failure.testName}`,
24
26
  failure.classification !== null ? /* @__PURE__ */ jsx(Text, {
25
27
  color: "yellow",
26
28
  children: classification
@@ -1,4 +1,5 @@
1
1
  import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
2
+ import { useFileLink } from "./FileLink.js";
2
3
  import { useGlyphs } from "@effected/cli/ui";
3
4
  import { Box, Text } from "ink";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -13,9 +14,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
13
14
  * `module › suite › test` path, the classification tag when known, and
14
15
  * one indented line — the first line of the error message.
15
16
  */
16
- const pathOf = (f) => {
17
+ const pathOf = (f, link) => {
17
18
  return [
18
- f.modulePath,
19
+ link({ file: f.modulePath }, f.modulePath),
19
20
  ...f.suitePath,
20
21
  f.testName
21
22
  ].join(" › ");
@@ -33,6 +34,7 @@ const FailuresSection = ({ failures, limit }) => {
33
34
  const shown = failures.slice(0, limit);
34
35
  const overflow = failures.length - shown.length;
35
36
  const glyphs = useGlyphs();
37
+ const link = useFileLink();
36
38
  return /* @__PURE__ */ jsxs(Box, {
37
39
  flexDirection: "column",
38
40
  children: [
@@ -54,7 +56,7 @@ const FailuresSection = ({ failures, limit }) => {
54
56
  children: statusGlyph(failureStatus(f), glyphs)
55
57
  }),
56
58
  " ",
57
- pathOf(f),
59
+ pathOf(f, link),
58
60
  f.classification !== null ? /* @__PURE__ */ jsxs(Text, {
59
61
  ...inkStyle(VitestAgentTokens.classification),
60
62
  children: [
@@ -0,0 +1,24 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+
4
+ // src/ink/FileLink.tsx
5
+ const plain = (_target, label) => label;
6
+ const FileLinkContext = createContext(plain);
7
+ /**
8
+ * Provide the file-link writer to an Ink tree.
9
+ *
10
+ * @public
11
+ */
12
+ const FileLinkProvider = ({ link, children }) => /* @__PURE__ */ jsx(FileLinkContext.Provider, {
13
+ value: link ?? plain,
14
+ children
15
+ });
16
+ /**
17
+ * The file-link writer of the tree: the nearest {@link FileLinkProvider}'s,
18
+ * or the label alone.
19
+ *
20
+ * @public
21
+ */
22
+ const useFileLink = () => useContext(FileLinkContext);
23
+
24
+ export { FileLinkProvider, useFileLink };
package/ink/StreamApp.js CHANGED
@@ -3,12 +3,13 @@ import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../the
3
3
  import { formatDisplayDuration } from "../format-duration.js";
4
4
  import { spinnerFrame } from "../spinner.js";
5
5
  import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
6
+ import { useFileLink } from "./FileLink.js";
6
7
  import { StatusIcon } from "./StatusIcon.js";
7
8
  import { TagColumns, tagUnion } from "./TagColumns.js";
8
9
  import { ProjectRow } from "./ProjectRow.js";
9
10
  import { TestRow } from "./TestRow.js";
10
11
  import { TrendLine } from "./TrendLine.js";
11
- import { formatStrayOutputNote } from "@vitest-agent/sdk";
12
+ import { formatConsoleLeaksNote, formatStrayOutputNote } from "@vitest-agent/sdk";
12
13
  import { Fmt } from "@effected/cli";
13
14
  import { useGlyphs } from "@effected/cli/ui";
14
15
  import { Box, Text } from "ink";
@@ -109,6 +110,7 @@ const moduleIcon = (m) => {
109
110
  * and finished rows (resolved glyph, `running=false`).
110
111
  */
111
112
  const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut, tagUnion: rowTagUnion }) => {
113
+ const link = useFileLink();
112
114
  const running = moduleRunning(module);
113
115
  const queued = module.status === "queued";
114
116
  let glyph;
@@ -131,7 +133,8 @@ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut, tagUnion:
131
133
  glyph,
132
134
  /* @__PURE__ */ jsxs(Text, { children: [
133
135
  " ",
134
- module.modulePath.padEnd(nameWidth),
136
+ link({ file: module.modulePath }, module.modulePath),
137
+ " ".repeat(Math.max(0, nameWidth - module.modulePath.length)),
135
138
  " "
136
139
  ] }),
137
140
  /* @__PURE__ */ jsx(CountColumns, {
@@ -204,8 +207,8 @@ const UnhandledErrorItem = ({ error }) => {
204
207
  });
205
208
  };
206
209
  const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
207
- const failurePath = (f) => [
208
- f.modulePath,
210
+ const failurePath = (f, link) => [
211
+ link({ file: f.modulePath }, f.modulePath),
209
212
  ...f.suitePath,
210
213
  f.testName
211
214
  ].join(" › ");
@@ -213,6 +216,7 @@ const failureStatus = (f) => f.timedOut === true ? "timeout" : "failure";
213
216
  /** A failure entry rendered in the Live region. */
214
217
  const FailureItem = ({ failure }) => {
215
218
  const glyphs = useGlyphs();
219
+ const link = useFileLink();
216
220
  return /* @__PURE__ */ jsxs(Box, {
217
221
  flexDirection: "column",
218
222
  children: [/* @__PURE__ */ jsxs(Text, { children: [
@@ -222,7 +226,7 @@ const FailureItem = ({ failure }) => {
222
226
  children: statusGlyph(failureStatus(failure), glyphs)
223
227
  }),
224
228
  " ",
225
- failurePath(failure),
229
+ failurePath(failure, link),
226
230
  failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
227
231
  ...inkStyle(VitestAgentTokens.classification),
228
232
  children: [
@@ -290,7 +294,7 @@ const samePath = (a, b) => {
290
294
  * run picture — all rows in discovery order, with running rows capped
291
295
  * at MAX_LIVE_RUNNING_ROWS and finished rows always shown.
292
296
  */
293
- const liveRegion = (state, shape, nowMs, frame, glyphs) => {
297
+ const liveRegion = (state, shape, nowMs, frame, glyphs, link) => {
294
298
  const groups = groupByProject(state);
295
299
  const timedOut = state.phase === "timed-out";
296
300
  const finished = state.phase === "finished" || state.phase === "timed-out";
@@ -471,7 +475,7 @@ const liveRegion = (state, shape, nowMs, frame, glyphs) => {
471
475
  return /* @__PURE__ */ jsxs(Fragment, { children: [
472
476
  /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
473
477
  bold: true,
474
- children: sole.modulePath
478
+ children: link({ file: sole.modulePath }, sole.modulePath)
475
479
  }), /* @__PURE__ */ jsxs(Text, {
476
480
  dimColor: true,
477
481
  children: [
@@ -505,21 +509,35 @@ const liveRegion = (state, shape, nowMs, frame, glyphs) => {
505
509
  const StreamApp = ({ state, frameIndex, nowMs }) => {
506
510
  const now = nowMs ?? Date.now();
507
511
  const glyphs = useGlyphs();
512
+ const link = useFileLink();
508
513
  const frame = spinnerFrame(frameIndex, glyphs);
509
514
  const projects = groupByProject(state).map((g) => buildProjectSummary(g.name, sumCounts(g.modules)));
510
515
  const shape = classifyRunShape(state, projects);
511
- const liveContent = liveRegion(state, shape, now, frame, glyphs);
516
+ const liveContent = liveRegion(state, shape, now, frame, glyphs, link);
512
517
  const stray = state.strayOutput !== void 0 ? formatStrayOutputNote(state.strayOutput) : null;
518
+ const leakLines = state.consoleLeaks !== void 0 ? formatConsoleLeaksNote(state.consoleLeaks) : [];
519
+ const leaks = leakLines.length > 0 ? leakLines : null;
513
520
  return /* @__PURE__ */ jsxs(Box, {
514
521
  flexDirection: "column",
515
- children: [liveContent, stray !== null ? /* @__PURE__ */ jsx(Box, {
516
- flexDirection: "column",
517
- marginTop: 1,
518
- children: stray.map((line, i) => /* @__PURE__ */ jsx(Text, {
519
- ...i === 0 ? statusInkStyle("warning") : {},
520
- children: line
521
- }, `stray:${i}`))
522
- }) : null]
522
+ children: [
523
+ liveContent,
524
+ stray !== null ? /* @__PURE__ */ jsx(Box, {
525
+ flexDirection: "column",
526
+ marginTop: 1,
527
+ children: stray.map((line, i) => /* @__PURE__ */ jsx(Text, {
528
+ ...i === 0 ? statusInkStyle("warning") : {},
529
+ children: line
530
+ }, `stray:${i}`))
531
+ }) : null,
532
+ leaks !== null ? /* @__PURE__ */ jsx(Box, {
533
+ flexDirection: "column",
534
+ marginTop: 1,
535
+ children: leaks.map((line, i) => /* @__PURE__ */ jsx(Text, {
536
+ ...i === 0 ? statusInkStyle("warning") : {},
537
+ children: line
538
+ }, `leaks:${i}`))
539
+ }) : null
540
+ ]
523
541
  });
524
542
  };
525
543
 
@@ -1,4 +1,4 @@
1
- import { dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor } from "../dispatcher/dispatch.js";
1
+ import { consoleLeaksNoteFor, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor, suggestedActionsNoteFor } from "../dispatcher/dispatch.js";
2
2
  import { renderAgentStringAsInk } from "./ink-helpers.js";
3
3
  import { Box, Text } from "ink";
4
4
  import { createElement } from "react";
@@ -52,9 +52,16 @@ const dispatchInk = (inputs, opts) => {
52
52
  if (cell === void 0) return null;
53
53
  const element = cell(inputs, opts);
54
54
  const note = scopedCoverageNoteFor(inputs);
55
- const stray = strayOutputNoteFor(inputs);
56
- if (note === null && stray === null) return element;
57
- return createElement(Box, { flexDirection: "column" }, element, note !== null ? createElement(Text, null, note) : null, stray !== null ? createElement(Box, { marginTop: 1 }, createElement(Text, null, stray)) : null);
55
+ const trailers = [
56
+ suggestedActionsNoteFor(inputs),
57
+ strayOutputNoteFor(inputs),
58
+ consoleLeaksNoteFor(inputs)
59
+ ].filter((trailer) => trailer !== null);
60
+ if (note === null && trailers.length === 0) return element;
61
+ return createElement(Box, { flexDirection: "column" }, element, note !== null ? createElement(Text, null, note) : null, ...trailers.map((trailer, index) => createElement(Box, {
62
+ key: `trailer:${index}`,
63
+ marginTop: 1
64
+ }, createElement(Text, null, trailer))));
58
65
  };
59
66
 
60
67
  export { dispatchInk, inkDispatcherTable };
package/ink.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- import { FC, ReactElement } from "react";
2
+ import { FC, ReactElement, ReactNode } from "react";
3
3
  /**
4
4
  * Per-file coverage data including uncovered line ranges.
5
5
  * @public
@@ -7,10 +7,10 @@ import { FC, ReactElement } from "react";
7
7
  declare const FileCoverageReport: Schema.Struct<{
8
8
  readonly file: Schema.String;
9
9
  readonly summary: Schema.Struct<{
10
- readonly statements: Schema.Number;
11
- readonly branches: Schema.Number;
12
- readonly functions: Schema.Number;
13
- readonly lines: Schema.Number;
10
+ readonly statements: Schema.Finite;
11
+ readonly branches: Schema.Finite;
12
+ readonly functions: Schema.Finite;
13
+ readonly lines: Schema.Finite;
14
14
  }>;
15
15
  readonly uncoveredLines: Schema.String;
16
16
  }>;
@@ -107,24 +107,24 @@ type FailureRecord = typeof FailureRecord.Type;
107
107
  */
108
108
  declare const CoverageRenderState: Schema.Struct<{
109
109
  readonly metrics: Schema.Struct<{
110
- readonly statements: Schema.Number;
111
- readonly branches: Schema.Number;
112
- readonly functions: Schema.Number;
113
- readonly lines: Schema.Number;
110
+ readonly statements: Schema.Finite;
111
+ readonly branches: Schema.Finite;
112
+ readonly functions: Schema.Finite;
113
+ readonly lines: Schema.Finite;
114
114
  }>;
115
115
  readonly thresholds: Schema.Struct<{
116
- readonly lines: Schema.optional<Schema.Number>;
117
- readonly functions: Schema.optional<Schema.Number>;
118
- readonly branches: Schema.optional<Schema.Number>;
119
- readonly statements: Schema.optional<Schema.Number>;
116
+ readonly lines: Schema.optional<Schema.Finite>;
117
+ readonly functions: Schema.optional<Schema.Finite>;
118
+ readonly branches: Schema.optional<Schema.Finite>;
119
+ readonly statements: Schema.optional<Schema.Finite>;
120
120
  }>;
121
121
  readonly gaps: Schema.$Array<Schema.Struct<{
122
122
  readonly file: Schema.String;
123
123
  readonly missing: Schema.Struct<{
124
- readonly statements: Schema.Number;
125
- readonly branches: Schema.Number;
126
- readonly functions: Schema.Number;
127
- readonly lines: Schema.Number;
124
+ readonly statements: Schema.Finite;
125
+ readonly branches: Schema.Finite;
126
+ readonly functions: Schema.Finite;
127
+ readonly lines: Schema.Finite;
128
128
  }>;
129
129
  readonly uncoveredLines: Schema.optional<Schema.String>;
130
130
  }>>;
@@ -139,16 +139,16 @@ declare const CoverageRenderState: Schema.Struct<{
139
139
  readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
140
140
  readonly pattern: Schema.String;
141
141
  readonly summary: Schema.Struct<{
142
- readonly statements: Schema.Number;
143
- readonly branches: Schema.Number;
144
- readonly functions: Schema.Number;
145
- readonly lines: Schema.Number;
142
+ readonly statements: Schema.Finite;
143
+ readonly branches: Schema.Finite;
144
+ readonly functions: Schema.Finite;
145
+ readonly lines: Schema.Finite;
146
146
  }>;
147
147
  readonly thresholds: Schema.Struct<{
148
- readonly lines: Schema.optional<Schema.Number>;
149
- readonly functions: Schema.optional<Schema.Number>;
150
- readonly branches: Schema.optional<Schema.Number>;
151
- readonly statements: Schema.optional<Schema.Number>;
148
+ readonly lines: Schema.optional<Schema.Finite>;
149
+ readonly functions: Schema.optional<Schema.Finite>;
150
+ readonly branches: Schema.optional<Schema.Finite>;
151
+ readonly statements: Schema.optional<Schema.Finite>;
152
152
  }>;
153
153
  }>>>;
154
154
  }>;
@@ -229,24 +229,24 @@ declare const RenderState: Schema.Struct<{
229
229
  }>;
230
230
  readonly coverage: Schema.NullOr<Schema.Struct<{
231
231
  readonly metrics: Schema.Struct<{
232
- readonly statements: Schema.Number;
233
- readonly branches: Schema.Number;
234
- readonly functions: Schema.Number;
235
- readonly lines: Schema.Number;
232
+ readonly statements: Schema.Finite;
233
+ readonly branches: Schema.Finite;
234
+ readonly functions: Schema.Finite;
235
+ readonly lines: Schema.Finite;
236
236
  }>;
237
237
  readonly thresholds: Schema.Struct<{
238
- readonly lines: Schema.optional<Schema.Number>;
239
- readonly functions: Schema.optional<Schema.Number>;
240
- readonly branches: Schema.optional<Schema.Number>;
241
- readonly statements: Schema.optional<Schema.Number>;
238
+ readonly lines: Schema.optional<Schema.Finite>;
239
+ readonly functions: Schema.optional<Schema.Finite>;
240
+ readonly branches: Schema.optional<Schema.Finite>;
241
+ readonly statements: Schema.optional<Schema.Finite>;
242
242
  }>;
243
243
  readonly gaps: Schema.$Array<Schema.Struct<{
244
244
  readonly file: Schema.String;
245
245
  readonly missing: Schema.Struct<{
246
- readonly statements: Schema.Number;
247
- readonly branches: Schema.Number;
248
- readonly functions: Schema.Number;
249
- readonly lines: Schema.Number;
246
+ readonly statements: Schema.Finite;
247
+ readonly branches: Schema.Finite;
248
+ readonly functions: Schema.Finite;
249
+ readonly lines: Schema.Finite;
250
250
  }>;
251
251
  readonly uncoveredLines: Schema.optional<Schema.String>;
252
252
  }>>;
@@ -261,16 +261,16 @@ declare const RenderState: Schema.Struct<{
261
261
  readonly globShortfalls: Schema.optional<Schema.$Array<Schema.Struct<{
262
262
  readonly pattern: Schema.String;
263
263
  readonly summary: Schema.Struct<{
264
- readonly statements: Schema.Number;
265
- readonly branches: Schema.Number;
266
- readonly functions: Schema.Number;
267
- readonly lines: Schema.Number;
264
+ readonly statements: Schema.Finite;
265
+ readonly branches: Schema.Finite;
266
+ readonly functions: Schema.Finite;
267
+ readonly lines: Schema.Finite;
268
268
  }>;
269
269
  readonly thresholds: Schema.Struct<{
270
- readonly lines: Schema.optional<Schema.Number>;
271
- readonly functions: Schema.optional<Schema.Number>;
272
- readonly branches: Schema.optional<Schema.Number>;
273
- readonly statements: Schema.optional<Schema.Number>;
270
+ readonly lines: Schema.optional<Schema.Finite>;
271
+ readonly functions: Schema.optional<Schema.Finite>;
272
+ readonly branches: Schema.optional<Schema.Finite>;
273
+ readonly statements: Schema.optional<Schema.Finite>;
274
274
  }>;
275
275
  }>>>;
276
276
  }>>;
@@ -327,15 +327,35 @@ declare const RenderState: Schema.Struct<{
327
327
  * wrote nothing stray.
328
328
  */
329
329
  readonly strayOutput: Schema.optional<Schema.Struct<{
330
- readonly total: Schema.Number;
331
- readonly stdout: Schema.Number;
332
- readonly stderr: Schema.Number;
333
- readonly bytes: Schema.Number;
330
+ readonly total: Schema.Finite;
331
+ readonly stdout: Schema.Finite;
332
+ readonly stderr: Schema.Finite;
333
+ readonly bytes: Schema.Finite;
334
334
  readonly samples: Schema.$Array<Schema.Struct<{
335
335
  readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
336
336
  readonly text: Schema.String;
337
337
  }>>;
338
338
  }>>;
339
+ /**
340
+ * `console.*` output Vitest captured from tests that did not fail, folded
341
+ * from `RunFinished.consoleLeaks`. Absent when the run logged nothing
342
+ * (issue #544).
343
+ */
344
+ readonly consoleLeaks: Schema.optional<Schema.Struct<{
345
+ readonly total: Schema.Finite;
346
+ readonly byFile: Schema.$Array<Schema.Struct<{
347
+ readonly file: Schema.String;
348
+ readonly stdout: Schema.Finite;
349
+ readonly stderr: Schema.Finite;
350
+ readonly tests: Schema.optional<Schema.$Array<Schema.String>>;
351
+ readonly sample: Schema.optional<Schema.String>;
352
+ }>>;
353
+ readonly truncated: Schema.optional<Schema.Boolean>;
354
+ readonly fromFailingTests: Schema.optional<Schema.Struct<{
355
+ readonly total: Schema.Finite;
356
+ readonly files: Schema.Finite;
357
+ }>>;
358
+ }>>;
339
359
  }>;
340
360
  /** @public */
341
361
  type RenderState = typeof RenderState.Type;
@@ -438,27 +458,41 @@ interface DispatchInputs {
438
458
  readonly runCommand: string | null;
439
459
  }
440
460
  /**
441
- * Render-time options threaded into each cell from the resolved
442
- * `ReporterKit`. These are concerns the cell needs to format its
443
- * output but that do not belong on the state-derived
444
- * {@link DispatchInputs} struct.
461
+ * Where a file link points: a path, with an optional line and column.
445
462
  *
446
- * The plugin builds this once per run from the kit and passes it into
463
+ * The file half of `@effected/cli`'s `LinkTarget`, spelled structurally so
464
+ * this platform-free contract names no kit type. A relative path resolves
465
+ * against the working directory the kit's `CliLinks` layer was built with.
466
+ * @public
467
+ */
468
+ interface FileLinkTarget {
469
+ readonly file: string;
470
+ readonly line?: number;
471
+ readonly col?: number;
472
+ }
473
+ /**
474
+ * Render-time options threaded into each cell. These are concerns the
475
+ * cell needs to format its output but that do not belong on the
476
+ * state-derived {@link DispatchInputs} struct.
477
+ *
478
+ * The caller builds this once per render and passes it into
447
479
  * `dispatch(inputs, opts)`. Cells destructure only what they consume.
448
480
  * @public
449
481
  */
450
482
  interface CellOptions {
451
483
  /**
452
- * Resolved value of the `NO_COLOR` env var. Cells gate ANSI escape
453
- * sequences and OSC-8 hyperlinks on this.
484
+ * Writes a file path as a link for the audience and terminal the output
485
+ * is for: the label wrapped in an OSC 8 hyperlink when the reader can
486
+ * follow it, the label unchanged otherwise. The kit decides both halves
487
+ * (`@effected/cli`'s `CliLinks.linker`, `Render.context("stdout").link`):
488
+ * `vscode://file/<path>:<line>:<col>` under VS Code, `file://` otherwise,
489
+ * never for an agent audience, and nothing on a terminal that cannot
490
+ * render hyperlinks (a pipe, tmux, GNU screen, `NO_COLOR`).
491
+ *
492
+ * Omitted, every path is its label alone, which is what an agent reads;
493
+ * a cell therefore never writes an escape sequence of its own.
454
494
  */
455
- readonly noColor: boolean;
456
- /**
457
- * Pre-bound OSC-8 hyperlink helper. The plugin has already decided
458
- * whether OSC-8 should be enabled (`target=stdout`, `!noColor`) so
459
- * cells can call this directly without re-consulting the environment.
460
- */
461
- readonly osc8: (url: string, label: string) => string;
495
+ readonly link?: (target: FileLinkTarget, label: string) => string;
462
496
  }
463
497
  /**
464
498
  * Props for the `CountColumns` component.
@@ -574,6 +608,37 @@ interface FailuresSectionProps {
574
608
  * @public
575
609
  */
576
610
  export declare const FailuresSection: FC<FailuresSectionProps>;
611
+ /**
612
+ * The link writer: a file target and its label in, the label (linked when
613
+ * the reader can follow it) out. The same shape as `CellOptions.link`.
614
+ *
615
+ * @public
616
+ */
617
+ type FileLink = NonNullable<CellOptions["link"]>;
618
+ /**
619
+ * Props for {@link FileLinkProvider}.
620
+ *
621
+ * @public
622
+ */
623
+ interface FileLinkProviderProps {
624
+ /** The writer; omitted, paths are their labels alone. */
625
+ readonly link?: FileLink | undefined;
626
+ /** The tree. */
627
+ readonly children?: ReactNode;
628
+ }
629
+ /**
630
+ * Provide the file-link writer to an Ink tree.
631
+ *
632
+ * @public
633
+ */
634
+ export declare const FileLinkProvider: FC<FileLinkProviderProps>;
635
+ /**
636
+ * The file-link writer of the tree: the nearest {@link FileLinkProvider}'s,
637
+ * or the label alone.
638
+ *
639
+ * @public
640
+ */
641
+ export declare const useFileLink: () => FileLink;
577
642
  /**
578
643
  * Props for the `ModuleHeader` component.
579
644
  *
@@ -753,5 +818,5 @@ interface TrendLineProps {
753
818
  * @public
754
819
  */
755
820
  export declare const TrendLine: FC<TrendLineProps>;
756
- export type { CellOptions, CountColumnsProps, CoverageBlockProps, CoverageRenderState, DispatchInputs, FailureRecord, FailureSectionProps, FailuresSectionProps, FileCoverageReport, InkCellFn, ModuleHeaderProps, ModuleRecord, ProjectRowProps, ProjectSummary, RenderState, RunOutcome, RunShape, StatusIconKind, StatusIconProps, StreamAppProps, SuggestedActionRecord, SuggestedActionsProps, TagColumnsProps, TestRecord, TestRowProps, TrendLineProps, TrendSummary };
821
+ export type { CellOptions, CountColumnsProps, CoverageBlockProps, CoverageRenderState, DispatchInputs, FailureRecord, FailureSectionProps, FailuresSectionProps, FileCoverageReport, FileLink, FileLinkProviderProps, FileLinkTarget, InkCellFn, ModuleHeaderProps, ModuleRecord, ProjectRowProps, ProjectSummary, RenderState, RunOutcome, RunShape, StatusIconKind, StatusIconProps, StreamAppProps, SuggestedActionRecord, SuggestedActionsProps, TagColumnsProps, TestRecord, TestRowProps, TrendLineProps, TrendSummary };
757
822
  //# sourceMappingURL=ink.d.ts.map
package/ink.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CountColumns, DURATION_CELL_WIDTH } from "./ink/CountColumns.js";
2
+ import { FileLinkProvider, useFileLink } from "./ink/FileLink.js";
2
3
  import { CoverageBlock } from "./ink/CoverageBlock.js";
3
4
  import { dispatchInk, inkDispatcherTable } from "./ink/dispatch-ink.js";
4
5
  import { FailureSection } from "./ink/FailureSection.js";
@@ -12,4 +13,4 @@ import { TrendLine } from "./ink/TrendLine.js";
12
13
  import { StreamApp } from "./ink/StreamApp.js";
13
14
  import { SuggestedActions } from "./ink/SuggestedActions.js";
14
15
 
15
- export { CountColumns, CoverageBlock, DURATION_CELL_WIDTH, FailureSection, FailuresSection, ModuleHeader, ProjectRow, StatusIcon, StreamApp, SuggestedActions, TagColumns, TestRow, TrendLine, dispatchInk, inkDispatcherTable, tagUnion };
16
+ export { CountColumns, CoverageBlock, DURATION_CELL_WIDTH, FailureSection, FailuresSection, FileLinkProvider, ModuleHeader, ProjectRow, StatusIcon, StreamApp, SuggestedActions, TagColumns, TestRow, TrendLine, dispatchInk, inkDispatcherTable, tagUnion, useFileLink };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/ui",
3
- "version": "5.0.8",
3
+ "version": "5.1.0",
4
4
  "private": false,
5
5
  "description": "Shared event-sourced renderer for vitest-agent. React Ink view for humans, plain-string view for agents, both fed by the same reducer over a RunEvent stream.",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "./package.json": "./package.json"
45
45
  },
46
46
  "dependencies": {
47
- "@vitest-agent/sdk": "7.0.5",
47
+ "@vitest-agent/sdk": "8.0.0",
48
48
  "effect": "^4.0.6"
49
49
  },
50
50
  "peerDependencies": {
package/reducer.js CHANGED
@@ -225,7 +225,8 @@ const reduceRenderState = (state, event) => {
225
225
  },
226
226
  ...e.collectedModules !== void 0 && { collectedModules: e.collectedModules },
227
227
  ...e.unhandledErrors !== void 0 && { unhandledErrors: e.unhandledErrors },
228
- ...e.strayOutput !== void 0 && { strayOutput: e.strayOutput }
228
+ ...e.strayOutput !== void 0 && { strayOutput: e.strayOutput },
229
+ ...e.consoleLeaks !== void 0 && { consoleLeaks: e.consoleLeaks }
229
230
  }),
230
231
  RunTimedOut: () => ({
231
232
  ...state,
package/render-agent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { formatTotalsLine } from "./counts.js";
2
- import { formatFailure as formatFailure$1, formatGlobShortfallLines } from "./dispatcher/helpers.js";
3
- import { formatStrayOutputNote } from "@vitest-agent/sdk";
2
+ import { formatFailure as formatFailure$1, formatGlobShortfallLines, formatSuggestedActionLines } from "./dispatcher/helpers.js";
3
+ import { formatConsoleLeaksNote, formatStrayOutputNote } from "@vitest-agent/sdk";
4
4
  import { Fmt } from "@effected/cli";
5
5
 
6
6
  // src/render-agent.ts
@@ -16,11 +16,6 @@ import { Fmt } from "@effected/cli";
16
16
  */
17
17
  const DEFAULT_WIDTH = 80;
18
18
  const DEFAULT_MAX_GAPS = 3;
19
- const severityLabel = {
20
- info: "info",
21
- warn: "warn",
22
- blocker: "blocker"
23
- };
24
19
  const formatHeader = (state) => formatTotalsLine({
25
20
  label: "Tests",
26
21
  ...state.totals,
@@ -106,14 +101,13 @@ const formatCoverageSection = (state, maxGaps) => {
106
101
  return lines.join("\n");
107
102
  };
108
103
  const formatActionsSection = (state) => {
109
- if (state.suggestedActions.length === 0) return null;
110
- const lines = ["Actions:"];
111
- for (const a of state.suggestedActions) lines.push(formatAction(a));
112
- return lines.join("\n");
104
+ const lines = formatSuggestedActionLines(state.suggestedActions);
105
+ return lines.length === 0 ? null : lines.join("\n");
113
106
  };
114
- const formatAction = (a) => {
115
- const tool = a.targetTool !== void 0 ? ` (tool: ${a.targetTool})` : "";
116
- return `- ${severityLabel[a.severity]}: ${a.title}${tool}\n ${a.detail}`;
107
+ const formatConsoleLeaksSection = (state) => {
108
+ if (state.consoleLeaks === void 0) return null;
109
+ const lines = formatConsoleLeaksNote(state.consoleLeaks);
110
+ return lines.length === 0 ? null : lines.join("\n");
117
111
  };
118
112
  /**
119
113
  * Project the reduced `RenderState` into a single string suitable
@@ -143,6 +137,8 @@ const renderAgent = (state, options = {}) => {
143
137
  const actions = formatActionsSection(state);
144
138
  if (actions !== null) sections.push(actions);
145
139
  if (state.strayOutput !== void 0) sections.push(formatStrayOutputNote(state.strayOutput).join("\n"));
140
+ const leaks = formatConsoleLeaksSection(state);
141
+ if (leaks !== null) sections.push(leaks);
146
142
  return `${sections.join("\n\n")}\n`;
147
143
  };
148
144
 
package/synthesize.js CHANGED
@@ -387,7 +387,8 @@ const synthesizeFromAgentReport = (report, options = {}) => {
387
387
  ...totalTimeoutCount > 0 && { timeoutCount: totalTimeoutCount },
388
388
  ...report.summary.modules !== void 0 && { collectedModules: report.summary.modules },
389
389
  ...report.unhandledErrors.length > 0 && { unhandledErrors: report.unhandledErrors },
390
- ...report.strayOutput !== void 0 && { strayOutput: report.strayOutput }
390
+ ...report.strayOutput !== void 0 && { strayOutput: report.strayOutput },
391
+ ...report.consoleLeaks !== void 0 && { consoleLeaks: report.consoleLeaks }
391
392
  });
392
393
  return events;
393
394
  };