@clanker-code/pi-subagents 0.10.7 → 0.11.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/AGENTS.md CHANGED
@@ -31,6 +31,8 @@ See the general guide at `~/.llm-general/npm-autopublish-via-ci.md` for other re
31
31
 
32
32
  ## Keeping Upstream In Sync
33
33
 
34
+ **Do NOT pull in upstream changes without consulting Max first.** You may preview the changes and whether there will be any conflicts, and if so estimate how complex it will be to fix — but do not merge, rebase, or cherry-pick without explicit approval.
35
+
34
36
  Periodically run:
35
37
 
36
38
  ```bash
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] - 2026-06-28
11
+
12
+ ### Added
13
+ - **Dashboard UI module integration** — `pi-agent-dashboard` users can now see subagents in the dashboard. Three integration points: a footer-segment decorator showing running/completed agent counts, a `/subagents` management-modal command that opens a table view of all subagent history with row actions (view result, abort, steer), and round-trip event handlers wired through the `ui_management` protocol. Lifecycle events trigger automatic dashboard invalidation.
14
+ - **Compact view for `get_subagent_result`** — when tool output is collapsed in the TUI, `get_subagent_result` now shows first 20 + last 20 lines of the result body with a styled divider (`─────── ⋐ N lines hidden from preview ⋑ ───────`). The full content is still passed through to the LLM. The divider format also applies to the `Agent` tool's collapsed view.
15
+ - **`list_subagents` and `clear_subagents` tools** — agents can inspect retained subagent records with a compact `List Agents` renderer and clear stale completed records with a compact `Clear Agents` renderer. `list_subagents` defaults to active/problem agents plus the two most recent successful completions and reports hidden done counts; `all: true` shows the full retained list. `clear_subagents` defaults to successful completions older than 5 minutes, accepts explicit IDs/prefixes, and refuses to clear running or queued agents.
16
+
17
+ ### Changed
18
+ - **`Agent` defaults omitted `subagent_type` to `general-purpose`** — callers can omit the type for the default general-purpose agent instead of receiving a missing-argument error.
19
+ - **Built-in `Explore` no longer pins Haiku** — the default Explore agent now inherits the parent/session model unless the caller or a custom agent definition supplies a model.
20
+ - **`snipMiddleLines` divider updated** — the collapsed-output divider now uses a styled format with locale-aware line counts instead of plain text.
21
+
22
+ ### Fixed
23
+ - **Nested subagents now appear in the live widget as soon as they are created** — the widget listens to `subagents:created` lifecycle events in addition to started/completed/failed updates, and created events now include record metadata so queued recursive agents do not render as running while waiting.
24
+ - **`get_subagent_result` peek now bounds embedded multiline output by rendered lines** — JSONL transcript entries whose text blocks contain newlines are split before `peek.lines`, `peek.after`, regex filtering, and character truncation are applied, preventing a small requested line count from returning huge multiline tool/file output records.
25
+ - **`get_subagent_result` renderResult uses typed status** — `GetResultDetails.status` is now typed as `AgentRecord['status']` and correctly renders `queued` status with the accent spinner icon.
26
+
27
+ ## [0.10.8] - 2026-06-23
28
+
29
+ ### Changed
30
+ - **`get_subagent_result` wait:true now detects queued user messages** — when the parent session has user messages waiting (e.g. the user typed while waiting), the tool returns early with a `pending_message` outcome instead of blocking for the full wait timeout. The queued message is delivered to the parent LLM immediately. Uses `ctx.hasPendingMessages()` polling during the wait. The subagent continues running undisturbed.
31
+
10
32
  ## [0.10.7] - 2026-06-23
11
33
 
12
34
  ### Fixed
package/README.md CHANGED
@@ -32,6 +32,7 @@ Upstream changes are reviewed for cherry-picking when practical; otherwise they
32
32
  - **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). Stop a still-running agent from here by pressing `x` (then `x` again to confirm) — works for background agents too
33
33
  - **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
34
34
  - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
35
+ - **Agent record maintenance** — `list_subagents` gives the LLM a compact retained-agent summary (active, problem, recent done, hidden done count) and `clear_subagents` clears old completed records without touching active agents
35
36
  - **Session resume** — pick up where an agent left off, preserving full conversation context
36
37
  - **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
37
38
  - **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. Unknown types fall back to general-purpose with a note
@@ -43,6 +44,7 @@ Upstream changes are reviewed for cherry-picking when practical; otherwise they
43
44
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
44
45
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show a bounded preview and transcript path. Group completions render each agent individually
45
46
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
47
+ - **Dashboard UI integration** — `pi-agent-dashboard` users can see subagents in the dashboard: a footer badge shows running/completed counts, the `/subagents` command opens a table view of all subagent history with row actions (view result, abort, steer), and lifecycle events trigger automatic UI refresh. No React or SDK required — uses the dashboard's Extension UI Module System
46
48
  - **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
47
49
  - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same steering-style `subagent-notification` path as manual background completions; manage via `/agents → Scheduled jobs`
48
50
  - **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
@@ -153,7 +155,7 @@ Group completions render each agent as a separate block. The LLM receives struct
153
155
  | Type | Tools | Model | Prompt Mode | Description |
154
156
  |------|-------|-------|-------------|-------------|
155
157
  | `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
