@tintinweb/pi-subagents 0.4.11 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,22 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.4.11] - 2026-03-18
8
+ ## [Unreleased]
9
9
 
10
- ### Fixed
11
- - **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
12
-
13
- ## [0.4.10] - 2026-03-18
14
-
15
- ### Changed
16
- - **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
17
- - **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress.
10
+ ## [0.5.0] - 2026-03-22
18
11
 
19
12
  ### Added
13
+ - **RPC stop handler** — new `subagents:rpc:stop` event bus RPC allows other extensions to stop running subagents by agent ID. Returns structured error ("Agent not found") on failure.
14
+ - **`abort` in `SpawnCapable` interface** — cross-extension RPC consumers can now stop agents, not just spawn them.
15
+ - **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress via `onTurnEnd` callback.
20
16
  - **Biome linting** — added [Biome](https://biomejs.dev/) for correctness linting (unused imports, suspicious patterns). Style rules disabled. Run `npm run lint` to check, `npm run lint:fix` to auto-fix.
21
17
  - **CI workflow** — GitHub Actions runs lint, typecheck, and tests on push to master and PRs.
18
+ - **Auto-trigger parent turn on background completion** — background agent completion notifications now use `triggerTurn: true`, automatically prompting the parent agent to process results instead of waiting for user input.
19
+
20
+ ### Changed
21
+ - **Standardized RPC envelope** — cross-extension RPC handlers (`ping`, `spawn`, `stop`) now use a `handleRpc` wrapper that emits structured envelopes (`{ success: true, data }` / `{ success: false, error }`), matching pi-mono's `RpcResponse` convention.
22
+ - **Protocol versioning via ping** — ping reply now includes `{ version: PROTOCOL_VERSION }` (currently v2). Callers can detect version mismatches and warn users to update.
23
+ - **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
24
+ - **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
22
25
 
23
26
  ### Fixed
27
+ - **Tool name display** — `getAgentConversation` now reads `ToolCall.name` (the correct property) instead of `toolName`, resolving `[Tool: unknown]` in conversation viewer and verbose output.
24
28
  - **Env test CI failure** — `detectEnv` test assumed a branch name exists, but CI checks out detached HEAD. Split into separate tests for repo detection and branch detection with a controlled temp repo.
25
29
 
26
30
  ## [0.4.9] - 2026-03-18
@@ -325,6 +329,7 @@ Initial release.
325
329
  - **Thinking level** — per-agent extended thinking control
326
330
  - **`/agent` and `/agents` commands**
327
331
 
332
+ [0.5.0]: https://github.com/tintinweb/pi-subagents/compare/v0.4.9...v0.5.0
328
333
  [0.4.9]: https://github.com/tintinweb/pi-subagents/compare/v0.4.8...v0.4.9
329
334
  [0.4.8]: https://github.com/tintinweb/pi-subagents/compare/v0.4.7...v0.4.8
330
335
  [0.4.7]: https://github.com/tintinweb/pi-subagents/compare/v0.4.6...v0.4.7
package/README.md CHANGED
@@ -29,7 +29,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
29
29
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
30
30
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
31
31
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
32
- - **Cross-extension RPC** — other pi extensions can spawn subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`). Emits `subagents:ready` on load
32
+ - **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
33
33
 
34
34
  ## Install
35
35
 
@@ -289,7 +289,9 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
289
289
 
290
290
  ## Cross-Extension RPC
291
291
 
292
- Other pi extensions can spawn subagents programmatically via the `pi.events` event bus, without importing this package directly.
292
+ Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
293
+
294
+ All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
293
295
 
294
296
  ### Discovery
295
297
 
@@ -297,19 +299,19 @@ Listen for `subagents:ready` to know when RPC handlers are available:
297
299
 
298
300
  ```typescript
299
301
  pi.events.on("subagents:ready", () => {
300
- // RPC handlers are registered — safe to call ping/spawn
302
+ // RPC handlers are registered — safe to call ping/spawn/stop
301
303
  });
302
304
  ```
303
305
 
304
306
  ### Ping
305
307
 
306
- Check if the subagents extension is loaded:
308
+ Check if the subagents extension is loaded and get the protocol version:
307
309
 
308
310
  ```typescript
309
311
  const requestId = crypto.randomUUID();
310
- const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, () => {
312
+ const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
311
313
  unsub();
312
- // Extension is alive
314
+ if (reply.success) console.log("Protocol version:", reply.data.version);
313
315
  });
