pi-subagents 0.18.0 → 0.19.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/execution.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
6
7
  import type { Message } from "@mariozechner/pi-ai";
7
8
  import type { AgentConfig } from "./agents.ts";
8
9
  import {
@@ -242,13 +243,11 @@ async function runSingleAttempt(
242
243
  };
243
244
 
244
245
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
245
- if (!options.allowIntercomDetach || detached || processClosed) return;
246
+ if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
246
247
  if (!payload || typeof payload !== "object") return;
247
248
  const requestId = (payload as { requestId?: unknown }).requestId;
248
249
  if (typeof requestId !== "string" || requestId.length === 0) return;
249
- const accepted = intercomStarted;
250
- options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted });
251
- if (!accepted) return;
250
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
252
251
  detachForIntercom();
253
252
  });
254
253
 
@@ -726,12 +725,11 @@ export async function runSync(
726
725
  if (truncationResult.truncated) result.truncation = truncationResult;
727
726
  }
728
727
 
729
- if (shareEnabled) {
730
- const sessionFile = options.sessionFile
731
- ?? (options.sessionDir ? findLatestSessionFile(options.sessionDir) : null);
732
- if (sessionFile) {
733
- result.sessionFile = sessionFile;
734
- }
728
+ if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
729
+ result.sessionFile = options.sessionFile;
730
+ } else if (shareEnabled && options.sessionDir) {
731
+ const sessionFile = findLatestSessionFile(options.sessionDir);
732
+ if (sessionFile) result.sessionFile = sessionFile;
735
733
  }
736
734
 
737
735
  return result;
package/index.ts CHANGED
@@ -21,7 +21,7 @@ import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWi
21
21
  import { discoverAgents } from "./agents.ts";
22
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.ts";
23
23
  import { cleanupOldChainDirs } from "./settings.ts";
24
- import { renderWidget, renderSubagentResult } from "./render.ts";
24
+ import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "./render.ts";
25
25
  import { SubagentParams } from "./schemas.ts";
26
26
  import { createSubagentExecutor } from "./subagent-executor.ts";
27
27
  import { createAsyncJobTracker } from "./async-job-tracker.ts";
@@ -32,7 +32,8 @@ import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge
32
32
  import { registerSlashSubagentBridge } from "./slash-bridge.ts";
33
33
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.ts";
34
34
  import { inspectSubagentStatus } from "./run-status.ts";
