@vitest-agent/ui 4.1.0 → 5.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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dispatcher/cells/single-file-fail.js +1 -5
  3. package/dispatcher/cells/single-file-pass.js +1 -5
  4. package/dispatcher/cells/single-file-threshold.js +1 -5
  5. package/dispatcher/cells/single-project-fail.js +1 -5
  6. package/dispatcher/cells/single-project-pass.js +1 -5
  7. package/dispatcher/cells/single-project-threshold.js +1 -5
  8. package/dispatcher/cells/single-test-fail.js +1 -5
  9. package/dispatcher/cells/single-test-pass.js +1 -5
  10. package/dispatcher/cells/workspace-fail.js +1 -5
  11. package/dispatcher/cells/workspace-pass.js +1 -5
  12. package/dispatcher/cells/workspace-threshold.js +1 -5
  13. package/dispatcher/dispatch.js +20 -25
  14. package/index.d.ts +45 -316
  15. package/index.js +4 -17
  16. package/{render-ink → ink}/CountColumns.js +2 -2
  17. package/{render-ink → ink}/CoverageBlock.js +2 -2
  18. package/{render-ink → ink}/FailureSection.js +2 -2
  19. package/{render-ink → ink}/FailuresSection.js +2 -2
  20. package/{render-ink → ink}/ModuleHeader.js +1 -1
  21. package/{render-ink → ink}/ProjectRow.js +1 -1
  22. package/{render-ink → ink}/StatusIcon.js +2 -2
  23. package/{render-ink → ink}/StreamApp.js +14 -5
  24. package/{render-ink → ink}/SuggestedActions.js +2 -2
  25. package/{render-ink → ink}/TagColumns.js +1 -1
  26. package/{render-ink → ink}/TestRow.js +1 -1
  27. package/{render-ink → ink}/TrendLine.js +2 -2
  28. package/ink/dispatch-ink.js +61 -0
  29. package/{dispatcher → ink}/ink-helpers.js +1 -1
  30. package/ink.d.ts +789 -0
  31. package/ink.js +15 -0
  32. package/package.json +12 -7
  33. package/reducer.js +2 -1
  34. package/render-agent.js +2 -0
  35. package/{render-ink/spinner.js → spinner.js} +1 -1
  36. package/synthesize.js +2 -1
  37. package/render-ink/index.js +0 -15
package/README.md CHANGED
@@ -11,8 +11,8 @@ Pure rendering primitives for vitest-agent. Owns the `RunEvent` reducer, the sha
11
11
  ## Features
12
12
 
13
13
  - **Reducer** — pure `(state, event) => state` fold over the `RunEvent` discriminated union; `reduceRenderStateAll` for one-shot replay
14
- - **Dispatcher matrix** — `dispatch` and `dispatchInk` route a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
15
- - **Ink components** — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames
14
+ - **Dispatcher matrix** — `dispatch` routes a `DispatchInputs` to the matching `(RunShape, RunOutcome)` cell; `classifyRunShape` and `classifyOutcome` derive the coordinates
15
+ - **Ink components** (`@vitest-agent/ui/ink`) — `StreamApp` and the supporting primitives (`ModuleHeader`, `TestRow`, `CoverageBlock`, `TrendLine`, `FailureSection`, etc.) for live `stream`-mode frames, plus `dispatchInk` / `inkDispatcherTable`, the Ink half of the matrix. The package root never loads React or Ink; only this subpath does
16
16
  - **Synthesizers** — `synthesizeRunEvents` from live Vitest modules; `synthesizeFromAgentReport` from a stored `AgentReport`
17
17
  - **Footer builder** — `buildFooter` assembles the L1 MCP-tool-pointer footer; `dominantClassification` picks the most actionable failure class
18
18
 
@@ -24,7 +24,7 @@ npm install @vitest-agent/ui
24
24
  pnpm add @vitest-agent/ui