156
- | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
158
+ | `Explore` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Fast codebase exploration (read-only) |
157
159
  | `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
158
160
 
159
161
  The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
@@ -270,7 +272,7 @@ Launch a sub-agent.
270
272
  |-----------|------|----------|-------------|
271
273
  | `prompt` | string | yes | The task for the agent |
272
274
  | `description` | string | yes | Short 3-5 word summary (shown in UI) |
273
- | `subagent_type` | string | yes | Agent type (built-in or custom) |
275
+ | `subagent_type` | string | no | Agent type (built-in or custom). Defaults to `general-purpose` |
274
276
  | `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
275
277
  | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh |
276
278
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
@@ -301,6 +303,24 @@ Send a steering message to a running agent. The message interrupts after the cur
301
303
  | `agent_id` | string | yes | Agent ID to steer |
302
304
  | `message` | string | yes | Message to inject into agent conversation |
303
305
 
306
+ ### `list_subagents`
307
+
308
+ List retained subagent records with a compact custom renderer. By default it shows queued/running agents, failed/stopped/aborted agents, and the two most recent successful agents that have not been cleaned up yet. It also reports how many successful completed agents are hidden. Pass `all: true` for the full retained list.
309
+
310
+ | Parameter | Type | Required | Description |
311
+ |-----------|------|----------|-------------|
312
+ | `all` | boolean | no | Show every retained subagent instead of the default compact subset |
313
+
314
+ ### `clear_subagents`
315
+
316
+ Clear retained terminal subagent records with a compact custom renderer. By default it clears successful completed/steered agents older than 5 minutes. You can provide explicit IDs or unique prefixes to clear specific terminal agents. Running and queued agents are never cleared; attempts to clear them are reported as errors.
317
+
318
+ | Parameter | Type | Required | Description |
319
+ |-----------|------|----------|-------------|
320
+ | `agent_ids` | string[] | no | Exact IDs or unique prefixes to clear; ignores the age threshold |
321
+ | `older_than_minutes` | number | no | Default-mode age threshold. Defaults to 5 |
322
+ | `include_errors` | boolean | no | Also clear failed/stopped/aborted terminal records older than the threshold |
323
+
304
324
  ## Commands
305
325
 
306
326
  | Command | Description |
package/bugs.txt ADDED
@@ -0,0 +1,57 @@
1
+ # pi-subagents bugs
2
+
3
+ ## 2026-06-24: subagent-spawned reviewer c2c alias registrations route to coordinator
4
+
5
+ Observed in `/home/xertrov/src/autoplanet-harness` session while coordinating nested pi Agent reviewers.
6
+
7
+ Symptom:
8
+ - Coordinator received transcript notifications like `Subagent <id> registered as pi-...` for reviewer subagents spawned by an implementer subagent.
9
+ - The spawning implementer subagent did **not** receive those c2c alias registration notifications.
10
+ - This made the coordinator see aliases for subagents it did not directly start, while the parent subagent could not map its Agent handles to c2c aliases.
11
+
12
+ Concrete example:
13
+ - Coordinator directly started T244/G9 implementer: agent `81a4688f-82ce-40e`, alias `pi-8391d3-ae5887a`.
14
+ - T244 spawned reviewer/rereviewer Agent subagents with IDs:
15
+ - `436193fa-ef20-4d0`
16
+ - `97bddbcf-53a8-48c`
17
+ - `56a58df4-c91c-48a`
18
+ - `82ae1d14-707d-415`
19
+ - Coordinator received registrations for aliases including `pi-8391d3-a723e9a`, `pi-8391d3-a0a8a00`, `pi-8391d3-ab7ed6f`, `pi-8391d3-ab167dc`.
20
+ - T244 reported: its transcript only showed Agent result handles/output paths/completion summaries; it did not see `Subagent <id> registered as pi-...` messages and could not map reviewer IDs to c2c aliases.
21
+
22
+ Expected behavior:
23
+ - Alias registration notifications for a subagent spawned by another subagent should be delivered to the spawning subagent's transcript, or at least to both the spawning subagent and coordinator with parent/owner metadata.
24
+ - The notification should include parent/spawner identity so coordinators can distinguish direct children from nested reviewer subagents.
25
+
26
+ Impact:
27
+ - Coordinator receives noisy/ambiguous registrations for unknown aliases.
28
+ - Spawning subagent cannot c2c-message its own reviewers by alias or report alias ownership accurately.
29
+ - Requires extra coordinator debugging messages to identify ownership.
30
+
31
+ Suggested fix:
32
+ - Route `Subagent <id> registered as <alias>` to the agent that invoked the Agent tool.
33
+ - Add fields like `parent_agent_id`, `parent_alias`, and maybe `root_coordinator_alias` to registration notifications.
34
+ - If root coordinator must also receive nested registrations, label them explicitly as nested.
35
+
36
+ Additional observation after filing:
37
+ - Coordinator later received another nested-looking registration: `Subagent 8262e0d6-457e-49c registered as pi-8391d3-ab41e8a`, again without the coordinator directly starting that subagent.
38
+ - Confirmed with T245/G6 implementer `pi-8391d3-a861bf0`: it spawned reviewer Agent IDs `8262e0d6-457e-49c` and `14d29ca7-348e-437`, while coordinator received alias registrations `pi-8391d3-ab41e8a` and `pi-8391d3-aa90cd5`; T245 reported it had not received any c2c alias registration notifications for them.
39
+ - After crash recovery restart, coordinator received another nested-looking registration: `Subagent 4f396572-cab5-42c registered as pi-8391d3-a52ab8a`; likely spawned by a recovery worker/reviewer rather than directly by coordinator.
40
+ - Coordinator also received `Subagent 09dc9e7f-f960-411 registered as pi-8391d3-a219275` during recovery worker activity, again likely nested/reviewer notification routed to coordinator.
41
+ - Coordinator also received `Subagent ee594feb-8fd1-458 registered as pi-8391d3-a8f6d40` while nested reviewer activity was ongoing; likely another nested alias notification routed to coordinator.
42
+ - Coordinator also received `Subagent dd29fe5b-357a-48f registered as pi-8391d3-afd6cfc` during T246 nested rereview activity; likely another nested alias notification routed to coordinator.
43
+ - Coordinator received nested-looking registrations `Subagent 9aa66b38-3ba8-405 registered as pi-8391d3-a6cefa4` and `Subagent d33bb514-be85-4a5 registered as pi-8391d3-ae99126` during active worker review activity.
44
+ - Coordinator received nested-looking registration `Subagent ab7cb925-d76c-489 registered as pi-8391d3-a8c6afd` during active worker review activity.
45
+ - Coordinator received nested-looking registration `Subagent ba23eaaf-a706-459 registered as pi-8391d3-af5d393` during active worker review activity.
46
+ - Coordinator received nested-looking registration `Subagent adf52e5e-862e-4c6 registered as pi-8391d3-aa897ea` during active worker review activity.
47
+ - Coordinator received nested-looking registration `Subagent e6eb1662-8223-458 registered as pi-8391d3-adb2231` during active worker review activity.
48
+ - Coordinator received nested-looking registration `Subagent 565012aa-43b6-407 registered as pi-8391d3-a66be3e` during active worker review activity.
49
+ - Coordinator received nested-looking registration `Subagent 603b211a-9daa-421 registered as pi-8391d3-aa92c43` during active worker review activity.
50
+ - Coordinator received nested-looking registration `Subagent 734f3fa3-958b-4bd registered as pi-8391d3-a028432` during active T248 review activity.
51
+ - Coordinator received nested-looking registration `Subagent 2d565584-fcbc-496 registered as pi-8391d3-ac6e43c` during active T250/T251/T252 review activity.
52
+ - Coordinator received nested-looking registration `Subagent 5e7ee0d1-5112-4b9 registered as pi-8391d3-a7722c2` during active T250/T251/T252 review activity.
53
+ - Coordinator received nested-looking registration `Subagent eae7393b-dfb4-445 registered as pi-8391d3-a2a9d62` during active T250/T251/T252 review activity.
54
+ - Coordinator received nested-looking registration `Subagent 6629a0fe-daef-401 registered as pi-8391d3-ad0b4e5` during active T250 review activity.
55
+ - Coordinator received nested-looking registration `Subagent 49eb5d11-178d-4d2 registered as pi-8391d3-a979978` during active T250 review activity.
56
+ - Coordinator received nested-looking registration `Subagent bf846951-6b2f-4b5 registered as pi-8391d3-aabc2c5` during active T250 rereview activity.
57
+ - Coordinator received nested-looking registration `Subagent 3250e286-4fd4-4a2 registered as pi-8391d3-a78b404` during active T250 rereview activity.
@@ -94,6 +94,10 @@ export declare class AgentManager {
94
94
  private queue;
95
95
  /** Number of currently running background agents. */
96
96
  private runningBackground;
97
+ /** Background agents by id; foreground agents must not emit background completion callbacks. */
98
+ private backgroundAgentIds;
99
+ /** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
100
+ private completedBackgroundCallbacks;
97
101
  constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart, onCompact?: OnAgentCompact);
98
102
  /** Update the max concurrent background agents limit. */
99
103
  setMaxConcurrent(n: number): void;
@@ -103,6 +107,8 @@ export declare class AgentManager {
103
107
  * If the concurrency limit is reached, the agent is queued.
104
108
  */
105
109
  spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string;
110
+ /** Emit background completion once, optionally releasing a running concurrency slot. */
111
+ private completeBackground;
106
112
  /** Actually start an agent (called immediately or from queue drain). */
107
113
  private startAgent;
108
114
  /** Start queued agents up to the concurrency limit. */
@@ -119,6 +125,8 @@ export declare class AgentManager {
119
125
  abort(id: string): boolean;
120
126
  /** Dispose a record's session and remove it from the map. */
121
127
  private removeRecord;
128
+ /** Remove selected terminal records. Running and queued records are never removed. */
129
+ clearRecords(ids: string[]): string[];
122
130
  private cleanup;
123
131
  /**
124
132
  * Remove all completed/stopped/errored records immediately.
@@ -51,6 +51,10 @@ export class AgentManager {
51
51
  queue = [];
52
52
  /** Number of currently running background agents. */
53
53
  runningBackground = 0;
54
+ /** Background agents by id; foreground agents must not emit background completion callbacks. */
55
+ backgroundAgentIds = new Set();
56
+ /** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
57
+ completedBackgroundCallbacks = new Set();
54
58
  constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, onCompact) {
55
59
  this.onComplete = onComplete;
56
60
  this.onStart = onStart;
@@ -103,6 +107,8 @@ export class AgentManager {
103
107
  options.onOutputFileCreated?.(record.outputFile, id);
104
108
  }
105
109
  this.agents.set(id, record);
110
+ if (options.isBackground)
111
+ this.backgroundAgentIds.add(id);
106
112
  const args = { pi, ctx, type, prompt, options };
107
113
  if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
108
114
  // Queue it — will be started when a running agent completes
@@ -115,11 +121,26 @@ export class AgentManager {
115
121
  this.startAgent(id, record, args);
116
122
  }
117
123
  catch (err) {
124
+ this.backgroundAgentIds.delete(id);
118
125
  this.agents.delete(id);
119
126
  throw err;
120
127
  }
121
128
  return id;
122
129
  }
130
+ /** Emit background completion once, optionally releasing a running concurrency slot. */
131
+ completeBackground(record, releaseRunningSlot, drain = true) {
132
+ if (this.completedBackgroundCallbacks.has(record.id))
133
+ return;
134
+ this.completedBackgroundCallbacks.add(record.id);
135
+ if (releaseRunningSlot)
136
+ this.runningBackground = Math.max(0, this.runningBackground - 1);
137
+ try {
138
+ this.onComplete?.(record);
139
+ }
140
+ catch { /* ignore completion side-effect errors */ }
141
+ if (drain)
142
+ this.drainQueue();
143
+ }
123
144
  /** Actually start an agent (called immediately or from queue drain). */
124
145
  startAgent(id, record, { pi, ctx, type, prompt, options }) {
125
146
  // Re-validate a caller-supplied cwd: queued spawns can start minutes after
@@ -239,12 +260,7 @@ export class AgentManager {
239
260
  }
240
261
  }
241
262
  if (options.isBackground) {
242
- this.runningBackground--;
243
- try {
244
- this.onComplete?.(record);
245
- }
246
- catch { /* ignore completion side-effect errors */ }
247
- this.drainQueue();
263
+ this.completeBackground(record, true);
248
264
  }
249
265
  return responseText;
250
266
  })
@@ -273,9 +289,7 @@ export class AgentManager {
273
289
  catch { /* ignore cleanup errors */ }
274
290
  }
275
291
  if (options.isBackground) {
276
- this.runningBackground--;
277
- this.onComplete?.(record);
278
- this.drainQueue();
292
+ this.completeBackground(record, true);
279
293
  }
280
294
  return "";
281
295
  });