35
- import registerSubagentNotify from "./notify.ts";
35
+ import registerSubagentNotify, { type SubagentNotifyDetails } from "./notify.ts";
36
+ import { formatDuration, shortenPath } from "./formatters.ts";
36
37
  import {
37
38
  type ControlEvent,
38
39
  type Details,
@@ -130,12 +131,15 @@ function createSlashResultComponent(
130
131
  details: SlashMessageDetails,
131
132
  options: { expanded: boolean },
132
133
  theme: ExtensionContext["ui"]["theme"],
134
+ requestRender: () => void,
133
135
  ): Container {
134
136
  const container = new Container();
137
+ const animationState: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> } = {};
135
138
  let lastVersion = -1;
136
139
  container.render = (width: number): string[] => {
137
140
  const snapshot = getSlashRenderableSnapshot(details);
138
- if (snapshot.version !== lastVersion) {
141
+ syncResultAnimation(snapshot.result, { state: animationState, invalidate: requestRender });
142
+ if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
139
143
  lastVersion = snapshot.version;
140
144
  rebuildSlashResultContainer(container, snapshot.result, options, theme);
141
145
  }
@@ -162,6 +166,38 @@ function formatSubagentControlNotice(details: SubagentControlMessageDetails, con
162
166
  return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
163
167
  }
164
168
 
169
+ function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
170
+ const lines = content.split("\n");
171
+ const header = lines[0] ?? "";
172
+ const match = header.match(/^Background task (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
173
+ if (!match) return undefined;
174
+ const body = lines.slice(2);
175
+ let sessionIndex = -1;
176
+ for (let i = body.length - 1; i >= 1; i--) {
177
+ if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
178
+ sessionIndex = i;
179
+ break;
180
+ }
181
+ }
182
+ const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
183
+ const resultLines = sessionIndex >= 0 ? body.slice(0, sessionIndex) : body;
184
+ const resultPreview = resultLines.join("\n").trim() || "(no output)";
185
+ let sessionLabel: string | undefined;
186
+ let sessionValue: string | undefined;
187
+ if (sessionLine) {
188
+ const separator = sessionLine.indexOf(":");
189
+ sessionLabel = sessionLine.slice(0, separator).toLowerCase();
190
+ sessionValue = sessionLine.slice(separator + 1).trim();
191
+ }
192
+ return {
193
+ agent: match[2]!,
194
+ status: match[1] as SubagentNotifyDetails["status"],
195
+ ...(match[3] ? { taskInfo: match[3] } : {}),
196
+ resultPreview,
197
+ ...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
198
+ };
199
+ }
200
+
165
201
  class SubagentControlNoticeComponent implements Component {
166
202
  constructor(
167
203
  private readonly details: SubagentControlMessageDetails,
@@ -191,6 +227,17 @@ class SubagentControlNoticeComponent implements Component {
191
227
  }
192
228
 
193
229
  export default function registerSubagentExtension(pi: ExtensionAPI): void {
230
+ const globalStore = globalThis as Record<string, unknown>;
231
+ const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
232
+ const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
233
+ if (typeof previousRuntimeCleanup === "function") {
234
+ try {
235
+ previousRuntimeCleanup();
236
+ } catch {
237
+ // Best effort cleanup for stale timers from an older reload.
238
+ }
239
+ }
240
+
194
241
  ensureAccessibleDir(RESULTS_DIR);
195
242
  ensureAccessibleDir(ASYNC_DIR);
196
243
  cleanupOldChainDirs();
@@ -227,6 +274,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
227
274
  startResultWatcher();
228
275
  primeExistingResults();
229
276
 
277
+ const runtimeCleanup = () => {
278
+ stopWidgetAnimation();
279
+ stopResultAnimations();
280
+ if (state.poller) {
281
+ clearInterval(state.poller);
282
+ state.poller = null;
283
+ }
284
+ };
285
+ globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
286
+
230
287
  const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
231
288
  const executor = createSubagentExecutor({
232
289
  pi,
@@ -242,7 +299,37 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
242
299
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
243
300
  const details = resolveSlashMessageDetails(message.details);
244
301
  if (!details) return undefined;
245
- return createSlashResultComponent(details, options, theme);
302
+ return createSlashResultComponent(details, options, theme, () => state.lastUiContext?.ui.requestRender?.());
303
+ });
304
+
305
+ pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
306
+ const content = typeof message.content === "string" ? message.content : "";
307
+ const details = (message.details as SubagentNotifyDetails | undefined) ?? parseSubagentNotifyContent(content);
308
+ if (!details) return new Text(content, 0, 0);
309
+ const icon = details.status === "completed"
310
+ ? theme.fg("success", "✓")
311
+ : details.status === "paused"
312
+ ? theme.fg("warning", "■")
313
+ : theme.fg("error", "✗");
314
+ const parts: string[] = [];
315
+ if (details.taskInfo) parts.push(details.taskInfo);
316
+ if (details.durationMs !== undefined) parts.push(formatDuration(details.durationMs));
317
+ let text = `${icon} ${theme.bold(details.agent)} ${theme.fg("dim", details.status)}`;
318
+ if (parts.length > 0) text += ` ${theme.fg("dim", "·")} ${parts.map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `)}`;
319
+ const trimmedPreview = details.resultPreview.trim();
320
+ const previewLines = options.expanded
321
+ ? trimmedPreview.split("\n").filter((line) => line.trim())
322
+ : [trimmedPreview.split("\n", 1)[0] ?? ""].filter((line) => line.trim());
323
+ for (const line of previewLines.length > 0 ? previewLines : ["(no output)"]) {
324
+ text += `\n ${theme.fg("dim", `⎿ ${line}`)}`;
325
+ }
326
+ if (!options.expanded && trimmedPreview.includes("\n")) {
327
+ text += `\n ${theme.fg("dim", "Ctrl+O full notification")}`;
328
+ }
329
+ if (details.sessionLabel && details.sessionValue) {
330
+ text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
331
+ }
332
+ return new Text(text, 0, 0);
246
333
  });
247
334
 
248
335
  pi.registerMessageRenderer<SubagentControlMessageDetails>(SUBAGENT_CONTROL_MESSAGE_TYPE, (message, _options, theme) => {
@@ -311,9 +398,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
311
398
  description: `Delegate to subagents or manage agent definitions.
312
399
 
313
400
  EXECUTION (use exactly ONE mode):
314
- SINGLE: { agent, task } - one task
315
- CHAIN: { chain: [{agent:"scout"}, {parallel:[{agent:"worker",count:3}]}] } - sequential pipeline with optional parallel fan-out
316
- PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
401
+ Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
402
+ SINGLE: { agent, task? } - one task; omit task for self-contained agents
403
+ CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
404
+ • PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
317
405
  • Optional context: { context: "fresh" | "fork" } (default: "fresh")
318
406
 
319
407
  CHAIN TEMPLATE VARIABLES (use in task strings):
@@ -321,10 +409,10 @@ CHAIN TEMPLATE VARIABLES (use in task strings):
321
409
  • {previous} - Text response from the previous step (empty for first step)
322
410
  • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
323
411
 
324
- Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
412
+ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
325
413
 
326
414
  MANAGEMENT (use action field, omit agent/task/chain/tasks):
327
- • { action: "list" } - discover agents/chains
415
+ • { action: "list" } - discover executable agents/chains and any disabled builtins
328
416
  • { action: "get", agent: "name" } - full detail
329
417
  • { action: "create", config: { name, systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, ... } }
330
418
  • { action: "update", agent: "name", config: { ... } } - merge
@@ -370,7 +458,8 @@ CONTROL:
370
458
  );
371
459
  },
372
460
 
373
- renderResult(result, options, theme) {
461
+ renderResult(result, options, theme, context) {
462
+ syncResultAnimation(result, context);
374
463
  return renderSubagentResult(result, options, theme);
375
464
  },
376
465
 
@@ -381,7 +470,6 @@ CONTROL:
381
470
 
382
471
  const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
383
472
  const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
384
- const globalStore = globalThis as Record<string, unknown>;
385
473
  const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
386
474
  if (Array.isArray(previousEventUnsubscribes)) {
387
475
  for (const unsubscribe of previousEventUnsubscribes) {
@@ -480,6 +568,11 @@ CONTROL:
480
568
  slashBridge.dispose();
481
569
  promptTemplateBridge.cancelAll();
482
570
  promptTemplateBridge.dispose();
571
+ stopWidgetAnimation();
572
+ stopResultAnimations();
573
+ if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
574
+ delete globalStore[runtimeCleanupStoreKey];
575
+ }
483
576
  if (state.lastUiContext?.hasUI) {
484
577
  state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
485
578
  }
@@ -8,14 +8,14 @@ const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "
8
8
  const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
9
9
  const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
10
10
  const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
11
- const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
11
+ export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
12
12
  const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
13
13
 
14
14
  Use intercom only for coordination with the orchestrator session "{orchestratorTarget}".
15
15
  - Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
16
- - Need to report progress or a completion handoff: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
16
+ - Blocked or explicitly asked to send progress: intercom({ action: "send", to: "{orchestratorTarget}", message: "UPDATE: <summary>" })
17
17
 
18
- If no upstream coordination is needed, continue the task normally and return a focused task result.`;
18
+ Do not send routine completion handoffs through intercom. If no coordination is needed, return a focused task result.`;
19
19
 
20
20
  export interface IntercomBridgeState {
21
21
  active: boolean;
package/notify.ts CHANGED
@@ -12,6 +12,16 @@ interface ChainStepResult {
12
12
  success: boolean;
13
13
  }
14
14
 
15
+ export interface SubagentNotifyDetails {
16
+ agent: string;
17
+ status: "completed" | "failed" | "paused";
18
+ taskInfo?: string;
19
+ resultPreview: string;
20
+ durationMs?: number;
21
+ sessionLabel?: string;
22
+ sessionValue?: string;
23
+ }
24
+
15
25
  interface SubagentResult {
16
26
  id: string | null;
17
27
  agent: string | null;
@@ -20,6 +30,7 @@ interface SubagentResult {
20
30
  exitCode?: number;
21
31
  state?: string;
22
32
  timestamp: number;
33
+ durationMs?: number;
23
34
  sessionFile?: string;
24
35
  shareUrl?: string;
25
36
  gistUrl?: string;
@@ -64,22 +75,21 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
64
75
  ? ` (${result.taskIndex + 1}/${result.totalTasks})`
65
76
  : "";
66
77
 
67
- const extra: string[] = [];
68
- if (result.shareUrl) {
69
- extra.push(`Session: ${result.shareUrl}`);
70
- } else if (result.shareError) {
71
- extra.push(`Session share error: ${result.shareError}`);
72
- } else if (result.sessionFile) {
73
- extra.push(`Session file: ${result.sessionFile}`);
74
- }
78
+ const sessionLine = result.shareUrl
79
+ ? `Session: ${result.shareUrl}`
80
+ : result.shareError
81
+ ? `Session share error: ${result.shareError}`
82
+ : result.sessionFile
83
+ ? `Session file: ${result.sessionFile}`
84
+ : undefined;
75
85
 
76
86
  const displaySummary = summary.trim() ? summary : "(no output)";
77
87
  const content = [
78
88
  `Background task ${status}: **${agent}**${taskInfo}`,
79
89
  "",
80
90
  displaySummary,
81
- extra.length ? "" : undefined,
82
- extra.length ? extra.join("\n") : undefined,
91
+ sessionLine ? "" : undefined,
92
+ sessionLine,
83
93
  ]
84
94
  .filter((line) => line !== undefined)
85
95
  .join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  "*.mjs",
32
32
  "agents/",
33
33
  "skills/**/*",
34
+ "prompts/**/*",
34
35
  "README.md",
35
36
  "CHANGELOG.md"
36
37
  ],
@@ -46,6 +47,9 @@
46
47
  ],
47
48
  "skills": [
48
49
  "./skills"
50
+ ],
51
+ "prompts": [
52
+ "./prompts"
49
53
  ]
50
54
  },
51
55
  "peerDependencies": {
package/pi-args.ts CHANGED
@@ -43,6 +43,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
43
43
  const args = [...input.baseArgs];
44
44
 
45
45
  if (input.sessionFile) {
46
+ fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
46
47
  args.push("--session", input.sessionFile);
47
48
  } else {
48
49
  if (!input.sessionEnabled) {
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Parallel subagents review
3
+ ---
4
+ Great. Now let's launch parallel reviewers to conduct an adversarial review.
5
+
6
+ Important: launch reviewers with fresh context, not forked context. Reviewers should inspect the repository and current diff directly from files and commands, without inheriting the main agent chat. Use forked context only if I explicitly ask for it.
7
+
8
+ $@