@juicesharp/rpiv-pi 0.12.0 → 0.12.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.
@@ -199,3 +199,53 @@ describe("run-tracker __resetState", () => {
199
199
  expect(hasAnyVisible()).toBe(false);
200
200
  });
201
201
  });
202
+
203
+ describe("run-tracker newline sanitization", () => {
204
+ it("collapses embedded newlines in single-mode task descriptions", () => {
205
+ __resetState();
206
+ onStart("t1", {
207
+ agent: "peer-comparator",
208
+ task: "Peer-mirror check.\n\nPeerPairs (orchestrator-computed):\n[list of tuples]\n\nFor each pair, Read BOTH files.",
209
+ });
210
+ const [run] = listRuns();
211
+ expect(run.description).not.toMatch(/[\r\n]/);
212
+ expect(run.description.startsWith("Peer-mirror check.")).toBe(true);
213
+ expect(run.description).toContain("PeerPairs (orchestrator-computed):");
214
+ });
215
+
216
+ it("collapses newlines in displayName too (defensive)", () => {
217
+ __resetState();
218
+ onStart("t1", { agent: "weird\nname", task: "x" });
219
+ const [run] = listRuns();
220
+ expect(run.displayName).not.toMatch(/[\r\n]/);
221
+ expect(run.displayName).toBe("weird name");
222
+ });
223
+
224
+ it("sanitizes errorMessage on terminal state", () => {
225
+ __resetState();
226
+ onStart("t1", { agent: "x", task: "t" });
227
+ onEnd(
228
+ "t1",
229
+ {
230
+ details: makeDetails("single", [
231
+ makeResult({
232
+ exitCode: 1,
233
+ stopReason: "error",
234
+ errorMessage: "boom\nat foo\nat bar",
235
+ }),
236
+ ]),
237
+ },
238
+ true,
239
+ );
240
+ const [run] = listRuns();
241
+ expect(run.errorMessage).not.toMatch(/[\r\n]/);
242
+ expect(run.errorMessage).toBe("boom at foo at bar");
243
+ });
244
+
245
+ it("preserves single-line descriptions unchanged", () => {
246
+ __resetState();
247
+ onStart("t1", { agent: "scout", task: "probe auth module" });
248
+ const [run] = listRuns();
249
+ expect(run.description).toBe("probe auth module");
250
+ });
251
+ });
@@ -21,6 +21,13 @@ interface SubagentArgs {
21
21
  chain?: Array<{ agent: string; task: string }>;
22
22
  }
23
23
 
24
+ // Multi-line skill prompts (e.g. peer-comparator with `PeerPairs (orchestrator-computed):` inlined)
25
+ // must not ship embedded newlines into the widget's string[] output — pi-tui tracks logical lines
26
+ // but the terminal splits on \n into physical rows it can't clear, leaving stale duplicate blocks.
27
+ function oneLine(s: string): string {
28
+ return s.replace(/\s*[\r\n]+\s*/g, " ").trim();
29
+ }
30
+
24
31
  function inferMode(args: SubagentArgs): RunMode {
25
32
  if (args.chain && args.chain.length > 0) return "chain";
26
33
  if (args.tasks && args.tasks.length > 0) return "parallel";
@@ -44,17 +51,17 @@ function deriveDisplay(args: SubagentArgs, mode: RunMode): { displayName: string
44
51
  const steps = args.chain ?? [];
45
52
  return {
46
53
  displayName: `chain (${steps.length} steps)`,
47
- description: steps.map((s) => s.agent).join(" → "),
54
+ description: oneLine(steps.map((s) => s.agent).join(" → ")),
48
55
  };
49
56
  }
50
57
  if (mode === "parallel") {
51
58
  const tasks = args.tasks ?? [];
52
59
  return {
53
60
  displayName: `parallel (${tasks.length} tasks)`,
54
- description: tasks[0]?.agent ?? "",
61
+ description: oneLine(tasks[0]?.agent ?? ""),
55
62
  };
56
63
  }
57
- return { displayName: args.agent ?? "subagent", description: args.task ?? "" };
64
+ return { displayName: oneLine(args.agent ?? "subagent"), description: oneLine(args.task ?? "") };
58
65
  }
59
66
 
60
67
  function deriveTerminalStatus(isError: boolean, results: readonly SingleResult[]): RunStatus {
@@ -107,7 +114,8 @@ export function onEnd(toolCallId: string, result: { details?: SubagentDetails }
107
114
  run.completedAt = Date.now();
108
115
  run.status = deriveTerminalStatus(isError, run.results);
109
116
  const last = run.results[run.results.length - 1];
110
- run.errorMessage = last?.errorMessage || (isError ? last?.stderr : undefined) || undefined;
117
+ const rawErr = last?.errorMessage || (isError ? last?.stderr : undefined) || undefined;
118
+ run.errorMessage = rawErr ? oneLine(rawErr) : undefined;
111
119
  finishedAge.set(toolCallId, 0);
112
120
  }
113
121
 
@@ -220,3 +220,49 @@ describe("SubagentWidget render — invalidate", () => {
220
220
  expect(captured.length).toBe(2);
221
221
  });
222
222
  });
