@vitest-agent/ui 5.0.9 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { formatFailure, soleModulePath } from "../helpers.js";
4
4
 
5
5
  // src/dispatcher/cells/single-file-fail.ts
6
6
  const DEFAULT_WIDTH = 80;
7
- const renderAgent = (inputs) => {
7
+ const renderAgent = (inputs, opts) => {
8
8
  const modulePath = soleModulePath(inputs.state);
9
9
  if (modulePath === void 0) return "";
10
10
  const { passCount, failCount, skipCount, timeoutCount, durationMs } = inputs.state.totals;
@@ -15,7 +15,7 @@ const renderAgent = (inputs) => {
15
15
  const sections = [`${modulePath}: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`];
16
16
  if (inputs.state.failures.length > 0) {
17
17
  sections.push("Failures:");
18
- for (const failure of inputs.state.failures) sections.push(...formatFailure(failure, DEFAULT_WIDTH));
18
+ for (const failure of inputs.state.failures) sections.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
19
19
  }
20
20
  return `${sections.join("\n")}\n${buildFooter(inputs)}`;
21
21
  };
@@ -3,11 +3,11 @@ import { formatFailure, formatTotals } from "../helpers.js";
3
3
 
4
4
  // src/dispatcher/cells/single-project-fail.ts
5
5
  const DEFAULT_WIDTH = 80;
6
- const renderAgent = (inputs) => {
6
+ const renderAgent = (inputs, opts) => {
7
7
  const sections = [[formatTotals(inputs.state)]];
8
8
  if (inputs.state.failures.length > 0) {
9
9
  const failureLines = ["Failures:"];
10
- for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH));
10
+ for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
11
11
  sections.push(failureLines);
12
12
  }
13
13
  const moduleLines = ["Modules:"];
@@ -3,7 +3,7 @@ import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, forma
3
3
 
4
4
  // src/dispatcher/cells/single-project-threshold.ts
5
5
  const BELOW_TARGET_LIMIT = 5;
6
- const renderAgent = (inputs) => {
6
+ const renderAgent = (inputs, opts) => {
7
7
  const sections = [[formatTotals(inputs.state)]];
8
8
  const coverage = formatCoverageSummaryLines(inputs.state);
9
9
  const trend = formatTrendLine(inputs.trend);
@@ -11,7 +11,7 @@ const renderAgent = (inputs) => {
11
11
  summaryLines.push(...coverage);
12
12
  if (trend !== null) summaryLines.push(trend);
13
13
  if (summaryLines.length > 0) sections.push(summaryLines);
14
- const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
14
+ const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT, opts.link);
15
15
  if (below.length > 0) sections.push([...below]);
16
16
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
17
17
  };
@@ -5,13 +5,13 @@ import { formatFailure, formatTestName, soleTest } from "../helpers.js";
5
5
 
6
6
  // src/dispatcher/cells/single-test-fail.ts
7
7
  const DEFAULT_WIDTH = 80;
8
- const renderAgent = (inputs) => {
8
+ const renderAgent = (inputs, opts) => {
9
9
  const test = soleTest(inputs.state);
10
10
  if (test === void 0) return "";
11
11
  const duration = test.durationMs ?? 0;
12
12
  const header = `${statusGlyph("failure")} ${formatTestName(test)} (${formatDisplayDuration(duration)})`;
13
13
  const failure = inputs.state.failures[0];
14
- return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
14
+ return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH, opts.link).join("\n")}\n`}${buildFooter(inputs)}`;
15
15
  };
16
16
  const renderSingleTestFail = { agent: renderAgent };
17
17
 
@@ -3,13 +3,13 @@ import { formatFailure, formatProjectsTable, formatTrendLine, formatWorkspaceTot
3
3
 
4
4
  // src/dispatcher/cells/workspace-fail.ts
5
5
  const DEFAULT_WIDTH = 80;
6
- const renderAgent = (inputs) => {
6
+ const renderAgent = (inputs, opts) => {
7
7
  const sections = [];
8
8
  const projects = formatProjectsTable(inputs.projects);
9
9
  if (projects.length > 0) sections.push([...projects]);
10
10
  if (inputs.state.failures.length > 0) {
11
11
  const failureLines = ["Failures:"];
12
- for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH));
12
+ for (const failure of inputs.state.failures) failureLines.push(...formatFailure(failure, DEFAULT_WIDTH, opts.link));
13
13
  sections.push(failureLines);
14
14
  }
15
15
  const trend = formatTrendLine(inputs.trend);
@@ -3,7 +3,7 @@ import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable
3
3
 
4
4
  // src/dispatcher/cells/workspace-threshold.ts
5
5
  const BELOW_TARGET_LIMIT = 10;
6
- const renderAgent = (inputs) => {
6
+ const renderAgent = (inputs, opts) => {
7
7
  const sections = [];
8
8
  const projects = formatProjectsTable(inputs.projects);
9
9
  if (projects.length > 0) sections.push([...projects]);
@@ -13,7 +13,7 @@ const renderAgent = (inputs) => {
13
13
  const trend = formatTrendLine(inputs.trend);
14
14
  if (trend !== null) summaryLines.push(trend);
15
15
  if (summaryLines.length > 0) sections.push(summaryLines);
16
- const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT);
16
+ const below = formatBelowTargetTable(inputs.belowTarget, BELOW_TARGET_LIMIT, opts.link);
17
17
  if (below.length > 0) sections.push([...below]);
18
18
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
19
19
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
@@ -1,3 +1,4 @@
1
+ import { formatSuggestedActionLines } from "./helpers.js";
1
2
  import { renderSingleFileFail } from "./cells/single-file-fail.js";
2
3
  import { renderSingleFilePass } from "./cells/single-file-pass.js";
3
4
  import { renderSingleFileThreshold } from "./cells/single-file-threshold.js";
@@ -10,7 +11,7 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
10
11
  import { renderWorkspaceFail } from "./cells/workspace-fail.js";
11
12
  import { renderWorkspacePass } from "./cells/workspace-pass.js";
12
13
  import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
13
- import { formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
14
+ import { formatConsoleLeaksNote, formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
14
15
 
15
16
  // src/dispatcher/dispatch.ts
16
17
  /**
@@ -55,8 +56,40 @@ const dispatch = (inputs, opts) => {
55
56
  const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
56
57
  const note = scopedCoverageNoteFor(inputs);
57
58
  const withNote = note !== null ? `${body}\n${note}` : body;
58
- const stray = strayOutputNoteFor(inputs);
59
- return stray !== null ? `${withNote}\n${stray}\n` : withNote;
59
+ const trailers = [
60
+ suggestedActionsNoteFor(inputs),
61
+ strayOutputNoteFor(inputs),
62
+ consoleLeaksNoteFor(inputs)
63
+ ].filter((trailer) => trailer !== null);
64
+ return trailers.length === 0 ? withNote : `${withNote}\n${trailers.join("\n\n")}\n`;
65
+ };
66
+ /**
67
+ * The suggested-actions list for the run, or `null` when there is nothing to
68
+ * suggest. `dispatch` and `dispatchInk` append it after the cell's output;
69
+ * cells never emit it themselves.
70
+ *
71
+ * @param inputs - the classified dispatch inputs
72
+ * @returns the list's lines joined, or `null`
73
+ * @internal
74
+ */
75
+ const suggestedActionsNoteFor = (inputs) => {
76
+ const lines = formatSuggestedActionLines(inputs.state.suggestedActions);
77
+ return lines.length === 0 ? null : lines.join("\n");
78
+ };
79
+ /**
80
+ * The console-leaks note for a run whose passing tests wrote `console.*`
81
+ * output, or `null` when none did. Appended after the cell's output like the
82
+ * stray-output note; cells never emit it themselves.
83
+ *
84
+ * @param inputs - the classified dispatch inputs
85
+ * @returns the note's lines joined, or `null`
86
+ * @internal
87
+ */
88
+ const consoleLeaksNoteFor = (inputs) => {
89
+ const leaks = inputs.state.consoleLeaks;
90
+ if (leaks === void 0) return null;
91
+ const lines = formatConsoleLeaksNote(leaks);
92
+ return lines.length === 0 ? null : lines.join("\n");
60
93
  };
61
94
  /**
62
95
  * The stray-output note for a run that wrote past Vitest's console capture,
@@ -88,4 +121,4 @@ const scopedCoverageNoteFor = (inputs) => {
88
121
  return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
89
122
  };
90
123
 
91
- export { dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor };
124
+ export { consoleLeaksNoteFor, dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor, suggestedActionsNoteFor };
@@ -57,23 +57,30 @@ const formatTestName = (test) => {
57
57
  * by default to keep the agent string compact.
58
58
  *
59
59
  * @remarks
60
- * A kit document rendered plain for the agent audience: one compact list
61
- * item whose title is verbatim (never wrapped), whose first message line is
62
- * a truncated `Doc.line`, and whose diff is `Doc.diffText` with `truncate`.
60
+ * A kit document rendered at colour `none` (so no paint, whatever the
61
+ * audience): one compact list item whose title line is never wrapped and
62
+ * opens with the test file as a `Doc.link` (written through `link`, so a
63
+ * human at a terminal with hyperlink support gets an OSC 8 link to the
64
+ * file and everyone else the path alone), whose first message line is a
65
+ * truncated `Doc.line`, and whose diff is `Doc.diffText` with `truncate`.
63
66
  * Each content line is cut to `width - 2` (floor 20) after the item indent.
64
67
  * The kit sanitizes the text, so a tab becomes a space and an escape
65
- * sequence is removed.
68
+ * sequence is removed — which is why the link is a `Doc.link` the renderer
69
+ * writes, never an escape pasted into the text.
66
70
  */
67
- const formatFailure = (f, width) => {
71
+ const formatFailure = (f, width, link = (_target, label) => label) => {
68
72
  const suite = f.suitePath.length > 0 ? `${f.suitePath.join(" > ")} > ` : "";
69
73
  const classification = f.classification !== null ? ` [${f.classification}]` : "";
70
- const parts = [Doc.verbatim(`${f.modulePath} > ${suite}${f.testName}${classification}`)];
74
+ const parts = [Doc.line([Doc.link({ file: f.modulePath }, f.modulePath, { suffix: false }), Doc.text(` > ${suite}${f.testName}${classification}`)], { wrap: false })];
71
75
  if (f.error?.message !== void 0) parts.push(Doc.line(f.error.message.split("\n", 1)[0] ?? "", { truncate: true }));
72
76
  if (f.error?.diff !== void 0) parts.push(Doc.diffText(f.error.diff, { truncate: true }));
73
- return Render.plain([Doc.list([Doc.section(void 0, parts)], { compact: true })], Render.contextOf({
74
- audience: "agent",
75
- width: Math.max(22, width)
76
- })).split("\n");
77
+ return Render.ansi([Doc.list([Doc.section(void 0, parts)], { compact: true })], {
78
+ ...Render.contextOf({
79
+ audience: "human",
80
+ width: Math.max(22, width)
81
+ }),
82
+ link: (target, label) => "file" in target ? link(target, label) : label
83
+ }).split("\n");
77
84
  };
78
85
  /**
79
86
  * The `Glob aggregates below threshold:` block — one
@@ -200,9 +207,10 @@ const TABLE_COL_FILE_MIN = 60;
200
207
  * (never narrower than {@link TABLE_COL_FILE_MIN}), so a printed path is
201
208
  * never truncated (issue #237 follow-up) — only the omitted rows (past
202
209
  * `limit`) are unaffected, since their width never entered the
203
- * calculation.
210
+ * calculation. Each path is written through `link` after the column is
211
+ * padded, so a hyperlink wraps the path alone and never the padding.
204
212
  */
205
- const formatBelowTargetTable = (belowTarget, limit) => {
213
+ const formatBelowTargetTable = (belowTarget, limit, link = (_target, label) => label) => {
206
214
  if (belowTarget.length === 0) return [];
207
215
  const top = belowTarget.slice(0, limit);
208
216
  const omitted = belowTarget.length - top.length;
@@ -213,7 +221,7 @@ const formatBelowTargetTable = (belowTarget, limit) => {
213
221
  buildTableHeader(fileColWidth),
214
222
  buildTableSeparator(fileColWidth)
215
223
  ];
216
- const rows = top.map((file) => buildTableRow(file, fileColWidth));
224
+ const rows = top.map((file) => buildTableRow(file, fileColWidth, link));
217
225
  const footer = [];
218
226
  if (omitted > 0) footer.push(`… ${omitted} more (use the test_coverage MCP tool for the full list)`);
219
227
  return [
@@ -228,8 +236,8 @@ const buildTableSeparator = (fileColWidth) => {
228
236
  const buildTableHeader = (fileColWidth) => {
229
237
  return ` ${"File".padEnd(fileColWidth - 1)}| % Stmts | % Branch| % Funcs | % Lines | Uncovered Line #s`;
230
238
  };
231
- const buildTableRow = (file, fileColWidth) => {
232
- return `${` ${file.file.padEnd(fileColWidth - 1)}`}|${pctCell(file.summary.statements)}|${pctCell(file.summary.branches)}|${pctCell(file.summary.functions)}|${pctCell(file.summary.lines)}|${` ${file.uncoveredLines}`}`;
239
+ const buildTableRow = (file, fileColWidth, link) => {
240
+ return `${` ${link({ file: file.file }, file.file)}${" ".repeat(Math.max(0, fileColWidth - 1 - file.file.length))}`}|${pctCell(file.summary.statements)}|${pctCell(file.summary.branches)}|${pctCell(file.summary.functions)}|${pctCell(file.summary.lines)}|${` ${file.uncoveredLines}`}`;
233
241
  };
234
242
  const pctCell = (n) => {
235
243
  const text = `${Math.round(n)}`;
@@ -238,5 +246,25 @@ const pctCell = (n) => {
238
246
  const right = pad - left;
239
247
  return `${" ".repeat(left + 1)}${text}${" ".repeat(right)}`;
240
248
  };
249
+ const SEVERITY_LABEL = {
250
+ info: "info",
251
+ warn: "warn",
252
+ blocker: "blocker"
253
+ };
254
+ /**
255
+ * The suggested-actions block as a compact list: `Actions:` then, per action,
256
+ * `- <severity>: <title> (tool: <targetTool>)` with the detail indented under
257
+ * it. Empty when there is nothing to suggest. Shared by `renderAgent` and the
258
+ * dispatcher, so the one-shot and cell paths print the same list (issue #544).
259
+ */
260
+ const formatSuggestedActionLines = (actions) => {
261
+ if (actions.length === 0) return [];
262
+ const lines = ["Actions:"];
263
+ for (const action of actions) {
264
+ const tool = action.targetTool !== void 0 ? ` (tool: ${action.targetTool})` : "";
265
+ lines.push(`- ${SEVERITY_LABEL[action.severity]}: ${action.title}${tool}`, ` ${action.detail}`);
266
+ }
267
+ return lines;
268
+ };
241
269
 
242
- export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
270
+ export { formatBelowTargetTable, formatCoverageJudgmentLine, formatCoverageSummaryLines, formatDisplayDuration, formatFailure, formatGlobShortfallLines, formatPercent, formatProjectRow, formatProjectsTable, formatSuggestedActionLines, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };