@narumitw/pi-subagents 0.35.0 → 0.38.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.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/package.json +2 -1
  3. package/src/config-ui.ts +476 -718
package/README.md CHANGED
@@ -240,7 +240,13 @@ Auto-resume is best-effort because Pi's custom-message API is fire-and-forget. S
240
240
 
241
241
  The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history. 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.
242
242
 
243
- Run `/subagents` in TUI mode to open the primary manager. It leads with the current delegation workflow, human-readable async completion behavior, and active/retained counts. **Change delegation**, **Current agents**, and **Completion behavior** cover the common workflows; agent permissions, transport/runtime details, source, and settings path remain under **Advanced settings**. Escape returns from a nested screen to a newly refreshed manager and then closes it.
243
+ Run `/subagents` in TUI mode to open the standard primary manager. It leads with the current
244
+ delegation workflow, human-readable async completion behavior, and active/retained counts. **Change
245
+ delegation**, **Current agents**, and **Completion behavior** cover the common workflows; agent
246
+ permissions, transport/runtime details, source, and settings path remain under **Advanced settings**.
247
+ Escape returns from a nested screen to a newly refreshed manager; Ctrl+C closes the full flow.
248
+ Exact workflow/reload and project-agent safety confirmations remain extension-owned because they
249
+ guard live agent and trust-boundary policy rather than ordinary navigation.
244
250
 
245
251
  The direct routes remain predictable: `/subagents settings` changes user completion delivery and applies it immediately, including refreshing the model-facing spawn guidance; `/subagents status` reports current-session runtime values separately from the configured value, source, and path; `/subagents help` summarizes the single-command interface. In RPC mode, bare `/subagents` emits the same bounded status through Pi's notification protocol instead of opening a custom TUI. JSON and print modes do not emit ad hoc command output. Manual edits use `~/.pi/agent/pi-subagents.json` and take effect after reloading Pi:
246
252
 
@@ -382,7 +388,10 @@ Built-in agents inherit the active/default Pi model instead of forcing a provide
382
388
 
383
389
  ## ⚙️ Configure agent tools
384
390
 
385
- Open `/subagents`, choose **Advanced settings**, then **Agent tool settings** in an interactive Pi session to edit the tools each subagent may use. These are user settings stored in `~/.pi/agent/pi-subagents.json` and affect future sessions.
391
+ Open `/subagents`, choose **Advanced settings**, then **Agent tool permissions** in an interactive
392
+ Pi session to edit the tools each subagent may use. The standard bounded multi-select keeps a
393
+ one-save draft: toggles do not write until **Save changes**, Escape discards the draft, and unavailable
394
+ configured tool names remain visible and preserved. These are user settings stored in `~/.pi/agent/pi-subagents.json` and affect future sessions.
386
395
 
387
396
  Compatibility: a valid legacy `pi-subagents-config.json` remains readable with a warning and is never modified automatically; rename it to `pi-subagents.json`. The first subsequent settings save writes the canonical file. If both files exist, the new filename takes precedence.
388
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.35.0",
3
+ "version": "0.38.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
+ "@narumitw/pi-tui-kit": "<1",
32
33
  "proper-lockfile": "^4.1.2",
33
34
  "typebox": "^1.3.8"
34
35
  },