@@ -297,7 +311,7 @@ export class AgentManager {
297
311
  record.status = "error";
298
312
  record.error = err instanceof Error ? err.message : String(err);
299
313
  record.completedAt = Date.now();
300
- this.onComplete?.(record);
314
+ this.completeBackground(record, false, false);
301
315
  }
302
316
  }
303
317
  }
@@ -326,6 +340,8 @@ export class AgentManager {
326
340
  record.error = undefined;
327
341
  record.resultConsumed = false;
328
342
  record.abortController = new AbortController();
343
+ this.backgroundAgentIds.add(id);
344
+ this.completedBackgroundCallbacks.delete(id);
329
345
  this.runningBackground++;
330
346
  this.onStart?.(record);
331
347
  const onParentAbort = () => this.abort(id);
@@ -354,12 +370,7 @@ export class AgentManager {
354
370
  record.result = responseText;
355
371
  record.completedAt = Date.now();
356
372
  detach();
357
- this.runningBackground--;
358
- try {
359
- this.onComplete?.(record);
360
- }
361
- catch { /* ignore completion side-effect errors */ }
362
- this.drainQueue();
373
+ this.completeBackground(record, true);
363
374
  return responseText;
364
375
  }).catch((err) => {
365
376
  if (record.status !== "stopped")
@@ -367,12 +378,7 @@ export class AgentManager {
367
378
  record.error = err instanceof Error ? err.message : String(err);
368
379
  record.completedAt = Date.now();
369
380
  detach();
370
- this.runningBackground--;
371
- try {
372
- this.onComplete?.(record);
373
- }
374
- catch { /* ignore completion side-effect errors */ }
375
- this.drainQueue();
381
+ this.completeBackground(record, true);
376
382
  return "";
377
383
  });
378
384
  record.promise = promise;
@@ -393,6 +399,7 @@ export class AgentManager {
393
399
  this.queue = this.queue.filter(q => q.id !== id);
394
400
  record.status = "stopped";
395
401
  record.completedAt = Date.now();
402
+ this.completeBackground(record, false);
396
403
  return true;
397
404
  }
398
405
  if (record.status !== "running")
@@ -400,14 +407,32 @@ export class AgentManager {
400
407
  record.abortController?.abort();
401
408
  record.status = "stopped";
402
409
  record.completedAt = Date.now();
410
+ if (this.backgroundAgentIds.has(id))
411
+ this.completeBackground(record, true);
403
412
  return true;
404
413
  }
405
414
  /** Dispose a record's session and remove it from the map. */
406
415
  removeRecord(id, record) {
407
416
  record.session?.dispose?.();
408
417
  record.session = undefined;
418
+ this.backgroundAgentIds.delete(id);
419
+ this.completedBackgroundCallbacks.delete(id);
409
420
  this.agents.delete(id);
410
421
  }
422
+ /** Remove selected terminal records. Running and queued records are never removed. */
423
+ clearRecords(ids) {
424
+ const removed = [];
425
+ for (const id of ids) {
426
+ const record = this.agents.get(id);
427
+ if (!record)
428
+ continue;
429
+ if (record.status === "running" || record.status === "queued")
430
+ continue;
431
+ this.removeRecord(id, record);
432
+ removed.push(id);
433
+ }
434
+ return removed;
435
+ }
411
436
  cleanup() {
412
437
  const cutoff = Date.now() - 10 * 60_000;
413
438
  for (const [id, record] of this.agents) {
@@ -442,6 +467,8 @@ export class AgentManager {
442
467
  if (record) {
443
468
  record.status = "stopped";
444
469
  record.completedAt = Date.now();
470
+ if (this.backgroundAgentIds.has(record.id))
471
+ this.completedBackgroundCallbacks.add(record.id);
445
472
  count++;
446
473
  }
447
474
  }
@@ -452,9 +479,12 @@ export class AgentManager {
452
479
  record.abortController?.abort();
453
480
  record.status = "stopped";
454
481
  record.completedAt = Date.now();
482
+ if (this.backgroundAgentIds.has(record.id))
483
+ this.completedBackgroundCallbacks.add(record.id);
455
484
  count++;
456
485
  }
457
486
  }
487
+ this.runningBackground = 0;
458
488
  return count;
459
489
  }
460
490
  /** Wait for all running and queued agents to complete (including queued ones). */
@@ -480,6 +510,8 @@ export class AgentManager {
480
510
  record.session?.dispose();
481
511
  }
482
512
  this.agents.clear();
513
+ this.backgroundAgentIds.clear();
514
+ this.completedBackgroundCallbacks.clear();
483
515
  // Prune any orphaned git worktrees (crash recovery)
484
516
  try {
485
517
  pruneWorktrees(process.cwd());
@@ -15,10 +15,13 @@ export declare const SUBAGENT_TOOL_NAMES: {
15
15
  readonly AGENT: "Agent";
16
16
  readonly GET_RESULT: "get_subagent_result";
17
17
  readonly STEER: "steer_subagent";
18
+ readonly LIST_SUBAGENTS: "list_subagents";
19
+ readonly CLEAR_SUBAGENTS: "clear_subagents";
18
20
  readonly LIST_MODELS: "list_models";
19
21
  };
20
22
  export declare function getCurrentExtensionDepth(): number;
21
23
  export declare function getCurrentExtensionAgentId(): string | undefined;
24
+ export declare function getCurrentExtensionParentAgentId(): string | undefined;
22
25
  /**
23
26
  * Canonical name of an extension for `extensions: [...]` allowlist matching.
24
27
  * Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
@@ -23,6 +23,8 @@ export const SUBAGENT_TOOL_NAMES = {
23
23
  AGENT: "Agent",
24
24
  GET_RESULT: "get_subagent_result",
25
25
  STEER: "steer_subagent",
26
+ LIST_SUBAGENTS: "list_subagents",
27
+ CLEAR_SUBAGENTS: "clear_subagents",
26
28
  LIST_MODELS: "list_models",
27
29
  };
28
30
  /**
@@ -53,7 +55,11 @@ function getLoadingExtensionAgentId() {
53
55
  const value = globalThis[EXTENSION_DEPTH_KEY];
54
56
  return value && typeof value.agentId === "string" ? value.agentId : undefined;
55
57
  }
56
- async function withLoadingExtensionDepth(depth, agentId, fn) {
58
+ function getLoadingExtensionParentAgentId() {
59
+ const value = globalThis[EXTENSION_DEPTH_KEY];
60
+ return value && typeof value.parentAgentId === "string" ? value.parentAgentId : undefined;
61
+ }
62
+ async function withLoadingExtensionDepth(depth, agentId, parentAgentId, fn) {
57
63
  const previous = extensionDepthLoadChain;
58
64
  let release;
59
65
  extensionDepthLoadChain = new Promise((resolve) => {
@@ -63,7 +69,7 @@ async function withLoadingExtensionDepth(depth, agentId, fn) {
63
69
  try {
64
70
  const g = globalThis;
65
71
  const prev = g[EXTENSION_DEPTH_KEY];
66
- g[EXTENSION_DEPTH_KEY] = { depth, agentId };
72
+ g[EXTENSION_DEPTH_KEY] = { depth, agentId, parentAgentId };
67
73
  try {
68
74
  return await fn();
69
75
  }
@@ -84,6 +90,9 @@ export function getCurrentExtensionDepth() {
84
90
  export function getCurrentExtensionAgentId() {
85
91
  return getLoadingExtensionAgentId();
86
92
  }
93
+ export function getCurrentExtensionParentAgentId() {
94
+ return getLoadingExtensionParentAgentId();
95
+ }
87
96
  /**
88
97
  * Canonical name of an extension for `extensions: [...]` allowlist matching.
89
98
  * Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
@@ -442,7 +451,7 @@ export async function runAgent(ctx, type, prompt, options) {
442
451
  systemPromptOverride: () => systemPrompt,
443
452
  appendSystemPromptOverride: () => [],
444
453
  });
445
- await withLoadingExtensionDepth(depth, options.agentId, () => loader.reload());
454
+ await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
446
455
  // Plain entries in `tools:` are expected to be built-in names (extension tools
447
456
  // go through `ext:`), so an unknown name there is unambiguously a typo. Previously
448
457
  // this produced a silently broken agent (#75) — pi-mono accepted the bogus name
@@ -0,0 +1,15 @@
1
+ /**
2
+ * dashboard-ui.ts — Register dashboard UI modules for subagent visibility.
3
+ *
4
+ * Provides three integration points with pi-agent-dashboard:
5
+ * 1. Footer-segment decorator showing running/completed agent counts
6
+ * 2. Management-modal module with a table view of all subagent history
7
+ * 3. Round-trip event handlers for data fetch, abort, and steer actions
8
+ */
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import type { AgentManager } from "./agent-manager.js";
11
+ /**
12
+ * Register all dashboard UI integration points.
13
+ * Call once during extension setup when pi.events is available.
14
+ */
15
+ export declare function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager): void;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * dashboard-ui.ts — Register dashboard UI modules for subagent visibility.
3
+ *
4
+ * Provides three integration points with pi-agent-dashboard:
5
+ * 1. Footer-segment decorator showing running/completed agent counts
6
+ * 2. Management-modal module with a table view of all subagent history
7
+ * 3. Round-trip event handlers for data fetch, abort, and steer actions
8
+ */
9
+ import { formatMs, getDisplayName } from "./ui/agent-widget.js";
10
+ import { getLifetimeTotal } from "./usage.js";
11
+ const NAMESPACE = "subagents";
12
+ const MODULE_ID = "subagents-overview";
13
+ const DATA_EVENT = "subagents:rows";
14
+ const INVALIDATE_DEBOUNCE_MS = 500;
15
+ /**
16
+ * Build a row for the management-modal table from an AgentRecord.
17
+ */
18
+ function buildAgentRow(record) {
19
+ const durationMs = record.completedAt
20
+ ? record.completedAt - record.startedAt
21
+ : Date.now() - record.startedAt;
22
+ const totalTokens = getLifetimeTotal(record.lifetimeUsage);
23
+ return {
24
+ id: record.id,
25
+ type: getDisplayName(record.type),
26
+ description: record.description ?? "",
27
+ status: record.status,
28
+ toolUses: record.toolUses ?? 0,
29
+ tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
30
+ duration: formatMs(durationMs),
31
+ outputFile: record.outputFile ?? "",
32
+ startedAt: record.startedAt,
33
+ };
34
+ }
35
+ function formatTokenCount(n) {
36
+ if (n >= 1_000_000)
37
+ return `${(n / 1_000_000).toFixed(1)}M`;
38
+ if (n >= 1_000)
39
+ return `${(n / 1_000).toFixed(1)}k`;
40
+ return String(n);
41
+ }
42
+ /**
43
+ * Register all dashboard UI integration points.
44
+ * Call once during extension setup when pi.events is available.
45
+ */
46
+ export function registerDashboardModules(pi, manager) {
47
+ if (!pi.events)
48
+ return;
49
+ let invalidateTimer;
50
+ function scheduleInvalidate() {
51
+ if (invalidateTimer)
52
+ return;
53
+ invalidateTimer = setTimeout(() => {
54
+ invalidateTimer = undefined;
55
+ pi.events.emit("ui:invalidate", {});
56
+ }, INVALIDATE_DEBOUNCE_MS);
57
+ }
58
+ // ── 1. Module Discovery (ui:list-modules) ──────────────────────────
59
+ pi.events.on("ui:list-modules", ((probe) => {
60
+ const agents = manager.listAgents();
61
+ const running = agents.filter(a => a.status === "running").length;
62
+ const completed = agents.filter(a => a.status === "completed").length;
63
+ const total = agents.length;
64
+ // Footer-segment: running/completed counts
65
+ const parts = [];
66
+ if (running > 0)
67
+ parts.push(`● ${running} running`);
68
+ if (completed > 0)
69
+ parts.push(`✓ ${completed} done`);
70
+ if (total === 0)
71
+ parts.push("No agents");
72
+ probe.modules.push({
73
+ kind: "footer-segment",
74
+ namespace: NAMESPACE,
75
+ id: "agent-counts",
76
+ payload: {
77
+ text: parts.join(" · "),
78
+ tooltip: `${total} total agents (${running} running, ${completed} completed)`,
79
+ icon: "mdiRobot",
80
+ },
81
+ });
82
+ // Management-modal: subagent overview table
83
+ probe.modules.push({
84
+ kind: "management-modal",
85
+ id: MODULE_ID,
86
+ command: "/subagents",
87
+ title: "Subagents",
88
+ description: "View and manage background subagents",
89
+ icon: "mdiRobotOutline",
90
+ category: "subagents",
91
+ view: {
92
+ kind: "table",
93
+ dataEvent: DATA_EVENT,
94
+ rowKey: "id",
95
+ fields: [
96
+ { key: "id", label: "ID", kind: "text", width: 120 },
97
+ { key: "type", label: "Type", kind: "text", width: 100 },
98
+ { key: "description", label: "Description", kind: "text" },
99
+ { key: "status", label: "Status", kind: "text", width: 90 },
100
+ { key: "toolUses", label: "Tools", kind: "number", width: 60 },
101
+ { key: "tokens", label: "Tokens", kind: "text", width: 80 },
102
+ { key: "duration", label: "Duration", kind: "text", width: 80 },
103
+ ],
104
+ rowActions: [
105
+ {
106
+ id: "view-result",
107
+ label: "View Result",
108
+ icon: "mdiEye",
109
+ variant: "primary",
110
+ event: "subagents:ui:view-result",
111
+ },
112
+ {
113
+ id: "abort",
114
+ label: "Abort",
115
+ icon: "mdiStop",
116
+ variant: "danger",
117
+ event: "subagents:ui:abort",
118
+ confirm: "Abort this running agent?",
119
+ },
120
+ {
121
+ id: "steer",
122
+ label: "Steer",
123
+ icon: "mdiMessageArrowRight",
124
+ variant: "secondary",
125
+ event: "subagents:ui:steer",
126
+ },
127
+ ],
128
+ emptyState: "No subagents have been spawned in this session.",
129
+ actions: [
130
+ {
131
+ id: "refresh",
132
+ label: "Refresh",
133
+ icon: "mdiRefresh",
134
+ variant: "secondary",
135
+ event: "subagents:ui:refresh",
136
+ },
137
+ ],
138
+ },
139
+ });
140
+ }));
141
+ // ── 2. Data Fetch Handler ──────────────────────────────────────────
142
+ pi.events.on(DATA_EVENT, ((data) => {
143
+ const agents = manager.listAgents();
144
+ data.items = agents.map(buildAgentRow);
145
+ }));
146
+ // ── 3. Action Handlers ─────────────────────────────────────────────
147
+ // Refresh: just invalidate to re-probe + re-fetch
148
+ pi.events.on("subagents:ui:refresh", (() => {
149
+ scheduleInvalidate();
150
+ }));
151
+ // View Result: emit the result as a toast so the dashboard shows it
152
+ pi.events.on("subagents:ui:view-result", ((data) => {
153
+ const agentId = data.params?.id ?? data.id;
154
+ const record = manager.getRecord(agentId);
155
+ if (!record) {
156
+ pi.events.emit("ui:invalidate", { id: MODULE_ID });
157
+ return;
158
+ }
159
+ const resultText = record.result?.trim() || "No output yet.";
160
+ const preview = resultText.length > 2000
161
+ ? resultText.slice(0, 2000) + "\n…(truncated)"
162
+ : resultText;
163
+ pi.events.emit("ui:invalidate", { id: MODULE_ID });
164
+ // Return result as items so it shows in a detail view
165
+ data.items = [{
166
+ id: record.id,
167
+ type: getDisplayName(record.type),
168
+ description: record.description,
169
+ status: record.status,
170
+ result: preview,
171
+ outputFile: record.outputFile ?? "",
172
+ }];
173
+ }));
174
+ // Abort: signal the agent to stop
175
+ pi.events.on("subagents:ui:abort", ((data) => {
176
+ const agentId = data.params?.id ?? data.id;
177
+ const record = manager.getRecord(agentId);
178
+ if (record?.session) {
179
+ // Use the session's abort mechanism
180
+ try {
181
+ record.session.dispose?.();
182
+ }
183
+ catch {
184
+ // Ignore disposal errors
185
+ }
186
+ }
187
+ scheduleInvalidate();
188
+ }));
189
+ // Steer: for now just invalidate — full steer requires a prompt input
190
+ // which the management-modal form view could support in the future
191
+ pi.events.on("subagents:ui:steer", ((_data) => {
192
+ // TODO: Could open a form view for entering the steer message
193
+ scheduleInvalidate();
194
+ }));
195
+ // ── 4. Invalidate on agent lifecycle events ────────────────────────
196
+ const lifecycleEvents = [
197
+ "subagents:created",
198
+ "subagents:started",
199
+ "subagents:completed",
200
+ "subagents:failed",
201
+ "subagents:compacted",
202
+ ];
203
+ for (const event of lifecycleEvents) {
204
+ pi.events.on(event, (() => scheduleInvalidate()));
205
+ }
206
+ }