pi-subagents 0.9.2 → 0.10.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/CHANGELOG.md +24 -0
- package/README.md +41 -4
- package/async-execution.ts +64 -30
- package/chain-execution.ts +1 -1
- package/execution.ts +16 -1
- package/index.ts +90 -13
- package/package.json +11 -2
- package/parallel-utils.ts +93 -0
- package/render.ts +78 -8
- package/schemas.ts +1 -1
- package/settings.ts +16 -14
- package/skills.ts +25 -1
- package/subagent-runner.ts +360 -176
- package/utils.ts +23 -7
package/render.ts
CHANGED
|
@@ -20,9 +20,68 @@ function getTermWidth(): number {
|
|
|
20
20
|
return process.stdout.columns || 120;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Grapheme segmenter for proper Unicode handling (shared instance)
|
|
24
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Truncate a line to maxWidth, preserving ANSI styling through the ellipsis.
|
|
28
|
+
*
|
|
29
|
+
* pi-tui's truncateToWidth adds \x1b[0m before ellipsis which resets all styling,
|
|
30
|
+
* causing background color bleed in the TUI. This implementation tracks active
|
|
31
|
+
* ANSI styles and re-applies them before the ellipsis.
|
|
32
|
+
*
|
|
33
|
+
* Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
|
|
34
|
+
*/
|
|
23
35
|
function truncLine(text: string, maxWidth: number): string {
|
|
24
36
|
if (visibleWidth(text) <= maxWidth) return text;
|
|
25
|
-
|
|
37
|
+
|
|
38
|
+
const targetWidth = maxWidth - 1; // Room for single ellipsis character
|
|
39
|
+
let result = "";
|
|
40
|
+
let currentWidth = 0;
|
|
41
|
+
let activeStyles: string[] = []; // Track ALL active styles (not just last)
|
|
42
|
+
let i = 0;
|
|
43
|
+
|
|
44
|
+
while (i < text.length) {
|
|
45
|
+
// Check for ANSI escape code
|
|
46
|
+
const ansiMatch = text.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
47
|
+
if (ansiMatch) {
|
|
48
|
+
const code = ansiMatch[0];
|
|
49
|
+
result += code;
|
|
50
|
+
|
|
51
|
+
if (code === "\x1b[0m" || code === "\x1b[m") {
|
|
52
|
+
activeStyles = []; // Reset clears all styles
|
|
53
|
+
} else {
|
|
54
|
+
activeStyles.push(code); // Stack styles (bold + color, etc.)
|
|
55
|
+
}
|
|
56
|
+
i += code.length;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find end of non-ANSI text segment
|
|
61
|
+
let end = i;
|
|
62
|
+
while (end < text.length && !text.slice(end).match(/^\x1b\[[0-9;]*m/)) {
|
|
63
|
+
end++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Segment into graphemes for proper Unicode handling
|
|
67
|
+
const textPortion = text.slice(i, end);
|
|
68
|
+
for (const seg of segmenter.segment(textPortion)) {
|
|
69
|
+
const grapheme = seg.segment;
|
|
70
|
+
const graphemeWidth = visibleWidth(grapheme);
|
|
71
|
+
|
|
72
|
+
if (currentWidth + graphemeWidth > targetWidth) {
|
|
73
|
+
// Re-apply all active styles before ellipsis to preserve background/colors
|
|
74
|
+
return result + activeStyles.join("") + "…";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
result += grapheme;
|
|
78
|
+
currentWidth += graphemeWidth;
|
|
79
|
+
}
|
|
80
|
+
i = end;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reached end without exceeding width (shouldn't happen given initial check)
|
|
84
|
+
return result + activeStyles.join("") + "…";
|
|
26
85
|
}
|
|
27
86
|
|
|
28
87
|
// Track last rendered widget state to avoid no-op re-renders
|
|
@@ -319,7 +378,9 @@ export function renderSubagentResult(
|
|
|
319
378
|
c.addChild(new Text(truncLine(stepHeader, w), 0, 0));
|
|
320
379
|
|
|
321
380
|
const taskMaxLen = Math.max(20, w - 12);
|
|
322
|
-
const taskPreview = r.task.
|
|
381
|
+
const taskPreview = r.task.length > taskMaxLen
|
|
382
|
+
? `${r.task.slice(0, taskMaxLen)}...`
|
|
383
|
+
: r.task;
|
|
323
384
|
c.addChild(new Text(truncLine(theme.fg("dim", ` task: ${taskPreview}`), w), 0, 0));
|
|
324
385
|
|
|
325
386
|
const outputTarget = extractOutputTarget(r.task);
|
|
@@ -340,22 +401,31 @@ export function renderSubagentResult(
|
|
|
340
401
|
}
|
|
341
402
|
// Current tool for running step
|
|
342
403
|
if (rProg.currentTool) {
|
|
343
|
-
const
|
|
344
|
-
|
|
404
|
+
const maxToolArgsLen = Math.max(50, w - 20);
|
|
405
|
+
const toolArgsPreview = rProg.currentToolArgs
|
|
406
|
+
? (rProg.currentToolArgs.length > maxToolArgsLen
|
|
407
|
+
? `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`
|
|
408
|
+
: rProg.currentToolArgs)
|
|
409
|
+
: "";
|
|
410
|
+
const toolLine = toolArgsPreview
|
|
411
|
+
? `${rProg.currentTool}: ${toolArgsPreview}`
|
|
345
412
|
: rProg.currentTool;
|
|
346
413
|
c.addChild(new Text(truncLine(theme.fg("warning", ` > ${toolLine}`), w), 0, 0));
|
|
347
414
|
}
|
|
348
415
|
// Recent tools
|
|
349
416
|
if (rProg.recentTools?.length) {
|
|
350
417
|
for (const t of rProg.recentTools.slice(0, 3)) {
|
|
351
|
-
const
|
|
352
|
-
|
|
418
|
+
const maxArgsLen = Math.max(40, w - 30);
|
|
419
|
+
const argsPreview = t.args.length > maxArgsLen
|
|
420
|
+
? `${t.args.slice(0, maxArgsLen)}...`
|
|
421
|
+
: t.args;
|
|
422
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ${t.tool}: ${argsPreview}`), w), 0, 0));
|
|
353
423
|
}
|
|
354
424
|
}
|
|
355
|
-
// Recent output
|
|
425
|
+
// Recent output - let truncLine handle truncation entirely
|
|
356
426
|
const recentLines = (rProg.recentOutput ?? []).slice(-5);
|
|
357
427
|
for (const line of recentLines) {
|
|
358
|
-
c.addChild(new Text(truncLine(theme.fg("dim", ` ${line
|
|
428
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ${line}`), w), 0, 0));
|
|
359
429
|
}
|
|
360
430
|
}
|
|
361
431
|
|
package/schemas.ts
CHANGED
|
@@ -83,7 +83,7 @@ export const SubagentParams = Type.Object({
|
|
|
83
83
|
maxOutput: MaxOutputSchema,
|
|
84
84
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
85
85
|
includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
|
|
86
|
-
share: Type.Optional(Type.Boolean({ description: "
|
|
86
|
+
share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
|
|
87
87
|
sessionDir: Type.Optional(
|
|
88
88
|
Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
|
|
89
89
|
),
|
package/settings.ts
CHANGED
|
@@ -213,7 +213,7 @@ export function resolveStepBehavior(
|
|
|
213
213
|
* Resolve a file path: absolute paths pass through, relative paths get chainDir prepended.
|
|
214
214
|
*/
|
|
215
215
|
function resolveChainPath(filePath: string, chainDir: string): string {
|
|
216
|
-
return path.isAbsolute(filePath) ? filePath :
|
|
216
|
+
return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -243,7 +243,7 @@ export function buildChainInstructions(
|
|
|
243
243
|
|
|
244
244
|
// Progress instructions in suffix (less critical)
|
|
245
245
|
if (behavior.progress) {
|
|
246
|
-
const progressPath =
|
|
246
|
+
const progressPath = path.join(chainDir, "progress.md");
|
|
247
247
|
if (isFirstProgressAgent) {
|
|
248
248
|
suffixParts.push(`Create and maintain progress at: ${progressPath}`);
|
|
249
249
|
} else {
|
|
@@ -288,7 +288,7 @@ export function resolveParallelBehaviors(
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
// Build subdirectory path for this parallel task
|
|
291
|
-
const subdir = `parallel-${stepIndex}
|
|
291
|
+
const subdir = path.join(`parallel-${stepIndex}`, `${taskIndex}-${task.agent}`);
|
|
292
292
|
|
|
293
293
|
// Output: task override > agent default (namespaced) > false
|
|
294
294
|
// Absolute paths pass through unchanged; relative paths get namespaced under subdir
|
|
@@ -299,11 +299,11 @@ export function resolveParallelBehaviors(
|
|
|
299
299
|
} else if (path.isAbsolute(task.output)) {
|
|
300
300
|
output = task.output; // Absolute path: use as-is
|
|
301
301
|
} else {
|
|
302
|
-
output =
|
|
302
|
+
output = path.join(subdir, task.output); // Relative: namespace under subdir
|
|
303
303
|
}
|
|
304
304
|
} else if (config.output) {
|
|
305
305
|
// Agent defaults are always relative, so namespace them
|
|
306
|
-
output =
|
|
306
|
+
output = path.join(subdir, config.output);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// Reads: task override > agent default > false
|
|
@@ -372,15 +372,17 @@ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string
|
|
|
372
372
|
.map((r, i) => {
|
|
373
373
|
const header = `=== Parallel Task ${i + 1} (${r.agent}) ===`;
|
|
374
374
|
const hasTextOutput = Boolean(r.output?.trim());
|
|
375
|
-
const status = r.exitCode
|
|
376
|
-
?
|
|
377
|
-
: r.
|
|
378
|
-
? `⚠️
|
|
379
|
-
:
|
|
380
|
-
? `⚠️
|
|
381
|
-
: !hasTextOutput &&
|
|
382
|
-
?
|
|
383
|
-
:
|
|
375
|
+
const status = r.exitCode === -1
|
|
376
|
+
? "⏭️ SKIPPED"
|
|
377
|
+
: r.exitCode !== 0
|
|
378
|
+
? `⚠️ FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
|
|
379
|
+
: r.error
|
|
380
|
+
? `⚠️ WARNING: ${r.error}`
|
|
381
|
+
: !hasTextOutput && r.outputTargetPath && r.outputTargetExists === false
|
|
382
|
+
? `⚠️ EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
|
|
383
|
+
: !hasTextOutput && !r.outputTargetPath
|
|
384
|
+
? "⚠️ EMPTY OUTPUT (no textual response returned)"
|
|
385
|
+
: "";
|
|
384
386
|
const body = status
|
|
385
387
|
? (hasTextOutput ? `${status}\n${r.output}` : status)
|
|
386
388
|
: r.output;
|
package/skills.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Skill resolution and caching for subagent extension
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
@@ -89,11 +90,31 @@ function getPackageSkillPaths(packageRoot: string): string[] {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
let cachedGlobalNpmRoot: string | null = null;
|
|
94
|
+
|
|
95
|
+
function getGlobalNpmRoot(): string | null {
|
|
96
|
+
if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
|
|
97
|
+
try {
|
|
98
|
+
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
99
|
+
return cachedGlobalNpmRoot;
|
|
100
|
+
} catch {
|
|
101
|
+
cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
function collectPackageSkillPaths(cwd: string): string[] {
|
|
93
107
|
const dirs = [
|
|
94
108
|
path.join(cwd, CONFIG_DIR, "npm", "node_modules"),
|
|
95
109
|
path.join(AGENT_DIR, "npm", "node_modules"),
|
|
96
110
|
];
|
|
111
|
+
|
|
112
|
+
// Add global npm root if available (where pi installs global packages)
|
|
113
|
+
const globalRoot = getGlobalNpmRoot();
|
|
114
|
+
if (globalRoot) {
|
|
115
|
+
dirs.push(globalRoot);
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
const results: string[] = [];
|
|
98
119
|
|
|
99
120
|
for (const dir of dirs) {
|
|
@@ -178,6 +199,8 @@ function inferSkillSource(rawSource: unknown, filePath: string, cwd: string): Sk
|
|
|
178
199
|
const projectRoot = path.resolve(cwd, CONFIG_DIR);
|
|
179
200
|
const isProjectScoped = isWithinPath(filePath, projectRoot);
|
|
180
201
|
const isUserScoped = isWithinPath(filePath, AGENT_DIR);
|
|
202
|
+
const globalRoot = getGlobalNpmRoot();
|
|
203
|
+
const isGlobalPackage = globalRoot ? isWithinPath(filePath, globalRoot) : false;
|
|
181
204
|
|
|
182
205
|
if (source === "project") return "project";
|
|
183
206
|
if (source === "user") return "user";
|
|
@@ -188,7 +211,7 @@ function inferSkillSource(rawSource: unknown, filePath: string, cwd: string): Sk
|
|
|
188
211
|
}
|
|
189
212
|
if (source === "package") {
|
|
190
213
|
if (isProjectScoped) return "project-package";
|
|
191
|
-
if (isUserScoped) return "user-package";
|
|
214
|
+
if (isUserScoped || isGlobalPackage) return "user-package";
|
|
192
215
|
return "unknown";
|
|
193
216
|
}
|
|
194
217
|
if (source === "extension") return "extension";
|
|
@@ -196,6 +219,7 @@ function inferSkillSource(rawSource: unknown, filePath: string, cwd: string): Sk
|
|
|
196
219
|
|
|
197
220
|
if (isProjectScoped) return "project";
|
|
198
221
|
if (isUserScoped) return "user";
|
|
222
|
+
if (isGlobalPackage) return "user-package";
|
|
199
223
|
return "unknown";
|
|
200
224
|
}
|
|
201
225
|
|