@ferris1225/pi-subagents 0.14.0 → 0.15.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 +1 -1
- package/src/index.ts +5 -5
- package/src/monitor.ts +134 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -134,7 +134,7 @@ function formatCompletionBlock(result: SingleResult, maxResultLines: number): st
|
|
|
134
134
|
const fallbackNote = result.modelFallbackFrom
|
|
135
135
|
? ` (model fell back from ${result.modelFallbackFrom} to ${result.model ?? "main-window model"})`
|
|
136
136
|
: "";
|
|
137
|
-
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}`, "", `Task: ${formatTaskSummary(result.task)}`, "", text];
|
|
137
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, "", text];
|
|
138
138
|
if (truncated) {
|
|
139
139
|
// The full text lives on disk so the main agent can read it on demand.
|
|
140
140
|
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent)})`);
|
|
@@ -587,14 +587,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
587
587
|
if (args.tasks && args.tasks.length > 0) {
|
|
588
588
|
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${args.tasks.length})`)}`;
|
|
589
589
|
for (const t of args.tasks.slice(0, 4)) {
|
|
590
|
-
const preview = t.task
|
|
590
|
+
const preview = formatTaskSummary(t.task, 48);
|
|
591
591
|
text += `\n ${theme.fg("accent", t.agent)} ${theme.fg("dim", preview)}`;
|
|
592
592
|
}
|
|
593
593
|
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
594
594
|
return new Text(text, 0, 0);
|
|
595
595
|
}
|
|
596
596
|
const task: string = args.task ?? "";
|
|
597
|
-
const preview = task
|
|
597
|
+
const preview = formatTaskSummary(task, 60);
|
|
598
598
|
return new Text(
|
|
599
599
|
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")} ${theme.fg("dim", preview)}`,
|
|
600
600
|
0,
|
|
@@ -663,9 +663,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
663
663
|
// parent reviewer; summarize() already carries the relationLabel.
|
|
664
664
|
const head = r.groupId ? theme.fg("dim", " ↳ ") : " ";
|
|
665
665
|
const note = r.annotation ? theme.fg("dim", ` · ${r.annotation}`) : "";
|
|
666
|
-
lines.push(truncateToWidth(`${head}${icon} ${monitor.summarize(r)} · ${label}${note}`, width, ""));
|
|
666
|
+
lines.push(truncateToWidth(`${head}${icon} #${r.id} ${monitor.summarize(r)} · ${label}${note}`, width, ""));
|
|
667
667
|
if (r.status === "queued" || r.status === "running") {
|
|
668
|
-
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
668
|
+
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task, Math.max(20, width - 11))}`), width, ""));
|
|
669
669
|
}
|
|
670
670
|
// Activity sits one indent level below the agent name.
|
|
671
671
|
if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
|
package/src/monitor.ts
CHANGED
|
@@ -56,23 +56,145 @@ export interface RunChainMeta {
|
|
|
56
56
|
|
|
57
57
|
const TASK_SUMMARY_MAX = 80;
|
|
58
58
|
const TASK_SUMMARY_ELLIPSIS = "…";
|
|
59
|
+
/** Columns reserved at the END of a truncated summary so the distinguishing
|
|
60
|
+
* keywords (paths, symbols, ...) survive; the head gets the rest. */
|
|
61
|
+
const TASK_SUMMARY_TAIL_MAX = 28;
|
|
62
|
+
/** Tail share of a non-default maxWidth (narrow widgets keep a usable tail). */
|
|
63
|
+
const TASK_SUMMARY_TAIL_SHARE = 0.35;
|
|
64
|
+
const TASK_SUMMARY_TAIL_MIN = 8;
|
|
65
|
+
const TASK_SUMMARY_KEY_SEP = " · ";
|
|
66
|
+
/** kebab/snake words that are task boilerplate, never distinguishing signal. */
|
|
67
|
+
const KEY_FRAGMENT_STOPWORDS = new Set([
|
|
68
|
+
"self-contained",
|
|
69
|
+
"read-only",
|
|
70
|
+
"write-only",
|
|
71
|
+
"auto-fix",
|
|
72
|
+
"re-review",
|
|
73
|
+
"one-line",
|
|
74
|
+
"pre-commit",
|
|
75
|
+
]);
|
|
59
76
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
interface KeyFragment {
|
|
79
|
+
text: string;
|
|
80
|
+
index: number;
|
|
81
|
+
}
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Pull the most distinguishing fragments out of a task: file paths, quoted
|
|
85
|
+
* phrases, camelCase/PascalCase symbols and kebab/snake compounds. Sorted by
|
|
86
|
+
* first occurrence and deduped (a path covers its own sub-fragments). These
|
|
87
|
+
* are what make parallel tasks of the same agent look different.
|
|
88
|
+
*/
|
|
89
|
+
export function extractKeyFragments(text: string): string[] {
|
|
90
|
+
const fragments: KeyFragment[] = [];
|
|
91
|
+
const add = (re: RegExp, group = 0): void => {
|
|
92
|
+
for (const m of text.matchAll(re)) {
|
|
93
|
+
const g = m[group];
|
|
94
|
+
if (g === undefined) continue;
|
|
95
|
+
fragments.push({ text: g, index: m.index ?? 0 });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// Quoted phrases first (highest signal).
|
|
99
|
+
add(/["'`]([^"'`]{4,60})["'`]/g, 1);
|
|
100
|
+
// Paths with a known extension (src/index.ts, build/out.js.map).
|
|
101
|
+
add(/(?<![A-Za-z0-9_.-])[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,5}(?![A-Za-z0-9_.-])/g);
|
|
102
|
+
// Paths with a slash but no extension (src/components, .github/workflows).
|
|
103
|
+
add(/(?<![A-Za-z0-9_.-])[A-Za-z0-9_.-]+(?:[\\/][A-Za-z0-9_.-]+)+(?![\\/])/g);
|
|
104
|
+
// camelCase / PascalCase identifiers (function or type names).
|
|
105
|
+
add(/\b[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*\b/g);
|
|
106
|
+
// snake_case / kebab-case compound words.
|
|
107
|
+
add(/\b[a-z][a-z0-9]+[-_][a-z0-9][a-z0-9_-]*\b/g);
|
|
108
|
+
|
|
109
|
+
fragments.sort((a, b) => a.index - b.index);
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
const out: string[] = [];
|
|
112
|
+
for (const f of fragments) {
|
|
113
|
+
const t = f.text.trim();
|
|
114
|
+
if (t.length < 4 || KEY_FRAGMENT_STOPWORDS.has(t)) continue;
|
|
115
|
+
if (seen.has(t)) continue;
|
|
116
|
+
// A longer fragment (the full path) covers its own sub-fragments.
|
|
117
|
+
if (out.some((o) => o.includes(t) || t.includes(o))) continue;
|
|
118
|
+
seen.add(t);
|
|
119
|
+
out.push(t);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function takeGraphemes(segments: string[], maxWidth: number): string {
|
|
125
|
+
let width = 0;
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
for (const segment of segments) {
|
|
70
128
|
const segmentWidth = visibleWidth(segment);
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
if (width + segmentWidth > maxWidth) break;
|
|
130
|
+
out.push(segment);
|
|
131
|
+
width += segmentWidth;
|
|
132
|
+
}
|
|
133
|
+
return out.join("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function tailGraphemes(segments: string[], maxWidth: number): string {
|
|
137
|
+
let width = 0;
|
|
138
|
+
const tail: string[] = [];
|
|
139
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
140
|
+
const segmentWidth = visibleWidth(segments[i]);
|
|
141
|
+
if (width + segmentWidth > maxWidth) break;
|
|
142
|
+
tail.unshift(segments[i]);
|
|
143
|
+
width += segmentWidth;
|
|
144
|
+
}
|
|
145
|
+
return tail.join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One-line task preview, capped by `maxWidth` display columns (default 80).
|
|
150
|
+
* `keysOnly` (default): extracted key fragments (paths, quoted phrases,
|
|
151
|
+
* symbols) are shown bare — the agent name is already displayed next to the
|
|
152
|
+
* task line, so templated prose ("explore: trace how ...") adds nothing.
|
|
153
|
+
* `keysOnly: false` keeps the prose as `head…tail` (used for completion
|
|
154
|
+
* messages, where the Task line is the reader's only context).
|
|
155
|
+
* Grapheme-safe — CJK, ZWJ emoji and combining sequences are never split.
|
|
156
|
+
*/
|
|
157
|
+
export function formatTaskSummary(task: string, maxWidth: number = TASK_SUMMARY_MAX, keysOnly = true): string {
|
|
158
|
+
const oneLine = stripVTControlCharacters(task).replace(/\s+/g, " ").trim();
|
|
159
|
+
if (maxWidth <= 0 || visibleWidth(oneLine) <= maxWidth) return oneLine;
|
|
160
|
+
|
|
161
|
+
const segments = [...graphemeSegmenter.segment(oneLine)].map((s) => s.segment);
|
|
162
|
+
const ellipsisWidth = visibleWidth(TASK_SUMMARY_ELLIPSIS);
|
|
163
|
+
|
|
164
|
+
if (keysOnly) {
|
|
165
|
+
const fragments = extractKeyFragments(oneLine);
|
|
166
|
+
if (fragments.length > 0) {
|
|
167
|
+
const keyMax = maxWidth - 1;
|
|
168
|
+
let keys = "";
|
|
169
|
+
for (const fragment of fragments) {
|
|
170
|
+
const piece = keys ? `${TASK_SUMMARY_KEY_SEP}${fragment}` : fragment;
|
|
171
|
+
const total = keys + piece;
|
|
172
|
+
if (visibleWidth(total) > keyMax) {
|
|
173
|
+
// Budget exhausted: keep what fits, unless nothing fits yet.
|
|
174
|
+
if (!keys) {
|
|
175
|
+
// A single over-long fragment keeps its tail
|
|
176
|
+
// (extension/symbol) and is prefixed with the ellipsis.
|
|
177
|
+
const fragmentSegments = [...graphemeSegmenter.segment(piece)].map((s) => s.segment);
|
|
178
|
+
keys = `${TASK_SUMMARY_ELLIPSIS}${tailGraphemes(fragmentSegments, keyMax - ellipsisWidth)}`;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
keys = total;
|
|
183
|
+
}
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// No distinctive fragments (or prose mode): fall back to head…tail.
|
|
189
|
+
const tailMax = Math.max(
|
|
190
|
+
TASK_SUMMARY_TAIL_MIN,
|
|
191
|
+
Math.min(TASK_SUMMARY_TAIL_MAX, Math.round(maxWidth * TASK_SUMMARY_TAIL_SHARE)),
|
|
192
|
+
);
|
|
193
|
+
const headMax = maxWidth - ellipsisWidth - tailMax;
|
|
194
|
+
if (headMax <= 0) {
|
|
195
|
+
return `${TASK_SUMMARY_ELLIPSIS}${tailGraphemes(segments, maxWidth - ellipsisWidth)}`;
|
|
74
196
|
}
|
|
75
|
-
return `${
|
|
197
|
+
return `${takeGraphemes(segments, headMax)}${TASK_SUMMARY_ELLIPSIS}${tailGraphemes(segments, tailMax)}`;
|
|
76
198
|
}
|
|
77
199
|
|
|
78
200
|
function formatTokens(count: number): string {
|