@clanker-code/pi-subagents 0.10.8 → 0.11.1

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.
@@ -0,0 +1,231 @@
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
+ model: record.invocation?.modelName ?? "—",
28
+ status: record.status,
29
+ toolUses: record.toolUses ?? 0,
30
+ tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
31
+ duration: formatMs(durationMs),
32
+ outputFile: record.outputFile ?? "",
33
+ startedAt: record.startedAt,
34
+ };
35
+ }
36
+ function formatTokenCount(n) {
37
+ if (n >= 1_000_000)
38
+ return `${(n / 1_000_000).toFixed(1)}M`;
39
+ if (n >= 1_000)
40
+ return `${(n / 1_000).toFixed(1)}k`;
41
+ return String(n);
42
+ }
43
+ /**
44
+ * Register all dashboard UI integration points.
45
+ * Call once during extension setup when pi.events is available.
46
+ */
47
+ export function registerDashboardModules(pi, manager) {
48
+ if (!pi.events)
49
+ return;
50
+ let invalidateTimer;
51
+ function scheduleInvalidate() {
52
+ if (invalidateTimer)
53
+ return;
54
+ invalidateTimer = setTimeout(() => {
55
+ invalidateTimer = undefined;
56
+ pi.events.emit("ui:invalidate", {});
57
+ }, INVALIDATE_DEBOUNCE_MS);
58
+ }
59
+ // ── 1. Module Discovery (ui:list-modules) ──────────────────────────
60
+ // Guard against duplicate pushes: the bridge may call refreshUiModules
61
+ // multiple times per probe cycle when multiple sessions each register
62
+ // their own ui:invalidate listener. Check if our modules are already
63
+ // present before pushing.
64
+ pi.events.on("ui:list-modules", ((probe) => {
65
+ const alreadyContributed = probe.modules.some((m) => m.kind === "management-modal" && m.id === MODULE_ID);
66
+ if (alreadyContributed)
67
+ return;
68
+ const agents = manager.listAgents();
69
+ const running = agents.filter(a => a.status === "running").length;
70
+ const completed = agents.filter(a => a.status === "completed").length;
71
+ const total = agents.length;
72
+ // Footer-segment: running/completed counts
73
+ const parts = [];
74
+ if (running > 0)
75
+ parts.push(`● ${running} running`);
76
+ if (completed > 0)
77
+ parts.push(`✓ ${completed} done`);
78
+ if (total === 0)
79
+ parts.push("No agents");
80
+ probe.modules.push({
81
+ kind: "footer-segment",
82
+ namespace: NAMESPACE,
83
+ id: "agent-counts",
84
+ payload: {
85
+ text: parts.join(" · "),
86
+ tooltip: `${total} total agents (${running} running, ${completed} completed)`,
87
+ icon: "mdiRobot",
88
+ },
89
+ });
90
+ // Management-modal: subagent overview table
91
+ probe.modules.push({
92
+ kind: "management-modal",
93
+ id: MODULE_ID,
94
+ command: "/subagents",
95
+ title: "Subagents",
96
+ description: "View and manage background subagents",
97
+ icon: "mdiRobotOutline",
98
+ category: "subagents",
99
+ view: {
100
+ kind: "table",
101
+ dataEvent: DATA_EVENT,
102
+ rowKey: "id",
103
+ fields: [
104
+ { key: "id", label: "ID", kind: "text", width: 120 },
105
+ { key: "type", label: "Type", kind: "text", width: 100 },
106
+ { key: "description", label: "Description", kind: "text" },
107
+ { key: "model", label: "Model", kind: "text", width: 80 },
108
+ { key: "status", label: "Status", kind: "text", width: 90 },
109
+ { key: "toolUses", label: "Tools", kind: "number", width: 60 },
110
+ { key: "tokens", label: "Tokens", kind: "text", width: 80 },
111
+ { key: "duration", label: "Duration", kind: "text", width: 80 },
112
+ ],
113
+ rowActions: [
114
+ {
115
+ id: "view-result",
116
+ label: "View Result",
117
+ icon: "mdiEye",
118
+ variant: "primary",
119
+ event: "subagents:ui:view-result",
120
+ },
121
+ {
122
+ id: "abort",
123
+ label: "Abort",
124
+ icon: "mdiStop",
125
+ variant: "danger",
126
+ event: "subagents:ui:abort",
127
+ confirm: "Abort this running agent?",
128
+ },
129
+ {
130
+ id: "steer",
131
+ label: "Steer",
132
+ icon: "mdiMessageArrowRight",
133
+ variant: "secondary",
134
+ event: "subagents:ui:steer",
135
+ },
136
+ ],
137
+ emptyState: "No subagents have been spawned in this session.",
138
+ actions: [
139
+ {
140
+ id: "refresh",
141
+ label: "Refresh",
142
+ icon: "mdiRefresh",
143
+ variant: "secondary",
144
+ event: "subagents:ui:refresh",
145
+ },
146
+ ],
147
+ },
148
+ });
149
+ }));
150
+ // ── 2. Data Fetch Handler ──────────────────────────────────────────
151
+ pi.events.on(DATA_EVENT, ((data) => {
152
+ const agents = manager.listAgents();
153
+ data.items = agents.map(buildAgentRow);
154
+ }));
155
+ // ── 3. Action Handlers ─────────────────────────────────────────────
156
+ // Refresh: just invalidate to re-probe + re-fetch
157
+ pi.events.on("subagents:ui:refresh", (() => {
158
+ scheduleInvalidate();
159
+ }));
160
+ // View Result: return the agent's result as table rows so the modal
161
+ // displays it. The bridge's synchronous fast path calls `_reply(items)`
162
+ // when `data.items` is populated by the handler — do NOT call
163
+ // `scheduleInvalidate()` here as the subsequent re-probe would
164
+ // overwrite the returned rows with the original table data.
165
+ pi.events.on("subagents:ui:view-result", ((data) => {
166
+ // Bridge spreads msg.params into data; row identity is at data.row.id.
167
+ const agentId = data.row?.id ?? data.id;
168
+ if (!agentId)
169
+ return;
170
+ const record = manager.getRecord(agentId);
171
+ if (!record)
172
+ return;
173
+ const resultText = record.result?.trim() || "No output yet.";
174
+ const preview = resultText.length > 2000
175
+ ? resultText.slice(0, 2000) + "\n…(truncated)"
176
+ : resultText;
177
+ // Populate data.items — the bridge's synchronous fast path forwards
178
+ // this as a `ui_data_list` message back to the dashboard.
179
+ data.items = [{
180
+ id: record.id,
181
+ type: getDisplayName(record.type),
182
+ description: record.description,
183
+ status: record.status,
184
+ result: preview,
185
+ outputFile: record.outputFile ?? "",
186
+ }];
187
+ }));
188
+ // Abort: stop the running agent via the manager's abort() method
189
+ // which properly cancels the AbortController and cleans up state.
190
+ pi.events.on("subagents:ui:abort", ((data) => {
191
+ const agentId = data.row?.id ?? data.id;
192
+ if (!agentId)
193
+ return;
194
+ manager.abort(agentId);
195
+ scheduleInvalidate();
196
+ }));
197
+ // Steer: send a steering message to a running agent's session.
198
+ // The management-modal row action carries the row identity; we steer
199
+ // with a default "Continue" nudge. A future form view could accept
200
+ // custom text.
201
+ pi.events.on("subagents:ui:steer", ((data) => {
202
+ const agentId = data.row?.id ?? data.id;
203
+ if (!agentId)
204
+ return;
205
+ const record = manager.getRecord(agentId);
206
+ if (!record)
207
+ return;
208
+ if (record.status === "running" && record.session) {
209
+ // Session is live — steer immediately
210
+ record.session.steer("Continue").catch(() => { });
211
+ }
212
+ else if (record.status === "queued") {
213
+ // Session not yet created — queue the steer for flush on start
214
+ if (!record.pendingSteers)
215
+ record.pendingSteers = [];
216
+ record.pendingSteers.push("Continue");
217
+ }
218
+ scheduleInvalidate();
219
+ }));
220
+ // ── 4. Invalidate on agent lifecycle events ────────────────────────
221
+ const lifecycleEvents = [
222
+ "subagents:created",
223
+ "subagents:started",
224
+ "subagents:completed",
225
+ "subagents:failed",
226
+ "subagents:compacted",
227
+ ];
228
+ for (const event of lifecycleEvents) {
229
+ pi.events.on(event, (() => scheduleInvalidate()));
230
+ }
231
+ }
@@ -30,7 +30,6 @@ export const DEFAULT_AGENTS = new Map([
30
30
  builtinToolNames: READ_ONLY_TOOLS,
31
31
  extensions: true,
32
32
  skills: true,
33
- model: "anthropic/claude-haiku-4-5-20251001",
34
33
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
35
34
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
36
35
  Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
package/dist/index.js CHANGED
@@ -23,8 +23,10 @@ import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDe
23
23
  import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
24
24
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
25
25
  import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
26
+ import { extractText } from "./context.js";
26
27
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
27
28
  import { loadCustomAgents } from "./custom-agents.js";
29
+ import { registerDashboardModules } from "./dashboard-ui.js";
28
30
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
29
31
  import { GroupJoinManager } from "./group-join.js";
30
32
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
@@ -36,8 +38,9 @@ import { SubagentScheduler } from "./schedule.js";
36
38
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
37
39
  import { applyAndEmitLoaded, DEFAULT_WAIT_TIMEOUT_SECONDS, saveAndEmitChanged } from "./settings.js";
38
40
  import { getStatusNote } from "./status-note.js";
41
+ import { registerSubagentListClearTools } from "./subagent-list-clear.js";
39
42
  import { MAX_RECURSIVE_DEPTH } from "./types.js";
40
- import { renderAgentCall, renderAgentResult, renderSteerCall, tailPreview } from "./ui/agent-tool-rendering.js";
43
+ import { renderAgentCall, renderAgentResult, renderSteerCall, snipMiddleLines, tailPreview } from "./ui/agent-tool-rendering.js";
41
44
  import { AgentWidget, buildInvocationTags, describeActivity, formatContextWindow, formatDuration, getDisplayName, getPromptModeLabel, } from "./ui/agent-widget.js";
42
45
  import { menuSelect } from "./ui/menu-select.js";
43
46
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
@@ -376,7 +379,7 @@ export default function (pi) {
376
379
  return; // sessionId not yet available — try again on next event
377
380
  const path = resolveStorePath(ctx.cwd, sessionId);
378
381
  const store = new ScheduleStore(path);
379
- scheduler.start(pi, ctx, manager, store);
382
+ scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
380
383
  pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
381
384
  }
382
385
  catch (err) {
@@ -388,6 +391,8 @@ export default function (pi) {
388
391
  // Capture ctx from session_start for RPC spawn handler + start the scheduler.
389
392
  pi.on("session_start", async (_event, ctx) => {
390
393
  currentCtx = ctx;
394
+ clearBatchState();
395
+ groupJoin.dispose();
391
396
  manager.clearCompleted();
392
397
  widget.clearSnapshots();
393
398
  retryStash.clear();
@@ -395,8 +400,10 @@ export default function (pi) {
395
400
  startScheduler(ctx);
396
401
  });
397
402
  pi.on("session_before_switch", () => {
403
+ clearBatchState();
404
+ groupJoin.dispose();
398
405
  manager.clearCompleted();
399
- widget.clearSnapshots();
406
+ widget.dispose();
400
407
  retryStash.clear();
401
408
  scheduler.stop();
402
409
  });
@@ -405,6 +412,8 @@ export default function (pi) {
405
412
  pi,
406
413
  getCtx: () => currentCtx,
407
414
  manager,
415
+ depth: nextSubagentDepth,
416
+ parentAgentId: extensionAgentId,
408
417
  });
409
418
  // Broadcast readiness so extensions loaded after us can discover us
410
419
  pi.events.emit("subagents:ready", {});
@@ -414,13 +423,17 @@ export default function (pi) {
414
423
  unsubSpawnRpc();
415
424
  unsubStopRpc();
416
425
  unsubPingRpc();
426
+ unsubWidgetCreated?.();
417
427
  unsubWidgetStarted?.();
418
428
  unsubWidgetCompleted?.();
419
429
  unsubWidgetFailed?.();
420
430
  currentCtx = undefined;
421
431
  delete globalThis[MANAGER_KEY];
422
432
  scheduler.stop();
433
+ clearBatchState();
434
+ groupJoin.dispose();
423
435
  manager.abortAll();
436
+ widget.dispose();
424
437
  for (const timer of pendingNudges.values())
425
438
  clearTimeout(timer);
426
439
  pendingNudges.clear();
@@ -434,6 +447,7 @@ export default function (pi) {
434
447
  if (snapshot)
435
448
  widget.upsertSnapshot(snapshot);
436
449
  };
450
+ const unsubWidgetCreated = pi.events.on("subagents:created", upsertWidgetEventSnapshot);
437
451
  const unsubWidgetStarted = pi.events.on("subagents:started", upsertWidgetEventSnapshot);
438
452
  const unsubWidgetCompleted = pi.events.on("subagents:completed", upsertWidgetEventSnapshot);
439
453
  const unsubWidgetFailed = pi.events.on("subagents:failed", upsertWidgetEventSnapshot);
@@ -501,6 +515,13 @@ export default function (pi) {
501
515
  let currentBatchAgents = [];
502
516
  let batchFinalizeTimer;
503
517
  let batchCounter = 0;
518
+ function clearBatchState() {
519
+ if (batchFinalizeTimer) {
520
+ clearTimeout(batchFinalizeTimer);
521
+ batchFinalizeTimer = undefined;
522
+ }
523
+ currentBatchAgents = [];
524
+ }
504
525
  /** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
505
526
  function finalizeBatch() {
506
527
  batchFinalizeTimer = undefined;
@@ -575,7 +596,7 @@ export default function (pi) {
575
596
  const scheduleParam = isSchedulingEnabled() ? scheduleParamShape : {};
576
597
  const agentToolDescription = buildAgentToolDescription({
577
598
  mode: getToolDescriptionMode(),
578
- extensionDepth,
599
+ nextSubagentDepth,
579
600
  schedulingEnabled: isSchedulingEnabled(),
580
601
  });
581
602
  pi.registerTool(defineTool({
@@ -597,7 +618,7 @@ export default function (pi) {
597
618
  description: "A short (3-5 word) description of the task (shown in UI).",
598
619
  }),
599
620
  subagent_type: Type.Optional(Type.String({
600
- description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
621
+ description: `The type of specialized agent to use. Defaults to general-purpose when omitted. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
601
622
  })),
602
623
  model: Type.Optional(Type.String({
603
624
  description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
@@ -673,14 +694,14 @@ export default function (pi) {
673
694
  const { retry: _omit, ...overrides } = params;
674
695
  P = { ...stashed.params, ...overrides };
675
696
  }
676
- emitPromptPreview(P.prompt, P.description, P.subagent_type);
677
- // Retry supplied the prompt/type from the stash; otherwise both are required.
678
- if (!retryHandle && (!P.prompt || !P.subagent_type)) {
679
- return textResult(`Missing required argument${!P.prompt && !P.subagent_type ? "s" : ""}: ` +
680
- [!P.prompt && "prompt", !P.subagent_type && "subagent_type"].filter(Boolean).join(", ") +
681
- ".");
697
+ const requestedSubagentType = (P.subagent_type ?? "general-purpose");
698
+ emitPromptPreview(P.prompt, P.description, requestedSubagentType);
699
+ // Retry supplied the prompt from the stash; otherwise prompt is required.
700
+ // subagent_type defaults to general-purpose when omitted.
701
+ if (!retryHandle && !P.prompt) {
702
+ return textResult("Missing required argument: prompt.");
682
703
  }
683
- const rawType = P.subagent_type;
704
+ const rawType = requestedSubagentType;
684
705
  const resolved = resolveType(rawType);
685
706
  if (!resolved) {
686
707
  // Unknown agent type — recoverable. List valid types so the orchestrator
@@ -853,6 +874,7 @@ export default function (pi) {
853
874
  invocation: agentInvocation,
854
875
  depth: nextSubagentDepth,
855
876
  parentAgentId: extensionAgentId,
877
+ eventBus: pi.events,
856
878
  outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
857
879
  onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt, ctx.cwd),
858
880
  ...bgCallbacks,
@@ -896,6 +918,10 @@ export default function (pi) {
896
918
  isBackground: true,
897
919
  depth: record?.depth ?? nextSubagentDepth,
898
920
  parentAgentId: extensionAgentId,
921
+ status: record?.status ?? "running",
922
+ startedAt: record?.startedAt,
923
+ toolUses: record?.toolUses ?? 0,
924
+ invocation: record?.invocation,
899
925
  });
900
926
  const isQueued = record?.status === "queued";
901
927
  return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\nAgent ID: ${id}\nType: ${displayName}\nDescription: ${P.description}\n` +
@@ -928,6 +954,56 @@ export default function (pi) {
928
954
  description: "Return a lightweight tail/filter view of the agent's result or live output file, with line numbers. Ignored when verbose is true.",
929
955
  })),
930
956
  }),
957
+ renderResult(result, { expanded }, theme) {
958
+ const details = result.details;
959
+ const text = extractText(result.content);
960
+ // Header: status + stats + description
961
+ let line = "";
962
+ if (details) {
963
+ const icon = details.status === "error" || details.status === "stopped" || details.status === "aborted"
964
+ ? theme.fg("error", "✗")
965
+ : details.status === "running" || details.status === "queued"
966
+ ? theme.fg("accent", "◌")
967
+ : theme.fg("success", "✓");
968
+ const parts = [];
969
+ if (details.toolUses > 0)
970
+ parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
971
+ if (details.tokens)
972
+ parts.push(details.tokens);
973
+ if (details.contextPercent !== null)
974
+ parts.push(`ctx ${Math.round(details.contextPercent)}%`);
975
+ if (details.duration)
976
+ parts.push(details.duration);
977
+ const stats = parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
978
+ line = `${icon} ${theme.bold(details.description)} ${theme.fg("dim", details.status)}`;
979
+ if (stats)
980
+ line += "\n " + stats;
981
+ }
982
+ // Body: snip when collapsed, full when expanded
983
+ // Extract the body portion (after the first blank line) to keep the
984
+ // tool-output header always visible and only snip the actual result.
985
+ if (text.trim()) {
986
+ const firstBlank = text.indexOf("\n\n");
987
+ const body = firstBlank >= 0 ? text.slice(firstBlank + 2) : text;
988
+ if (expanded) {
989
+ for (const l of text.split("\n")) {
990
+ line += "\n" + theme.fg("dim", ` ${l}`);
991
+ }
992
+ }
993
+ else {
994
+ // Show the tool-output header verbatim, then snip only the body
995
+ if (firstBlank >= 0) {
996
+ for (const l of text.slice(0, firstBlank).split("\n")) {
997
+ line += "\n" + theme.fg("dim", ` ${l}`);
998
+ }
999
+ }
1000
+ for (const l of snipMiddleLines(body, 20)) {
1001
+ line += "\n" + theme.fg("dim", ` ${l}`);
1002
+ }
1003
+ }
1004
+ }
1005
+ return new Text(line, 0, 0);
1006
+ },
931
1007
  execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
932
1008
  const record = manager.getRecord(params.agent_id);
933
1009
  if (!record) {
@@ -974,6 +1050,15 @@ export default function (pi) {
974
1050
  if (record.compactionCount)
975
1051
  statsParts.push(`Compactions: ${record.compactionCount}`);
976
1052
  statsParts.push(`Duration: ${duration}`);
1053
+ const details = {
1054
+ status: record.status,
1055
+ description: record.description,
1056
+ toolUses: record.toolUses,
1057
+ tokens: tokens || null,
1058
+ contextPercent,
1059
+ duration,
1060
+ outputFile: record.outputFile,
1061
+ };
977
1062
  let output = `Agent: ${record.id}\n` +
978
1063
  `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
979
1064
  `Description: ${record.description}\n` +
@@ -1014,7 +1099,7 @@ export default function (pi) {
1014
1099
  }
1015
1100
  }
1016
1101
  }
1017
- return textResult(output);
1102
+ return textResult(output, details);
1018
1103
  },
1019
1104
  }));
1020
1105
  // ---- steer_subagent tool ----
@@ -1072,6 +1157,9 @@ export default function (pi) {
1072
1157
  }
1073
1158
  },
1074
1159
  }));
1160
+ // ---- list_subagents / clear_subagents tools ----
1161
+ registerSubagentListClearTools(pi, manager);
1162
+ registerDashboardModules(pi, manager);
1075
1163
  // ---- list_models tool ----
1076
1164
  pi.registerTool(defineTool({
1077
1165
  name: SUBAGENT_TOOL_NAMES.LIST_MODELS,
@@ -1547,6 +1635,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
1547
1635
  const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
1548
1636
  description: `Generate ${name} agent`,
1549
1637
  maxTurns: 5,
1638
+ eventBus: pi.events,
1639
+ depth: nextSubagentDepth,
1640
+ parentAgentId: extensionAgentId,
1550
1641
  });
1551
1642
  if (record.status === "error") {
1552
1643
  ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
package/dist/peek.js CHANGED
@@ -81,6 +81,12 @@ function parseOutputFileLines(path) {
81
81
  return [];
82
82
  }
83
83
  const out = [];
84
+ const pushRenderedLines = (text) => {
85
+ for (const renderedLine of text.trimEnd().split("\n")) {
86
+ if (renderedLine.trim())
87
+ out.push(renderedLine);
88
+ }
89
+ };
84
90
  for (const line of raw.split("\n")) {
85
91
  const trimmed = line.trim();
86
92
  if (!trimmed)
@@ -96,12 +102,12 @@ function parseOutputFileLines(path) {
96
102
  if (!Array.isArray(content)) {
97
103
  // Some entries may carry a plain string content.
98
104
  if (typeof content === "string" && content.trim())
99
- out.push(content.trim());
105
+ pushRenderedLines(content);
100
106
  continue;
101
107
  }
102
108
  for (const block of content) {
103
109
  if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
104
- out.push(block.text.trimEnd());
110
+ pushRenderedLines(block.text);
105
111
  }
106
112
  }
107
113
  }
@@ -51,6 +51,12 @@ export interface NewJobInput {
51
51
  isolated?: boolean;
52
52
  isolation?: IsolationMode;
53
53
  }
54
+ interface SchedulerSpawnDefaults {
55
+ /** Recursive depth for scheduled subagents fired from this session. */
56
+ depth?: number;
57
+ /** Parent subagent id for scheduled subagents fired from this session. */
58
+ parentAgentId?: string;
59
+ }
54
60
  export declare class SubagentScheduler {
55
61
  private jobs;
56
62
  private intervals;
@@ -58,8 +64,9 @@ export declare class SubagentScheduler {
58
64
  private pi;
59
65
  private ctx;
60
66
  private manager;
67
+ private spawnDefaults;
61
68
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
62
- start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore): void;
69
+ start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore, spawnDefaults?: SchedulerSpawnDefaults): void;
63
70
  /** Stop all timers; drop refs. Safe to call repeatedly. */
64
71
  stop(): void;
65
72
  /** True if start() has bound a store and the scheduler is active. */
@@ -107,3 +114,4 @@ export declare class SubagentScheduler {
107
114
  /** "10s"/"5m"/"1h"/"2d" → milliseconds. */
108
115
  static parseInterval(s: string): number | null;
109
116
  }
117
+ export {};
package/dist/schedule.js CHANGED
@@ -24,12 +24,14 @@ export class SubagentScheduler {
24
24
  pi;
25
25
  ctx;
26
26
  manager;
27
+ spawnDefaults = {};
27
28
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
28
- start(pi, ctx, manager, store) {
29
+ start(pi, ctx, manager, store, spawnDefaults = {}) {
29
30
  this.pi = pi;
30
31
  this.ctx = ctx;
31
32
  this.manager = manager;
32
33
  this.store = store;
34
+ this.spawnDefaults = spawnDefaults;
33
35
  for (const job of store.list()) {
34
36
  if (job.enabled)
35
37
  this.scheduleJob(job);
@@ -47,6 +49,7 @@ export class SubagentScheduler {
47
49
  this.pi = undefined;
48
50
  this.ctx = undefined;
49
51
  this.manager = undefined;
52
+ this.spawnDefaults = {};
50
53
  }
51
54
  /** True if start() has bound a store and the scheduler is active. */
52
55
  isActive() {
@@ -222,6 +225,9 @@ export class SubagentScheduler {
222
225
  isolated: job.isolated,
223
226
  thinkingLevel: job.thinking,
224
227
  isolation: job.isolation,
228
+ eventBus: pi.events,
229
+ depth: this.spawnDefaults.depth,
230
+ parentAgentId: this.spawnDefaults.parentAgentId,
225
231
  });
226
232
  }
227
233
  catch (err) {
@@ -0,0 +1,57 @@
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { Component } from "@earendil-works/pi-tui";
3
+ import type { AgentManager } from "./agent-manager.js";
4
+ import type { AgentRecord } from "./types.js";
5
+ export interface ListSubagentsOptions {
6
+ all?: boolean;
7
+ now?: number;
8
+ recentSuccessLimit?: number;
9
+ }
10
+ export interface ListSubagentsAgentDetails {
11
+ id: string;
12
+ type: AgentRecord["type"];
13
+ description: string;
14
+ status: AgentRecord["status"];
15
+ startedAt: number;
16
+ completedAt?: number;
17
+ }
18
+ export interface ListSubagentsDetails {
19
+ total: number;
20
+ all: boolean;
21
+ visible: ListSubagentsAgentDetails[];
22
+ hiddenDoneCount: number;
23
+ activeCount: number;
24
+ problemCount: number;
25
+ recentDoneCount: number;
26
+ now: number;
27
+ }
28
+ export interface ClearSubagentsOptions {
29
+ agentIds?: string[];
30
+ now?: number;
31
+ olderThanMs?: number;
32
+ includeErrors?: boolean;
33
+ }
34
+ export interface ClearSelectionResult {
35
+ clearIds: string[];
36
+ errors: string[];
37
+ requestedCount: number;
38
+ keptActiveCount: number;
39
+ keptFailedCount: number;
40
+ keptYoungSuccessCount: number;
41
+ }
42
+ export interface ClearSubagentsDetails extends ClearSelectionResult {
43
+ clearedCount: number;
44
+ }
45
+ export type RenderTheme = {
46
+ fg(color: string, text: string): string;
47
+ bold(text: string): string;
48
+ };
49
+ export declare function buildListSubagentsDetails(records: AgentRecord[], options?: ListSubagentsOptions): ListSubagentsDetails;
50
+ export declare function clearSubagentRecords(records: AgentRecord[], options?: ClearSubagentsOptions): ClearSelectionResult;
51
+ export declare function buildClearSubagentsDetails(result: ClearSelectionResult): ClearSubagentsDetails;
52
+ export declare function renderListSubagentsDetails(details: ListSubagentsDetails, theme: RenderTheme): Component;
53
+ export declare function renderClearSubagentsDetails(details: ClearSubagentsDetails, theme: RenderTheme): Component;
54
+ export declare function renderEmptyCall(): Component;
55
+ export declare function formatListSubagentsText(details: ListSubagentsDetails): string;
56
+ export declare function formatClearSubagentsText(details: ClearSubagentsDetails): string;
57
+ export declare function registerSubagentListClearTools(pi: ExtensionAPI, manager: AgentManager): void;