223
+
224
+ describe("SubagentWidget render — newline safety", () => {
225
+ it("never emits embedded newlines even with multi-line tasks (running)", () => {
226
+ onStart("t1", {
227
+ agent: "peer-comparator",
228
+ task: "Peer-mirror check.\n\nPeerPairs (orchestrator-computed):\n[list of (new_file, peer_file) tuples]\n\nFor each pair, Read BOTH files in full.",
229
+ });
230
+ onUpdate("t1", makeDetails("single", [makeResult()]));
231
+ const { ctx, captured } = makeUICtx();
232
+ const lines = renderOnce(new SubagentWidget(), ctx, captured);
233
+ for (const line of lines) {
234
+ expect(line).not.toMatch(/[\r\n]/);
235
+ }
236
+ });
237
+
238
+ it("never emits embedded newlines after the run completes (finished line)", () => {
239
+ onStart("t1", {
240
+ agent: "peer-comparator",
241
+ task: "Peer-mirror check.\n\nPeerPairs (orchestrator-computed):\n[list]",
242
+ });
243
+ onEnd("t1", { details: makeDetails("single", [makeResult()]) }, false);
244
+ const { ctx, captured } = makeUICtx();
245
+ const lines = renderOnce(new SubagentWidget(), ctx, captured);
246
+ for (const line of lines) {
247
+ expect(line).not.toMatch(/[\r\n]/);
248
+ }
249
+ });
250
+
251
+ it("never emits embedded newlines in error trail", () => {
252
+ onStart("t1", { agent: "scout", task: "x" });
253
+ onEnd(
254
+ "t1",
255
+ {
256
+ details: makeDetails("single", [
257
+ makeResult({ exitCode: 1, stopReason: "error", errorMessage: "boom\nstack\nmore" }),
258
+ ]),
259
+ },
260
+ true,
261
+ );
262
+ const { ctx, captured } = makeUICtx();
263
+ const lines = renderOnce(new SubagentWidget(), ctx, captured);
264
+ for (const line of lines) {
265
+ expect(line).not.toMatch(/[\r\n]/);
266
+ }
267
+ });
268
+ });
@@ -21,6 +21,10 @@ import type { AgentProgress, SingleResult, TrackedRun } from "./types.js";
21
21
  // Strip SGR ANSI escapes to measure visible width for layout math.
22
22
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
23
23
  const visibleLen = (s: string): number => s.replace(ANSI_RE, "").length;
24
+ // Defensive: any \n in a returned string[] element splits into physical rows pi-tui can't
25
+ // track, leaving stale artifacts every frame. Run-tracker sanitizes at ingest; this guard
26
+ // covers any future caller that mutates description/displayName directly on the tracked run.
27
+ const oneLine = (s: string): string => s.replace(/\s*[\r\n]+\s*/g, " ");
24
28
 
25
29
  export class SubagentWidget {
26
30
  private uiCtx: ExtensionUIContext | undefined;
@@ -187,10 +191,10 @@ export class SubagentWidget {
187
191
  const total = run.results.length;
188
192
  descriptor = total > 0 ? `${done}/${total} done` : "starting";
189
193
  } else {
190
- descriptor = run.description;
194
+ descriptor = oneLine(run.description);
191
195
  }
192
196
 
193
- const prefix = `${theme.fg("dim", "├─")} ${theme.fg("accent", frame)} ${theme.bold(run.displayName)}`;
197
+ const prefix = `${theme.fg("dim", "├─")} ${theme.fg("accent", frame)} ${theme.bold(oneLine(run.displayName))}`;
194
198
  const tail = `${theme.fg("dim", "·")} ${theme.fg("dim", stats)}`;
195
199
  // Overhead: " " between prefix+descriptor, " " between descriptor+tail.
196
200
  const budget = Math.max(0, width - visibleLen(prefix) - 2 - 1 - visibleLen(tail));
@@ -221,11 +225,11 @@ export class SubagentWidget {
221
225
  trail = theme.fg("warning", " aborted");
222
226
  } else {
223
227
  icon = theme.fg("error", "✗");
224
- const msg = run.errorMessage ? `: ${run.errorMessage.slice(0, 60)}` : "";
228
+ const msg = run.errorMessage ? `: ${oneLine(run.errorMessage).slice(0, 60)}` : "";
225
229
  trail = theme.fg("error", ` error${msg}`);
226
230
  }
227
231
  const body =
228
- `${icon} ${theme.fg("dim", run.displayName)} ${theme.fg("dim", run.description)} ` +
232
+ `${icon} ${theme.fg("dim", oneLine(run.displayName))} ${theme.fg("dim", oneLine(run.description))} ` +
229
233
  `${theme.fg("dim", "·")} ${theme.fg("dim", stats)}${trail}`;
230
234
  return truncate(`${theme.fg("dim", "├─")} ${body}`);
231
235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-pi",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
5
5
  "keywords": [
6
6
  "pi-package",