@ferris1225/pi-subagents 3.0.0 → 4.0.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 +475 -457
- package/agents/cleaner.md +11 -21
- package/agents/{explore.md → explorer.md} +3 -3
- package/agents/reviewer.md +47 -41
- package/agents/worker.md +3 -3
- package/package.json +1 -1
- package/src/config.ts +299 -299
- package/src/dispatch.ts +8 -25
- package/src/monitor.ts +1 -1
- package/src/prompt.ts +75 -58
- package/src/setup.ts +438 -438
- package/src/thread-lifecycle.ts +1 -1
package/src/setup.ts
CHANGED
|
@@ -1,438 +1,438 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive configuration wizard for /subagents-setup.
|
|
3
|
-
*
|
|
4
|
-
* The UI intentionally has no backup pool or global thinking menu. Each agent
|
|
5
|
-
* gets one optional model override; failures hand directly to the current main
|
|
6
|
-
* model. Thinking defaults to Auto and manual choices are limited to levels Pi
|
|
7
|
-
* reports as supported by the selected model.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { stat } from "node:fs/promises";
|
|
11
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
AGENT_SCOPE_VALUES,
|
|
15
|
-
BUILTIN_AGENT_NAMES,
|
|
16
|
-
DEFAULT_CONFIG,
|
|
17
|
-
DEFAULT_ENABLED_AGENTS,
|
|
18
|
-
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
19
|
-
DEFAULT_MAX_CONCURRENCY,
|
|
20
|
-
DEFAULT_MAX_FIX_ROUNDS,
|
|
21
|
-
DEFAULT_THINKING_LEVEL,
|
|
22
|
-
type AgentScope,
|
|
23
|
-
type SubagentsConfig,
|
|
24
|
-
type ThinkingLevel,
|
|
25
|
-
errorMessage,
|
|
26
|
-
getConfigPath,
|
|
27
|
-
loadConfig,
|
|
28
|
-
saveConfig,
|
|
29
|
-
} from "./config.ts";
|
|
30
|
-
import {
|
|
31
|
-
CURRENT_MAIN_MODEL,
|
|
32
|
-
applyAgentModelChoice,
|
|
33
|
-
availableModelsInScope,
|
|
34
|
-
buildModelPickerItems,
|
|
35
|
-
currentModelRef,
|
|
36
|
-
findModelByRef,
|
|
37
|
-
modelRef,
|
|
38
|
-
resolveThinkingLevel,
|
|
39
|
-
supportedThinkingLevels,
|
|
40
|
-
} from "./models.ts";
|
|
41
|
-
import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
42
|
-
import { discoverAgents } from "./agents.ts";
|
|
43
|
-
|
|
44
|
-
const AUTO_THINKING = "__auto_thinking__";
|
|
45
|
-
|
|
46
|
-
function actualAgentThinkingDefault(
|
|
47
|
-
ctx: ExtensionCommandContext,
|
|
48
|
-
config: SubagentsConfig,
|
|
49
|
-
agentName: string,
|
|
50
|
-
): ThinkingLevel {
|
|
51
|
-
const { agents } = discoverAgents(ctx.cwd, {
|
|
52
|
-
scope: config.agentScope,
|
|
53
|
-
enabledNames: config.enabledAgents,
|
|
54
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
55
|
-
});
|
|
56
|
-
return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
60
|
-
const MODULE_HINTS: Record<string, string> = {
|
|
61
|
-
|
|
62
|
-
worker: "implement / fix / refactor / test (full tools)",
|
|
63
|
-
cleaner: "
|
|
64
|
-
reviewer: "
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function moduleLabel(name: string): string {
|
|
68
|
-
const hint = MODULE_HINTS[name];
|
|
69
|
-
return hint ? `${name} — ${hint}` : name;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function configExists(configPath: string): Promise<boolean> {
|
|
73
|
-
try {
|
|
74
|
-
await stat(configPath);
|
|
75
|
-
return true;
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function pickEnabledAgents(
|
|
82
|
-
ctx: ExtensionCommandContext,
|
|
83
|
-
current: readonly string[],
|
|
84
|
-
): Promise<string[] | undefined> {
|
|
85
|
-
const items = BUILTIN_AGENT_NAMES.map((name) => ({ value: name, label: moduleLabel(name) }));
|
|
86
|
-
return promptSelectMany(
|
|
87
|
-
ctx,
|
|
88
|
-
"Enable which sub-agents?",
|
|
89
|
-
"Space toggles • Enter confirms • Esc cancels",
|
|
90
|
-
items,
|
|
91
|
-
current,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function pickConfiguredModel(
|
|
96
|
-
ctx: ExtensionCommandContext,
|
|
97
|
-
title: string,
|
|
98
|
-
configuredRef: string | undefined,
|
|
99
|
-
escNote: string,
|
|
100
|
-
): Promise<string | undefined> {
|
|
101
|
-
const models = availableModelsInScope(ctx);
|
|
102
|
-
const items = buildModelPickerItems({
|
|
103
|
-
models,
|
|
104
|
-
configuredRef,
|
|
105
|
-
mainRef: currentModelRef(ctx),
|
|
106
|
-
});
|
|
107
|
-
return promptSelectOne(
|
|
108
|
-
ctx,
|
|
109
|
-
title,
|
|
110
|
-
`Type to filter by provider, model, capability, or thinking level • ↑/↓ • Enter selects • Esc ${escNote}`,
|
|
111
|
-
items,
|
|
112
|
-
configuredRef ?? CURRENT_MAIN_MODEL,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function pickAgentModel(
|
|
117
|
-
ctx: ExtensionCommandContext,
|
|
118
|
-
agentName: string,
|
|
119
|
-
currentRef: string | undefined,
|
|
120
|
-
escNote = "cancels setup",
|
|
121
|
-
): Promise<string | undefined> {
|
|
122
|
-
return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
126
|
-
off: "no reasoning tokens",
|
|
127
|
-
minimal: "minimal reasoning",
|
|
128
|
-
low: "light reasoning",
|
|
129
|
-
medium: "balanced reasoning",
|
|
130
|
-
high: "deep reasoning",
|
|
131
|
-
xhigh: "extra-deep reasoning",
|
|
132
|
-
max: "strongest reasoning",
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
function effectiveModelForChoice(
|
|
136
|
-
ctx: ExtensionCommandContext,
|
|
137
|
-
choice: string,
|
|
138
|
-
): Model<Api> | undefined {
|
|
139
|
-
if (choice === CURRENT_MAIN_MODEL) return ctx.model;
|
|
140
|
-
return findModelByRef(availableModelsInScope(ctx), choice);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Auto is the default. Manual rows are exactly the levels Pi exposes for the
|
|
144
|
-
* selected model; unsupported xhigh/max entries never appear. */
|
|
145
|
-
async function pickAgentStrength(
|
|
146
|
-
ctx: ExtensionCommandContext,
|
|
147
|
-
agentName: string,
|
|
148
|
-
model: Model<Api> | undefined,
|
|
149
|
-
current: ThinkingLevel | undefined,
|
|
150
|
-
agentDefault: ThinkingLevel,
|
|
151
|
-
escNote = "cancels setup",
|
|
152
|
-
): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
|
|
153
|
-
const supported = supportedThinkingLevels(model);
|
|
154
|
-
const automatic = resolveThinkingLevel(model, agentDefault);
|
|
155
|
-
// No model metadata, or a non-reasoning model whose only valid value is off:
|
|
156
|
-
// Auto is already the complete and least surprising choice.
|
|
157
|
-
if (supported.length <= 1) return AUTO_THINKING;
|
|
158
|
-
|
|
159
|
-
const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
|
|
160
|
-
const modelName = model ? modelRef(model) : "current main model";
|
|
161
|
-
const options = [
|
|
162
|
-
{
|
|
163
|
-
value: AUTO_THINKING,
|
|
164
|
-
label: `auto — ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
|
|
165
|
-
},
|
|
166
|
-
...supported.map((level) => ({
|
|
167
|
-
value: level,
|
|
168
|
-
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
|
|
169
|
-
})),
|
|
170
|
-
];
|
|
171
|
-
return promptSelectOne(
|
|
172
|
-
ctx,
|
|
173
|
-
`Thinking for "${agentName}"?`,
|
|
174
|
-
`Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
|
|
175
|
-
options,
|
|
176
|
-
current === undefined ? AUTO_THINKING : currentEffective,
|
|
177
|
-
) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function pickAgentToConfigure(
|
|
181
|
-
ctx: ExtensionCommandContext,
|
|
182
|
-
enabledAgents: readonly string[],
|
|
183
|
-
): Promise<string | undefined> {
|
|
184
|
-
if (enabledAgents.length === 0) {
|
|
185
|
-
ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
return promptSelectOne(
|
|
189
|
-
ctx,
|
|
190
|
-
"Configure which agent?",
|
|
191
|
-
"Type to filter • ↑/↓ • Enter selects • Esc ends this pass",
|
|
192
|
-
enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** One agent: model, then thinking if the model exposes a choice. Esc at any
|
|
197
|
-
* step ends the caller's pass; earlier agents in that pass stay applied. */
|
|
198
|
-
async function configureOneAgent(
|
|
199
|
-
ctx: ExtensionCommandContext,
|
|
200
|
-
config: SubagentsConfig,
|
|
201
|
-
): Promise<
|
|
202
|
-
| {
|
|
203
|
-
name: string;
|
|
204
|
-
model: string;
|
|
205
|
-
strength: ThinkingLevel | typeof AUTO_THINKING;
|
|
206
|
-
}
|
|
207
|
-
| undefined
|
|
208
|
-
> {
|
|
209
|
-
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
210
|
-
if (name === undefined) return undefined;
|
|
211
|
-
const modelChoice = await pickAgentModel(
|
|
212
|
-
ctx,
|
|
213
|
-
name,
|
|
214
|
-
config.agentModels[name],
|
|
215
|
-
"stops — earlier agent changes are kept",
|
|
216
|
-
);
|
|
217
|
-
if (modelChoice === undefined) return undefined;
|
|
218
|
-
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
219
|
-
const strength = await pickAgentStrength(
|
|
220
|
-
ctx,
|
|
221
|
-
name,
|
|
222
|
-
model,
|
|
223
|
-
config.agentThinkingLevels[name],
|
|
224
|
-
actualAgentThinkingDefault(ctx, config, name),
|
|
225
|
-
"stops — earlier agent changes are kept",
|
|
226
|
-
);
|
|
227
|
-
if (strength === undefined) return undefined;
|
|
228
|
-
return { name, model: modelChoice, strength };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
|
|
232
|
-
const on = "On — inject the delegation directive (recommended)";
|
|
233
|
-
const off = "Off — rely on tool descriptions only";
|
|
234
|
-
const choice = await ctx.ui.select("Proactive dispatch injection?", [current ? `${on} (current)` : on, current ? off : `${off} (current)`]);
|
|
235
|
-
if (choice === undefined) return undefined;
|
|
236
|
-
return choice.startsWith("On");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
240
|
-
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
241
|
-
const IDLE_TIMEOUT_STEPS = [0, 30, 60, 90, 120, 180, 300, 600];
|
|
242
|
-
|
|
243
|
-
async function pickCount(
|
|
244
|
-
ctx: ExtensionCommandContext,
|
|
245
|
-
title: string,
|
|
246
|
-
steps: readonly number[],
|
|
247
|
-
current: number,
|
|
248
|
-
defaultValue: number,
|
|
249
|
-
): Promise<number | undefined> {
|
|
250
|
-
const values = [...new Set([...steps, current])].sort((a, b) => a - b);
|
|
251
|
-
const options = values.map((value) => {
|
|
252
|
-
const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
|
|
253
|
-
.filter(Boolean)
|
|
254
|
-
.join(", ");
|
|
255
|
-
return tags ? `${value} (${tags})` : String(value);
|
|
256
|
-
});
|
|
257
|
-
const choice = await ctx.ui.select(title, options);
|
|
258
|
-
return choice === undefined ? undefined : Number.parseInt(choice, 10);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
|
|
262
|
-
const labels: Record<AgentScope, string> = {
|
|
263
|
-
user: "user — built-in + ~/.pi/agent/agents (default)",
|
|
264
|
-
project: "project — built-in + nearest .pi/agents only",
|
|
265
|
-
both: "both — user agents, overridden by project agents",
|
|
266
|
-
};
|
|
267
|
-
const options = AGENT_SCOPE_VALUES.map((scope) =>
|
|
268
|
-
scope === current ? `${labels[scope]} (current)` : labels[scope],
|
|
269
|
-
);
|
|
270
|
-
const choice = await ctx.ui.select("Which agent directories to discover from?", options);
|
|
271
|
-
if (choice === undefined) return undefined;
|
|
272
|
-
return AGENT_SCOPE_VALUES.find((scope) => choice.startsWith(scope));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
|
|
276
|
-
const keep = new Set(enabled);
|
|
277
|
-
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<void> {
|
|
281
|
-
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
282
|
-
if (enabled === undefined) return notifyCancelled(ctx);
|
|
283
|
-
|
|
284
|
-
let agentModels = keepAgentEntries(base.agentModels, enabled);
|
|
285
|
-
for (const agentName of enabled) {
|
|
286
|
-
const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
|
|
287
|
-
if (choice === undefined) return notifyCancelled(ctx);
|
|
288
|
-
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
292
|
-
if (injection === undefined) return notifyCancelled(ctx);
|
|
293
|
-
const scope = await pickScope(ctx, base.agentScope);
|
|
294
|
-
if (scope === undefined) return notifyCancelled(ctx);
|
|
295
|
-
const maxConcurrency = await pickCount(
|
|
296
|
-
ctx,
|
|
297
|
-
"Max sub-agents running at once?",
|
|
298
|
-
CONCURRENCY_STEPS,
|
|
299
|
-
base.maxConcurrency,
|
|
300
|
-
DEFAULT_MAX_CONCURRENCY,
|
|
301
|
-
);
|
|
302
|
-
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
303
|
-
const maxFixRounds = await pickCount(
|
|
304
|
-
ctx,
|
|
305
|
-
"Reviewer auto-fix rounds? (0 = main agent handles fixes)",
|
|
306
|
-
FIX_ROUNDS_STEPS,
|
|
307
|
-
base.maxFixRounds,
|
|
308
|
-
DEFAULT_MAX_FIX_ROUNDS,
|
|
309
|
-
);
|
|
310
|
-
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
311
|
-
const idleTimeoutSec = await pickCount(
|
|
312
|
-
ctx,
|
|
313
|
-
"Idle timeout in seconds? (0 = disabled)",
|
|
314
|
-
IDLE_TIMEOUT_STEPS,
|
|
315
|
-
base.idleTimeoutSec,
|
|
316
|
-
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
317
|
-
);
|
|
318
|
-
if (idleTimeoutSec === undefined) return notifyCancelled(ctx);
|
|
319
|
-
|
|
320
|
-
const next: SubagentsConfig = {
|
|
321
|
-
enabledAgents: enabled,
|
|
322
|
-
agentModels,
|
|
323
|
-
// Full setup returns every agent to capability-aware Auto thinking.
|
|
324
|
-
agentThinkingLevels: {},
|
|
325
|
-
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
326
|
-
maxResultLines: base.maxResultLines,
|
|
327
|
-
proactiveInjection: injection,
|
|
328
|
-
agentScope: scope,
|
|
329
|
-
maxConcurrency,
|
|
330
|
-
maxFixRounds,
|
|
331
|
-
idleTimeoutSec,
|
|
332
|
-
announcedFeatures: base.announcedFeatures,
|
|
333
|
-
};
|
|
334
|
-
await saveConfig(next, configPath);
|
|
335
|
-
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function updateRuntimeSetting(
|
|
339
|
-
ctx: ExtensionCommandContext,
|
|
340
|
-
config: SubagentsConfig,
|
|
341
|
-
): Promise<SubagentsConfig | undefined> {
|
|
342
|
-
const choice = await ctx.ui.select("Runtime setting", [
|
|
343
|
-
"Proactive injection",
|
|
344
|
-
"Agent scope",
|
|
345
|
-
"Max concurrency",
|
|
346
|
-
"Reviewer auto-fix rounds",
|
|
347
|
-
"Idle timeout",
|
|
348
|
-
]);
|
|
349
|
-
if (choice === undefined) return undefined;
|
|
350
|
-
const next = { ...config };
|
|
351
|
-
if (choice.startsWith("Proactive")) {
|
|
352
|
-
const value = await pickInjection(ctx, config.proactiveInjection);
|
|
353
|
-
if (value === undefined) return undefined;
|
|
354
|
-
next.proactiveInjection = value;
|
|
355
|
-
} else if (choice.startsWith("Agent scope")) {
|
|
356
|
-
const value = await pickScope(ctx, config.agentScope);
|
|
357
|
-
if (value === undefined) return undefined;
|
|
358
|
-
next.agentScope = value;
|
|
359
|
-
} else if (choice.startsWith("Max concurrency")) {
|
|
360
|
-
const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
|
|
361
|
-
if (value === undefined) return undefined;
|
|
362
|
-
next.maxConcurrency = value;
|
|
363
|
-
} else if (choice.startsWith("Reviewer")) {
|
|
364
|
-
const value = await pickCount(ctx, "Reviewer auto-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
|
|
365
|
-
if (value === undefined) return undefined;
|
|
366
|
-
next.maxFixRounds = value;
|
|
367
|
-
} else {
|
|
368
|
-
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
369
|
-
if (value === undefined) return undefined;
|
|
370
|
-
next.idleTimeoutSec = value;
|
|
371
|
-
}
|
|
372
|
-
return next;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
376
|
-
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
377
|
-
"Enable/disable agents",
|
|
378
|
-
"Configure an agent (model + thinking)",
|
|
379
|
-
"Runtime settings",
|
|
380
|
-
"Full re-setup",
|
|
381
|
-
]);
|
|
382
|
-
if (choice === undefined) return notifyCancelled(ctx);
|
|
383
|
-
if (choice.startsWith("Full")) return runFullSetup(ctx, configPath, config);
|
|
384
|
-
|
|
385
|
-
let next: SubagentsConfig = {
|
|
386
|
-
...config,
|
|
387
|
-
agentModels: { ...config.agentModels },
|
|
388
|
-
agentThinkingLevels: { ...config.agentThinkingLevels },
|
|
389
|
-
};
|
|
390
|
-
if (choice.startsWith("Enable")) {
|
|
391
|
-
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
392
|
-
if (enabled === undefined) return notifyCancelled(ctx);
|
|
393
|
-
next.enabledAgents = enabled;
|
|
394
|
-
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
395
|
-
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
396
|
-
} else if (choice.startsWith("Configure")) {
|
|
397
|
-
// Per-agent loop: model (+ thinking when the model exposes a choice), then
|
|
398
|
-
// back to the agent picker so several agents can be set in one pass. Esc
|
|
399
|
-
// at any step ends the loop; agents already configured in this pass are kept.
|
|
400
|
-
let configuredAny = false;
|
|
401
|
-
while (true) {
|
|
402
|
-
const picked = await configureOneAgent(ctx, next);
|
|
403
|
-
if (picked === undefined) break;
|
|
404
|
-
configuredAny = true;
|
|
405
|
-
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
406
|
-
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
407
|
-
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
408
|
-
}
|
|
409
|
-
if (!configuredAny) return notifyCancelled(ctx);
|
|
410
|
-
} else {
|
|
411
|
-
const updated = await updateRuntimeSetting(ctx, next);
|
|
412
|
-
if (updated === undefined) return notifyCancelled(ctx);
|
|
413
|
-
next = updated;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
await saveConfig(next, configPath);
|
|
417
|
-
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function notifyCancelled(ctx: ExtensionCommandContext): void {
|
|
421
|
-
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/** Entry point for the /subagents-setup command. */
|
|
425
|
-
export async function runSetup(ctx: ExtensionCommandContext, configPath: string = getConfigPath()): Promise<void> {
|
|
426
|
-
if (ctx.mode !== "tui") {
|
|
427
|
-
ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
try {
|
|
431
|
-
const exists = await configExists(configPath);
|
|
432
|
-
const config = await loadConfig(configPath);
|
|
433
|
-
if (exists) await runMenu(ctx, configPath, config);
|
|
434
|
-
else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] });
|
|
435
|
-
} catch (error) {
|
|
436
|
-
ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
|
|
437
|
-
}
|
|
438
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Interactive configuration wizard for /subagents-setup.
|
|
3
|
+
*
|
|
4
|
+
* The UI intentionally has no backup pool or global thinking menu. Each agent
|
|
5
|
+
* gets one optional model override; failures hand directly to the current main
|
|
6
|
+
* model. Thinking defaults to Auto and manual choices are limited to levels Pi
|
|
7
|
+
* reports as supported by the selected model.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { stat } from "node:fs/promises";
|
|
11
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
AGENT_SCOPE_VALUES,
|
|
15
|
+
BUILTIN_AGENT_NAMES,
|
|
16
|
+
DEFAULT_CONFIG,
|
|
17
|
+
DEFAULT_ENABLED_AGENTS,
|
|
18
|
+
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
19
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
20
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
21
|
+
DEFAULT_THINKING_LEVEL,
|
|
22
|
+
type AgentScope,
|
|
23
|
+
type SubagentsConfig,
|
|
24
|
+
type ThinkingLevel,
|
|
25
|
+
errorMessage,
|
|
26
|
+
getConfigPath,
|
|
27
|
+
loadConfig,
|
|
28
|
+
saveConfig,
|
|
29
|
+
} from "./config.ts";
|
|
30
|
+
import {
|
|
31
|
+
CURRENT_MAIN_MODEL,
|
|
32
|
+
applyAgentModelChoice,
|
|
33
|
+
availableModelsInScope,
|
|
34
|
+
buildModelPickerItems,
|
|
35
|
+
currentModelRef,
|
|
36
|
+
findModelByRef,
|
|
37
|
+
modelRef,
|
|
38
|
+
resolveThinkingLevel,
|
|
39
|
+
supportedThinkingLevels,
|
|
40
|
+
} from "./models.ts";
|
|
41
|
+
import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
42
|
+
import { discoverAgents } from "./agents.ts";
|
|
43
|
+
|
|
44
|
+
const AUTO_THINKING = "__auto_thinking__";
|
|
45
|
+
|
|
46
|
+
function actualAgentThinkingDefault(
|
|
47
|
+
ctx: ExtensionCommandContext,
|
|
48
|
+
config: SubagentsConfig,
|
|
49
|
+
agentName: string,
|
|
50
|
+
): ThinkingLevel {
|
|
51
|
+
const { agents } = discoverAgents(ctx.cwd, {
|
|
52
|
+
scope: config.agentScope,
|
|
53
|
+
enabledNames: config.enabledAgents,
|
|
54
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
55
|
+
});
|
|
56
|
+
return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
60
|
+
const MODULE_HINTS: Record<string, string> = {
|
|
61
|
+
explorer: "read-only codebase recon (fast model)",
|
|
62
|
+
worker: "implement / fix / refactor / test (full tools)",
|
|
63
|
+
cleaner: "prove and apply safe cleanup cuts (full tools)",
|
|
64
|
+
reviewer: "read-only audits and pre-commit gates",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function moduleLabel(name: string): string {
|
|
68
|
+
const hint = MODULE_HINTS[name];
|
|
69
|
+
return hint ? `${name} — ${hint}` : name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function configExists(configPath: string): Promise<boolean> {
|
|
73
|
+
try {
|
|
74
|
+
await stat(configPath);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function pickEnabledAgents(
|
|
82
|
+
ctx: ExtensionCommandContext,
|
|
83
|
+
current: readonly string[],
|
|
84
|
+
): Promise<string[] | undefined> {
|
|
85
|
+
const items = BUILTIN_AGENT_NAMES.map((name) => ({ value: name, label: moduleLabel(name) }));
|
|
86
|
+
return promptSelectMany(
|
|
87
|
+
ctx,
|
|
88
|
+
"Enable which sub-agents?",
|
|
89
|
+
"Space toggles • Enter confirms • Esc cancels",
|
|
90
|
+
items,
|
|
91
|
+
current,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function pickConfiguredModel(
|
|
96
|
+
ctx: ExtensionCommandContext,
|
|
97
|
+
title: string,
|
|
98
|
+
configuredRef: string | undefined,
|
|
99
|
+
escNote: string,
|
|
100
|
+
): Promise<string | undefined> {
|
|
101
|
+
const models = availableModelsInScope(ctx);
|
|
102
|
+
const items = buildModelPickerItems({
|
|
103
|
+
models,
|
|
104
|
+
configuredRef,
|
|
105
|
+
mainRef: currentModelRef(ctx),
|
|
106
|
+
});
|
|
107
|
+
return promptSelectOne(
|
|
108
|
+
ctx,
|
|
109
|
+
title,
|
|
110
|
+
`Type to filter by provider, model, capability, or thinking level • ↑/↓ • Enter selects • Esc ${escNote}`,
|
|
111
|
+
items,
|
|
112
|
+
configuredRef ?? CURRENT_MAIN_MODEL,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function pickAgentModel(
|
|
117
|
+
ctx: ExtensionCommandContext,
|
|
118
|
+
agentName: string,
|
|
119
|
+
currentRef: string | undefined,
|
|
120
|
+
escNote = "cancels setup",
|
|
121
|
+
): Promise<string | undefined> {
|
|
122
|
+
return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
126
|
+
off: "no reasoning tokens",
|
|
127
|
+
minimal: "minimal reasoning",
|
|
128
|
+
low: "light reasoning",
|
|
129
|
+
medium: "balanced reasoning",
|
|
130
|
+
high: "deep reasoning",
|
|
131
|
+
xhigh: "extra-deep reasoning",
|
|
132
|
+
max: "strongest reasoning",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function effectiveModelForChoice(
|
|
136
|
+
ctx: ExtensionCommandContext,
|
|
137
|
+
choice: string,
|
|
138
|
+
): Model<Api> | undefined {
|
|
139
|
+
if (choice === CURRENT_MAIN_MODEL) return ctx.model;
|
|
140
|
+
return findModelByRef(availableModelsInScope(ctx), choice);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Auto is the default. Manual rows are exactly the levels Pi exposes for the
|
|
144
|
+
* selected model; unsupported xhigh/max entries never appear. */
|
|
145
|
+
async function pickAgentStrength(
|
|
146
|
+
ctx: ExtensionCommandContext,
|
|
147
|
+
agentName: string,
|
|
148
|
+
model: Model<Api> | undefined,
|
|
149
|
+
current: ThinkingLevel | undefined,
|
|
150
|
+
agentDefault: ThinkingLevel,
|
|
151
|
+
escNote = "cancels setup",
|
|
152
|
+
): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
|
|
153
|
+
const supported = supportedThinkingLevels(model);
|
|
154
|
+
const automatic = resolveThinkingLevel(model, agentDefault);
|
|
155
|
+
// No model metadata, or a non-reasoning model whose only valid value is off:
|
|
156
|
+
// Auto is already the complete and least surprising choice.
|
|
157
|
+
if (supported.length <= 1) return AUTO_THINKING;
|
|
158
|
+
|
|
159
|
+
const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
|
|
160
|
+
const modelName = model ? modelRef(model) : "current main model";
|
|
161
|
+
const options = [
|
|
162
|
+
{
|
|
163
|
+
value: AUTO_THINKING,
|
|
164
|
+
label: `auto — ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
|
|
165
|
+
},
|
|
166
|
+
...supported.map((level) => ({
|
|
167
|
+
value: level,
|
|
168
|
+
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
|
|
169
|
+
})),
|
|
170
|
+
];
|
|
171
|
+
return promptSelectOne(
|
|
172
|
+
ctx,
|
|
173
|
+
`Thinking for "${agentName}"?`,
|
|
174
|
+
`Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
|
|
175
|
+
options,
|
|
176
|
+
current === undefined ? AUTO_THINKING : currentEffective,
|
|
177
|
+
) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function pickAgentToConfigure(
|
|
181
|
+
ctx: ExtensionCommandContext,
|
|
182
|
+
enabledAgents: readonly string[],
|
|
183
|
+
): Promise<string | undefined> {
|
|
184
|
+
if (enabledAgents.length === 0) {
|
|
185
|
+
ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return promptSelectOne(
|
|
189
|
+
ctx,
|
|
190
|
+
"Configure which agent?",
|
|
191
|
+
"Type to filter • ↑/↓ • Enter selects • Esc ends this pass",
|
|
192
|
+
enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** One agent: model, then thinking if the model exposes a choice. Esc at any
|
|
197
|
+
* step ends the caller's pass; earlier agents in that pass stay applied. */
|
|
198
|
+
async function configureOneAgent(
|
|
199
|
+
ctx: ExtensionCommandContext,
|
|
200
|
+
config: SubagentsConfig,
|
|
201
|
+
): Promise<
|
|
202
|
+
| {
|
|
203
|
+
name: string;
|
|
204
|
+
model: string;
|
|
205
|
+
strength: ThinkingLevel | typeof AUTO_THINKING;
|
|
206
|
+
}
|
|
207
|
+
| undefined
|
|
208
|
+
> {
|
|
209
|
+
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
210
|
+
if (name === undefined) return undefined;
|
|
211
|
+
const modelChoice = await pickAgentModel(
|
|
212
|
+
ctx,
|
|
213
|
+
name,
|
|
214
|
+
config.agentModels[name],
|
|
215
|
+
"stops — earlier agent changes are kept",
|
|
216
|
+
);
|
|
217
|
+
if (modelChoice === undefined) return undefined;
|
|
218
|
+
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
219
|
+
const strength = await pickAgentStrength(
|
|
220
|
+
ctx,
|
|
221
|
+
name,
|
|
222
|
+
model,
|
|
223
|
+
config.agentThinkingLevels[name],
|
|
224
|
+
actualAgentThinkingDefault(ctx, config, name),
|
|
225
|
+
"stops — earlier agent changes are kept",
|
|
226
|
+
);
|
|
227
|
+
if (strength === undefined) return undefined;
|
|
228
|
+
return { name, model: modelChoice, strength };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
|
|
232
|
+
const on = "On — inject the delegation directive (recommended)";
|
|
233
|
+
const off = "Off — rely on tool descriptions only";
|
|
234
|
+
const choice = await ctx.ui.select("Proactive dispatch injection?", [current ? `${on} (current)` : on, current ? off : `${off} (current)`]);
|
|
235
|
+
if (choice === undefined) return undefined;
|
|
236
|
+
return choice.startsWith("On");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
240
|
+
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
241
|
+
const IDLE_TIMEOUT_STEPS = [0, 30, 60, 90, 120, 180, 300, 600];
|
|
242
|
+
|
|
243
|
+
async function pickCount(
|
|
244
|
+
ctx: ExtensionCommandContext,
|
|
245
|
+
title: string,
|
|
246
|
+
steps: readonly number[],
|
|
247
|
+
current: number,
|
|
248
|
+
defaultValue: number,
|
|
249
|
+
): Promise<number | undefined> {
|
|
250
|
+
const values = [...new Set([...steps, current])].sort((a, b) => a - b);
|
|
251
|
+
const options = values.map((value) => {
|
|
252
|
+
const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join(", ");
|
|
255
|
+
return tags ? `${value} (${tags})` : String(value);
|
|
256
|
+
});
|
|
257
|
+
const choice = await ctx.ui.select(title, options);
|
|
258
|
+
return choice === undefined ? undefined : Number.parseInt(choice, 10);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
|
|
262
|
+
const labels: Record<AgentScope, string> = {
|
|
263
|
+
user: "user — built-in + ~/.pi/agent/agents (default)",
|
|
264
|
+
project: "project — built-in + nearest .pi/agents only",
|
|
265
|
+
both: "both — user agents, overridden by project agents",
|
|
266
|
+
};
|
|
267
|
+
const options = AGENT_SCOPE_VALUES.map((scope) =>
|
|
268
|
+
scope === current ? `${labels[scope]} (current)` : labels[scope],
|
|
269
|
+
);
|
|
270
|
+
const choice = await ctx.ui.select("Which agent directories to discover from?", options);
|
|
271
|
+
if (choice === undefined) return undefined;
|
|
272
|
+
return AGENT_SCOPE_VALUES.find((scope) => choice.startsWith(scope));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
|
|
276
|
+
const keep = new Set(enabled);
|
|
277
|
+
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<void> {
|
|
281
|
+
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
282
|
+
if (enabled === undefined) return notifyCancelled(ctx);
|
|
283
|
+
|
|
284
|
+
let agentModels = keepAgentEntries(base.agentModels, enabled);
|
|
285
|
+
for (const agentName of enabled) {
|
|
286
|
+
const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
|
|
287
|
+
if (choice === undefined) return notifyCancelled(ctx);
|
|
288
|
+
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
292
|
+
if (injection === undefined) return notifyCancelled(ctx);
|
|
293
|
+
const scope = await pickScope(ctx, base.agentScope);
|
|
294
|
+
if (scope === undefined) return notifyCancelled(ctx);
|
|
295
|
+
const maxConcurrency = await pickCount(
|
|
296
|
+
ctx,
|
|
297
|
+
"Max sub-agents running at once?",
|
|
298
|
+
CONCURRENCY_STEPS,
|
|
299
|
+
base.maxConcurrency,
|
|
300
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
301
|
+
);
|
|
302
|
+
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
303
|
+
const maxFixRounds = await pickCount(
|
|
304
|
+
ctx,
|
|
305
|
+
"Reviewer auto-fix rounds? (0 = main agent handles fixes)",
|
|
306
|
+
FIX_ROUNDS_STEPS,
|
|
307
|
+
base.maxFixRounds,
|
|
308
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
309
|
+
);
|
|
310
|
+
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
311
|
+
const idleTimeoutSec = await pickCount(
|
|
312
|
+
ctx,
|
|
313
|
+
"Idle timeout in seconds? (0 = disabled)",
|
|
314
|
+
IDLE_TIMEOUT_STEPS,
|
|
315
|
+
base.idleTimeoutSec,
|
|
316
|
+
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
317
|
+
);
|
|
318
|
+
if (idleTimeoutSec === undefined) return notifyCancelled(ctx);
|
|
319
|
+
|
|
320
|
+
const next: SubagentsConfig = {
|
|
321
|
+
enabledAgents: enabled,
|
|
322
|
+
agentModels,
|
|
323
|
+
// Full setup returns every agent to capability-aware Auto thinking.
|
|
324
|
+
agentThinkingLevels: {},
|
|
325
|
+
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
326
|
+
maxResultLines: base.maxResultLines,
|
|
327
|
+
proactiveInjection: injection,
|
|
328
|
+
agentScope: scope,
|
|
329
|
+
maxConcurrency,
|
|
330
|
+
maxFixRounds,
|
|
331
|
+
idleTimeoutSec,
|
|
332
|
+
announcedFeatures: base.announcedFeatures,
|
|
333
|
+
};
|
|
334
|
+
await saveConfig(next, configPath);
|
|
335
|
+
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function updateRuntimeSetting(
|
|
339
|
+
ctx: ExtensionCommandContext,
|
|
340
|
+
config: SubagentsConfig,
|
|
341
|
+
): Promise<SubagentsConfig | undefined> {
|
|
342
|
+
const choice = await ctx.ui.select("Runtime setting", [
|
|
343
|
+
"Proactive injection",
|
|
344
|
+
"Agent scope",
|
|
345
|
+
"Max concurrency",
|
|
346
|
+
"Reviewer auto-fix rounds",
|
|
347
|
+
"Idle timeout",
|
|
348
|
+
]);
|
|
349
|
+
if (choice === undefined) return undefined;
|
|
350
|
+
const next = { ...config };
|
|
351
|
+
if (choice.startsWith("Proactive")) {
|
|
352
|
+
const value = await pickInjection(ctx, config.proactiveInjection);
|
|
353
|
+
if (value === undefined) return undefined;
|
|
354
|
+
next.proactiveInjection = value;
|
|
355
|
+
} else if (choice.startsWith("Agent scope")) {
|
|
356
|
+
const value = await pickScope(ctx, config.agentScope);
|
|
357
|
+
if (value === undefined) return undefined;
|
|
358
|
+
next.agentScope = value;
|
|
359
|
+
} else if (choice.startsWith("Max concurrency")) {
|
|
360
|
+
const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
|
|
361
|
+
if (value === undefined) return undefined;
|
|
362
|
+
next.maxConcurrency = value;
|
|
363
|
+
} else if (choice.startsWith("Reviewer")) {
|
|
364
|
+
const value = await pickCount(ctx, "Reviewer auto-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
|
|
365
|
+
if (value === undefined) return undefined;
|
|
366
|
+
next.maxFixRounds = value;
|
|
367
|
+
} else {
|
|
368
|
+
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
369
|
+
if (value === undefined) return undefined;
|
|
370
|
+
next.idleTimeoutSec = value;
|
|
371
|
+
}
|
|
372
|
+
return next;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
376
|
+
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
377
|
+
"Enable/disable agents",
|
|
378
|
+
"Configure an agent (model + thinking)",
|
|
379
|
+
"Runtime settings",
|
|
380
|
+
"Full re-setup",
|
|
381
|
+
]);
|
|
382
|
+
if (choice === undefined) return notifyCancelled(ctx);
|
|
383
|
+
if (choice.startsWith("Full")) return runFullSetup(ctx, configPath, config);
|
|
384
|
+
|
|
385
|
+
let next: SubagentsConfig = {
|
|
386
|
+
...config,
|
|
387
|
+
agentModels: { ...config.agentModels },
|
|
388
|
+
agentThinkingLevels: { ...config.agentThinkingLevels },
|
|
389
|
+
};
|
|
390
|
+
if (choice.startsWith("Enable")) {
|
|
391
|
+
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
392
|
+
if (enabled === undefined) return notifyCancelled(ctx);
|
|
393
|
+
next.enabledAgents = enabled;
|
|
394
|
+
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
395
|
+
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
396
|
+
} else if (choice.startsWith("Configure")) {
|
|
397
|
+
// Per-agent loop: model (+ thinking when the model exposes a choice), then
|
|
398
|
+
// back to the agent picker so several agents can be set in one pass. Esc
|
|
399
|
+
// at any step ends the loop; agents already configured in this pass are kept.
|
|
400
|
+
let configuredAny = false;
|
|
401
|
+
while (true) {
|
|
402
|
+
const picked = await configureOneAgent(ctx, next);
|
|
403
|
+
if (picked === undefined) break;
|
|
404
|
+
configuredAny = true;
|
|
405
|
+
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
406
|
+
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
407
|
+
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
408
|
+
}
|
|
409
|
+
if (!configuredAny) return notifyCancelled(ctx);
|
|
410
|
+
} else {
|
|
411
|
+
const updated = await updateRuntimeSetting(ctx, next);
|
|
412
|
+
if (updated === undefined) return notifyCancelled(ctx);
|
|
413
|
+
next = updated;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await saveConfig(next, configPath);
|
|
417
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function notifyCancelled(ctx: ExtensionCommandContext): void {
|
|
421
|
+
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Entry point for the /subagents-setup command. */
|
|
425
|
+
export async function runSetup(ctx: ExtensionCommandContext, configPath: string = getConfigPath()): Promise<void> {
|
|
426
|
+
if (ctx.mode !== "tui") {
|
|
427
|
+
ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const exists = await configExists(configPath);
|
|
432
|
+
const config = await loadConfig(configPath);
|
|
433
|
+
if (exists) await runMenu(ctx, configPath, config);
|
|
434
|
+
else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] });
|
|
435
|
+
} catch (error) {
|
|
436
|
+
ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
|
|
437
|
+
}
|
|
438
|
+
}
|