314
316
  pi.events.emit("subagents:rpc:ping", { requestId });
315
317
  ```
@@ -322,10 +324,10 @@ Spawn a subagent and receive its ID:
322
324
  const requestId = crypto.randomUUID();
323
325
  const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
324
326
  unsub();
325
- if (reply.error) {
327
+ if (!reply.success) {
326
328
  console.error("Spawn failed:", reply.error);
327
329
  } else {
328
- console.log("Agent ID:", reply.id);
330
+ console.log("Agent ID:", reply.data.id);
329
331
  }
330
332
  });
331
333
  pi.events.emit("subagents:rpc:spawn", {
@@ -336,6 +338,19 @@ pi.events.emit("subagents:rpc:spawn", {
336
338
  });
337
339
  ```
338
340
 
341
+ ### Stop
342
+
343
+ Stop a running agent by ID:
344
+
345
+ ```typescript
346
+ const requestId = crypto.randomUUID();
347
+ const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
348
+ unsub();
349
+ if (!reply.success) console.error("Stop failed:", reply.error);
350
+ });
351
+ pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
352
+ ```
353
+
339
354
  Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
340
355
 
341
356
  ## Persistent Agent Memory
@@ -302,7 +302,7 @@ export function getAgentConversation(session) {
302
302
  if (c.type === "text" && c.text)
303
303
  textParts.push(c.text);
304
304
  else if (c.type === "toolCall")
305
- toolCalls.push(` Tool: ${c.toolName ?? "unknown"}`);
305
+ toolCalls.push(` Tool: ${c.name ?? c.toolName ?? "unknown"}`);
306
306
  }
307
307
  if (textParts.length > 0)
308
308
  parts.push(`[Assistant]: ${textParts.join("\n")}`);
@@ -1,17 +1,32 @@
1
1
  /**
2
2
  * Cross-extension RPC handlers for the subagents extension.
3
3
  *
4
- * Exposes ping and spawn RPCs over the pi.events event bus,
4
+ * Exposes ping, spawn, and stop RPCs over the pi.events event bus,
5
5
  * using per-request scoped reply channels.
6
+ *
7
+ * Reply envelope follows pi-mono convention:
8
+ * success → { success: true, data?: T }
9
+ * error → { success: false, error: string }
6
10
  */
7
11
  /** Minimal event bus interface needed by the RPC handlers. */
8
12
  export interface EventBus {
9
13
  on(event: string, handler: (data: unknown) => void): () => void;
10
14
  emit(event: string, data: unknown): void;
11
15
  }
12
- /** Minimal AgentManager interface needed by the spawn RPC. */
16
+ /** RPC reply envelope matches pi-mono's RpcResponse shape. */
17
+ export type RpcReply<T = void> = {
18
+ success: true;
19
+ data?: T;
20
+ } | {
21
+ success: false;
22
+ error: string;
23
+ };
24
+ /** RPC protocol version — bumped when the envelope or method contracts change. */
25
+ export declare const PROTOCOL_VERSION = 2;
26
+ /** Minimal AgentManager interface needed by the spawn/stop RPCs. */
13
27
  export interface SpawnCapable {
14
28
  spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
29
+ abort(id: string): boolean;
15
30
  }
