@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.
- package/README.md +11 -2
- package/package.json +2 -1
- 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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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)
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
464
|
+
async function showSubagentSettings(
|
|
424
465
|
ctx: ExtensionCommandContext,
|
|
425
466
|
runtime: SubagentSettingsRuntime,
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
473
|
+
`User settings apply to this and future sessions. Edit settings manually: ${safeTerminalText(snapshot.path)}`,
|
|
432
474
|
"info",
|
|
433
475
|
);
|
|
434
476
|
}
|
|
435
|
-
return
|
|
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
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
"
|
|
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":
|