@narumitw/pi-subagents 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -613,6 +613,9 @@ A detached agent additionally needs a concrete isolation or specialization benef
613
613
  The bounded persisted completion outbox provides ordered at-least-once delivery across process restart without replaying the child turn. When state must be reduced to its storage bound, persistence drops roots without pending completions first and trims old history rather than discarding an outbox-owned root. A completion is acknowledged only after parent context assembly observes its exact `completionId`; an injection that returns synchronously but never reaches context remains pending for retry. If the process exits after context assembly but before acknowledgement is persisted, the same ID can be delivered again and consumers must deduplicate it. Auto-resume wake admission remains best-effort because Pi's custom-message API is fire-and-forget, but an unacknowledged terminal completion itself remains available for redelivery on the next start of the owning session. Transient terminal-persistence failures retry with bounded exponential backoff and keep the run pending; shutdown cancels retry waits and reports a final persistence failure instead of silently resolving unsaved work.
614
614
 
615
615
  The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history.
616
+ Pi registers every Subagents tool and command during startup, but loads blocking execution, manager UI, inspection work, and the selected detached transport implementation only on first use.
617
+ Session restoration, pending completion delivery, settings validation, and cleanup ownership remain eager.
618
+ A failed first-use code load is reported normally and can be retried.
616
619
  Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
617
620
  Set it to `rpc` to retain one `pi --mode rpc --no-session --no-extensions` process per active retained agent, preserving native child history with a separate process boundary.
618
621
  Set it to `auto` for deterministic preflight selection: read-only built-in tools use in-process, write-capable built-in tools use RPC, and extension/custom tools use subprocess.
@@ -1049,20 +1052,25 @@ Downgrading is safe: older extension versions ignore this separate state directo
1049
1052
  packages/pi-subagents/
1050
1053
  ├── src/
1051
1054
  │ ├── index.ts # Pi package entrypoint
1052
- │ ├── subagents.ts # Extension registration and blocking tool schema
1053
- │ ├── automation.ts # Explicit autonomous planning tool and lifecycle owner
1055
+ │ ├── subagents.ts # Lightweight extension composition and blocking registration
1056
+ │ ├── cached-module-loader.ts # Retryable first-use code-module cache
1057
+ │ ├── automation-registration.ts # Lightweight autonomous tool registration
1058
+ │ ├── automation.ts # First-use autonomous planning execution
1054
1059
  │ ├── automation-contract.ts # Strict request, proposal, and graph-patch contracts
1055
1060
  │ ├── automation-planner.ts # Bounded read-only planner prompt and resource policy
1056
1061
  │ ├── workflow-plan-compiler.ts # Deterministic admission, routing, and workflow compilation
1057
1062
  │ ├── workflow-plan-patch.ts # Generation-safe revisions and atomic plan persistence
1058
1063
  │ ├── workflow-planning-benchmark.ts # Frozen matched offline evaluation protocol
1059
- │ ├── inspect.ts # Side-effect-free metadata inspection tool
1060
- │ ├── consult.ts # Synchronous read-only consultation tool
1064
+ │ ├── inspect-registration.ts # Lightweight inspection tool registration
1065
+ │ ├── inspect.ts # First-use side-effect-free metadata inspection
1066
+ │ ├── consult-registration.ts # Lightweight consultation tool registration
1067
+ │ ├── consult.ts # First-use synchronous read-only consultation
1061
1068
  │ ├── consult-policy.ts # Enforced read-only tool intersection
1062
1069
  │ ├── cwd-policy.ts # Canonical target and saved-trust resolution
1063
1070
  │ ├── prompt-resources.ts # Core-selected SYSTEM and APPEND_SYSTEM resources
1064
1071
  │ ├── safe-text.ts # Shared byte/line/path sanitization
1065
1072
  │ ├── stateful.ts # Detached lifecycle registration and dispatch
1073
+ │ ├── create-stateful-transport.ts # First-turn selected transport loader
1066
1074
  │ ├── rpc-transport.ts # Persistent strict-JSONL Pi RPC child transport
1067
1075
  │ ├── rpc-timeout-finalization.ts # RPC abort-settle-summary recovery
1068
1076
  │ ├── rpc-transport-metadata.ts # RPC result policy and bounded metadata helpers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,12 +39,12 @@
39
39
  "typebox": "*"
40
40
  },
41
41
  "devDependencies": {
42
- "@biomejs/biome": "2.5.7",
43
- "@earendil-works/pi-agent-core": "0.84.1",
44
- "@earendil-works/pi-ai": "0.84.1",
45
- "@earendil-works/pi-coding-agent": "0.84.1",
46
- "@earendil-works/pi-tui": "0.84.1",
47
- "typebox": "1.3.11",
42
+ "@biomejs/biome": "2.5.8",
43
+ "@earendil-works/pi-agent-core": "0.84.2",
44
+ "@earendil-works/pi-ai": "0.84.2",
45
+ "@earendil-works/pi-coding-agent": "0.84.2",
46
+ "@earendil-works/pi-tui": "0.84.2",
47
+ "typebox": "1.3.14",
48
48
  "typescript": "7.0.2"
49
49
  },
50
50
  "dependencies": {
@@ -0,0 +1,137 @@
1
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import type { AutomationExecutionOptions } from "./automation.js";
4
+ import { type AutomationDetails, SubagentAutomationParams } from "./automation-tool.js";
5
+ import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
6
+ import { renderFallbackResult, safeLine, toolHeader } from "./render-common.js";
7
+
8
+ interface AutomationExecutionModule {
9
+ executeAutomationRequest: typeof import("./automation.js").executeAutomationRequest;
10
+ }
11
+
12
+ export interface AutomationRegistrationDependencies {
13
+ loadExecution?: () => Promise<AutomationExecutionModule>;
14
+ }
15
+
16
+ export function registerSubagentAutomation(
17
+ pi: ExtensionAPI,
18
+ options: AutomationExecutionOptions,
19
+ dependencies: AutomationRegistrationDependencies = {},
20
+ ): void {
21
+ const loadExecution = cachedModuleLoader(
22
+ dependencies.loadExecution ?? (() => import("./automation.js")),
23
+ );
24
+ let generation = 0;
25
+ const activeControllers = new Set<AbortController>();
26
+ const activeWork = new Set<Promise<unknown>>();
27
+ const cancelAndWait = async (reason: string) => {
28
+ generation++;
29
+ for (const controller of activeControllers) {
30
+ controller.abort(new DOMException(reason, "AbortError"));
31
+ }
32
+ await Promise.allSettled([...activeWork]);
33
+ };
34
+ pi.on("session_start", () => cancelAndWait("Autonomous workflow session replaced"));
35
+ pi.on("session_shutdown", () => cancelAndWait("Autonomous workflow session shut down"));
36
+ const definition: ToolDefinition<typeof SubagentAutomationParams, AutomationDetails> = {
37
+ name: "subagent_auto",
38
+ label: "Autonomous Subagent Workflow",
39
+ description: [
40
+ "Explicitly opt in to one bounded read-only planning turn that compiles a high-level objective into the smallest justified existing workflow.",
41
+ "The deterministic compiler may return parent-owned work, request missing input, or reject without launching execution workers.",
42
+ "Mutating workflows require an authoritative integration path and an independent verifier, allow at most two concurrent mutating workers, and never allow workflow grandchildren.",
43
+ "The first version routes only built-in and user-scoped agents; use caller-authored workflow mode for project-local agents.",
44
+ ].join(" "),
45
+ promptSnippet:
46
+ "Explicitly compile one high-level objective into a bounded capability-matched workflow",
47
+ promptGuidelines: [
48
+ "Use subagent_auto only when the caller explicitly opts into autonomous workflow planning.",
49
+ "Provide a complete authority ceiling and aggregate budget; parent-owned and insufficient-evidence results launch no execution workers.",
50
+ "Use caller-authored subagent workflow mode as the compatibility fallback when deterministic task control is required.",
51
+ ],
52
+ parameters: SubagentAutomationParams,
53
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
54
+ const ownerGeneration = generation;
55
+ const controller = new AbortController();
56
+ activeControllers.add(controller);
57
+ const combined = combineSignals(signal, controller.signal);
58
+ const work = (async () => {
59
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
60
+ let executionModule: AutomationExecutionModule;
61
+ try {
62
+ executionModule = await loadExecution();
63
+ } catch (error) {
64
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
65
+ throw error;
66
+ }
67
+ throwIfAborted(combined.signal, "Autonomous workflow loading was cancelled");
68
+ if (ownerGeneration !== generation) {
69
+ throw new DOMException("Autonomous workflow owner was replaced", "AbortError");
70
+ }
71
+ return executionModule.executeAutomationRequest(
72
+ toolCallId,
73
+ params,
74
+ combined.signal,
75
+ onUpdate,
76
+ ctx,
77
+ options,
78
+ () => ownerGeneration === generation,
79
+ );
80
+ })();
81
+ activeWork.add(work);
82
+ try {
83
+ return await work;
84
+ } finally {
85
+ combined.dispose();
86
+ activeControllers.delete(controller);
87
+ activeWork.delete(work);
88
+ }
89
+ },
90
+ renderCall(args, theme) {
91
+ const request = (args as { request?: { objective?: string; version?: string } }).request;
92
+ return new Text(
93
+ toolHeader(theme, "subagent_auto", request?.objective, [request?.version ?? "request"]),
94
+ 0,
95
+ 0,
96
+ );
97
+ },
98
+ renderResult(result, renderOptions, theme) {
99
+ const status = safeLine(result.details?.status, "completed", 128);
100
+ return renderFallbackResult(
101
+ result,
102
+ renderOptions,
103
+ theme,
104
+ result.details?.isError === true ||
105
+ status.endsWith("failed") ||
106
+ status.endsWith("rejected"),
107
+ );
108
+ },
109
+ };
110
+ pi.registerTool<typeof SubagentAutomationParams, AutomationDetails>(definition);
111
+ pi.on("tool_result", (event) => {
112
+ if (event.toolName !== "subagent_auto") return;
113
+ if ((event.details as AutomationDetails | undefined)?.isError) return { isError: true };
114
+ });
115
+ }
116
+
117
+ function combineSignals(
118
+ external: AbortSignal | undefined,
119
+ owned: AbortSignal,
120
+ ): { signal: AbortSignal; dispose(): void } {
121
+ const controller = new AbortController();
122
+ const signals = [external, owned].filter((value): value is AbortSignal => value !== undefined);
123
+ const listeners = signals.map((source) => {
124
+ const listener = () => {
125
+ if (!controller.signal.aborted) controller.abort(source.reason);
126
+ };
127
+ if (source.aborted) listener();
128
+ else source.addEventListener("abort", listener, { once: true });
129
+ return { source, listener };
130
+ });
131
+ return {
132
+ signal: controller.signal,
133
+ dispose() {
134
+ for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,40 @@
1
+ import { type Static, Type } from "typebox";
2
+ import { AutomationRequestSchema } from "./automation-contract.js";
3
+ import type { SubagentDetails } from "./runner.js";
4
+ import type { CompiledWorkflowPlan } from "./workflow-plan-compiler.js";
5
+
6
+ export const SubagentAutomationParams = Type.Object(
7
+ { request: AutomationRequestSchema },
8
+ { additionalProperties: false },
9
+ );
10
+ export type SubagentAutomationParams = Static<typeof SubagentAutomationParams>;
11
+
12
+ export interface AutomationDetails {
13
+ status:
14
+ | "planning"
15
+ | "planner-failed"
16
+ | "parent-owned"
17
+ | "needs-input"
18
+ | "compiler-rejected"
19
+ | "executed";
20
+ requestVersion: string;
21
+ planVersion?: string;
22
+ planId?: string;
23
+ workflowGeneration?: number;
24
+ revision?: number;
25
+ childCount: number;
26
+ reasonCodes: string[];
27
+ missingInputs?: string[];
28
+ planner?: {
29
+ agent: string;
30
+ tools: string[];
31
+ resources: "project-context" | "none";
32
+ timeoutMs: number;
33
+ maxTurns: number;
34
+ maxToolCalls: number;
35
+ failed?: boolean;
36
+ };
37
+ compiled?: CompiledWorkflowPlan;
38
+ execution?: SubagentDetails;
39
+ isError?: boolean;
40
+ }
package/src/automation.ts CHANGED
@@ -1,22 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
4
- import {
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- getAgentDir,
8
- type ToolDefinition,
9
- } from "@earendil-works/pi-coding-agent";
10
- import { Text } from "@earendil-works/pi-tui";
11
- import { type Static, Type } from "typebox";
4
+ import { type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
12
5
  import { getBuiltInAgent } from "./agents/built-ins.js";
13
6
  import { discoverAgents } from "./agents/discovery.js";
14
7
  import type { SubagentSettings } from "./agents/types.js";
15
- import {
16
- AutomationRequestSchema,
17
- parseAutomationRequest,
18
- type WorkflowPlan,
19
- } from "./automation-contract.js";
8
+ import { parseAutomationRequest, type WorkflowPlan } from "./automation-contract.js";
20
9
  import {
21
10
  AUTOMATION_PLANNER_MAX_TIMEOUT_MS,
22
11
  AUTOMATION_PLANNER_MAX_TOOL_CALLS,
@@ -26,13 +15,13 @@ import {
26
15
  parseAutomationPlannerOutput,
27
16
  resolveAutomationPlannerPolicy,
28
17
  } from "./automation-planner.js";
18
+ import type { AutomationDetails, SubagentAutomationParams } from "./automation-tool.js";
29
19
  import {
30
20
  assertDelegationTargetAllowed,
31
21
  resolveSubagentTarget,
32
22
  targetPolicyAudit,
33
23
  } from "./cwd-policy.js";
34
24
  import { executeSubagent } from "./execution.js";
35
- import { renderFallbackResult, safeLine, toolHeader } from "./render-common.js";
36
25
  import {
37
26
  getResultFinalOutput,
38
27
  isResultError,
@@ -53,41 +42,9 @@ import {
53
42
  import { AutomationPlanPersistence, createWorkflowPlanRecord } from "./workflow-plan-patch.js";
54
43
  import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
55
44
 
56
- export const SubagentAutomationParams = Type.Object(
57
- { request: AutomationRequestSchema },
58
- { additionalProperties: false },
59
- );
60
- export type SubagentAutomationParams = Static<typeof SubagentAutomationParams>;
61
-
62
- export interface AutomationDetails {
63
- status:
64
- | "planning"
65
- | "planner-failed"
66
- | "parent-owned"
67
- | "needs-input"
68
- | "compiler-rejected"
69
- | "executed";
70
- requestVersion: string;
71
- planVersion?: string;
72
- planId?: string;
73
- workflowGeneration?: number;
74
- revision?: number;
75
- childCount: number;
76
- reasonCodes: string[];
77
- missingInputs?: string[];
78
- planner?: {
79
- agent: string;
80
- tools: string[];
81
- resources: "project-context" | "none";
82
- timeoutMs: number;
83
- maxTurns: number;
84
- maxToolCalls: number;
85
- failed?: boolean;
86
- };
87
- compiled?: CompiledWorkflowPlan;
88
- execution?: SubagentDetails;
89
- isError?: boolean;
90
- }
45
+ export { registerSubagentAutomation } from "./automation-registration.js";
46
+ export type { AutomationDetails } from "./automation-tool.js";
47
+ export { SubagentAutomationParams } from "./automation-tool.js";
91
48
 
92
49
  export interface AutomationPlannerRequest {
93
50
  prompt: string;
@@ -110,91 +67,6 @@ export interface AutomationExecutionOptions {
110
67
  persistCompiled?: (compiled: CompiledWorkflowPlan, ctx: ExtensionContext) => Promise<void>;
111
68
  }
112
69
 
113
- export function registerSubagentAutomation(
114
- pi: ExtensionAPI,
115
- options: AutomationExecutionOptions,
116
- ): void {
117
- let generation = 0;
118
- const activeControllers = new Set<AbortController>();
119
- const activeWork = new Set<Promise<unknown>>();
120
- const cancelAndWait = async (reason: string) => {
121
- generation++;
122
- for (const controller of activeControllers) {
123
- controller.abort(new DOMException(reason, "AbortError"));
124
- }
125
- await Promise.allSettled([...activeWork]);
126
- };
127
- pi.on("session_start", () => cancelAndWait("Autonomous workflow session replaced"));
128
- pi.on("session_shutdown", () => cancelAndWait("Autonomous workflow session shut down"));
129
- const description = () =>
130
- [
131
- "Explicitly opt in to one bounded read-only planning turn that compiles a high-level objective into the smallest justified existing workflow.",
132
- "The deterministic compiler may return parent-owned work, request missing input, or reject without launching execution workers.",
133
- "Mutating workflows require an authoritative integration path and an independent verifier, allow at most two concurrent mutating workers, and never allow workflow grandchildren.",
134
- "The first version routes only built-in and user-scoped agents; use caller-authored workflow mode for project-local agents.",
135
- ].join(" ");
136
- const definition: ToolDefinition<typeof SubagentAutomationParams, AutomationDetails> = {
137
- name: "subagent_auto",
138
- label: "Autonomous Subagent Workflow",
139
- description: description(),
140
- promptSnippet:
141
- "Explicitly compile one high-level objective into a bounded capability-matched workflow",
142
- promptGuidelines: [
143
- "Use subagent_auto only when the caller explicitly opts into autonomous workflow planning.",
144
- "Provide a complete authority ceiling and aggregate budget; parent-owned and insufficient-evidence results launch no execution workers.",
145
- "Use caller-authored subagent workflow mode as the compatibility fallback when deterministic task control is required.",
146
- ],
147
- parameters: SubagentAutomationParams,
148
- async execute(toolCallId, params, signal, onUpdate, ctx) {
149
- const ownerGeneration = generation;
150
- const controller = new AbortController();
151
- activeControllers.add(controller);
152
- const combined = combineSignals(signal, controller.signal);
153
- const work = executeAutomationRequest(
154
- toolCallId,
155
- params,
156
- combined.signal,
157
- onUpdate,
158
- ctx,
159
- options,
160
- () => ownerGeneration === generation,
161
- );
162
- activeWork.add(work);
163
- try {
164
- return await work;
165
- } finally {
166
- combined.dispose();
167
- activeControllers.delete(controller);
168
- activeWork.delete(work);
169
- }
170
- },
171
- renderCall(args, theme) {
172
- const request = (args as { request?: { objective?: string; version?: string } }).request;
173
- return new Text(
174
- toolHeader(theme, "subagent_auto", request?.objective, [request?.version ?? "request"]),
175
- 0,
176
- 0,
177
- );
178
- },
179
- renderResult(result, renderOptions, theme) {
180
- const status = safeLine(result.details?.status, "completed", 128);
181
- return renderFallbackResult(
182
- result,
183
- renderOptions,
184
- theme,
185
- result.details?.isError === true ||
186
- status.endsWith("failed") ||
187
- status.endsWith("rejected"),
188
- );
189
- },
190
- };
191
- pi.registerTool<typeof SubagentAutomationParams, AutomationDetails>(definition);
192
- pi.on("tool_result", (event) => {
193
- if (event.toolName !== "subagent_auto") return;
194
- if ((event.details as AutomationDetails | undefined)?.isError) return { isError: true };
195
- });
196
- }
197
-
198
70
  export async function executeAutomationRequest(
199
71
  toolCallId: string,
200
72
  params: SubagentAutomationParams,
@@ -561,25 +433,3 @@ function abortError(message: string): Error {
561
433
  error.name = "AbortError";
562
434
  return error;
563
435
  }
564
-
565
- function combineSignals(
566
- external: AbortSignal | undefined,
567
- owned: AbortSignal,
568
- ): { signal: AbortSignal; dispose(): void } {
569
- const controller = new AbortController();
570
- const signals = [external, owned].filter((value): value is AbortSignal => value !== undefined);
571
- const listeners = signals.map((source) => {
572
- const listener = () => {
573
- if (!controller.signal.aborted) controller.abort(source.reason);
574
- };
575
- if (source.aborted) listener();
576
- else source.addEventListener("abort", listener, { once: true });
577
- return { source, listener };
578
- });
579
- return {
580
- signal: controller.signal,
581
- dispose() {
582
- for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
583
- },
584
- };
585
- }
@@ -0,0 +1,18 @@
1
+ export function cachedModuleLoader<Module>(load: () => Promise<Module>): () => Promise<Module> {
2
+ let pending: Promise<Module> | undefined;
3
+ return () => {
4
+ if (!pending) {
5
+ pending = load().catch((error) => {
6
+ pending = undefined;
7
+ throw error;
8
+ });
9
+ }
10
+ return pending;
11
+ };
12
+ }
13
+
14
+ export function throwIfAborted(signal: AbortSignal | undefined, message: string): void {
15
+ if (!signal?.aborted) return;
16
+ if (signal.reason instanceof Error) throw signal.reason;
17
+ throw new DOMException(message, "AbortError");
18
+ }
@@ -0,0 +1,114 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { cachedModuleLoader } from "./cached-module-loader.js";
3
+ import type { SubagentMenuOwner, SubagentSettingsRuntime } from "./config-ui.js";
4
+
5
+ const SUBCOMMANDS = [
6
+ { value: "settings", label: "settings", description: "Configure subagent user settings" },
7
+ { value: "status", label: "status", description: "Show effective subagent settings" },
8
+ { value: "help", label: "help", description: "Show subagent settings help" },
9
+ ];
10
+
11
+ type ConfigUiModule = Pick<
12
+ typeof import("./config-ui.js"),
13
+ "showSubagentManager" | "showSubagentSettings"
14
+ >;
15
+
16
+ type ConfigStatusModule = Pick<
17
+ typeof import("./config-status.js"),
18
+ "showSubagentHelp" | "showSubagentStatus"
19
+ >;
20
+
21
+ export interface ConfigRegistrationDependencies {
22
+ loadConfigUi?: () => Promise<ConfigUiModule>;
23
+ loadConfigStatus?: () => Promise<ConfigStatusModule>;
24
+ }
25
+
26
+ export function registerSubagentConfigLifecycle(pi: ExtensionAPI): SubagentMenuOwner {
27
+ const owner: SubagentMenuOwner = { generation: 0, controller: new AbortController() };
28
+ pi.on("session_start", () => {
29
+ owner.generation += 1;
30
+ owner.controller.abort(new DOMException("Subagent session replaced", "AbortError"));
31
+ owner.controller = new AbortController();
32
+ });
33
+ pi.on("session_shutdown", () => {
34
+ owner.generation += 1;
35
+ owner.controller.abort(new DOMException("Subagent session shut down", "AbortError"));
36
+ });
37
+ return owner;
38
+ }
39
+
40
+ export function registerSubagentConfigCommand(
41
+ pi: ExtensionAPI,
42
+ runtime: SubagentSettingsRuntime,
43
+ owner = registerSubagentConfigLifecycle(pi),
44
+ dependencies: ConfigRegistrationDependencies = {},
45
+ ): void {
46
+ const loadConfigUi = cachedModuleLoader(
47
+ dependencies.loadConfigUi ?? (() => import("./config-ui.js")),
48
+ );
49
+ const loadConfigStatus = cachedModuleLoader<ConfigStatusModule>(
50
+ dependencies.loadConfigStatus ?? (() => import("./config-status.js")),
51
+ );
52
+ pi.registerCommand("subagents", {
53
+ description: "Manage current-session subagents and user settings",
54
+ getArgumentCompletions(prefix: string) {
55
+ const normalized = prefix.trim().toLowerCase();
56
+ const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
57
+ return matches.length > 0 ? matches : null;
58
+ },
59
+ async handler(args, ctx) {
60
+ const subcommand = args.trim().toLowerCase();
61
+ const runStatusCommand = async (show: (status: ConfigStatusModule) => void) => {
62
+ const generation = owner.generation;
63
+ const controller = owner.controller;
64
+ const isCurrent = () =>
65
+ generation === owner.generation &&
66
+ controller === owner.controller &&
67
+ !controller.signal.aborted;
68
+ let status: ConfigStatusModule;
69
+ try {
70
+ status = await loadConfigStatus();
71
+ } catch (error) {
72
+ if (!isCurrent()) return;
73
+ throw error;
74
+ }
75
+ if (!isCurrent()) return;
76
+ show(status);
77
+ };
78
+ if (!subcommand && ctx.mode !== "tui") {
79
+ await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
80
+ return;
81
+ }
82
+ if (subcommand === "status") {
83
+ await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
84
+ return;
85
+ }
86
+ if (subcommand === "help") {
87
+ await runStatusCommand((status) => status.showSubagentHelp(ctx, runtime));
88
+ return;
89
+ }
90
+ if (!subcommand || subcommand === "settings") {
91
+ const generation = owner.generation;
92
+ const controller = owner.controller;
93
+ const isCurrent = () =>
94
+ generation === owner.generation &&
95
+ controller === owner.controller &&
96
+ !controller.signal.aborted;
97
+ let configUi: ConfigUiModule;
98
+ try {
99
+ configUi = await loadConfigUi();
100
+ } catch (error) {
101
+ if (!isCurrent()) return;
102
+ throw error;
103
+ }
104
+ if (!isCurrent()) return;
105
+ if (!subcommand) await configUi.showSubagentManager(pi, ctx, runtime, owner);
106
+ else await configUi.showSubagentSettings(ctx, runtime, owner);
107
+ return;
108
+ }
109
+ if (ctx.mode === "tui" || ctx.hasUI) {
110
+ ctx.ui.notify(`Unknown /subagents subcommand: ${subcommand}`, "warning");
111
+ }
112
+ },
113
+ });
114
+ }
package/src/config-ui.ts CHANGED
@@ -168,7 +168,7 @@ function registerSubagentPrimaryCommand(
168
168
  });
169
169
  }
170
170
 
171
- async function showSubagentManager(
171
+ export async function showSubagentManager(
172
172
  pi: ExtensionAPI,
173
173
  ctx: ExtensionCommandContext,
174
174
  runtime: SubagentSettingsRuntime,
@@ -681,7 +681,7 @@ async function showSubagentManager(
681
681
  });
682
682
  }
683
683
 
684
- async function showSubagentSettings(
684
+ export async function showSubagentSettings(
685
685
  ctx: ExtensionCommandContext,
686
686
  runtime: SubagentSettingsRuntime,
687
687
  owner: SubagentMenuOwner,