16
31
  export interface RpcDeps {
17
32
  events: EventBus;
@@ -22,9 +37,10 @@ export interface RpcDeps {
22
37
  export interface RpcHandle {
23
38
  unsubPing: () => void;
24
39
  unsubSpawn: () => void;
40
+ unsubStop: () => void;
25
41
  }
26
42
  /**
27
- * Register ping and spawn RPC handlers on the event bus.
43
+ * Register ping, spawn, and stop RPC handlers on the event bus.
28
44
  * Returns unsub functions for cleanup.
29
45
  */
30
46
  export declare function registerRpcHandlers(deps: RpcDeps): RpcHandle;
@@ -1,33 +1,54 @@
1
1
  /**
2
2
  * Cross-extension RPC handlers for the subagents extension.
3
3
  *
4
- * Exposes ping and spawn RPCs over the pi.events event bus,
4
+ * Exposes ping, spawn, and stop RPCs over the pi.events event bus,
5
5
  * using per-request scoped reply channels.
6
+ *
7
+ * Reply envelope follows pi-mono convention:
8
+ * success → { success: true, data?: T }
9
+ * error → { success: false, error: string }
6
10
  */
11
+ /** RPC protocol version — bumped when the envelope or method contracts change. */
12
+ export const PROTOCOL_VERSION = 2;
7
13
  /**
8
- * Register ping and spawn RPC handlers on the event bus.
14
+ * Wire a single RPC handler: listen on `channel`, run `fn(params)`,
15
+ * emit the reply envelope on `channel:reply:${requestId}`.
16
+ */
17
+ function handleRpc(events, channel, fn) {
18
+ return events.on(channel, async (raw) => {
19
+ const params = raw;
20
+ try {
21
+ const data = await fn(params);
22
+ const reply = { success: true };
23
+ if (data !== undefined)
24
+ reply.data = data;
25
+ events.emit(`${channel}:reply:${params.requestId}`, reply);
26
+ }
27
+ catch (err) {
28
+ events.emit(`${channel}:reply:${params.requestId}`, {
29
+ success: false, error: err?.message ?? String(err),
30
+ });
31
+ }
32
+ });
33
+ }
34
+ /**
35
+ * Register ping, spawn, and stop RPC handlers on the event bus.
9
36
  * Returns unsub functions for cleanup.
10
37
  */
11
38
  export function registerRpcHandlers(deps) {
12
39
  const { events, pi, getCtx, manager } = deps;
13
- const unsubPing = events.on("subagents:rpc:ping", (raw) => {
14
- const { requestId } = raw;
15
- events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
40
+ const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
41
+ return { version: PROTOCOL_VERSION };
16
42
  });
17
- const unsubSpawn = events.on("subagents:rpc:spawn", async (raw) => {
18
- const { requestId, type, prompt, options } = raw;
43
+ const unsubSpawn = handleRpc(events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
19
44
  const ctx = getCtx();
20
- if (!ctx) {
21
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: "No active session" });
22
- return;
23
- }
24
- try {
25
- const id = manager.spawn(pi, ctx, type, prompt, options ?? {});
26
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { id });
27
- }
28
- catch (err) {
29
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
30
- }
45
+ if (!ctx)
46
+ throw new Error("No active session");
47
+ return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
48
+ });
49
+ const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
50
+ if (!manager.abort(agentId))
51
+ throw new Error("Agent not found");
31
52
  });
32
- return { unsubPing, unsubSpawn };
53
+ return { unsubPing, unsubSpawn, unsubStop };
33
54
  }
package/dist/index.js CHANGED
@@ -254,7 +254,7 @@ export default function (pi) {
254
254
  content: notification + footer,
255
255
  display: true,
256
256
  details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
257
- }, { deliverAs: "followUp" });
257
+ }, { deliverAs: "followUp", triggerTurn: true });
258
258
  }
259
259
  function sendIndividualNudge(record) {
260
260
  agentActivity.delete(record.id);
@@ -290,7 +290,7 @@ export default function (pi) {
290
290
  content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
291
291
  display: true,
292
292
  details,
293
- }, { deliverAs: "followUp" });
293
+ }, { deliverAs: "followUp", triggerTurn: true });
294
294
  });
295
295
  widget.update();
296
296
  }, 30_000);
@@ -383,7 +383,7 @@ export default function (pi) {
383
383
  manager.clearCompleted(); // preserve existing behavior
384
384
  });
385
385
  pi.on("session_switch", () => { manager.clearCompleted(); });
386
- const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
386
+ const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
387
387
  events: pi.events,
388
388
  pi,
389
389
  getCtx: () => currentCtx,