25
25
  ```
26
26
 
27
- React and Ink are required peers:
27
+ React and Ink are optional peers, needed only for the `@vitest-agent/ui/ink` subpath:
28
28
 
29
29
  ```bash
30
30
  npm install react ink
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
3
  import { formatFailure, soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
5
4
 
6
5
  //#region src/dispatcher/cells/single-file-fail.ts
7
6
  const DEFAULT_WIDTH = 80;
@@ -20,10 +19,7 @@ const renderAgent = (inputs) => {
20
19
  }
21
20
  return `${sections.join("\n")}\n${buildFooter(inputs)}`;
22
21
  };
23
- const renderSingleFileFail = {
24
- agent: renderAgent,
25
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
26
- };
22
+ const renderSingleFileFail = { agent: renderAgent };
27
23
 
28
24
  //#endregion
29
25
  export { renderSingleFileFail };
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
3
  import { soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
5
4
  import { Fmt } from "@effected/cli";
6
5
 
7
6
  //#region src/dispatcher/cells/single-file-pass.ts
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
11
10
  const { passCount, durationMs } = inputs.state.totals;
12
11
  return `${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})\n${buildFooter(inputs)}`;
13
12
  };
14
- const renderSingleFilePass = {
15
- agent: renderAgent,
16
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
17
- };
13
+ const renderSingleFilePass = { agent: renderAgent };
18
14
 
19
15
  //#endregion
20
16
  export { renderSingleFilePass };
@@ -1,7 +1,6 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatDisplayDuration } from "../../format-duration.js";
3
3
  import { formatCoverageSummaryLines, soleModulePath } from "../helpers.js";
4
- import { renderAgentStringAsInk } from "../ink-helpers.js";
5
4
  import { Fmt } from "@effected/cli";
6
5
 
7
6
  //#region src/dispatcher/cells/single-file-threshold.ts
@@ -13,10 +12,7 @@ const renderAgent = (inputs) => {
13
12
  lines.push(...formatCoverageSummaryLines(inputs.state));
14
13
  return `${lines.join("\n")}\n${buildFooter(inputs)}`;
15
14
  };
16
- const renderSingleFileThreshold = {
17
- agent: renderAgent,
18
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
19
- };
15
+ const renderSingleFileThreshold = { agent: renderAgent };
20
16
 
21
17
  //#endregion
22
18
  export { renderSingleFileThreshold };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatFailure, formatTotals } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/single-project-fail.ts
6
5
  const DEFAULT_WIDTH = 80;
@@ -25,10 +24,7 @@ const renderAgent = (inputs) => {
25
24
  if (moduleLines.length > 1) sections.push(moduleLines);
26
25
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
27
26
  };
28
- const renderSingleProjectFail = {
29
- agent: renderAgent,
30
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
31
- };
27
+ const renderSingleProjectFail = { agent: renderAgent };
32
28
 
33
29
  //#endregion
34
30
  export { renderSingleProjectFail };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatTotals } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
  import { Fmt } from "@effected/cli";
5
4
 
6
5
  //#region src/dispatcher/cells/single-project-pass.ts
@@ -10,10 +9,7 @@ const renderAgent = (inputs) => {
10
9
  const collected = inputs.state.collectedModules ?? inputs.state.moduleOrder.length;
11
10
  return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${Fmt.plural(collected, "module")} all-passed.`]).join("\n\n")}\n${buildFooter(inputs)}`;
12
11
  };
13
- const renderSingleProjectPass = {
14
- agent: renderAgent,
15
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
16
- };
12
+ const renderSingleProjectPass = { agent: renderAgent };
17
13
 
18
14
  //#endregion
19
15
  export { renderSingleProjectPass };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatBelowTargetTable, formatCoverageSummaryLines, formatTotals, formatTrendLine } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/single-project-threshold.ts
6
5
  const BELOW_TARGET_LIMIT = 5;
@@ -16,10 +15,7 @@ const renderAgent = (inputs) => {
16
15
  if (below.length > 0) sections.push([...below]);
17
16
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
18
17
  };
19
- const renderSingleProjectThreshold = {
20
- agent: renderAgent,
21
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
22
- };
18
+ const renderSingleProjectThreshold = { agent: renderAgent };
23
19
 
24
20
  //#endregion
25
21
  export { renderSingleProjectThreshold };
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
2
2
  import { statusGlyph } from "../../theme.js";
3
3
  import { formatDisplayDuration } from "../../format-duration.js";
4
4
  import { formatFailure, formatTestName, soleTest } from "../helpers.js";
5
- import { renderAgentStringAsInk } from "../ink-helpers.js";
6
5
 
7
6
  //#region src/dispatcher/cells/single-test-fail.ts
8
7
  const DEFAULT_WIDTH = 80;
@@ -14,10 +13,7 @@ const renderAgent = (inputs) => {
14
13
  const failure = inputs.state.failures[0];
15
14
  return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
16
15
  };
17
- const renderSingleTestFail = {
18
- agent: renderAgent,
19
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
20
- };
16
+ const renderSingleTestFail = { agent: renderAgent };
21
17
 
22
18
  //#endregion
23
19
  export { renderSingleTestFail };
@@ -2,7 +2,6 @@ import { buildFooter } from "../footer.js";
2
2
  import { statusGlyph } from "../../theme.js";
3
3
  import { formatDisplayDuration } from "../../format-duration.js";
4
4
  import { formatTestName, soleTest } from "../helpers.js";
5
- import { renderAgentStringAsInk } from "../ink-helpers.js";
6
5
 
7
6
  //#region src/dispatcher/cells/single-test-pass.ts
