pi-subagents 0.17.5 → 0.18.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
@@ -28,8 +28,10 @@ import {
28
28
  import {
29
29
  DEFAULT_CONTROL_CONFIG,
30
30
  buildControlEvent,
31
+ claimControlNotification,
31
32
  deriveActivityState,
32
33
  shouldEmitControlEvent,
34
+ shouldNotifyControlEvent,
33
35
  } from "./subagent-control.ts";
34
36
  import {
35
37
  getFinalOutput,
@@ -135,6 +137,7 @@ async function runSingleAttempt(
135
137
  systemPrompt: shared.systemPrompt,
136
138
  mcpDirectTools: agent.mcpDirectTools,
137
139
  promptFileStem: agent.name,
140
+ intercomSessionName: options.intercomSessionName,
138
141
  });
139
142
 
140
143
  const result: SingleResult = {
@@ -150,8 +153,6 @@ async function runSingleAttempt(
150
153
  };
151
154
  const startTime = Date.now();
152
155
  const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
153
- let hasSeenActivity = false;
154
- let pausedByInterrupt = false;
155
156
  let interruptedByControl = false;
156
157
  const allControlEvents: ControlEvent[] = [];
157
158
  let pendingControlEvents: ControlEvent[] = [];
@@ -160,7 +161,6 @@ async function runSingleAttempt(
160
161
  index: options.index ?? 0,
161
162
  agent: agent.name,
162
163
  status: "running",
163
- activityState: controlConfig.enabled ? "starting" : undefined,
164
164
  task,
165
165
  skills: shared.resolvedSkillNames,
166
166
  recentTools: [],
@@ -274,34 +274,39 @@ async function runSingleAttempt(
274
274
  return events;
275
275
  };
276
276
 
277
+ const emittedControlEventKeys = new Set<string>();
278
+ const emitControlEvent = (event: ControlEvent) => {
279
+ if (shouldNotifyControlEvent(controlConfig, event) && !claimControlNotification(controlConfig, event, emittedControlEventKeys)) return;
280
+ allControlEvents.push(event);
281
+ pendingControlEvents.push(event);
282
+ options.onControlEvent?.(event);
283
+ };
284
+
277
285
  const updateActivityState = (now: number): boolean => {
278
286
  const next = deriveActivityState({
279
287
  config: controlConfig,
280
288
  startedAt: startTime,
281
289
  lastActivityAt: progress.lastActivityAt,
282
- hasSeenActivity,
283
- paused: pausedByInterrupt,
284
290
  now,
285
291
  });
286
- if (!next || next === progress.activityState) return false;
292
+ if (next === progress.activityState) return false;
287
293
  const previous = progress.activityState;
288
294
  progress.activityState = next;
289
295
  if (shouldEmitControlEvent(controlConfig, previous, next)) {
290
- const event = buildControlEvent({
296
+ emitControlEvent(buildControlEvent({
291
297
  from: previous,
292
298
  to: next,
293
299
  runId: options.runId,
294
300
  agent: agent.name,
295
301
  index: options.index,
296
302
  ts: now,
297
- });
298
- allControlEvents.push(event);
299
- pendingControlEvents.push(event);
300
- options.onControlEvent?.(event);
303
+ lastActivityAt: progress.lastActivityAt,
304
+ }));
301
305
  }
302
306
  return true;
303
307
  };
304
308
 
309
+
305
310
  const emitUpdateSnapshot = (text: string) => {
306
311
  if (!options.onUpdate || processClosed) return;
307
312
  const progressSnapshot = snapshotProgress(progress);
@@ -338,7 +343,6 @@ async function runSingleAttempt(
338
343
  const now = Date.now();
339
344
  progress.durationMs = now - startTime;
340
345
  progress.lastActivityAt = now;
341
- hasSeenActivity = true;
342
346
  updateActivityState(now);
343
347
 
344
348
  if (evt.type === "tool_execution_start") {
@@ -481,27 +485,11 @@ async function runSingleAttempt(
481
485
  const interrupt = () => {
482
486
  if (processClosed || detached || settled) return;
483
487
  interruptedByControl = true;
484
- pausedByInterrupt = true;
485
488
  progress.status = "running";
486
489
  progress.durationMs = Date.now() - startTime;
487
490
  result.interrupted = true;
488
491
  result.finalOutput = "Interrupted. Waiting for explicit next action.";
489
- const now = Date.now();
490
- const previous = progress.activityState;
491
- progress.activityState = "paused";
492
- if (shouldEmitControlEvent(controlConfig, previous, "paused")) {
493
- const event = buildControlEvent({
494
- from: previous,
495
- to: "paused",
496
- runId: options.runId,
497
- agent: agent.name,
498
- index: options.index,
499
- ts: now,
500
- });
501
- allControlEvents.push(event);
502
- pendingControlEvents.push(event);
503
- options.onControlEvent?.(event);
504
- }
492
+ progress.activityState = undefined;
505
493
  fireUpdate();
506
494
  trySignalChild(proc, "SIGINT");
507
495
  setTimeout(() => {
@@ -523,7 +511,7 @@ async function runSingleAttempt(
523
511
  result.error = undefined;
524
512
  result.finalOutput = result.finalOutput || "Interrupted. Waiting for explicit next action.";
525
513
  result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
526
- progress.activityState = "paused";
514
+ progress.activityState = undefined;
527
515
  progress.durationMs = Date.now() - startTime;
528
516
  result.progressSummary = {
529
517
  toolCount: progress.toolCount,
package/index.ts CHANGED
@@ -17,22 +17,24 @@ import * as os from "node:os";
17
17
  import * as path from "node:path";
18
18
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
19
19
  import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
20
- import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
20
+ import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@mariozechner/pi-tui";
21
21
  import { discoverAgents } from "./agents.ts";
22
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.ts";
23
23
  import { cleanupOldChainDirs } from "./settings.ts";
24
24
  import { renderWidget, renderSubagentResult } from "./render.ts";
25
- import { SubagentParams, StatusParams } from "./schemas.ts";
26
- import { findByPrefix, readStatus } from "./utils.ts";
25
+ import { SubagentParams } from "./schemas.ts";
27
26
  import { createSubagentExecutor } from "./subagent-executor.ts";
28
27
  import { createAsyncJobTracker } from "./async-job-tracker.ts";
28
+ import { controlNotificationKey, formatControlNoticeMessage } from "./subagent-control.ts";
29
29
  import { createResultWatcher } from "./result-watcher.ts";
30
30
  import { registerSlashCommands } from "./slash-commands.ts";
31
31
  import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.ts";
32
32
  import { registerSlashSubagentBridge } from "./slash-bridge.ts";
33
33
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.ts";
34
- import { formatAsyncRunList, listAsyncRuns } from "./async-status.ts";
34
+ import { inspectSubagentStatus } from "./run-status.ts";
35
+ import registerSubagentNotify from "./notify.ts";
35
36
  import {
37
+ type ControlEvent,
36
38
  type Details,
37
39
  type ExtensionConfig,
38
40
  type SubagentState,
@@ -40,6 +42,9 @@ import {
40
42
  DEFAULT_ARTIFACT_CONFIG,
41
43
  RESULTS_DIR,
42
44
  SLASH_RESULT_TYPE,
45
+ SUBAGENT_ASYNC_COMPLETE_EVENT,
46
+ SUBAGENT_ASYNC_STARTED_EVENT,
47
+ SUBAGENT_CONTROL_EVENT,
43
48
  WIDGET_KEY,
44
49
  } from "./types.ts";
45
50
 
@@ -139,6 +144,52 @@ function createSlashResultComponent(
139
144
  return container;
140
145
  }
141
146
 
147
+ const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
148
+
149
+ interface SubagentControlMessageDetails {
150
+ event: ControlEvent;
151
+ source?: "foreground" | "async";
152
+ asyncDir?: string;
153
+ childIntercomTarget?: string;
154
+ noticeText?: string;
155
+ }
156
+
157
+ function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
158
+ return details.childIntercomTarget;
159
+ }
160
+
161
+ function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
162
+ return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
163
+ }
164
+
165
+ class SubagentControlNoticeComponent implements Component {
166
+ constructor(
167
+ private readonly details: SubagentControlMessageDetails,
168
+ private readonly theme: ExtensionContext["ui"]["theme"],
169
+ ) {}
170
+
171
+ invalidate(): void {}
172
+
173
+ render(width: number): string[] {
174
+ const eventLabel = this.details.event.type.replaceAll("_", " ");
175
+ if (width < 3) return [truncateToWidth(`Subagent ${eventLabel}`, width)];
176
+ const bodyWidth = Math.max(1, Math.min(width - 2, 68));
177
+ const borderChar = "─";
178
+ const header = ` ⚠ Subagent ${eventLabel}: ${this.details.event.agent} `;
179
+ const headerText = truncateToWidth(header, bodyWidth, "");
180
+ const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText));
181
+ const lines = [this.theme.fg("accent", `╭${headerText}${borderChar.repeat(headerPadding)}╮`)];
182
+
183
+ for (const line of wrapTextWithAnsi(formatSubagentControlNotice(this.details), bodyWidth)) {
184
+ const text = truncateToWidth(line, bodyWidth, "");
185
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
186
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
187
+ }
188
+ lines.push(this.theme.fg("accent", `╰${borderChar.repeat(bodyWidth)}╯`));
189
+ return lines;
190
+ }
191
+ }
192
+
142
193
  export default function registerSubagentExtension(pi: ExtensionAPI): void {
143
194
  ensureAccessibleDir(RESULTS_DIR);
144
195
  ensureAccessibleDir(ASYNC_DIR);
@@ -176,7 +227,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
176
227
  startResultWatcher();
177
228
  primeExistingResults();
178
229
 
179
- const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(state, ASYNC_DIR);
230
+ const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
180
231
  const executor = createSubagentExecutor({
181
232
  pi,
182
233
  state,
@@ -194,6 +245,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
194
245
  return createSlashResultComponent(details, options, theme);
195
246
  });
196
247
 
248
+ pi.registerMessageRenderer<SubagentControlMessageDetails>(SUBAGENT_CONTROL_MESSAGE_TYPE, (message, _options, theme) => {
249
+ const details = message.details as SubagentControlMessageDetails | undefined;
250
+ if (!details?.event) return undefined;
251
+ const content = typeof message.content === "string" ? message.content : undefined;
252
+ return new SubagentControlNoticeComponent({ ...details, noticeText: formatSubagentControlNotice(details, content) }, theme);
253
+ });
254
+
197
255
  const slashBridge = registerSlashSubagentBridge({
198
256
  events: pi.events,
199
257
  getContext: () => state.lastUiContext,
@@ -274,7 +332,8 @@ MANAGEMENT (use action field, omit agent/task/chain/tasks):
274
332
  • Use chainName for chain operations
275
333
 
276
334
  CONTROL:
277
- • { action: "interrupt", runId?: "..." } - soft-interrupt the current child turn and leave the run paused`,
335
+ • { action: "status", id: "..." } - inspect an async/background run by id or prefix
336
+ • { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused`,
278
337
  parameters: SubagentParams,
279
338
 
280
339
  execute(id, params, signal, onUpdate, ctx) {
@@ -317,134 +376,52 @@ CONTROL:
317
376
 
318
377
  };
319
378
 
320
- const statusTool: ToolDefinition<typeof StatusParams, Details> = {
321
- name: "subagent_status",
322
- label: "Subagent Status",
323
- description: "Inspect async subagent run status and artifacts",
324
- parameters: StatusParams,
325
-
326
- async execute(_id, params, _signal, _onUpdate, _ctx) {
327
- if (params.action === "list") {
328
- try {
329
- const runs = listAsyncRuns(ASYNC_DIR, { states: ["queued", "running"] });
330
- return {
331
- content: [{ type: "text", text: formatAsyncRunList(runs) }],
332
- details: { mode: "single", results: [] },
333
- };
334
- } catch (error) {
335
- const message = error instanceof Error ? error.message : String(error);
336
- return {
337
- content: [{ type: "text", text: message }],
338
- isError: true,
339
- details: { mode: "single", results: [] },
340
- };
341
- }
342
- }
343
-
344
- let asyncDir: string | null = null;
345
- let resolvedId = params.id;
346
-
347
- if (params.dir) {
348
- asyncDir = path.resolve(params.dir);
349
- } else if (params.id) {
350
- const direct = path.join(ASYNC_DIR, params.id);
351
- if (fs.existsSync(direct)) {
352
- asyncDir = direct;
353
- } else {
354
- const match = findByPrefix(ASYNC_DIR, params.id);
355
- if (match) {
356
- asyncDir = match;
357
- resolvedId = path.basename(match);
358
- }
359
- }
360
- }
361
-
362
- const resultPath =
363
- params.id && !asyncDir ? findByPrefix(RESULTS_DIR, params.id, ".json") : null;
364
-
365
- if (!asyncDir && !resultPath) {
366
- return {
367
- content: [{ type: "text", text: "Async run not found. Provide id or dir." }],
368
- isError: true,
369
- details: { mode: "single" as const, results: [] },
370
- };
371
- }
372
-
373
- if (asyncDir) {
374
- let status;
375
- try {
376
- status = readStatus(asyncDir);
377
- } catch (error) {
378
- const message = error instanceof Error ? error.message : String(error);
379
- return {
380
- content: [{ type: "text", text: message }],
381
- isError: true,
382
- details: { mode: "single" as const, results: [] },
383
- };
384
- }
385
- const logPath = path.join(asyncDir, `subagent-log-${resolvedId ?? "unknown"}.md`);
386
- const eventsPath = path.join(asyncDir, "events.jsonl");
387
- if (status) {
388
- const stepsTotal = status.steps?.length ?? 1;
389
- const current = status.currentStep !== undefined ? status.currentStep + 1 : undefined;
390
- const stepLine =
391
- current !== undefined ? `Step: ${current}/${stepsTotal}` : `Steps: ${stepsTotal}`;
392
- const started = new Date(status.startedAt).toISOString();
393
- const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
394
-
395
- const lines = [
396
- `Run: ${status.runId}`,
397
- `State: ${status.activityState ? `${status.state}/${status.activityState}` : status.state}`,
398
- `Mode: ${status.mode}`,
399
- stepLine,
400
- `Started: ${started}`,
401
- `Updated: ${updated}`,
402
- `Dir: ${asyncDir}`,
403
- ];
404
- for (const [index, step] of (status.steps ?? []).entries()) {
405
- const stepState = step.activityState ? `${step.status}/${step.activityState}` : step.status;
406
- lines.push(`Step ${index + 1}: ${step.agent} ${stepState}`);
407
- }
408
- if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
409
- if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
410
- if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
411
-
412
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
413
- }
414
- }
415
-
416
- if (resultPath) {
417
- try {
418
- const raw = fs.readFileSync(resultPath, "utf-8");
419
- const data = JSON.parse(raw) as { id?: string; success?: boolean; summary?: string };
420
- const status = data.success ? "complete" : "failed";
421
- const lines = [`Run: ${data.id ?? params.id}`, `State: ${status}`, `Result: ${resultPath}`];
422
- if (data.summary) lines.push("", data.summary);
423
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
424
- } catch (error) {
425
- const message = error instanceof Error ? error.message : String(error);
426
- return {
427
- content: [{ type: "text", text: `Failed to read async result file: ${message}` }],
428
- isError: true,
429
- details: { mode: "single" as const, results: [] },
430
- };
431
- }
432
- }
433
-
434
- return {
435
- content: [{ type: "text", text: "Status file not found." }],
436
- isError: true,
437
- details: { mode: "single" as const, results: [] },
438
- };
439
- },
440
- };
441
-
442
379
  pi.registerTool(tool);
443
- pi.registerTool(statusTool);
444
380
  registerSlashCommands(pi, state);
445
381
 
446
- pi.events.on("subagent:started", handleStarted);
447
- pi.events.on("subagent:complete", handleComplete);
382
+ const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
383
+ const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
384
+ const globalStore = globalThis as Record<string, unknown>;
385
+ const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
386
+ if (Array.isArray(previousEventUnsubscribes)) {
387
+ for (const unsubscribe of previousEventUnsubscribes) {
388
+ if (typeof unsubscribe !== "function") continue;
389
+ try {
390
+ unsubscribe();
391
+ } catch {
392
+ // Best effort cleanup for stale handlers from an older reload.
393
+ }
394
+ }
395
+ }
396
+ registerSubagentNotify(pi);
397
+
398
+ const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
399
+ const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
400
+ globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
401
+ const controlEventHandler = (payload: unknown) => {
402
+ const details = payload as SubagentControlMessageDetails;
403
+ if (!details?.event) return;
404
+ const childIntercomTarget = controlNoticeTarget(details);
405
+ const key = controlNotificationKey(details.event, childIntercomTarget);
406
+ if (visibleControlNotices.has(key)) return;
407
+ visibleControlNotices.add(key);
408
+ const noticeText = details.noticeText ?? formatControlNoticeMessage(details.event, childIntercomTarget);
409
+ pi.sendMessage(
410
+ {
411
+ customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
412
+ content: noticeText,
413
+ display: true,
414
+ details: { ...details, childIntercomTarget, noticeText },
415
+ },
416
+ { triggerTurn: true },
417
+ );
418
+ };
419
+ const eventUnsubscribes = [
420
+ pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
421
+ pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete),
422
+ pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
423
+ ];
424
+ globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
448
425
 
449
426
  pi.on("tool_result", (event, ctx) => {
450
427
  if (event.toolName !== "subagent") return;
@@ -480,6 +457,16 @@ CONTROL:
480
457
  resetSessionState(ctx);
481
458
  });
482
459
  pi.on("session_shutdown", () => {
460
+ for (const unsubscribe of eventUnsubscribes) {
461
+ try {
462
+ unsubscribe();
463
+ } catch {
464
+ // Best effort cleanup during shutdown.
465
+ }
466
+ }
467
+ if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
468
+ delete globalStore[eventUnsubscribeStoreKey];
469
+ }
483
470
  stopResultWatcher();
484
471
  if (state.poller) clearInterval(state.poller);
485
472
  state.poller = null;
package/install.mjs CHANGED
@@ -85,9 +85,8 @@ if (fs.existsSync(EXTENSION_DIR)) {
85
85
  }
86
86
 
87
87
  console.log(`
88
- The extension is now available in pi. Tools added:
89
- • subagent - Delegate tasks to agents (single, chain, parallel)
90
- • subagent_status - Check async run status
88
+ The extension is now available in pi. Tool added:
89
+ • subagent - Delegate tasks to agents and inspect run status
91
90
 
92
91
  Documentation: ${EXTENSION_DIR}/README.md
93
92
  `);
@@ -41,6 +41,15 @@ export function resolveIntercomSessionTarget(sessionName: string | undefined, se
41
41
  return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
42
42
  }
43
43
 
44
+ function sanitizeIntercomTargetPart(value: string): string {
45
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
46
+ }
47
+
48
+ export function resolveSubagentIntercomTarget(runId: string, agent: string, index?: number): string {
49
+ const stepSuffix = index !== undefined ? `-${index + 1}` : "";
50
+ return `subagent-${sanitizeIntercomTargetPart(agent)}-${sanitizeIntercomTargetPart(runId)}${stepSuffix}`;
51
+ }
52
+
44
53
  export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
45
54
  if (value === "off" || value === "always" || value === "fork-only") return value;
46
55
  return "always";
package/notify.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Subagent completion notifications (extension)
2
+ * Subagent completion notifications.
3
3
  */
4
4
 
5
5
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
6
  import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
7
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "./types.ts";
7
8
 
8
9
  interface ChainStepResult {
9
10
  agent: string;
@@ -16,7 +17,8 @@ interface SubagentResult {
16
17
  agent: string | null;
17
18
  success: boolean;
18
19
  summary: string;
19
- exitCode: number;
20
+ exitCode?: number;
21
+ state?: string;
20
22
  timestamp: number;
21
23
  sessionFile?: string;
22
24
  shareUrl?: string;
@@ -28,6 +30,17 @@ interface SubagentResult {
28
30
  }
29
31
 
30
32
  export default function registerSubagentNotify(pi: ExtensionAPI): void {
33
+ const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
34
+ const globalStore = globalThis as Record<string, unknown>;
35
+ const previousUnsubscribe = globalStore[unsubscribeStoreKey];
36
+ if (typeof previousUnsubscribe === "function") {
37
+ try {
38
+ previousUnsubscribe();
39
+ } catch {
40
+ // Best effort cleanup for stale handlers from an older reload.
41
+ }
42
+ }
43
+
31
44
  const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
32
45
  const ttlMs = 10 * 60 * 1000;
33
46
 
@@ -38,7 +51,13 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
38
51
  if (markSeenWithTtl(seen, key, now, ttlMs)) return;
39
52
 
40
53
  const agent = result.agent ?? "unknown";
41
- const status = result.success ? "completed" : "failed";
54
+ const summary = typeof result.summary === "string" ? result.summary : "";
55
+ const paused = !result.success && (
56
+ result.exitCode === 0
57
+ || result.state === "paused"
58
+ || summary.startsWith("Paused after interrupt.")
59
+ );
60
+ const status = paused ? "paused" : result.success ? "completed" : "failed";
42
61
 
43
62
  const taskInfo =
44
63
  result.taskIndex !== undefined && result.totalTasks !== undefined
@@ -54,11 +73,11 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
54
73
  extra.push(`Session file: ${result.sessionFile}`);
55
74
  }
56
75
 
57
- const summary = result.summary.trim() ? result.summary : "(no output)";
76
+ const displaySummary = summary.trim() ? summary : "(no output)";
58
77
  const content = [
59
78
  `Background task ${status}: **${agent}**${taskInfo}`,
60
79
  "",
61
- summary,
80
+ displaySummary,
62
81
  extra.length ? "" : undefined,
63
82
  extra.length ? extra.join("\n") : undefined,
64
83
  ]
@@ -75,5 +94,5 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
75
94
  );
76
95
  };
77
96
 
78
- pi.events.on("subagent:complete", handleComplete);
97
+ globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
79
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.17.5",
3
+ "version": "0.18.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",
@@ -38,13 +38,11 @@
38
38
  "test": "npm run test:unit",
39
39
  "test:unit": "node --experimental-strip-types --test test/unit/*.test.ts",
40
40
  "test:integration": "node --experimental-transform-types --import ./test/support/register-loader.mjs --test test/integration/*.test.ts",
41
- "test:e2e": "node --experimental-transform-types --import ./test/support/register-loader.mjs --test test/e2e/*.test.ts",
42
- "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e"
41
+ "test:all": "npm run test:unit && npm run test:integration"
43
42
  },
44
43
  "pi": {
45
44
  "extensions": [
46
- "./index.ts",
47
- "./notify.ts"
45
+ "./index.ts"
48
46
  ],
49
47
  "skills": [
50
48
  "./skills"
@@ -60,7 +58,6 @@
60
58
  "typebox": "^1.1.24"
61
59
  },
62
60
  "devDependencies": {
63
- "@marcfargas/pi-test-harness": "^0.5.0",
64
61
  "@mariozechner/pi-agent-core": "^0.65.0",
65
62
  "@mariozechner/pi-ai": "^0.65.0",
66
63
  "@mariozechner/pi-coding-agent": "^0.65.0"
package/pi-args.ts CHANGED
@@ -23,6 +23,7 @@ export interface BuildPiArgsInput {
23
23
  systemPrompt?: string | null;
24
24
  mcpDirectTools?: string[];
25
25
  promptFileStem?: string;
26
+ intercomSessionName?: string;
26
27
  }
27
28
 
28
29
  export interface BuildPiArgsResult {
@@ -112,6 +113,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
112
113
  const env: Record<string, string | undefined> = {};
113
114
  env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
114
115
  env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
116
+ if (input.intercomSessionName) {
117
+ env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
118
+ }
115
119
  if (input.mcpDirectTools?.length) {
116
120
  env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
117
121
  } else {
package/render.ts CHANGED
@@ -114,12 +114,16 @@ function getToolCallLines(
114
114
  return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
115
115
  }
116
116
 
117
- function formatActivityLabel(lastActivityAt: number | undefined, now = Date.now()): string | undefined {
118
- if (lastActivityAt === undefined) return undefined;
119
- const ago = Math.max(0, now - lastActivityAt);
120
- if (ago < 1000) return "active now";
121
- if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
122
- return `active ${Math.floor(ago / 60000)}m ago`;
117
+ function formatActivityAge(ms: number): string {
118
+ if (ms < 1000) return "now";
119
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
120
+ return `${Math.floor(ms / 60000)}m`;
121
+ }
122
+
123
+ function formatActivityLabel(lastActivityAt: number | undefined, needsAttention?: boolean, now = Date.now()): string | undefined {
124
+ if (lastActivityAt === undefined) return needsAttention ? "needs attention" : undefined;
125
+ const age = formatActivityAge(Math.max(0, now - lastActivityAt));
126
+ return needsAttention ? `no activity for ${age}` : age === "now" ? "active now" : `active ${age} ago`;
123
127
  }
124
128
 
125
129
  function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
@@ -138,13 +142,8 @@ function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "cu
138
142
  : `${progress.currentTool}${durationSuffix}`;
139
143
  }
140
144
 
141
- function buildLiveStatusLine(progress: Pick<AgentProgress, "lastActivityAt">): string | undefined {
142
- return formatActivityLabel(progress.lastActivityAt);
143
- }
144
-
145
- function formatActivityState(state: AgentProgress["activityState"]): string | undefined {
146
- if (!state) return undefined;
147
- return `activity: ${state}`;
145
+ function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "lastActivityAt">): string | undefined {
146
+ return formatActivityLabel(progress.lastActivityAt, progress.activityState === "needs_attention");
148
147
  }
149
148
 
150
149
  /**
@@ -192,7 +191,9 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
192
191
  const agentLabel = job.agents ? job.agents.join(" -> ") : (job.mode ?? "single");
193
192
 
194
193
  const tokenText = job.totalTokens ? ` | ${formatTokens(job.totalTokens.total)} tok` : "";
195
- const activityText = job.activityState ?? (job.status === "running" ? getLastActivity(job.outputFile) : "");
194
+ const activityText = job.currentTool && job.currentToolStartedAt
195
+ ? `tool ${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`
196
+ : formatActivityLabel(job.lastActivityAt, job.activityState === "needs_attention") ?? (job.status === "running" ? getLastActivity(job.outputFile) : "");
196
197
  const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
197
198
 
198
199
  lines.push(truncLine(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`, w));
@@ -266,10 +267,6 @@ export function renderSubagentResult(
266
267
  if (toolLine) {
267
268
  c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
268
269
  }
269
- const activityStateLine = formatActivityState(r.progress.activityState);
270
- if (activityStateLine) {
271
- c.addChild(new Text(fit(theme.fg("accent", activityStateLine)), 0, 0));
272
- }
273
270
  const liveStatusLine = buildLiveStatusLine(r.progress);
274
271
  if (liveStatusLine) {
275
272
  c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
@@ -478,10 +475,6 @@ export function renderSubagentResult(
478
475
  if (toolLine) {
479
476
  c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
480
477
  }
481
- const activityStateLine = formatActivityState(rProg.activityState);
482
- if (activityStateLine) {
483
- c.addChild(new Text(fit(theme.fg("accent", ` ${activityStateLine}`)), 0, 0));
484
- }
485
478
  const liveStatusLine = buildLiveStatusLine(rProg);
486
479
  if (liveStatusLine) {
487
480
  c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));