@@ -395,6 +395,7 @@ export default function (pi) {
395
395
  // If the session is going down, there's nothing left to consume agent results.
396
396
  pi.on("session_shutdown", async () => {
397
397
  unsubSpawnRpc();
398
+ unsubStopRpc();
398
399
  unsubPingRpc();
399
400
  currentCtx = undefined;
400
401
  delete globalThis[MANAGER_KEY];
@@ -179,7 +179,7 @@ export class ConversationViewer {
179
179
  if (c.type === "text" && c.text)
180
180
  textParts.push(c.text);
181
181
  else if (c.type === "toolCall") {
182
- toolCalls.push(c.toolName ?? "unknown");
182
+ toolCalls.push(c.name ?? c.toolName ?? "unknown");
183
183
  }
184
184
  }
185
185
  if (needsSeparator)
@@ -159,7 +159,7 @@ describe("ConversationViewer", () => {
159
159
  role: "assistant",
160
160
  content: [
161
161
  { type: "text", text: "Let me check that." },
162
- { type: "toolCall", toolUseId: "t1", toolName: "very_long_tool_name_" + "x".repeat(200), input: {} },
162
+ { type: "toolCall", toolUseId: "t1", name: "very_long_tool_name_" + "x".repeat(200), input: {} },
163
163
  ],
164
164
  },
165
165
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -21,9 +21,9 @@
21
21
  "autonomous"
22
22
  ],
23
23
  "dependencies": {
24
- "@mariozechner/pi-ai": "^0.60.0",
25
- "@mariozechner/pi-coding-agent": "^0.60.0",
26
- "@mariozechner/pi-tui": "^0.60.0",
24
+ "@mariozechner/pi-ai": "^0.61.1",
25
+ "@mariozechner/pi-coding-agent": "^0.61.1",
26
+ "@mariozechner/pi-tui": "^0.61.1",
27
27
  "@sinclair/typebox": "latest"
28
28
  },
29
29
  "scripts": {
@@ -36,9 +36,9 @@
36
36
  "lint:fix": "biome check --fix src/ test/"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^20.0.0",
40
- "typescript": "^5.0.0",
41
39
  "@biomejs/biome": "^2.3.5",
40
+ "@types/node": "^25.5.0",
41
+ "typescript": "^5.0.0",
42
42
  "vitest": "^4.0.18"
43
43
  },
44
44
  "pi": {
@@ -393,7 +393,7 @@ export function getAgentConversation(session: AgentSession): string {
393
393
  const toolCalls: string[] = [];
394
394
  for (const c of msg.content) {
395
395
  if (c.type === "text" && c.text) textParts.push(c.text);
396
- else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).toolName ?? "unknown"}`);
396
+ else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`);
397
397
  }
398
398
  if (textParts.length > 0) parts.push(`[Assistant]: ${textParts.join("\n")}`);
399
399
  if (toolCalls.length > 0) parts.push(`[Tool Calls]:\n${toolCalls.join("\n")}`);
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Cross-extension RPC handlers for the subagents extension.
3
3
  *
4
- * Exposes ping and spawn RPCs over the pi.events event bus,
4
+ * Exposes ping, spawn, and stop RPCs over the pi.events event bus,
5
5
  * using per-request scoped reply channels.
6
+ *
7
+ * Reply envelope follows pi-mono convention:
8
+ * success → { success: true, data?: T }
9
+ * error → { success: false, error: string }
6
10
  */
7
11
 
8
12
  /** Minimal event bus interface needed by the RPC handlers. */
@@ -11,9 +15,18 @@ export interface EventBus {
11
15
  emit(event: string, data: unknown): void;
12
16
  }
13
17
 
14
- /** Minimal AgentManager interface needed by the spawn RPC. */
18
+ /** RPC reply envelope matches pi-mono's RpcResponse shape. */
19
+ export type RpcReply<T = void> =
20
+ | { success: true; data?: T }
21
+ | { success: false; error: string };
22
+
23
+ /** RPC protocol version — bumped when the envelope or method contracts change. */
24
+ export const PROTOCOL_VERSION = 2;
25
+
26
+ /** Minimal AgentManager interface needed by the spawn/stop RPCs. */
15
27
  export interface SpawnCapable {
16
28
  spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
29
+ abort(id: string): boolean;
17
30
  }
18
31
 
19
32
  export interface RpcDeps {
@@ -26,36 +39,57 @@ export interface RpcDeps {
26
39
  export interface RpcHandle {
27
40
  unsubPing: () => void;
28
41
  unsubSpawn: () => void;
42
+ unsubStop: () => void;
29
43
  }
30
44
 
31
45
  /**
32
- * Register ping and spawn RPC handlers on the event bus.
46
+ * Wire a single RPC handler: listen on `channel`, run `fn(params)`,
47
+ * emit the reply envelope on `channel:reply:${requestId}`.
48
+ */
49
+ function handleRpc<P extends { requestId: string }>(
50
+ events: EventBus,
51
+ channel: string,
52
+ fn: (params: P) => unknown | Promise<unknown>,
53
+ ): () => void {
54
+ return events.on(channel, async (raw: unknown) => {
55
+ const params = raw as P;
56
+ try {
57
+ const data = await fn(params);
58
+ const reply: { success: true; data?: unknown } = { success: true };
59
+ if (data !== undefined) reply.data = data;
60
+ events.emit(`${channel}:reply:${params.requestId}`, reply);
61
+ } catch (err: any) {
62
+ events.emit(`${channel}:reply:${params.requestId}`, {
63
+ success: false, error: err?.message ?? String(err),
64
+ });
65
+ }
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Register ping, spawn, and stop RPC handlers on the event bus.
33
71
  * Returns unsub functions for cleanup.
34
72
  */
35
73
  export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
36
74
  const { events, pi, getCtx, manager } = deps;
37
75
 
38
- const unsubPing = events.on("subagents:rpc:ping", (raw: unknown) => {
39
- const { requestId } = raw as { requestId: string };
40
- events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
76
+ const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
77
+ return { version: PROTOCOL_VERSION };
41
78
  });
42
79
 
43
- const unsubSpawn = events.on("subagents:rpc:spawn", async (raw: unknown) => {
44
- const { requestId, type, prompt, options } = raw as {
45
- requestId: string; type: string; prompt: string; options?: any;
46
- };
47
- const ctx = getCtx();
48
- if (!ctx) {
49
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: "No active session" });
50
- return;
51
- }
52
- try {
53
- const id = manager.spawn(pi, ctx, type, prompt, options ?? {});
54
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { id });
55
- } catch (err: any) {
56
- events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
57
- }
58
- });
80
+ const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
81
+ events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
82
+ const ctx = getCtx();
83
+ if (!ctx) throw new Error("No active session");
84
+ return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
85
+ },
86
+ );
87
+
88
+ const unsubStop = handleRpc<{ requestId: string; agentId: string }>(
89
+ events, "subagents:rpc:stop", ({ agentId }) => {
90
+ if (!manager.abort(agentId)) throw new Error("Agent not found");
91
+ },
92
+ );
59
93
 
60
- return { unsubPing, unsubSpawn };
94
+ return { unsubPing, unsubSpawn, unsubStop };
61
95
  }
package/src/index.ts CHANGED
@@ -289,7 +289,7 @@ export default function (pi: ExtensionAPI) {
289
289
  content: notification + footer,
290
290
  display: true,
291
291
  details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
292
- }, { deliverAs: "followUp" });
292
+ }, { deliverAs: "followUp", triggerTurn: true });
293
293
  }
