@ferris1225/pi-subagents 0.8.0 → 0.9.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.8.0",
3
+ "version": "0.9.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",
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "^0.83.0",
44
44
  "@earendil-works/pi-tui": "^0.83.0",
45
45
  "@types/node": "^22.10.0",
46
- "typebox": "^1.3.7",
46
+ "typebox": "^1.3.9",
47
47
  "typescript": "^5.9.0",
48
48
  "vitest": "^4.1.0"
49
49
  },
package/src/index.ts CHANGED
@@ -33,17 +33,24 @@ import {
33
33
  type SubagentLiveEvent,
34
34
  type UsageStats,
35
35
  } from "./spawn.ts";
36
- import { formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
36
+ import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
37
+
38
+ const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
37
39
 
38
40
  const TaskItem = Type.Object({
39
41
  agent: Type.String({ description: "Name of the agent to invoke" }),
40
- task: Type.String({ description: "Self-contained task to delegate (the agent has no memory of this conversation)" }),
42
+ task: Type.String({
43
+ ...NON_BLANK_TASK_OPTIONS,
44
+ description: "Self-contained task to delegate (the agent has no memory of this conversation)",
45
+ }),
41
46
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
42
47
  });
43
48
 
44
49
  const SubagentParams = Type.Object({
45
50
  agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
46
- task: Type.Optional(Type.String({ description: "Self-contained task to delegate (single mode)" })),
51
+ task: Type.Optional(
52
+ Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
53
+ ),
47
54
  tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
48
55
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
49
56
  });
@@ -240,7 +247,7 @@ export default function (pi: ExtensionAPI): void {
240
247
  }));
241
248
 
242
249
  const hasTasks = (params.tasks?.length ?? 0) > 0;
243
- const hasSingle = Boolean(params.agent && params.task);
250
+ const hasSingle = Boolean(params.agent) && params.task !== undefined;
244
251
 
245
252
  const makeDetails =
246
253
  (mode: "single" | "parallel", background = false) =>
@@ -260,12 +267,37 @@ export default function (pi: ExtensionAPI): void {
260
267
  };
261
268
  }
262
269
 
270
+ if (hasTasks) {
271
+ const blankTaskIndex = params.tasks?.findIndex(({ task }) => task.trim().length === 0) ?? -1;
272
+ if (blankTaskIndex !== -1) {
273
+ return {
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: `Invalid parameters. tasks[${blankTaskIndex}].task must contain at least one non-whitespace character. No background tasks were started. Enabled agents: ${catalog}.`,
278
+ },
279
+ ],
280
+ details: makeDetails("parallel")([]),
281
+ };
282
+ }
283
+ } else if (params.task?.trim().length === 0) {
284
+ return {
285
+ content: [
286
+ {
287
+ type: "text",
288
+ text: `Invalid parameters. task must contain at least one non-whitespace character. Enabled agents: ${catalog}.`,
289
+ },
290
+ ],
291
+ details: makeDetails("single")([]),
292
+ };
293
+ }
294
+
263
295
  const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
264
296
  const agent = agents.find((candidate) => candidate.name === agentName);
265
297
  if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
266
298
 
267
299
  const pending = queuedResult(agent, task);
268
- const runId = monitor.addRun(agent.name, agent.model);
300
+ const runId = monitor.addRun(agent.name, task, agent.model);
269
301
  const onLive = makeLiveHandler(runId);
270
302
 
