@vitest-agent/ui 2.5.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,13 @@
1
+ import { inkStyle } from "../theme.js";
1
2
  import { Box, Text } from "ink";
3
+ import { Token } from "@effected/cli";
2
4
  import { jsx, jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/SuggestedActions.tsx
5
- const SEVERITY_COLOR = {
6
- info: "blue",
7
- warn: "yellow",
8
- blocker: "red"
7
+ const SEVERITY_TOKEN = {
8
+ info: Token.named("blue"),
9
+ warn: "warning",
10
+ blocker: "failure"
9
11
  };
10
12
  /**
11
13
  * Renders the suggested-actions queue as severity-prefixed rows with
@@ -24,7 +26,7 @@ const SuggestedActions = ({ actions }) => {
24
26
  flexDirection: "column",
25
27
  children: [/* @__PURE__ */ jsxs(Box, { children: [
26
28
  /* @__PURE__ */ jsx(Text, {
27
- color: SEVERITY_COLOR[action.severity],
29
+ ...inkStyle(SEVERITY_TOKEN[action.severity]),
28
30
  bold: true,
29
31
  children: ` ${action.severity}: `
30
32
  }),
@@ -1,3 +1,4 @@
1
+ import { VitestAgentTokens, inkStyle } from "../theme.js";
1
2
  import { Text } from "ink";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
 
@@ -34,7 +35,7 @@ const TagColumns = ({ tags, counts }) => {
34
35
  return /* @__PURE__ */ jsx(Text, { children: tags.map((tag, i) => {
35
36
  const count = counts?.[tag] ?? 0;
36
37
  return /* @__PURE__ */ jsxs(Text, {
37
- color: count > 0 ? "cyan" : "gray",
38
+ ...inkStyle(count > 0 ? VitestAgentTokens.tag : VitestAgentTokens.zero),
38
39
  children: [
39
40
  i > 0 ? " " : "",
40
41
  tag,
@@ -1,14 +1,14 @@
1
+ import { VitestAgentTokens, inkStyle } from "../theme.js";
1
2
  import { Text } from "ink";
3
+ import { Fmt } from "@effected/cli";
2
4
  import { jsx, jsxs } from "react/jsx-runtime";
3
5
 
4
6
  //#region src/render-ink/TrendLine.tsx
5
- /**
6
- * The one-line trend signal — `Trend: <direction> (<N> runs)`.
7
- */
8
- const COLOR = {
9
- improving: "green",
10
- regressing: "red",
11
- stable: "gray"
7
+ /** A regressing trend is a `warning`, not a failure (the kit's drift decision). */
8
+ const TOKEN = {
9
+ improving: "success",
10
+ regressing: "warning",
11
+ stable: VitestAgentTokens.stable
12
12
  };
13
13
  /**
14
14
  * Renders the one-line trend signal: direction (colored) and run count.
@@ -16,7 +16,7 @@ const COLOR = {
16
16
  * @public
17
17
  */
18
18
  const TrendLine = ({ trend }) => {
19
- const runs = trend.runCount === 1 ? "1 run" : `${trend.runCount} runs`;
19
+ const runs = Fmt.plural(trend.runCount, "run");
20
20
  return /* @__PURE__ */ jsxs(Text, { children: [
21
21
  /* @__PURE__ */ jsx(Text, {
22
22
  bold: true,
@@ -24,7 +24,7 @@ const TrendLine = ({ trend }) => {
24
24
  }),
25
25
  " ",
26
26
  /* @__PURE__ */ jsx(Text, {
27
- color: COLOR[trend.direction],
27
+ ...inkStyle(TOKEN[trend.direction]),
28
28
  children: trend.direction
29
29
  }),
30
30
  /* @__PURE__ */ jsxs(Text, {
@@ -6,10 +6,10 @@ import { StatusIcon } from "./StatusIcon.js";
6
6
  import { ModuleHeader } from "./ModuleHeader.js";
7
7
  import { TagColumns, tagUnion } from "./TagColumns.js";
8
8
  import { ProjectRow } from "./ProjectRow.js";
9
- import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime } from "./spinner.js";
9
+ import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame } from "./spinner.js";
10
10
  import { TestRow } from "./TestRow.js";
11
11
  import { TrendLine } from "./TrendLine.js";
12
12
  import { StreamApp } from "./StreamApp.js";
13
13
  import { SuggestedActions } from "./SuggestedActions.js";
14
14
 
15
- export { CountColumns, CoverageBlock, DURATION_CELL_WIDTH, FailureSection, FailuresSection, ModuleHeader, ProjectRow, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActions, TagColumns, TestRow, TrendLine, spinnerFrame, spinnerFrameForTime, tagUnion };
15
+ export { CountColumns, CoverageBlock, DURATION_CELL_WIDTH, FailureSection, FailuresSection, ModuleHeader, ProjectRow, SPINNER_FRAMES, SPINNER_FRAME_MS, StatusIcon, StreamApp, SuggestedActions, TagColumns, TestRow, TrendLine, spinnerFrame, tagUnion };
@@ -1,63 +1,34 @@
1
+ import { Glyphs } from "@effected/cli";
2
+
1
3
  //#region src/render-ink/spinner.ts
2
4
  /**
3
- * Hand-rolled Braille spinner for the `stream` live renderer.
4
- *
5
- * No `ink-spinner` dependency — that package is a thin wrapper over the
6
- * same ten Braille characters plus a timer. The `stream` renderer
7
- * already needs a frame clock for the ticking elapsed column, so the
8
- * timer is shared and only the frame array lives here.
9
- *
10
- * The frame index is presentation state: it is derived from wall-clock
11
- * time by `createLiveInk` and passed to `StreamApp` as a prop. It never
12
- * enters the event-sourced `RenderState`.
13
- */
14
- /**
15
- * The ten Braille spinner frames, in animation order.
5
+ * The Unicode spinner frames, in animation order (`Glyphs.unicode.spinner`).
16
6
  *
17
7
  * @public
18
8
  */
19
- const SPINNER_FRAMES = [
20
- "⠋",
21
- "⠙",
22
- "⠹",
23
- "⠸",
24
- "⠼",
25
- "⠴",
26
- "⠦",
27
- "⠧",
28
- "⠇",
29
- "⠏"
30
- ];
9
+ const SPINNER_FRAMES = Glyphs.unicode.spinner;
31
10
  /**
32
- * How long each spinner frame is held, in milliseconds.
11
+ * How long each spinner frame is held, in milliseconds
12
+ * (`Glyphs.unicode.spinnerIntervalMs`).
33
13
  *
34
14
  * @public
35
15
  */
36
- const SPINNER_FRAME_MS = 80;
16
+ const SPINNER_FRAME_MS = Glyphs.unicode.spinnerIntervalMs;
37
17
  /**
38
18
  * Resolve the spinner glyph for a frame index. The index wraps modulo
39
19
  * the frame count and tolerates negative values, so a wall-clock-derived
40
20
  * index is always valid.
41
21
  *
42
22
  * @param index - the frame index (wraps modulo frame count)
43
- * @returns the Braille glyph for the given frame
23
+ * @param glyphs - the glyph set whose frames to draw; Unicode by default
24
+ * @returns the spinner glyph for the given frame
44
25
  * @public
45
26
  */
46
- const spinnerFrame = (index) => {
47
- const count = SPINNER_FRAMES.length;
48
- const wrapped = (Math.trunc(index) % count + count) % count;
49
- return SPINNER_FRAMES[wrapped];
27
+ const spinnerFrame = (index, glyphs = Glyphs.unicode) => {
28
+ const frames = glyphs.spinner;
29
+ const count = frames.length;
30
+ return frames[(Math.trunc(index) % count + count) % count] ?? "";
50
31
  };
51
- /**
52
- * Derive the spinner frame index from a wall-clock timestamp. Using the
53
- * clock — rather than a monotonic counter — keeps the animation correct
54
- * across watch-mode remounts with no extra closure state to reset.
55
- *
56
- * @param nowMs - current wall-clock time in milliseconds
57
- * @returns the frame index for the given timestamp
58
- * @public
59
- */
60
- const spinnerFrameForTime = (nowMs) => Math.floor(nowMs / 80);
61
32
 
62
33
  //#endregion
63
- export { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime };
34
+ export { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame };
package/theme.js ADDED
@@ -0,0 +1,82 @@
1
+ import { Glyphs, Status, Token } from "@effected/cli";
2
+ import { inkProps } from "@effected/cli/ui";
3
+
4
+ //#region src/theme.ts
5
+ /**
6
+ * The status vocabulary every vitest-agent render path draws from.
7
+ *
8
+ * @remarks
9
+ * Ranks slot the run statuses into the core ladder: `queued` (25) and
10
+ * `running` (35) sit among the not-yet-decided statuses, and `timeout` (85)
11
+ * is just under `failure` (90), so `Status.worst` over a project's
12
+ * outcomes reads failure, then timeout, then warning.
13
+ *
14
+ * @public
15
+ */
16
+ const VitestAgentStatus = Status.extend({
17
+ timeout: {
18
+ glyph: "⧖",
19
+ ascii: "[time]",
20
+ token: Token.hex("#e09a4e"),
21
+ rank: 85
22
+ },
23
+ running: {
24
+ glyph: "…",
25
+ ascii: "[..]",
26
+ token: Token.named("yellow"),
27
+ rank: 35
28
+ },
29
+ queued: {
30
+ glyph: "·",
31
+ ascii: "[.]",
32
+ token: Token.named("blackBright"),
33
+ rank: 25
34
+ }
35
+ });
36
+ /**
37
+ * Named accents that are not statuses.
38
+ *
39
+ * @public
40
+ */
41
+ const VitestAgentTokens = {
42
+ /** The `[flaky]` / `[new-failure]` classification tag on a failure line. */
43
+ classification: Token.hex("#c98ae0"),
44
+ /** A zero in a count column: present for alignment, not signal. */
45
+ zero: Token.named("blackBright"),
46
+ /** The `stable` trend direction. */
47
+ stable: Token.named("blackBright"),
48
+ /** A non-zero tag count. */
49
+ tag: Token.named("cyan")
50
+ };
51
+ /**
52
+ * Ink `<Text>` props for a semantic token or an explicit style.
53
+ *
54
+ * @remarks
55
+ * Resolves the token through the kit's pure `Token.resolve` and maps the
56
+ * `Style` with the kit's `inkProps`. No colour level reaches this call
57
+ * site, so `inkProps` with no level emits every prop and Ink's own chalk
58
+ * level, set by `CliUi.live` or `CliUi.context`, gates what is drawn.
59
+ *
60
+ * @param token - a token name or a `Style`
61
+ * @public
62
+ */
63
+ const inkStyle = (token) => inkProps(Token.resolve(token));
64
+ /**
65
+ * The glyph of a status in a glyph set: its Unicode glyph, or its ASCII
66
+ * fallback when `glyphs` is the kit's ASCII set (the kit's `Status.glyph`).
67
+ *
68
+ * @param name - a status in {@link VitestAgentStatus}
69
+ * @param glyphs - the glyph set, from `Glyphs.select`; Unicode by default
70
+ * @public
71
+ */
72
+ const statusGlyph = (name, glyphs = Glyphs.unicode) => VitestAgentStatus.glyph(name, glyphs);
73
+ /**
74
+ * Ink `<Text>` props for a status's glyph.
75
+ *
76
+ * @param name - a status in {@link VitestAgentStatus}
77
+ * @public
78
+ */
79
+ const statusInkStyle = (name) => inkStyle(VitestAgentStatus.def(name).token);
80
+
81
+ //#endregion
82
+ export { VitestAgentStatus, VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle };
package/pubsub/Channel.js DELETED
@@ -1,30 +0,0 @@
1
- import { Context, Layer, PubSub } from "effect";
2
-
3
- //#region src/pubsub/Channel.ts
4
- /**
5
- * Service tag for the run-event channel. Consumers depend on this tag;
6
- * the {@link RunEventChannelLive} layer is the single point at which
7
- * the underlying Effect `PubSub` is constructed.
8
- *
9
- * @public
10
- */
11
- var RunEventChannel = class extends Context.Service()("vitest-agent-ui/RunEventChannel") {};
12
- /**
13
- * Default live layer providing an unbounded Effect `PubSub` for
14
- * the run-event channel. The PubSub is scoped — when the surrounding
15
- * scope closes, the channel shuts down and any pending subscribers
16
- * receive their dequeue closure.
17
- *
18
- * Unbounded is the right default for the plugin and CLI: events arrive
19
- * faster than a slow renderer can drain (e.g., several `TestFinished`
20
- * per millisecond during a large suite), and dropping events would
21
- * leave the rendered state inconsistent with the runner's truth.
22
- * If memory pressure becomes a concern, swap in `PubSub.sliding`
23
- * with a tuned capacity at the call site.
24
- *
25
- * @public
26
- */
27
- const RunEventChannelLive = Layer.effect(RunEventChannel, PubSub.unbounded());
28
-
29
- //#endregion
30
- export { RunEventChannel, RunEventChannelLive };
@@ -1,37 +0,0 @@
1
- import { RunEventChannel } from "./Channel.js";
2
- import { Effect, PubSub } from "effect";
3
-
4
- //#region src/pubsub/Publisher.ts
5
- /**
6
- * Emit one event onto the channel.
7
- *
8
- * Resolves when the event is enqueued. Returns the success boolean
9
- * from `PubSub.publish` so callers can detect a shutdown
10
- * channel if needed; in normal operation the result is always `true`.
11
- *
12
- * @param event - the run event to publish
13
- * @returns an Effect that resolves to the publish success boolean
14
- * @public
15
- */
16
- const publish = (event) => Effect.gen(function* () {
17
- const channel = yield* RunEventChannel;
18
- return yield* PubSub.publish(channel, event);
19
- });
20
- /**
21
- * Emit a batch of events onto the channel in order.
22
- *
23
- * Useful for the CLI replay path where the entire event sequence is
24
- * materialized synchronously from a database query before any
25
- * subscriber needs to read it.
26
- *
27
- * @param events - iterable of run events to publish in order
28
- * @returns an Effect that resolves when all events are enqueued
29
- * @public
30
- */
31
- const publishAll = (events) => Effect.gen(function* () {
32
- const channel = yield* RunEventChannel;
33
- yield* PubSub.publishAll(channel, events);
34
- });
35
-
36
- //#endregion
37
- export { publish, publishAll };
@@ -1,77 +0,0 @@
1
- import { RunEventChannel } from "./Channel.js";
2
- import { reduceRenderState } from "../reducer.js";
3
- import { initialRenderState } from "@vitest-agent/sdk";
4
- import { Effect, PubSub, Stream } from "effect";
5
-
6
- //#region src/pubsub/Subscriber.ts
7
- /**
8
- * Subscribe, fold the run-event stream into a terminal
9
- * `RenderState`, and return when `RunFinished` arrives.
10
- *
11
- * The subscription is scoped — when this effect completes (or is
12
- * interrupted), the underlying PubSub releases its slot.
13
- *
14
- * @param initial - the initial render state to fold from
15
- * @returns an Effect that resolves to the terminal render state
16
- * @public
17
- */
18
- const accumulateUntilFinished = (initial = initialRenderState) => Effect.scoped(Effect.gen(function* () {
19
- const channel = yield* RunEventChannel;
20
- const subscription = yield* PubSub.subscribe(channel);
21
- let state = initial;
22
- while (state.phase !== "finished" && state.phase !== "timed-out") {
23
- const event = yield* PubSub.take(subscription);
24
- state = reduceRenderState(state, event);
25
- }
26
- return state;
27
- }));
28
- /**
29
- * Run a callback for every projected `RenderState` as events
30
- * arrive. The stream terminates when the PubSub shuts down (typically
31
- * when the surrounding scope closes).
32
- *
33
- * The host runs this in a forked fiber so the publisher and the
34
- * renderer can make progress concurrently. Returning an Effect from
35
- * the callback lets the host suspend further rendering on backpressure
36
- * (e.g. throttling redraws to the next animation frame).
37
- *
38
- * @param onState - callback invoked with each projected render state
39
- * @param initial - the initial render state to fold from
40
- * @returns an Effect that completes when the PubSub shuts down
41
- * @public
42
- */
43
- const forEachRenderState = (onState, initial = initialRenderState) => Effect.scoped(Effect.gen(function* () {
44
- const channel = yield* RunEventChannel;
45
- yield* Stream.fromPubSub(channel).pipe(Stream.scan(() => initial, reduceRenderState), Stream.runForEach(onState));
46
- }));
47
- /**
48
- * Build an Effect `Stream` of accumulated states over the channel.
49
- *
50
- * Lower-level than {@link forEachRenderState} — useful when the host
51
- * wants to compose with other stream operators (debouncing, throttling,
52
- * tee-to-file) before running the terminal sink.
53
- *
54
- * @param initial - the initial render state to fold from
55
- * @returns an Effect that resolves to a Stream of render states
56
- * @public
57
- */
58
- const renderStateStream = (initial = initialRenderState) => Effect.gen(function* () {
59
- const channel = yield* RunEventChannel;
60
- return Stream.fromPubSub(channel).pipe(Stream.scan(() => initial, reduceRenderState));
61
- });
62
- /**
63
- * Convenience: subscribe and produce a low-level dequeue of raw
64
- * `RunEvent` values. Mostly for tests and ad-hoc consumers; most
65
- * application code should prefer {@link forEachRenderState} or
66
- * {@link renderStateStream}.
67
- *
68
- * @returns an Effect that resolves to a dequeue of raw run events
69
- * @public
70
- */
71
- const subscribeRaw = () => Effect.gen(function* () {
72
- const channel = yield* RunEventChannel;
73
- return yield* PubSub.subscribe(channel);
74
- });
75
-
76
- //#endregion
77
- export { accumulateUntilFinished, forEachRenderState, renderStateStream, subscribeRaw };
package/pubsub/index.js DELETED
@@ -1,5 +0,0 @@
1
- import { RunEventChannel, RunEventChannelLive } from "./Channel.js";
2
- import { publish, publishAll } from "./Publisher.js";
3
- import { accumulateUntilFinished, forEachRenderState, renderStateStream, subscribeRaw } from "./Subscriber.js";
4
-
5
- export { RunEventChannel, RunEventChannelLive, accumulateUntilFinished, forEachRenderState, publish, publishAll, renderStateStream, subscribeRaw };