294
294
 
295
295
  function sendIndividualNudge(record: AgentRecord) {
@@ -326,7 +326,7 @@ export default function (pi: ExtensionAPI) {
326
326
  content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
327
327
  display: true,
328
328
  details,
329
- }, { deliverAs: "followUp" });
329
+ }, { deliverAs: "followUp", triggerTurn: true });
330
330
  });
331
331
  widget.update();
332
332
  },
@@ -431,7 +431,7 @@ export default function (pi: ExtensionAPI) {
431
431
 
432
432
  pi.on("session_switch", () => { manager.clearCompleted(); });
433
433
 
434
- const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
434
+ const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
435
435
  events: pi.events,
436
436
  pi,
437
437
  getCtx: () => currentCtx,
@@ -445,6 +445,7 @@ export default function (pi: ExtensionAPI) {
445
445
  // If the session is going down, there's nothing left to consume agent results.
446
446
  pi.on("session_shutdown", async () => {
447
447
  unsubSpawnRpc();
448
+ unsubStopRpc();
448
449
  unsubPingRpc();
449
450
  currentCtx = undefined;
450
451
  delete (globalThis as any)[MANAGER_KEY];
@@ -191,7 +191,7 @@ export class ConversationViewer implements Component {
191
191
  for (const c of msg.content) {
192
192
  if (c.type === "text" && c.text) textParts.push(c.text);
193
193
  else if (c.type === "toolCall") {
194
- toolCalls.push((c as any).toolName ?? "unknown");
194
+ toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
195
195
  }
196
196
  }
197
197
  if (needsSeparator) lines.push(th.fg("dim", "───"));