8
7
  const renderAgent = (inputs) => {
@@ -11,10 +10,7 @@ const renderAgent = (inputs) => {
11
10
  const duration = test.durationMs ?? 0;
12
11
  return `${statusGlyph("success")} ${formatTestName(test)} (${formatDisplayDuration(duration)})\n${buildFooter(inputs)}`;
13
12
  };
14
- const renderSingleTestPass = {
15
- agent: renderAgent,
16
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
17
- };
13
+ const renderSingleTestPass = { agent: renderAgent };
18
14
 
19
15
  //#endregion
20
16
  export { renderSingleTestPass };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatFailure, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-fail.ts
6
5
  const DEFAULT_WIDTH = 80;
@@ -18,10 +17,7 @@ const renderAgent = (inputs) => {
18
17
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
19
18
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
20
19
  };
21
- const renderWorkspaceFail = {
22
- agent: renderAgent,
23
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
24
- };
20
+ const renderWorkspaceFail = { agent: renderAgent };
25
21
 
26
22
  //#endregion
27
23
  export { renderWorkspaceFail };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatCoverageJudgmentLine, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-pass.ts
6
5
  const renderAgent = (inputs) => {
@@ -16,10 +15,7 @@ const renderAgent = (inputs) => {
16
15
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
17
16
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
18
17
  };
19
- const renderWorkspacePass = {
20
- agent: renderAgent,
21
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
22
- };
18
+ const renderWorkspacePass = { agent: renderAgent };
23
19
 
24
20
  //#endregion
25
21
  export { renderWorkspacePass };
@@ -1,6 +1,5 @@
1
1
  import { buildFooter } from "../footer.js";
2
2
  import { formatBelowTargetTable, formatCoverageSummaryLines, formatProjectsTable, formatTrendLine, formatWorkspaceTotal } from "../helpers.js";
3
- import { renderAgentStringAsInk } from "../ink-helpers.js";
4
3
 
5
4
  //#region src/dispatcher/cells/workspace-threshold.ts
6
5
  const BELOW_TARGET_LIMIT = 10;
@@ -19,10 +18,7 @@ const renderAgent = (inputs) => {
19
18
  if (inputs.projects.length > 0) sections.push([formatWorkspaceTotal(inputs.projects)]);
20
19
  return `${sections.map((section) => section.join("\n")).join("\n\n")}\n${buildFooter(inputs)}`;
21
20
  };
22
- const renderWorkspaceThreshold = {
23
- agent: renderAgent,
24
- ink: (inputs) => renderAgentStringAsInk(renderAgent(inputs))
25
- };
21
+ const renderWorkspaceThreshold = { agent: renderAgent };
26
22
 
27
23
  //#endregion
28
24
  export { renderWorkspaceThreshold };
@@ -10,9 +10,7 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
10
10
  import { renderWorkspaceFail } from "./cells/workspace-fail.js";
11
11
  import { renderWorkspacePass } from "./cells/workspace-pass.js";
12
12
  import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
13
- import { formatScopedCoverageNote } from "@vitest-agent/sdk";
14
- import { Box, Text } from "ink";
15
- import { createElement } from "react";
13
+ import { formatScopedCoverageNote, formatStrayOutputNote } from "@vitest-agent/sdk";
16
14
 
17
15
  //#region src/dispatcher/dispatch.ts
18
16
  /**
@@ -56,11 +54,27 @@ const dispatcherTable = {
56
54
  const dispatch = (inputs, opts) => {
57
55
  const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
58
56
  const note = scopedCoverageNoteFor(inputs);
59
- return note !== null ? `${body}\n${note}` : body;
57
+ const withNote = note !== null ? `${body}\n${note}` : body;
58
+ const stray = strayOutputNoteFor(inputs);
59
+ return stray !== null ? `${withNote}\n${stray}\n` : withNote;
60
+ };
61
+ /**
62
+ * The stray-output note for a run that wrote past Vitest's console capture,
63
+ * or `null` when it wrote nothing stray. `dispatch` and `dispatchInk` append
64
+ * it after the cell's output (and the scoped-coverage note); cells never emit
65
+ * it themselves.
66
+ *
67
+ * @param inputs - the classified dispatch inputs
68
+ * @returns the note's lines joined, or `null`
69
+ * @internal
70
+ */
71
+ const strayOutputNoteFor = (inputs) => {
72
+ const stray = inputs.state.strayOutput;
73
+ return stray === void 0 ? null : formatStrayOutputNote(stray).join("\n");
60
74
  };
61
75
  /**
62
76
  * Build the scoped-coverage note for this run, or `null` on a full
63
- * (non-scoped) run. Shared by {@link dispatch} and {@link dispatchInk} so
77
+ * (non-scoped) run. Shared by {@link dispatch} and the Ink entry's `dispatchInk` so
64
78
  * both render paths surface the same information (issue #160 gap 1) —
65
79
  * Vitest's coverage thresholds are meaningless against a subset of the
66
80
  * project's test files, so every cell's own threshold-flavored coverage
@@ -73,25 +87,6 @@ const scopedCoverageNoteFor = (inputs) => {
73
87
  if (cov === null || cov.scoped !== true) return null;
74
88
  return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
75
89
  };
76
- /**
77
- * Dispatch to the cell that matches `(inputs.shape, inputs.outcome)`
78
- * and return its Ink-half React tree. Returns `null` when the matched
79
- * cell does not expose an `ink` half — callers should fall back to
80
- * the agent string in that case.
81
- *
82
- * @param inputs - the classified dispatch inputs
83
- * @param opts - cell rendering options
84
- * @returns the Ink React element, or `null` when the cell has no Ink half
85
- * @public
86
- */
87
- const dispatchInk = (inputs, opts) => {
88
- const cell = dispatcherTable[inputs.shape][inputs.outcome];
89
- if (cell.ink === void 0) return null;
90
- const element = cell.ink(inputs, opts);
91
- const note = scopedCoverageNoteFor(inputs);
92
- if (note === null) return element;
93
- return createElement(Box, { flexDirection: "column" }, element, createElement(Text, null, note));
94
- };
95
90
 
96
91
  //#endregion
97
- export { dispatch, dispatchInk, dispatcherTable };
92
+ export { dispatch, dispatcherTable, scopedCoverageNoteFor, strayOutputNoteFor };