@pi-archimedes/subagent 1.6.0 → 1.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/subagent",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "main": "./src/index.ts",
13
13
  "dependencies": {
14
- "@pi-archimedes/core": "1.6.0"
14
+ "@pi-archimedes/core": "1.7.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@earendil-works/pi-ai": ">=0.1.0",
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { truncLine } from "./format.js";
3
+
4
+ // ── truncLine ───────────────────────────────────────────────────────────────
5
+
6
+ describe("truncLine", () => {
7
+ it("returns text unchanged when within limit", () => {
8
+ expect(truncLine("hello", 10)).toBe("hello");
9
+ });
10
+
11
+ it("truncates with '...' when exceeding limit", () => {
12
+ expect(truncLine("hello world", 8)).toBe("hello...");
13
+ });
14
+
15
+ it("stops at newline boundary instead of bleeding into next line", () => {
16
+ expect(truncLine("line one\nline two\nline three", 15)).toBe("line one...");
17
+ });
18
+
19
+ it("truncates first line if it itself exceeds limit", () => {
20
+ expect(truncLine("this is a very long first line\nsecond", 12)).toBe("this is a...");
21
+ });
22
+
23
+ it("handles multiple consecutive newlines", () => {
24
+ expect(truncLine("a\n\n\nb", 10)).toBe("a...");
25
+ });
26
+
27
+ it("handles text starting with newline", () => {
28
+ expect(truncLine("\nhello", 10)).toBe("...");
29
+ });
30
+ });
package/src/format.ts CHANGED
@@ -27,8 +27,16 @@ export function formatCost(cost: number): string {
27
27
  * measure ANSI display width. The benefit: the "..." is plain text so it
28
28
  * inherits any ANSI color wrapper applied around the result (pi-tui's
29
29
  * truncateToWidth appends "..." after closing color codes, leaving it unstyled).
30
+ *
31
+ * If the text contains newlines, truncates at the first newline boundary
32
+ * rather than bleeding into the next line.
30
33
  */
31
34
  export function truncLine(text: string, maxLen: number): string {
35
+ // Check for newline first — it's a hard boundary regardless of length
36
+ const nlIdx = text.indexOf("\n");
37
+ if (nlIdx !== -1) {
38
+ return text.slice(0, nlIdx).slice(0, maxLen - 3) + "...";
39
+ }
32
40
  if (text.length <= maxLen) return text;
33
41
  return text.slice(0, maxLen - 3) + "...";
34
42
  }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractArgsPreview } from "./handlers.js";
3
+
4
+ // ── extractArgsPreview ──────────────────────────────────────────────────────
5
+
6
+ describe("extractArgsPreview", () => {
7
+ it("returns string args truncated", () => {
8
+ const long = "a".repeat(200);
9
+ expect(extractArgsPreview(long)).toBe("a".repeat(120));
10
+ });
11
+
12
+ it("replaces newlines in string args", () => {
13
+ expect(extractArgsPreview("hello\nworld")).toBe("hello world");
14
+ });
15
+
16
+ it("replaces newlines in single-key object value", () => {
17
+ const args = { command: "cat file.txt\ngrep -A5 \"test\"" };
18
+ const result = extractArgsPreview(args);
19
+ expect(result).not.toContain("\n");
20
+ expect(result).toBe("cat file.txt grep -A5 \"test\"");
21
+ });
22
+
23
+ it("replaces newlines in multi-key longest string value", () => {
24
+ const args = { short: "x", long: "hello\nworld\nfoo" };
25
+ expect(extractArgsPreview(args)).toBe("hello world foo");
26
+ });
27
+
28
+ it("handles number and boolean values", () => {
29
+ expect(extractArgsPreview({ count: 42 })).toBe("42");
30
+ expect(extractArgsPreview({ enabled: true })).toBe("true");
31
+ });
32
+ });
package/src/handlers.ts CHANGED
@@ -15,14 +15,17 @@ export interface JsonEvent {
15
15
  * Generic: no hardcoded tool names.
16
16
  */
17
17
  export function extractArgsPreview(args: unknown): string {
18
- if (typeof args === "string") return args.slice(0, ARGS_PREVIEW_MAX);
18
+ if (typeof args === "string") {
19
+ // Replace newlines so previews stay single-line
20
+ return args.replace(/\n/g, " ").slice(0, ARGS_PREVIEW_MAX);
21
+ }
19
22
  if (args && typeof args === "object" && !Array.isArray(args)) {
20
23
  const obj = args as Record<string, unknown>;
21
24
  const keys = Object.keys(obj);
22
25
  // Single-key object: show the value directly (or serialize if complex)
23
26
  if (keys.length === 1) {
24
27
  const v = obj[keys[0]!];
25
- if (typeof v === "string") return v.slice(0, ARGS_PREVIEW_MAX);
28
+ if (typeof v === "string") return v.replace(/\n/g, " ").slice(0, ARGS_PREVIEW_MAX);
26
29
  if (typeof v === "number" || typeof v === "boolean") return String(v);
27
30
  // Complex value (array/nested object) — serialize just this value
28
31
  const serialized = JSON.stringify(v);
@@ -35,7 +38,7 @@ export function extractArgsPreview(args: unknown): string {
35
38
  best = v;
36
39
  }
37
40
  }
38
- if (best) return best.slice(0, ARGS_PREVIEW_MAX);
41
+ if (best) return best.replace(/\n/g, " ").slice(0, ARGS_PREVIEW_MAX);
39
42
  }
40
43
  const serialized = JSON.stringify(args);
41
44
  return serialized?.slice(0, ARGS_PREVIEW_MAX) ?? "";