271
303
  backgroundQueue.enqueue(
@@ -301,7 +333,7 @@ export default function (pi: ExtensionAPI): void {
301
333
  pi.sendMessage(
302
334
  {
303
335
  customType: "subagent-result",
304
- content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${getResultOutput(result)}`,
336
+ content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\nTask: ${formatTaskSummary(result.task)}\n\n${getResultOutput(result)}`,
305
337
  display: true,
306
338
  },
307
339
  // The result is both durable context and a wake-up signal. If the
@@ -318,17 +350,17 @@ export default function (pi: ExtensionAPI): void {
318
350
  // Sub-agents intentionally detach from the foreground turn. This makes the
319
351
  // editor available immediately; completion messages later wake the main agent.
320
352
  if (params.tasks && params.tasks.length > 0) {
321
- if (params.tasks.length > config.maxParallelTasks) {
322
- return {
323
- content: [
324
- {
325
- type: "text",
326
- text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxParallelTasks} (configurable via /subagents-setup).`,
327
- },
328
- ],
329
- details: makeDetails("parallel", true)([]),
330
- };
331
- }
353
+ if (params.tasks.length > config.maxParallelTasks) {
354
+ return {
355
+ content: [
356
+ {
357
+ type: "text",
358
+ text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxParallelTasks} (configurable via /subagents-setup).`,
359
+ },
360
+ ],
361
+ details: makeDetails("parallel", true)([]),
362
+ };
363
+ }
332
364
 
333
365
  const results = params.tasks.map((task) => startBackground(task.agent, task.task, task.cwd));
334
366
  const started = results.filter((result) => result.exitCode === -1).length;
@@ -442,6 +474,9 @@ export default function (pi: ExtensionAPI): void {
442
474
  const icon = statusIcon(r.status, theme);
443
475
  const label = theme.fg(statusColor(r.status), statusLabel(r.status));
444
476
  lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
477
+ if (r.status === "queued" || r.status === "running") {
478
+ lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
479
+ }
445
480
  // Activity sits one indent level below the agent name.
446
481
  if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
447
482
  }
package/src/monitor.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  * conversation, so a stale "done" row must not linger in the widget.
11
11
  */
12
12
 
13
+ import { stripVTControlCharacters } from "node:util";
13
14
  import type { Theme } from "@earendil-works/pi-coding-agent";
15
+ import { visibleWidth } from "@earendil-works/pi-tui";
14
16
  import type { UsageStats } from "./spawn.ts";
15
17
 
16
18
  // ---------------------------------------------------------------------------
@@ -22,6 +24,7 @@ export type RunStatus = "queued" | "running" | "done" | "failed";
22
24
  export interface RunView {
23
25
  id: number;
24
26
  agent: string;
27
+ task: string;
25
28
  model?: string;
26
29
  status: RunStatus;
27
30
  usage: UsageStats;
@@ -37,6 +40,27 @@ export interface RunView {
37
40
  // Formatting helpers
38
41
  // ---------------------------------------------------------------------------
39
42
 
43
+ const TASK_SUMMARY_MAX = 80;
44
+ const TASK_SUMMARY_ELLIPSIS = "…";
45
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
46
+
47
+ /** One-line task preview, capped by terminal display columns (including the ellipsis). */
48
+ export function formatTaskSummary(task: string): string {
49
+ const oneLine = stripVTControlCharacters(task).replace(/\s+/g, " ").trim();
50
+ if (visibleWidth(oneLine) <= TASK_SUMMARY_MAX) return oneLine;
51
+
52
+ const prefixMax = TASK_SUMMARY_MAX - visibleWidth(TASK_SUMMARY_ELLIPSIS);
53
+ let prefix = "";
54
+ let prefixWidth = 0;
55
+ for (const { segment } of graphemeSegmenter.segment(oneLine)) {
56
+ const segmentWidth = visibleWidth(segment);
57
+ if (prefixWidth + segmentWidth > prefixMax) break;
58
+ prefix += segment;
59
+ prefixWidth += segmentWidth;
60
+ }
61
+ return `${prefix}${TASK_SUMMARY_ELLIPSIS}`;
62
+ }
63
+
40
64
  function formatTokens(count: number): string {
41
65
  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
42
66
  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
@@ -142,11 +166,12 @@ export class MonitorStore {
142
166
  this.notify();
143
167
  }
144
168
 
145
- addRun(agent: string, model?: string): number {
169
+ addRun(agent: string, task: string, model?: string): number {
146
170
  const id = this.nextId++;
147
171
  this.runs.push({
148
172
  id,
149
173
  agent,
174
+ task,
150
175
  model,
151
176
  status: "queued",
152
177
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },