@juicesharp/rpiv-pi 0.12.1 → 0.12.2

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.
@@ -0,0 +1,98 @@
1
+ import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import { Text } from "@mariozechner/pi-tui";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ // Stub the full renderer so the terminal-state branch returns a recognisable sentinel
6
+ // instead of executing nicobailon's real renderer (which needs a full Theme).
7
+ // vi.hoisted is required because vi.mock factories are top-hoisted and can't close
8
+ // over file-level consts.
9
+ const { renderSubagentResultMock } = vi.hoisted(() => ({
10
+ renderSubagentResultMock: vi.fn(() => ({ __sentinel: "full-render" }) as unknown),
11
+ }));
12
+ vi.mock("pi-subagents/render", () => ({ renderSubagentResult: renderSubagentResultMock }));
13
+
14
+ import { buildQuietRenderResult } from "./renderer-override.js";
15
+
16
+ function makeTheme(): Theme {
17
+ return {
18
+ fg: (_c: string, t: string) => t,
19
+ bold: (t: string) => t,
20
+ } as unknown as Theme;
21
+ }
22
+
23
+ describe("buildQuietRenderResult — layout-stable quiet card", () => {
24
+ it("emits ONE Text line when progress.status === 'running'", () => {
25
+ const render = buildQuietRenderResult();
26
+ const out = render(
27
+ { details: { results: [{ agent: "x", progress: { status: "running" } }] } },
28
+ { expanded: false },
29
+ makeTheme(),
30
+ );
31
+ expect(out).toBeInstanceOf(Text);
32
+ expect(renderSubagentResultMock).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it("emits ONE Text line when progress.status === 'pending'", () => {
36
+ const render = buildQuietRenderResult();
37
+ const out = render(
38
+ { details: { results: [{ agent: "x", progress: { status: "pending" } }] } },
39
+ { expanded: false },
40
+ makeTheme(),
41
+ );
42
+ expect(out).toBeInstanceOf(Text);
43
+ });
44
+
45
+ it("emits ONE Text line when progress is MISSING (pre-progress partial update)", () => {
46
+ const render = buildQuietRenderResult();
47
+ // First partialResult often arrives before pi-subagents stamps progress.
48
+ // Previously fell through to full renderer → N-line card mid-stream →
49
+ // layout shift → physical-row stacking. Now stays 1-line.
50
+ const out = render({ details: { results: [{ agent: "x" }] } }, { expanded: false }, makeTheme());
51
+ expect(out).toBeInstanceOf(Text);
52
+ expect(renderSubagentResultMock).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("emits ONE Text line when result.details itself is missing (very first frame)", () => {
56
+ const render = buildQuietRenderResult();
57
+ const out = render({}, { expanded: false }, makeTheme());
58
+ expect(out).toBeInstanceOf(Text);
59
+ });
60
+
61
+ it("delegates to full renderer once exitCode lands (terminal state)", () => {
62
+ renderSubagentResultMock.mockClear();
63
+ const render = buildQuietRenderResult();
64
+ const out = render(
65
+ { details: { results: [{ agent: "x", exitCode: 0, progress: { status: "complete" } }] } },
66
+ { expanded: false },
67
+ makeTheme(),
68
+ );
69
+ expect(renderSubagentResultMock).toHaveBeenCalledOnce();
70
+ expect((out as { __sentinel?: string }).__sentinel).toBe("full-render");
71
+ });
72
+
73
+ it("delegates to full renderer on error stopReason", () => {
74
+ renderSubagentResultMock.mockClear();
75
+ const render = buildQuietRenderResult();
76
+ const out = render(
77
+ { details: { results: [{ agent: "x", stopReason: "error" }] } },
78
+ { expanded: false },
79
+ makeTheme(),
80
+ );
81
+ expect(renderSubagentResultMock).toHaveBeenCalledOnce();
82
+ expect((out as { __sentinel?: string }).__sentinel).toBe("full-render");
83
+ });
84
+
85
+ it("treats exitCode present + status=running as NON-terminal (streaming finalisation window)", () => {
86
+ renderSubagentResultMock.mockClear();
87
+ const render = buildQuietRenderResult();
88
+ // Mid-transition frames can have exitCode set before progress.status
89
+ // leaves "running". Should stay quiet until status clears.
90
+ const out = render(
91
+ { details: { results: [{ agent: "x", exitCode: 0, progress: { status: "running" } }] } },
92
+ { expanded: false },
93
+ makeTheme(),
94
+ );
95
+ expect(out).toBeInstanceOf(Text);
96
+ expect(renderSubagentResultMock).not.toHaveBeenCalled();
97
+ });
98
+ });
@@ -34,30 +34,45 @@ interface ProgressLike {
34
34
  interface ResultLike {
35
35
  agent?: string;
36
36
  progress?: ProgressLike;
37
+ exitCode?: number;
38
+ stopReason?: string;
37
39
  }
38
40
  interface DetailsLike {
39
41
  results?: ResultLike[];
40
42
  }
41
43
 
42
- function buildQuietRenderResult(): (
44
+ // Layout-stable quiet card height: while the subagent is non-terminal, emit EXACTLY
45
+ // one `Text` line. If we fell through to the full renderer whenever progress.status
46
+ // was missing (pre-progress partial updates), the card would flip 1-line ↔ N-line
47
+ // every few frames mid-stream — same physical-row-stacking pathology that ghosted
48
+ // the overlay rows before the run-tracker newline fix.
49
+ //
50
+ // Terminal state: exitCode or stopReason is present on the last SingleResult AND
51
+ // progress.status is not in the running set. Anything else is treated as running,
52
+ // including "no progress yet" (status === undefined) and the first tool_execution_*
53
+ // frames before pi-subagents has stamped progress.
54
+ function isTerminal(r: ResultLike | undefined): boolean {
55
+ if (!r) return false;
56
+ const status = r.progress?.status;
57
+ if (status === "pending" || status === "running") return false;
58
+ return r.exitCode != null || r.stopReason != null;
59
+ }
60
+
61
+ export function buildQuietRenderResult(): (
43
62
  result: { details?: DetailsLike; content?: Array<{ type: string; text?: string }> },
44
63
  options: { expanded: boolean },
45
64
  theme: Theme,
46
65
  ) => unknown {
47
66
  return (result, options, theme) => {
48
67
  const r = result.details?.results?.[0];
49
- const status: string | undefined = r?.progress?.status;
50
- if (status !== "pending" && status !== "running") {
68
+ if (isTerminal(r)) {
51
69
  return renderSubagentResult(result, options, theme);
52
70
  }
53
- // renderCall already shows "subagent <agent>" above avoid repeating
54
- // the identity. Todo-style glyph + status (single line), directly
55
- // under the tool-call header. Glyphs mirror packages/rpiv-todo/
56
- // todo-overlay.ts:28-35 for a consistent status language across
57
- // overlays.
71
+ // Non-terminal single-line status. Renders under the tool-call header,
72
+ // glyph at column 0 to align with "subagent <agent>" above. Glyphs mirror
73
+ // packages/rpiv-todo/todo-overlay.ts:28-35 for consistent status language.
74
+ const status = r?.progress?.status ?? "running";
58
75
  const glyph = status === "pending" ? theme.fg("dim", "○") : theme.fg("warning", "◐");
59
- // Glyph at column 0 — aligned directly under the "s" of "subagent"
60
- // in the renderCall line above (pi prints that at column 0 too).
61
76
  return new Text(`${glyph} ${theme.fg("muted", status)}`, 0, 0);
62
77
  };
63
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
5
5
  "keywords": [
6
6
  "pi-package",