package/src/config-ui.ts CHANGED
@@ -1,18 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { DynamicBorder, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
3
- import {
4
- type AutocompleteItem,
5
- Container,
6
- Key,
7
- matchesKey,
8
- type SelectItem,
9
- SelectList,
10
- type SettingItem,
11
- SettingsList,
12
- Spacer,
13
- Text,
14
- truncateToWidth,
15
- } from "@earendil-works/pi-tui";
2
+ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
16
3
  import { type CompletionDelivery, discoverAgents } from "./agents.js";
17
4
  import type { ManagedAgent } from "./registry.js";
18
5
  import {
@@ -29,11 +16,12 @@ import {
29
16
  } from "./settings.js";
30
17
  import { formatStatefulAgentLine, type StatefulSubagentRuntimeStatus } from "./stateful.js";
31
18
 
32
- const SUBCOMMANDS: AutocompleteItem[] = [
19
+ const SUBCOMMANDS = [
33
20
  { value: "settings", label: "settings", description: "Configure completion behavior" },
34
21
  { value: "status", label: "status", description: "Show effective subagent settings" },
35
22
  { value: "help", label: "help", description: "Show subagent settings help" },
36
23
  ];
24
+ const TOOL_VIEWPORT_SIZE = 10;
37
25
 
38
26
  export interface SubagentSettingsRuntime {
39
27
  getBlockingEnabled(): boolean;
@@ -44,260 +32,42 @@ export interface SubagentSettingsRuntime {
44
32
  clearAgents(): Promise<number>;
45
33
  }
46
34
 
47
- export class ToolToggleList {
48
- private items: { name: string; displayName: string; selected: boolean }[];
49
- private cursor = 0;
50
- private cachedWidth?: number;
51
- private cachedLines?: string[];
52
- onDone?: (selected: string[]) => void;
53
- onCancel?: () => void;
54
-
55
- constructor(tools: string[], selected: Set<string>) {
56
- this.items = tools.map((name) => ({
57
- name,
58
- displayName: safeTerminalText(name),
59
- selected: selected.has(name),
60
- }));
61
- }
62
-
63
- private getSelectedNames(): string[] {
64
- return this.items.filter((i) => i.selected).map((i) => i.name);
65
- }
66
-
67
- handleInput(data: string): void {
68
- if (matchesKey(data, Key.escape)) {
69
- this.onCancel?.();
70
- return;
71
- }
72
- if (data === "s" || data === "S") {
73
- this.onDone?.(this.getSelectedNames());
74
- return;
75
- }
76
- if (this.items.length === 0) return;
77
-
78
- if (matchesKey(data, Key.up) && this.cursor > 0) {
79
- this.cursor--;
80
- this.invalidate();
81
- } else if (matchesKey(data, Key.down) && this.cursor < this.items.length - 1) {
82
- this.cursor++;
83
- this.invalidate();
84
- } else if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
85
- this.items[this.cursor].selected = !this.items[this.cursor].selected;
86
- this.invalidate();
87
- }
88
- }
89
-
90
- render(width: number): string[] {
91
- if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
92
- this.cachedWidth = width;
93
- this.cachedLines = this.items.map((item, i) => {
94
- const pointer = i === this.cursor ? ">" : " ";
95
- const check = item.selected ? "✓" : "○";
96
- return truncateToWidth(`${pointer} ${check} ${item.displayName}`, width);
97
- });
98
- return this.cachedLines;
99
- }
100
-
101
- invalidate(): void {
102
- this.cachedWidth = undefined;
103
- this.cachedLines = undefined;
104
- }
35
+ interface MenuOwner {
36
+ generation: number;
37
+ controller: AbortController;
105
38
  }
106
39
 
107
- export function registerSubagentConfigCommand(pi: ExtensionAPI, runtime: SubagentSettingsRuntime) {
108
- registerSubagentPrimaryCommand(pi, runtime);
40
+ interface ToolDraft {
41
+ agentName: string;
42
+ agentSource: string;
43
+ allTools: string[];
44
+ defaultTools?: string[];
45
+ orderedTools: string[];
46
+ selected: Set<string>;
109
47
  }
110
48
 
111
- async function showSubagentToolSettings(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
112
- if (ctx.mode !== "tui") {
113
- if (ctx.hasUI) ctx.ui.notify("Agent tool settings require TUI mode", "info");
114
- return;
115
- }
116
-
117
- // Get current settings
118
- const currentSettings = readSubagentSettings() ?? {};
119
- const currentAgents = currentSettings.agents ?? {};
120
-
121
- // Discover agents to show which ones are available
122
- const discovery = discoverAgents(ctx.cwd, "user", currentSettings);
123
- const agents = discovery.agents;
124
-
125
- if (agents.length === 0) {
126
- ctx.ui.notify("No agents found", "warning");
127
- return;
128
- }
129
-
130
- // Loop: agent selection → tool toggle (Esc in tools returns here)
131
- let selectedAgentIndex = 0;
132
- while (true) {
133
- // Step 1: pick an agent to configure
134
- const agentItems: SelectItem[] = agents.map((a) => {
135
- const cfg = currentAgents[a.name];
136
- const hasToolsOverride = cfg ? hasOwn(cfg, "tools") : false;
137
- const toolSummary = hasToolsOverride
138
- ? cfg?.tools && cfg.tools.length > 0
139
- ? cfg.tools.join(", ")
140
- : "none"
141
- : "defaults";
142
- return {
143
- value: a.name,
144
- label: safeTerminalText(a.name),
145
- description: safeTerminalText(`${a.source} · tools: ${toolSummary}`),
146
- };
147
- });
148
-
149
- const agentName = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
150
- const container = new Container();
151
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
152
- container.addChild(
153
- new Text(theme.fg("accent", theme.bold("Subagent Tool Configuration")), 1, 0),
154
- );
155
- container.addChild(new Spacer(1));
156
- container.addChild(
157
- new Text(theme.fg("muted", "Select an agent to configure its allowed tools:"), 1, 0),
158
- );
159
- container.addChild(new Spacer(1));
160
- const selectList = new SelectList(agentItems, Math.min(agentItems.length + 2, 15), {
161
- selectedPrefix: (t: string) => theme.fg("accent", t),
162
- selectedText: (t: string) => theme.fg("accent", t),
163
- description: (t: string) => theme.fg("muted", t),
164
- scrollInfo: (t: string) => theme.fg("dim", t),
165
- noMatch: (t: string) => theme.fg("warning", t),
166
- });
167
- selectList.setSelectedIndex(selectedAgentIndex);
168
- selectList.onSelectionChange = (item) => {
169
- selectedAgentIndex = Math.max(
170
- 0,
171
- agentItems.findIndex((candidate) => candidate.value === item.value),
172
- );
173
- };
174
- selectList.onSelect = (item) => {
175
- selectedAgentIndex = Math.max(
176
- 0,
177
- agentItems.findIndex((candidate) => candidate.value === item.value),
178
- );
179
- done(item.value);
180
- };
181
- selectList.onCancel = () => done(null);
182
- container.addChild(selectList);
183
- container.addChild(
184
- new Text(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"), 1, 0),
185
- );
186
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
187
- return {
188
- render: (w: number) => container.render(w),
189
- invalidate: () => container.invalidate(),
190
- handleInput: (data: string) => {
191
- selectList.handleInput(data);
192
- tui.requestRender();
193
- },
194
- };
195
- });
196
-
197
- if (!agentName) return;
198
-
199
- const agent = agents.find((a) => a.name === agentName);
200
- if (!agent) return;
201
-
202
- // Step 2: toggle tools for the selected agent
203
- // Discover without overrides to get original built-in/frontmatter defaults.
204
- // The main discovery above applies saved overrides, so agent.tools is already
205
- // overridden — using it for the reset-to-default comparison would match the
206
- // override against itself and silently delete it on a no-op save.
207
- const defaultDiscovery = discoverAgents(ctx.cwd, "user");
208
- const defaultTools = defaultDiscovery.agents.find((a) => a.name === agentName)?.tools;
209
- const currentAgentSettings = currentAgents[agentName];
210
- const configuredTools =
211
- currentAgentSettings && hasOwn(currentAgentSettings, "tools")
212
- ? (currentAgentSettings.tools ?? [])
213
- : undefined;
214
-
215
- // Get all available tools from pi's registry
216
- const allTools = uniqueToolNames(pi.getAllTools().map((t) => t.name)).sort((a, b) =>
217
- a.localeCompare(b),
218
- );
219
- const currentTools = uniqueToolNames(configuredTools ?? defaultTools ?? allTools);
220
- // Sort: currently selected tools first, then rest alphabetically. Preserve
221
- // unavailable configured tools so saving does not silently drop them.
222
- const currentSet = new Set(currentTools);
223
- const selectedFirst = [...currentTools, ...allTools.filter((t) => !currentSet.has(t))];
224
-
225
- const selectedTools = await ctx.ui.custom<string[] | null>((tui, theme, _kb, done) => {
226
- const toggleList = new ToolToggleList(selectedFirst, currentSet);
227
-
228
- const container = new Container();
229
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
230
- container.addChild(
231
- new Text(
232
- theme.fg("accent", theme.bold(`${safeTerminalText(agentName)} tools`)) +
233
- theme.fg("muted", ` (${agent.source})`),
234
- 1,
235
- 0,
236
- ),
237
- );
238
- container.addChild(new Spacer(1));
239
- container.addChild(
240
- new Text(
241
- theme.fg("muted", "Toggle tools with Enter/Space. S to save, Esc to cancel."),
242
- 1,
243
- 0,
244
- ),
245
- );
246
- container.addChild(new Spacer(1));
247
-
248
- const listContainer = new Container();
249
- listContainer.addChild({
250
- render: (w: number) => toggleList.render(w),
251
- invalidate: () => toggleList.invalidate(),
252
- });
253
- container.addChild(listContainer);
254
-
255
- container.addChild(new Spacer(1));
256
- container.addChild(
257
- new Text(theme.fg("dim", "↑↓ navigate · enter/space toggle · S save · esc cancel"), 1, 0),
258
- );
259
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
260
-
261
- toggleList.onDone = (tools) => done(tools);
262
- toggleList.onCancel = () => done(null);
263
-
264
- return {
265
- render: (w: number) => container.render(w),
266
- invalidate: () => container.invalidate(),
267
- handleInput: (data: string) => {
268
- toggleList.handleInput(data);
269
- tui.requestRender();
270
- },
271
- };
272
- });
273
-
274
- // null means user cancelled — loop back to agent selection
275
- if (selectedTools === null) continue;
276
-
277
- // Patch only this agent's tool field so forward-compatible settings survive.
278
- const restoredDefaults =
279
- defaultTools === undefined
280
- ? sameToolSet(selectedTools, allTools)
281
- : sameToolSet(selectedTools, defaultTools);
282
- updateAgentToolsSetting(agentName, restoredDefaults ? undefined : selectedTools);
283
- const safeAgentName = safeTerminalText(agentName);
284
- const message = restoredDefaults
285
- ? `${safeAgentName}: defaults restored`
286
- : `${safeAgentName}: ${selectedTools.length} tool${selectedTools.length !== 1 ? "s" : ""} configured`;
287
- ctx.ui.notify(message, "info");
288
- // Saved — exit the loop
289
- break;
290
- }
49
+ export function registerSubagentConfigCommand(pi: ExtensionAPI, runtime: SubagentSettingsRuntime) {
50
+ const owner: MenuOwner = { generation: 0, controller: new AbortController() };
51
+ pi.on("session_start", () => {
52
+ owner.generation += 1;
53
+ owner.controller.abort(new DOMException("Subagent session replaced", "AbortError"));
54
+ owner.controller = new AbortController();
55
+ });
56
+ pi.on("session_shutdown", () => {
57
+ owner.generation += 1;
58
+ owner.controller.abort(new DOMException("Subagent session shut down", "AbortError"));
59
+ });
60
+ registerSubagentPrimaryCommand(pi, runtime, owner);
291
61
  }
292
62
 
293
- type ManagerAction = "workflow" | "agents" | "completion" | "advanced" | "help";
294
- type AdvancedAction = "agent-tools" | "status" | "back";
295
- type AgentManagerAction = "back" | "clear";
296
-
297
- function registerSubagentPrimaryCommand(pi: ExtensionAPI, runtime: SubagentSettingsRuntime) {
63
+ function registerSubagentPrimaryCommand(
64
+ pi: ExtensionAPI,
65
+ runtime: SubagentSettingsRuntime,
66
+ owner: MenuOwner,
67
+ ) {
298
68
  pi.registerCommand("subagents", {
299
69
  description: "Manage current-session subagents and user settings",
300
- getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
70
+ getArgumentCompletions(prefix: string) {
301
71
  const normalized = prefix.trim().toLowerCase();
302
72
  const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
303
73
  return matches.length > 0 ? matches : null;
@@ -305,12 +75,12 @@ function registerSubagentPrimaryCommand(pi: ExtensionAPI, runtime: SubagentSetti
305
75
  async handler(args, ctx) {
306
76
  const subcommand = args.trim().toLowerCase();
307
77
  if (!subcommand) {
308
- await showSubagentManager(pi, ctx, runtime);
78
+ await showSubagentManager(pi, ctx, runtime, owner);
309
79
  return;
310
80
  }
311
81
  switch (subcommand) {
312
82
  case "settings":
313
- await showSubagentSettings(ctx, runtime);
83
+ await showSubagentSettings(ctx, runtime, owner);
314
84
  return;
315
85
  case "status":
316
86
  showSubagentStatus(ctx, runtime);
@@ -331,216 +101,440 @@ async function showSubagentManager(
331
101
  pi: ExtensionAPI,
332
102
  ctx: ExtensionCommandContext,
333
103
  runtime: SubagentSettingsRuntime,
104
+ owner: MenuOwner,
334
105
  ) {
335
106
  if (ctx.mode !== "tui") {
336
107
  showSubagentStatus(ctx, runtime);
337
108
  return;
338
109
  }
339
- while (true) {
340
- const action = await selectManagerAction(ctx, runtime);
341
- if (!action) return;
342
- switch (action) {
343
- case "workflow":
344
- if (await showDelegationWorkflow(ctx, runtime)) return;
345
- break;
346
- case "agents":
347
- await showCurrentSessionAgents(ctx, runtime);
348
- break;
349
- case "completion":
350
- await showSubagentSettings(ctx, runtime);
351
- break;
352
- case "advanced":
353
- await showAdvancedSettings(pi, ctx, runtime);
354
- break;
355
- case "help":
356
- showSubagentHelp(ctx);
357
- break;
358
- }
359
- }
360
- }
361
-
362
- async function selectManagerAction(
363
- ctx: ExtensionCommandContext,
364
- runtime: SubagentSettingsRuntime,
365
- ): Promise<ManagerAction | null> {
366
- const status = runtime.getRuntimeStatus();
367
- const workflow = inspectDelegationWorkflowSettings();
368
- const items: SelectItem[] = [
369
- {
370
- value: "workflow",
371
- label: "Change delegation",
372
- description: "Choose all methods, async only, or blocking only",
373
- },
374
- {
375
- value: "agents",
376
- label: "Current agents",
377
- description: `${status.activeAgents} active · ${status.retainedAgents} retained`,
378
- },
379
- {
380
- value: "completion",
381
- label: "Completion behavior",
382
- description: "Choose whether async completion waits or resumes automatically",
383
- },
384
- {
385
- value: "advanced",
386
- label: "Advanced settings",
387
- description: "Agent permissions, runtime details, and settings path",
110
+ const generation = owner.generation;
111
+ let availableAgents = discoverAgents(ctx.cwd, "user", readSubagentSettings() ?? {}).agents;
112
+ let toolDraft: ToolDraft | undefined;
113
+ type Screen =
114
+ | "main"
115
+ | "workflow"
116
+ | "agents"
117
+ | "completion"
118
+ | "advanced"
119
+ | "status"
120
+ | "help"
121
+ | "agent-picker"
122
+ | "tool-draft";
123
+ type Action =
124
+ | "set-workflow"
125
+ | "clear-agents"
126
+ | "set-completion"
127
+ | "load-agent-picker"
128
+ | "pick-agent"
129
+ | "toggle-tool"
130
+ | "save-tools"
131
+ | "discard-tools"
132
+ | "back";
133
+ const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
134
+ start: "main",
135
+ screens: {
136
+ main: () => {
137
+ const status = runtime.getRuntimeStatus();
138
+ const workflow = inspectDelegationWorkflowSettings();
139
+ return {
140
+ kind: "actions",
141
+ title: "Subagents",
142
+ lines: formatManagerSummary(runtime, status, workflow).split("\n"),
143
+ items: [
144
+ {
145
+ id: "workflow",
146
+ label: "Change delegation",
147
+ description: "Choose all methods, async only, or blocking only",
148
+ to: "workflow",
149
+ },
150
+ {
151
+ id: "agents",
152
+ label: "Current agents",
153
+ description: `${status.activeAgents} active · ${status.retainedAgents} retained`,
154
+ to: "agents",
155
+ },
156
+ {
157
+ id: "completion",
158
+ label: "Completion behavior",
159
+ description: "Choose whether async completion waits or resumes automatically",
160
+ to: "completion",
161
+ },
162
+ {
163
+ id: "advanced",
164
+ label: "Advanced settings",
165
+ description: "Agent permissions, runtime details, and settings path",
166
+ to: "advanced",
167
+ },
168
+ { id: "help", label: "Help", to: "help" },
169
+ ],
170
+ hint: "close",
171
+ };
172
+ },
173
+ workflow: () => {
174
+ const snapshot = inspectDelegationWorkflowSettings();
175
+ const active = currentWorkflow(runtime, runtime.getRuntimeStatus());
176
+ return {
177
+ kind: "actions",
178
+ title: "Change Delegation",
179
+ lines: [
180
+ `Current: ${workflowLabel(active)}`,
181
+ ...(snapshot.value !== active
182
+ ? [`Configured after reload: ${workflowLabel(snapshot.value)}`]
183
+ : []),
184
+ ...(snapshot.error
185
+ ? [
186
+ `Settings cannot be edited: ${safeTerminalText(snapshot.error)}`,
187
+ `Repair ${safeTerminalText(snapshot.path)} and retry.`,
188
+ ]
189
+ : []),
190
+ ],
191
+ items: snapshot.error
192
+ ? []
193
+ : [
194
+ {
195
+ id: "all",
196
+ label: "All delegation methods",
197
+ description: "Allow blocking batches and reusable async agents",
198
+ action: "set-workflow" as const,
199
+ },
200
+ {
201
+ id: "async-only",
202
+ label: "Async only",
203
+ description: "Keep the root responsive; remove blocking subagent",
204
+ action: "set-workflow" as const,
205
+ },
206
+ {
207
+ id: "blocking-only",
208
+ label: "Blocking only",
209
+ description: "Keep blocking batches; remove reusable async agents",
210
+ action: "set-workflow" as const,
211
+ },
212
+ ],
213
+ hint: "back",
214
+ };
215
+ },
216
+ agents: () => {
217
+ const agents = runtime.listAgents();
218
+ const status = runtime.getRuntimeStatus();
219
+ return {
220
+ kind: "actions",
221
+ title: "Current-session Subagents",
222
+ lines: agents.length ? agents.map(formatStatefulAgentLine) : [formatEmptyRuntime(status)],
223
+ items: [
224
+ ...(agents.length > 0
225
+ ? [
226
+ {
227
+ id: "clear",
228
+ label: "Clear current-session agents",
229
+ description: "Close and delete retained agents for this session",
230
+ action: "clear-agents" as const,
231
+ },
232
+ ]
233
+ : []),
234
+ { id: "back", label: "Back", action: "back" },
235
+ ],
236
+ hint: "back",
237
+ };
238
+ },
239
+ completion: () => completionSettingsScreen(),
240
+ advanced: () => ({
241
+ kind: "actions",
242
+ title: "Advanced Subagent Settings",
243
+ items: [
244
+ {
245
+ id: "agent-tools",
246
+ label: "Agent tool permissions",
247
+ description: "Customize persistent per-agent tool allow-lists",
248
+ action: "load-agent-picker",
249
+ },
250
+ {
251
+ id: "status",
252
+ label: "Runtime details",
253
+ description: "Show transport, configured source, and settings path",
254
+ to: "status",
255
+ },
256
+ { id: "back", label: "Back", action: "back" },
257
+ ],
258
+ hint: "back",
259
+ }),
260
+ status: () => ({
261
+ kind: "detail",
262
+ title: "Subagent runtime details",
263
+ lines: statusLines(runtime),
264
+ hint: "back",
265
+ }),
266
+ help: () => ({
267
+ kind: "detail",
268
+ title: "Subagents help",
269
+ lines: helpLines(),
270
+ hint: "back",
271
+ }),
272
+ "agent-picker": () => {
273
+ const settings = readSubagentSettings() ?? {};
274
+ const configured = settings.agents ?? {};
275
+ return {
276
+ kind: "actions",
277
+ title: "Subagent Tool Configuration",
278
+ lines: ["Select an agent to configure its allowed tools."],
279
+ items: availableAgents.map((agent) => {
280
+ const override = configured[agent.name];
281
+ const hasOverride = override ? hasOwn(override, "tools") : false;
282
+ const summary = hasOverride
283
+ ? override?.tools && override.tools.length > 0
284
+ ? override.tools.join(", ")
285
+ : "none"
286
+ : "defaults";
287
+ return {
288
+ id: agent.name,
289
+ label: safeTerminalText(agent.name),
290
+ description: safeTerminalText(`${agent.source} · tools: ${summary}`),
291
+ action: "pick-agent" as const,
292
+ };
293
+ }),
294
+ hint: "back",
295
+ };
296
+ },
297
+ "tool-draft": () => ({
298
+ kind: "multiSelect",
299
+ title: toolDraft ? `${safeTerminalText(toolDraft.agentName)} tools` : "Agent tools",
300
+ lines: toolDraft
301
+ ? [
302
+ `Source: ${safeTerminalText(toolDraft.agentSource)}`,
303
+ "Toggle a draft, then Save changes.",
304
+ ]
305
+ : ["No agent selected."],
306
+ viewportSize: TOOL_VIEWPORT_SIZE,
307
+ items:
308
+ toolDraft?.orderedTools.map((name) => {
309
+ const available = toolDraft?.allTools.includes(name) ?? false;
310
+ return {
311
+ id: name,
312
+ label: safeTerminalText(name),
313
+ description: available ? "Available tool" : "Configured tool is not currently loaded",
314
+ selected: toolDraft?.selected.has(name) ?? false,
315
+ disabled: !available,
316
+ disabledReason: available
317
+ ? undefined
318
+ : "Unavailable; preserved until explicitly changed in JSON",
319
+ };
320
+ }) ?? [],
321
+ action: "toggle-tool",
322
+ actions: [
323
+ { id: "save", label: "Save changes", action: "save-tools" },
324
+ { id: "discard", label: "Discard draft", action: "discard-tools" },
325
+ ],
326
+ hint: "back",
327
+ doneLabel: "Close without saving",
328
+ }),
388
329
  },
389
- { value: "help", label: "Help", description: "Show commands and manual configuration" },
390
- ];
391
- return ctx.ui.custom<ManagerAction | null>((tui, theme, _keybindings, done) => {
392
- const container = new Container();
393
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
394
- container.addChild(new Text(theme.fg("accent", theme.bold("Subagents")), 1, 0));
395
- container.addChild(new Spacer(1));
396
- container.addChild(
397
- new Text(theme.fg("muted", formatManagerSummary(runtime, status, workflow)), 1, 0),
398
- );
399
- container.addChild(new Spacer(1));
400
- const selectList = new SelectList(items, Math.min(items.length + 2, 15), {
401
- selectedPrefix: (text: string) => theme.fg("accent", text),
402
- selectedText: (text: string) => theme.fg("accent", text),
403
- description: (text: string) => theme.fg("muted", text),
404
- scrollInfo: (text: string) => theme.fg("dim", text),
405
- noMatch: (text: string) => theme.fg("warning", text),
406
- });
407
- selectList.onSelect = (item) => done(item.value as ManagerAction);
408
- selectList.onCancel = () => done(null);
409
- container.addChild(selectList);
410
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter select · esc close"), 1, 0));
411
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
412
- return {
413
- render: (width: number) => container.render(width),
414
- invalidate: () => container.invalidate(),
415
- handleInput(data: string) {
416
- selectList.handleInput(data);
417
- tui.requestRender();
330
+ actions: {
331
+ "set-workflow": async ({ itemId }) => {
332
+ if (!isWorkflow(itemId)) return { kind: "rejected" };
333
+ const snapshot = inspectDelegationWorkflowSettings();
334
+ if (snapshot.error) return { kind: "rejected" };
335
+ const active = currentWorkflow(runtime, runtime.getRuntimeStatus());
336
+ if (itemId === active && itemId === snapshot.value) {
337
+ ctx.ui.notify(`Delegation already uses ${workflowLabel(itemId)}.`, "info");
338
+ return { kind: "stay" };
339
+ }
340
+ const requiresReload = itemId !== active;
341
+ if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) {
342
+ return { kind: "rejected" };
343
+ }
344
+ if (!(await showWorkflowPreview(ctx, active, itemId, requiresReload))) {
345
+ return { kind: "rejected" };
346
+ }
347
+ if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) {
348
+ return { kind: "rejected" };
349
+ }
350
+ try {
351
+ updateDelegationWorkflowSetting(itemId);
352
+ } catch (error) {
353
+ ctx.ui.notify(
354
+ `Delegation settings were not saved: ${formatError(error)}. The current workflow is unchanged.`,
355
+ "error",
356
+ );
357
+ return { kind: "rejected" };
358
+ }
359
+ if (!requiresReload) {
360
+ ctx.ui.notify(
361
+ `Saved ${workflowLabel(itemId)}. The current tool surface already matches.`,
362
+ "info",
363
+ );
364
+ return { kind: "stay" };
365
+ }
366
+ ctx.ui.notify(
367
+ `Saved ${workflowLabel(itemId)}. Reloading subagent tools… If the tool surface does not refresh, run /reload.`,
368
+ "info",
369
+ );
370
+ await ctx.reload();
371
+ return { kind: "close" };
372
+ },
373
+ "clear-agents": async () => {
374
+ const agents = runtime.listAgents();
375
+ if (agents.length === 0) return { kind: "stay" };
376
+ const confirmed = await ctx.ui.confirm(
377
+ "Clear current-session subagents?",
378
+ `Close and delete ${agents.length} retained agent${agents.length === 1 ? "" : "s"}?`,
379
+ );
380
+ if (!confirmed) return { kind: "rejected" };
381
+ const cleared = await runtime.clearAgents();
382
+ ctx.ui.notify(
383
+ `Cleared ${cleared} current-session subagent${cleared === 1 ? "" : "s"}.`,
384
+ "info",
385
+ );
386
+ return { kind: "stay" };
387
+ },
388
+ "set-completion": async ({ value }) => applyCompletionSetting(value, ctx, runtime),
389
+ "load-agent-picker": async () => {
390
+ availableAgents = discoverAgents(ctx.cwd, "user", readSubagentSettings() ?? {}).agents;
391
+ if (availableAgents.length === 0) {
392
+ ctx.ui.notify("No agents found", "warning");
393
+ return { kind: "rejected" };
394
+ }
395
+ return { kind: "to", screen: "agent-picker" };
396
+ },
397
+ "pick-agent": async ({ itemId }) => {
398
+ const agent = availableAgents.find((candidate) => candidate.name === itemId);
399
+ if (!agent) return { kind: "rejected" };
400
+ const settings = readSubagentSettings() ?? {};
401
+ const configured = settings.agents?.[agent.name];
402
+ const configuredTools =
403
+ configured && hasOwn(configured, "tools") ? (configured.tools ?? []) : undefined;
404
+ const defaults = discoverAgents(ctx.cwd, "user").agents.find(
405
+ (candidate) => candidate.name === agent.name,
406
+ )?.tools;
407
+ const allTools = uniqueToolNames(pi.getAllTools().map((tool) => tool.name)).sort((a, b) =>
408
+ a.localeCompare(b),
409
+ );
410
+ const selected = uniqueToolNames(configuredTools ?? defaults ?? allTools);
411
+ const selectedSet = new Set(selected);
412
+ toolDraft = {
413
+ agentName: agent.name,
414
+ agentSource: agent.source,
415
+ allTools,
416
+ defaultTools: defaults,
417
+ orderedTools: [...selected, ...allTools.filter((name) => !selectedSet.has(name))],
418
+ selected: selectedSet,
419
+ };
420
+ return { kind: "to", screen: "tool-draft" };
421
+ },
422
+ "toggle-tool": async ({ itemId, selected }) => {
423
+ if (!toolDraft?.allTools.includes(itemId)) return { kind: "rejected" };
424
+ if (selected) toolDraft.selected.add(itemId);
425
+ else toolDraft.selected.delete(itemId);
426
+ return { kind: "stay" };
427
+ },
428
+ "save-tools": async () => {
429
+ if (!toolDraft) return { kind: "rejected" };
430
+ const selected = toolDraft.orderedTools.filter((name) => toolDraft?.selected.has(name));
431
+ const restoredDefaults =
432
+ toolDraft.defaultTools === undefined
433
+ ? sameToolSet(selected, toolDraft.allTools)
434
+ : sameToolSet(selected, toolDraft.defaultTools);
435
+ try {
436
+ updateAgentToolsSetting(toolDraft.agentName, restoredDefaults ? undefined : selected);
437
+ } catch (error) {
438
+ ctx.ui.notify(`Agent tool settings were not saved: ${formatError(error)}`, "error");
439
+ return { kind: "rejected" };
440
+ }
441
+ ctx.ui.notify(
442
+ restoredDefaults
443
+ ? `${safeTerminalText(toolDraft.agentName)}: defaults restored`
444
+ : `${safeTerminalText(toolDraft.agentName)}: ${selected.length} tool${selected.length === 1 ? "" : "s"} configured`,
445
+ "info",
446
+ );
447
+ toolDraft = undefined;
448
+ return { kind: "back" };
418
449
  },
419
- };
450
+ "discard-tools": async () => {
451
+ toolDraft = undefined;
452
+ return { kind: "back" };
453
+ },
454
+ back: async () => ({ kind: "back" }),
455
+ },
456
+ });
457
+ await runMenu(ctx, menu, {
458
+ getState: () => undefined,
459
+ signal: owner.controller.signal,
460
+ isCurrent: () => generation === owner.generation && !owner.controller.signal.aborted,
420
461
  });
421
462
  }
422
463
 
423
- async function showDelegationWorkflow(
464
+ async function showSubagentSettings(
424
465
  ctx: ExtensionCommandContext,
425
466
  runtime: SubagentSettingsRuntime,
426
- ): Promise<boolean> {
427
- const snapshot = inspectDelegationWorkflowSettings();
467
+ owner: MenuOwner,
468
+ ) {
469
+ const snapshot = inspectCompletionDeliverySettings();
428
470
  if (ctx.mode !== "tui") {
429
471
  if (ctx.hasUI) {
430
472
  ctx.ui.notify(
431
- `Edit delegation settings manually: ${safeTerminalText(snapshot.path)}`,
473
+ `User settings apply to this and future sessions. Edit settings manually: ${safeTerminalText(snapshot.path)}`,
432
474
  "info",
433
475
  );
434
476
  }
435
- return false;
436
- }
437
- if (snapshot.error) {
438
- ctx.ui.notify(
439
- `Delegation settings cannot be edited: ${safeTerminalText(snapshot.error)}. Repair ${safeTerminalText(snapshot.path)} and retry.`,
440
- "error",
441
- );
442
- return false;
477
+ return;
443
478
  }
444
- const activeWorkflow = currentWorkflow(runtime, runtime.getRuntimeStatus());
445
- const choices: SelectItem[] = [
446
- {
447
- value: "all",
448
- label: "All delegation methods",
449
- description: "Allow blocking batches and reusable async agents",
450
- },
451
- {
452
- value: "async-only",
453
- label: "Async only",
454
- description: "Keep the root responsive; remove blocking subagent",
455
- },
456
- {
457
- value: "blocking-only",
458
- label: "Blocking only",
459
- description: "Keep blocking batches; remove reusable async agents",
460
- },
461
- ];
462
- const selected = await ctx.ui.custom<DelegationWorkflow | null>(
463
- (tui, theme, _keybindings, done) => {
464
- const container = new Container();
465
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
466
- container.addChild(new Text(theme.fg("accent", theme.bold("Change Delegation")), 1, 0));
467
- container.addChild(
468
- new Text(
469
- theme.fg(
470
- "muted",
471
- [
472
- `Current: ${workflowLabel(activeWorkflow)}`,
473
- ...(snapshot.value !== activeWorkflow
474
- ? [`Configured after reload: ${workflowLabel(snapshot.value)}`]
475
- : []),
476
- ].join("\n"),
477
- ),
478
- 1,
479
- 0,
480
- ),
481
- );
482
- container.addChild(new Spacer(1));
483
- const selectList = new SelectList(choices, Math.min(choices.length + 2, 10), {
484
- selectedPrefix: (text: string) => theme.fg("accent", text),
485
- selectedText: (text: string) => theme.fg("accent", text),
486
- description: (text: string) => theme.fg("muted", text),
487
- scrollInfo: (text: string) => theme.fg("dim", text),
488
- noMatch: (text: string) => theme.fg("warning", text),
489
- });
490
- selectList.setSelectedIndex(
491
- Math.max(
492
- 0,
493
- choices.findIndex((item) => item.value === snapshot.value),
494
- ),
495
- );
496
- selectList.onSelect = (item) => done(item.value as DelegationWorkflow);
497
- selectList.onCancel = () => done(null);
498
- container.addChild(selectList);
499
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter preview · esc back"), 1, 0));
500
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
501
- return {
502
- render: (width: number) => container.render(width),
503
- invalidate: () => container.invalidate(),
504
- handleInput(data: string) {
505
- selectList.handleInput(data);
506
- tui.requestRender();
507
- },
508
- };
479
+ const generation = owner.generation;
480
+ const menu = defineMenu<undefined, "completion", "set-completion", ExtensionCommandContext>({
481
+ start: "completion",
482
+ screens: { completion: () => completionSettingsScreen() },
483
+ actions: {
484
+ "set-completion": async ({ value }) => applyCompletionSetting(value, ctx, runtime),
509
485
  },
510
- );
511
- if (!selected) return false;
512
- if (selected === activeWorkflow && selected === snapshot.value) {
513
- ctx.ui.notify(`Delegation already uses ${workflowLabel(selected)}.`, "info");
514
- return false;
515
- }
516
- const requiresReload = selected !== activeWorkflow;
517
- if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) return false;
486
+ });
487
+ await runMenu(ctx, menu, {
488
+ getState: () => undefined,
489
+ signal: owner.controller.signal,
490
+ isCurrent: () => generation === owner.generation && !owner.controller.signal.aborted,
491
+ });
492
+ }
518
493
 
519
- const confirmed = await showWorkflowPreview(ctx, activeWorkflow, selected, requiresReload);
520
- if (!confirmed) return false;
521
- if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) return false;
494
+ function completionSettingsScreen() {
495
+ const snapshot = inspectCompletionDeliverySettings();
496
+ return {
497
+ kind: "settings" as const,
498
+ title: snapshot.error ? "Subagent User Settings · Read only" : "Subagent User Settings",
499
+ lines: [
500
+ "Applies now and to future sessions",
501
+ safeTerminalText(snapshot.path),
502
+ ...(snapshot.error ? [`Settings cannot be edited: ${safeTerminalText(snapshot.error)}`] : []),
503
+ ],
504
+ items: snapshot.error
505
+ ? []
506
+ : [
507
+ {
508
+ id: "completionDelivery",
509
+ label: "When async work finishes",
510
+ description:
511
+ "Wait for your next turn, or request one synthesis turn after the root settles.",
512
+ currentValue: completionLabel(snapshot.value),
513
+ values: ["Wait until my next turn", "Resume automatically when finished"],
514
+ action: "set-completion" as const,
515
+ },
516
+ ],
517
+ };
518
+ }
519
+
520
+ function applyCompletionSetting(
521
+ value: string | undefined,
522
+ ctx: ExtensionCommandContext,
523
+ runtime: SubagentSettingsRuntime,
524
+ ) {
525
+ const previous = runtime.getCompletionDelivery();
526
+ const next: CompletionDelivery =
527
+ value === "Resume automatically when finished" ? "auto-resume" : "next-turn";
528
+ if (next === previous) return { kind: "stay" as const };
522
529
  try {
523
- updateDelegationWorkflowSetting(selected as Exclude<DelegationWorkflow, "disabled">);
530
+ updateCompletionDeliverySetting(next);
531
+ runtime.setCompletionDelivery(next);
532
+ ctx.ui.notify(`Saved and applied: ${completionLabel(next)}.`, "info");
533
+ return { kind: "stay" as const };
524
534
  } catch (error) {
525
- ctx.ui.notify(
526
- `Delegation settings were not saved: ${formatError(error)}. The current workflow is unchanged.`,
527
- "error",
528
- );
529
- return false;
530
- }
531
- if (!requiresReload) {
532
- ctx.ui.notify(
533
- `Saved ${workflowLabel(selected)}. The current tool surface already matches.`,
534
- "info",
535
- );
536
- return false;
535
+ ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
536
+ return { kind: "rejected" as const };
537
537
  }
538
- ctx.ui.notify(
539
- `Saved ${workflowLabel(selected)}. Reloading subagent tools… If the tool surface does not refresh, run /reload.`,
540
- "info",
541
- );
542
- await ctx.reload();
543
- return true;
544
538
  }
545
539
 
546
540
  function blockReloadWithRetainedAgents(
@@ -562,266 +556,20 @@ async function showWorkflowPreview(
562
556
  next: DelegationWorkflow,
563
557
  requiresReload: boolean,
564
558
  ): Promise<boolean> {
565
- const workflowChanges = workflowEffects(current, next);
566
- const effects = (
567
- workflowChanges.length > 0
568
- ? workflowChanges
569
- : ["Keep the current registered tools and cancel the pending workflow change"]
570
- )
571
- .map((effect) => `- ${effect}`)
572
- .join("\n");
573
- return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
574
- const container = new Container();
575
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
576
- container.addChild(new Text(theme.fg("accent", theme.bold("Review Delegation Change")), 1, 0));
577
- container.addChild(
578
- new Text(
579
- theme.fg(
580
- "muted",
581
- `Current: ${workflowLabel(current)}\nNew: ${workflowLabel(next)}\n\nEffect:\n${effects}\n- ${requiresReload ? "Reload the extension to apply this tool surface" : "No reload is needed because the active tools already match"}`,
582
- ),
583
- 1,
584
- 0,
585
- ),
586
- );
587
- container.addChild(new Spacer(1));
588
- const actions: SelectItem[] = [
589
- {
590
- value: "save",
591
- label: requiresReload ? "Save and reload" : "Save",
592
- description: requiresReload
593
- ? "Persist and apply this workflow"
594
- : "Persist the workflow that is already active",
595
- },
596
- { value: "cancel", label: "Cancel", description: "Leave settings and tools unchanged" },
597
- ];
598
- const selectList = new SelectList(actions, 4, {
599
- selectedPrefix: (text: string) => theme.fg("accent", text),
600
- selectedText: (text: string) => theme.fg("accent", text),
601
- description: (text: string) => theme.fg("muted", text),
602
- scrollInfo: (text: string) => theme.fg("dim", text),
603
- noMatch: (text: string) => theme.fg("warning", text),
604
- });
605
- selectList.onSelect = (item) => done(item.value === "save");
606
- selectList.onCancel = () => done(false);
607
- container.addChild(selectList);
608
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter choose · esc cancel"), 1, 0));
609
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
610
- return {
611
- render: (width: number) => container.render(width),
612
- invalidate: () => container.invalidate(),
613
- handleInput(data: string) {
614
- selectList.handleInput(data);
615
- tui.requestRender();
616
- },
617
- };
618
- });
619
- }
620
-
621
- async function showAdvancedSettings(
622
- pi: ExtensionAPI,
623
- ctx: ExtensionCommandContext,
624
- runtime: SubagentSettingsRuntime,
625
- ) {
626
- while (true) {
627
- const action = await ctx.ui.custom<AdvancedAction | null>((tui, theme, _keybindings, done) => {
628
- const container = new Container();
629
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
630
- container.addChild(
631
- new Text(theme.fg("accent", theme.bold("Advanced Subagent Settings")), 1, 0),
632
- );
633
- container.addChild(new Spacer(1));
634
- const items: SelectItem[] = [
635
- {
636
- value: "agent-tools",
637
- label: "Agent tool permissions",
638
- description: "Customize persistent per-agent tool allow-lists",
639
- },
640
- {
641
- value: "status",
642
- label: "Runtime details",
643
- description: "Show transport, configured source, and settings path",
644
- },
645
- { value: "back", label: "Back", description: "Return to the Subagents manager" },
646
- ];
647
- const selectList = new SelectList(items, 5, {
648
- selectedPrefix: (text: string) => theme.fg("accent", text),
649
- selectedText: (text: string) => theme.fg("accent", text),
650
- description: (text: string) => theme.fg("muted", text),
651
- scrollInfo: (text: string) => theme.fg("dim", text),
652
- noMatch: (text: string) => theme.fg("warning", text),
653
- });
654
- selectList.onSelect = (item) => done(item.value as AdvancedAction);
655
- selectList.onCancel = () => done(null);
656
- container.addChild(selectList);
657
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter select · esc back"), 1, 0));
658
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
659
- return {
660
- render: (width: number) => container.render(width),
661
- invalidate: () => container.invalidate(),
662
- handleInput(data: string) {
663
- selectList.handleInput(data);
664
- tui.requestRender();
665
- },
666
- };
667
- });
668
- if (!action || action === "back") return;
669
- if (action === "agent-tools") await showSubagentToolSettings(pi, ctx);
670
- else showSubagentStatus(ctx, runtime);
671
- }
672
- }
673
-
674
- async function showCurrentSessionAgents(
675
- ctx: ExtensionCommandContext,
676
- runtime: SubagentSettingsRuntime,
677
- ) {
678
- while (true) {
679
- const agents = runtime.listAgents();
680
- const status = runtime.getRuntimeStatus();
681
- const action = await ctx.ui.custom<AgentManagerAction | null>(
682
- (tui, theme, _keybindings, done) => {
683
- const container = new Container();
684
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
685
- container.addChild(
686
- new Text(theme.fg("accent", theme.bold("Current-session Subagents")), 1, 0),
687
- );
688
- container.addChild(new Spacer(1));
689
- container.addChild(
690
- new Text(
691
- theme.fg(
692
- "muted",
693
- agents.length
694
- ? agents.map(formatStatefulAgentLine).join("\n")
695
- : formatEmptyRuntime(status),
696
- ),
697
- 1,
698
- 0,
699
- ),
700
- );
701
- container.addChild(new Spacer(1));
702
- const actions: SelectItem[] = [
703
- { value: "back", label: "Back", description: "Return to the Subagents manager" },
704
- ...(agents.length > 0
705
- ? [
706
- {
707
- value: "clear",
708
- label: "Clear current-session agents",
709
- description: "Close and delete retained agents for this session",
710
- },
711
- ]
712
- : []),
713
- ];
714
- const selectList = new SelectList(actions, Math.min(actions.length + 2, 8), {
715
- selectedPrefix: (text: string) => theme.fg("accent", text),
716
- selectedText: (text: string) => theme.fg("accent", text),
717
- description: (text: string) => theme.fg("muted", text),
718
- scrollInfo: (text: string) => theme.fg("dim", text),
719
- noMatch: (text: string) => theme.fg("warning", text),
720
- });
721
- selectList.onSelect = (item) => done(item.value as AgentManagerAction);
722
- selectList.onCancel = () => done(null);
723
- container.addChild(selectList);
724
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
725
- return {
726
- render: (width: number) => container.render(width),
727
- invalidate: () => container.invalidate(),
728
- handleInput(data: string) {
729
- selectList.handleInput(data);
730
- tui.requestRender();
731
- },
732
- };
733
- },
734
- );
735
- if (!action || action === "back") return;
736
- const confirmed = await ctx.ui.confirm(
737
- "Clear current-session subagents?",
738
- `Close and delete ${agents.length} retained agent${agents.length === 1 ? "" : "s"}?`,
739
- );
740
- if (!confirmed) continue;
741
- const cleared = await runtime.clearAgents();
742
- ctx.ui.notify(
743
- `Cleared ${cleared} current-session subagent${cleared === 1 ? "" : "s"}.`,
744
- "info",
745
- );
746
- }
747
- }
748
-
749
- async function showSubagentSettings(
750
- ctx: ExtensionCommandContext,
751
- runtime: SubagentSettingsRuntime,
752
- ) {
753
- const snapshot = inspectCompletionDeliverySettings();
754
- if (ctx.mode !== "tui") {
755
- if (ctx.hasUI) {
756
- ctx.ui.notify(
757
- `User settings apply to this and future sessions. Edit settings manually: ${safeTerminalText(snapshot.path)}`,
758
- "info",
759
- );
760
- }
761
- return;
762
- }
763
- if (snapshot.error) {
764
- ctx.ui.notify(
765
- `Subagent settings cannot be edited: ${safeTerminalText(snapshot.error)}`,
766
- "error",
767
- );
768
- return;
769
- }
770
- let currentValue = snapshot.value;
771
- await ctx.ui.custom((tui, theme, _keybindings, done) => {
772
- const items: SettingItem[] = [
773
- {
774
- id: "completionDelivery",
775
- label: "When async work finishes",
776
- description:
777
- "Wait for your next turn, or request one synthesis turn after the root settles.",
778
- currentValue: completionLabel(currentValue),
779
- values: ["Wait until my next turn", "Resume automatically when finished"],
780
- },
781
- ];
782
- const container = new Container();
783
- container.addChild(new Text(theme.fg("accent", theme.bold("Subagent User Settings")), 1, 0));
784
- container.addChild(
785
- new Text(
786
- theme.fg("muted", `Applies now and to future sessions\n${safeTerminalText(snapshot.path)}`),
787
- 1,
788
- 0,
559
+ const changes = workflowEffects(current, next);
560
+ return ctx.ui.confirm(
561
+ requiresReload ? "Save delegation change and reload?" : "Save delegation change?",
562
+ [
563
+ `Current: ${workflowLabel(current)}`,
564
+ `New: ${workflowLabel(next)}`,
565
+ "",
566
+ "Effect:",
567
+ ...(changes.length > 0 ? changes : ["Keep the current registered tools"]).map(
568
+ (effect) => `- ${effect}`,
789
569
  ),
790
- );
791
- container.addChild(new Spacer(1));
792
- let settingsList: SettingsList;
793
- settingsList = new SettingsList(
794
- items,
795
- Math.min(items.length + 2, 15),
796
- getSettingsListTheme(),
797
- (id, newValue) => {
798
- if (id !== "completionDelivery") return;
799
- const previous = currentValue;
800
- const next: CompletionDelivery =
801
- newValue === "Resume automatically when finished" ? "auto-resume" : "next-turn";
802
- try {
803
- updateCompletionDeliverySetting(next);
804
- runtime.setCompletionDelivery(next);
805
- currentValue = next;
806
- ctx.ui.notify(`Saved and applied: ${completionLabel(next)}.`, "info");
807
- } catch (error) {
808
- settingsList.updateValue(id, completionLabel(previous));
809
- ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
810
- }
811
- tui.requestRender();
812
- },
813
- () => done(undefined),
814
- );
815
- container.addChild(settingsList);
816
- return {
817
- render: (width: number) => container.render(width),
818
- invalidate: () => container.invalidate(),
819
- handleInput(data: string) {
820
- settingsList.handleInput?.(data);
821
- tui.requestRender();
822
- },
823
- };
824
- });
570
+ `- ${requiresReload ? "Reload the extension to apply this tool surface" : "No reload is needed because the active tools already match"}`,
571
+ ].join("\n"),
572
+ );
825
573
  }
826
574
 
827
575
  function showSubagentStatus(ctx: ExtensionCommandContext, runtime: SubagentSettingsRuntime) {
@@ -835,17 +583,23 @@ function showSubagentStatus(ctx: ExtensionCommandContext, runtime: SubagentSetti
835
583
 
836
584
  function showSubagentHelp(ctx: ExtensionCommandContext) {
837
585
  if (ctx.mode !== "tui" && !ctx.hasUI) return;
586
+ ctx.ui.notify(helpLines().join("\n"), "info");
587
+ }
588
+
589
+ function statusLines(runtime: SubagentSettingsRuntime): string[] {
838
590
  const snapshot = inspectCompletionDeliverySettings();
839
- ctx.ui.notify(
840
- [
841
- "/subagents — choose delegation workflow, manage current agents, and configure agent tools",
842
- "/subagents settings configure async completion behavior",
843
- "/subagents status show current-session and user-setting values",
844
- "/subagents help — show this help",
845
- `User settings: ${safeTerminalText(snapshot.path)}`,
846
- ].join("\n"),
847
- "info",
848
- );
591
+ return formatStatus(runtime.getRuntimeStatus(), snapshot, runtime).split("\n");
592
+ }
593
+
594
+ function helpLines(): string[] {
595
+ const snapshot = inspectCompletionDeliverySettings();
596
+ return [
597
+ "/subagents choose delegation workflow, manage current agents, and configure agent tools",
598
+ "/subagents settings — configure async completion behavior",
599
+ "/subagents status — show current-session and user-setting values",
600
+ "/subagents help — show this help",
601
+ `User settings: ${safeTerminalText(snapshot.path)}`,
602
+ ];
849
603
  }
850
604
 
851
605
  function formatManagerSummary(
@@ -911,6 +665,10 @@ function currentWorkflow(
911
665
  return "disabled";
912
666
  }
913
667
 
668
+ function isWorkflow(value: string): value is Exclude<DelegationWorkflow, "disabled"> {
669
+ return value === "all" || value === "async-only" || value === "blocking-only";
670
+ }
671
+
914
672
  function workflowLabel(value: DelegationWorkflow): string {
915
673
  switch (value) {
916
674
  case "all":