@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.14.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.length > 48 ? `${t.task.slice(0, 48)}…` : 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.length > 60 ? `${task.slice(0, 60)}…` : 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
- /** One-line task preview, capped by terminal display columns (including the ellipsis). */
62
- export function formatTaskSummary(task: string): string {
63
- const oneLine = stripVTControlCharacters(task).replace(/\s+/g, " ").trim();
64
- if (visibleWidth(oneLine) <= TASK_SUMMARY_MAX) return oneLine;
78
+ interface KeyFragment {
79
+ text: string;
80
+ index: number;
81
+ }
65
82
 
66
- const prefixMax = TASK_SUMMARY_MAX - visibleWidth(TASK_SUMMARY_ELLIPSIS);
67
- let prefix = "";
68
- let prefixWidth = 0;
69
- for (const { segment } of graphemeSegmenter.segment(oneLine)) {
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 (prefixWidth + segmentWidth > prefixMax) break;
72
- prefix += segment;
73
- prefixWidth += segmentWidth;
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 `${prefix}${TASK_SUMMARY_ELLIPSIS}`;
197
+ return `${takeGraphemes(segments, headMax)}${TASK_SUMMARY_ELLIPSIS}${tailGraphemes(segments, tailMax)}`;
76
198
  }
77
199
 
78
200
  function formatTokens(count: number): string {