@ferris1225/pi-subagents 4.1.1 → 4.1.3
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 +461 -381
- package/agents/cleaner.md +16 -6
- package/agents/documenter.md +46 -0
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +8 -4
- package/agents/worker.md +9 -4
- package/package.json +55 -55
- package/src/agents.ts +53 -0
- package/src/announcements.ts +18 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +43 -13
- package/src/dispatch.ts +233 -303
- package/src/fixloop.ts +259 -62
- package/src/index.ts +3 -3
- package/src/models.ts +189 -189
- package/src/monitor.ts +101 -22
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +29 -10
- package/src/runtime.ts +13 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +162 -130
- package/src/spawn.ts +53 -13
- package/src/thread-lifecycle.ts +240 -54
- package/src/tools.ts +65 -37
- package/src/widget.ts +68 -22
- package/src/worktree.ts +27 -4
package/src/session-fork.ts
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
|
|
2
|
-
|
|
3
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface ForkedSession {
|
|
10
|
-
sessionDir: string;
|
|
11
|
-
sessionId: string;
|
|
12
|
-
sessionFile: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Locate one retained session by its authoritative header id. */
|
|
16
|
-
export async function findRetainedSessionFile(
|
|
17
|
-
sessionDir: string,
|
|
18
|
-
sessionId: string,
|
|
19
|
-
): Promise<string> {
|
|
20
|
-
// The retained header may point at a worktree that has since been removed.
|
|
21
|
-
// The session id is authoritative inside this explicit private directory;
|
|
22
|
-
// listing the directory directly avoids a stale-cwd filter rejecting it.
|
|
23
|
-
const sessions = await SessionManager.listAll(sessionDir);
|
|
24
|
-
const matches = sessions.filter((session) => session.id === sessionId);
|
|
25
|
-
if (matches.length === 0) {
|
|
26
|
-
throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
|
|
27
|
-
}
|
|
28
|
-
if (matches.length > 1) {
|
|
29
|
-
throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
|
|
30
|
-
}
|
|
31
|
-
return matches[0].path;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Copy only the source file's active branch into a new isolated temp session
|
|
36
|
-
* directory. SessionManager performs all JSONL/tree handling; source state is
|
|
37
|
-
* never mutated.
|
|
38
|
-
*/
|
|
39
|
-
export async function forkRetainedSession(options: {
|
|
40
|
-
/** Cwd stored in the source session header (used for exact lookup). */
|
|
41
|
-
cwd: string;
|
|
42
|
-
/** Optional cwd for the cloned session header and future child tools. */
|
|
43
|
-
targetCwd?: string;
|
|
44
|
-
sessionDir: string;
|
|
45
|
-
sessionId: string;
|
|
46
|
-
}): Promise<ForkedSession> {
|
|
47
|
-
const sourceSessionFile = await findRetainedSessionFile(
|
|
48
|
-
options.sessionDir,
|
|
49
|
-
options.sessionId,
|
|
50
|
-
);
|
|
51
|
-
const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
|
|
52
|
-
try {
|
|
53
|
-
// Supplying the new directory makes createBranchedSession write there.
|
|
54
|
-
// cwdOverride rewrites the cloned header so a settled isolated session can
|
|
55
|
-
// safely continue in its fresh worktree instead of a removed old path.
|
|
56
|
-
const manager = SessionManager.open(
|
|
57
|
-
sourceSessionFile,
|
|
58
|
-
sessionDir,
|
|
59
|
-
options.targetCwd ?? options.cwd,
|
|
60
|
-
);
|
|
61
|
-
const leafId = manager.getLeafId();
|
|
62
|
-
if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
|
|
63
|
-
const sessionFile = manager.createBranchedSession(leafId);
|
|
64
|
-
if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
|
|
65
|
-
// Pi defers branch files that contain no assistant response. Such a file
|
|
66
|
-
// cannot be resumed by RPC without creating a blank session, so reject
|
|
67
|
-
// rather than pretending context was preserved.
|
|
68
|
-
if (!existsSync(sessionFile)) {
|
|
69
|
-
throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
sessionDir,
|
|
73
|
-
sessionId: manager.getSessionId(),
|
|
74
|
-
sessionFile,
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
1
|
+
/** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
|
|
2
|
+
|
|
3
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export interface ForkedSession {
|
|
10
|
+
sessionDir: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
sessionFile: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Locate one retained session by its authoritative header id. */
|
|
16
|
+
export async function findRetainedSessionFile(
|
|
17
|
+
sessionDir: string,
|
|
18
|
+
sessionId: string,
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
// The retained header may point at a worktree that has since been removed.
|
|
21
|
+
// The session id is authoritative inside this explicit private directory;
|
|
22
|
+
// listing the directory directly avoids a stale-cwd filter rejecting it.
|
|
23
|
+
const sessions = await SessionManager.listAll(sessionDir);
|
|
24
|
+
const matches = sessions.filter((session) => session.id === sessionId);
|
|
25
|
+
if (matches.length === 0) {
|
|
26
|
+
throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
|
|
27
|
+
}
|
|
28
|
+
if (matches.length > 1) {
|
|
29
|
+
throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
|
|
30
|
+
}
|
|
31
|
+
return matches[0].path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Copy only the source file's active branch into a new isolated temp session
|
|
36
|
+
* directory. SessionManager performs all JSONL/tree handling; source state is
|
|
37
|
+
* never mutated.
|
|
38
|
+
*/
|
|
39
|
+
export async function forkRetainedSession(options: {
|
|
40
|
+
/** Cwd stored in the source session header (used for exact lookup). */
|
|
41
|
+
cwd: string;
|
|
42
|
+
/** Optional cwd for the cloned session header and future child tools. */
|
|
43
|
+
targetCwd?: string;
|
|
44
|
+
sessionDir: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
}): Promise<ForkedSession> {
|
|
47
|
+
const sourceSessionFile = await findRetainedSessionFile(
|
|
48
|
+
options.sessionDir,
|
|
49
|
+
options.sessionId,
|
|
50
|
+
);
|
|
51
|
+
const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
|
|
52
|
+
try {
|
|
53
|
+
// Supplying the new directory makes createBranchedSession write there.
|
|
54
|
+
// cwdOverride rewrites the cloned header so a settled isolated session can
|
|
55
|
+
// safely continue in its fresh worktree instead of a removed old path.
|
|
56
|
+
const manager = SessionManager.open(
|
|
57
|
+
sourceSessionFile,
|
|
58
|
+
sessionDir,
|
|
59
|
+
options.targetCwd ?? options.cwd,
|
|
60
|
+
);
|
|
61
|
+
const leafId = manager.getLeafId();
|
|
62
|
+
if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
|
|
63
|
+
const sessionFile = manager.createBranchedSession(leafId);
|
|
64
|
+
if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
|
|
65
|
+
// Pi defers branch files that contain no assistant response. Such a file
|
|
66
|
+
// cannot be resumed by RPC without creating a blank session, so reject
|
|
67
|
+
// rather than pretending context was preserved.
|
|
68
|
+
if (!existsSync(sessionFile)) {
|
|
69
|
+
throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
sessionDir,
|
|
73
|
+
sessionId: manager.getSessionId(),
|
|
74
|
+
sessionFile,
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AGENT_SCOPE_VALUES,
|
|
15
15
|
BUILTIN_AGENT_NAMES,
|
|
16
16
|
CLEANER_DEFAULTED_FEATURE,
|
|
17
|
+
DOCUMENTER_DEFAULTED_FEATURE,
|
|
17
18
|
DEFAULT_CONFIG,
|
|
18
19
|
DEFAULT_ENABLED_AGENTS,
|
|
19
20
|
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
@@ -61,7 +62,8 @@ function actualAgentThinkingDefault(
|
|
|
61
62
|
const MODULE_HINTS: Record<string, string> = {
|
|
62
63
|
explorer: "read-only codebase recon (fast model)",
|
|
63
64
|
worker: "implement / fix / refactor / test (full tools)",
|
|
64
|
-
cleaner: "
|
|
65
|
+
cleaner: "apply proven cleanup and deduplicate code (full tools)",
|
|
66
|
+
documenter: "sync diff or whole-codebase comments/docs (docs write)",
|
|
65
67
|
reviewer: "read-only audits and pre-commit gates",
|
|
66
68
|
};
|
|
67
69
|
|
|
@@ -87,7 +89,7 @@ async function pickEnabledAgents(
|
|
|
87
89
|
return promptSelectMany(
|
|
88
90
|
ctx,
|
|
89
91
|
"Enable which sub-agents?",
|
|
90
|
-
"Space toggles • Enter confirms • Esc
|
|
92
|
+
"Space toggles • Enter confirms • Esc returns to settings",
|
|
91
93
|
items,
|
|
92
94
|
current,
|
|
93
95
|
);
|
|
@@ -118,7 +120,7 @@ async function pickAgentModel(
|
|
|
118
120
|
ctx: ExtensionCommandContext,
|
|
119
121
|
agentName: string,
|
|
120
122
|
currentRef: string | undefined,
|
|
121
|
-
escNote = "cancels setup",
|
|
123
|
+
escNote = "cancels this setup pass",
|
|
122
124
|
): Promise<string | undefined> {
|
|
123
125
|
return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
|
|
124
126
|
}
|
|
@@ -149,7 +151,7 @@ async function pickAgentStrength(
|
|
|
149
151
|
model: Model<Api> | undefined,
|
|
150
152
|
current: ThinkingLevel | undefined,
|
|
151
153
|
agentDefault: ThinkingLevel,
|
|
152
|
-
escNote = "cancels setup",
|
|
154
|
+
escNote = "cancels this setup pass",
|
|
153
155
|
): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
|
|
154
156
|
const supported = supportedThinkingLevels(model);
|
|
155
157
|
const automatic = resolveThinkingLevel(model, agentDefault);
|
|
@@ -189,44 +191,48 @@ async function pickAgentToConfigure(
|
|
|
189
191
|
return promptSelectOne(
|
|
190
192
|
ctx,
|
|
191
193
|
"Configure which agent?",
|
|
192
|
-
"Type to filter • ↑/↓ • Enter selects • Esc
|
|
194
|
+
"Type to filter • ↑/↓ • Enter selects • Esc returns to settings",
|
|
193
195
|
enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
|
|
194
196
|
);
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
interface ConfiguredAgentChoice {
|
|
200
|
+
name: string;
|
|
201
|
+
model: string;
|
|
202
|
+
strength: ThinkingLevel | typeof AUTO_THINKING;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Configure one agent while preserving the UI back stack: thinking → model →
|
|
206
|
+
* agent selection. Esc from agent selection ends this configuration pass. */
|
|
199
207
|
async function configureOneAgent(
|
|
200
208
|
ctx: ExtensionCommandContext,
|
|
201
209
|
config: SubagentsConfig,
|
|
202
|
-
): Promise<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (strength === undefined) return undefined;
|
|
229
|
-
return { name, model: modelChoice, strength };
|
|
210
|
+
): Promise<ConfiguredAgentChoice | undefined> {
|
|
211
|
+
while (true) {
|
|
212
|
+
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
213
|
+
if (name === undefined) return undefined;
|
|
214
|
+
|
|
215
|
+
while (true) {
|
|
216
|
+
const modelChoice = await pickAgentModel(
|
|
217
|
+
ctx,
|
|
218
|
+
name,
|
|
219
|
+
config.agentModels[name],
|
|
220
|
+
"returns to agent selection",
|
|
221
|
+
);
|
|
222
|
+
if (modelChoice === undefined) break;
|
|
223
|
+
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
224
|
+
const strength = await pickAgentStrength(
|
|
225
|
+
ctx,
|
|
226
|
+
name,
|
|
227
|
+
model,
|
|
228
|
+
config.agentThinkingLevels[name],
|
|
229
|
+
actualAgentThinkingDefault(ctx, config, name),
|
|
230
|
+
"returns to model selection",
|
|
231
|
+
);
|
|
232
|
+
if (strength === undefined) continue;
|
|
233
|
+
return { name, model: modelChoice, strength };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
|
|
@@ -278,21 +284,21 @@ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string
|
|
|
278
284
|
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
279
285
|
}
|
|
280
286
|
|
|
281
|
-
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<
|
|
287
|
+
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
|
|
282
288
|
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
283
|
-
if (enabled === undefined) return
|
|
289
|
+
if (enabled === undefined) return false;
|
|
284
290
|
|
|
285
291
|
let agentModels = keepAgentEntries(base.agentModels, enabled);
|
|
286
292
|
for (const agentName of enabled) {
|
|
287
293
|
const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
|
|
288
|
-
if (choice === undefined) return
|
|
294
|
+
if (choice === undefined) return false;
|
|
289
295
|
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
293
|
-
if (injection === undefined) return
|
|
299
|
+
if (injection === undefined) return false;
|
|
294
300
|
const scope = await pickScope(ctx, base.agentScope);
|
|
295
|
-
if (scope === undefined) return
|
|
301
|
+
if (scope === undefined) return false;
|
|
296
302
|
const maxConcurrency = await pickCount(
|
|
297
303
|
ctx,
|
|
298
304
|
"Max sub-agents running at once?",
|
|
@@ -300,15 +306,15 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
300
306
|
base.maxConcurrency,
|
|
301
307
|
DEFAULT_MAX_CONCURRENCY,
|
|
302
308
|
);
|
|
303
|
-
if (maxConcurrency === undefined) return
|
|
309
|
+
if (maxConcurrency === undefined) return false;
|
|
304
310
|
const maxFixRounds = await pickCount(
|
|
305
311
|
ctx,
|
|
306
|
-
"Reviewer
|
|
312
|
+
"Reviewer worker-fix rounds? (0 = no automatic fixes)",
|
|
307
313
|
FIX_ROUNDS_STEPS,
|
|
308
314
|
base.maxFixRounds,
|
|
309
315
|
DEFAULT_MAX_FIX_ROUNDS,
|
|
310
316
|
);
|
|
311
|
-
if (maxFixRounds === undefined) return
|
|
317
|
+
if (maxFixRounds === undefined) return false;
|
|
312
318
|
const idleTimeoutSec = await pickCount(
|
|
313
319
|
ctx,
|
|
314
320
|
"Idle timeout in seconds? (0 = disabled)",
|
|
@@ -316,7 +322,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
316
322
|
base.idleTimeoutSec,
|
|
317
323
|
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
318
324
|
);
|
|
319
|
-
if (idleTimeoutSec === undefined) return
|
|
325
|
+
if (idleTimeoutSec === undefined) return false;
|
|
320
326
|
|
|
321
327
|
const next: SubagentsConfig = {
|
|
322
328
|
enabledAgents: enabled,
|
|
@@ -330,109 +336,133 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
330
336
|
maxConcurrency,
|
|
331
337
|
maxFixRounds,
|
|
332
338
|
idleTimeoutSec,
|
|
333
|
-
// Full setup is an explicit decision point: mark
|
|
334
|
-
//
|
|
335
|
-
announcedFeatures: [...new Set([
|
|
339
|
+
// Full setup is an explicit decision point: mark role-enable migrations as
|
|
340
|
+
// processed so the user's saved selection is kept as-is.
|
|
341
|
+
announcedFeatures: [...new Set([
|
|
342
|
+
...base.announcedFeatures,
|
|
343
|
+
CLEANER_DEFAULTED_FEATURE,
|
|
344
|
+
DOCUMENTER_DEFAULTED_FEATURE,
|
|
345
|
+
])],
|
|
336
346
|
};
|
|
337
347
|
await saveConfig(next, configPath);
|
|
338
348
|
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
349
|
+
return true;
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
async function updateRuntimeSetting(
|
|
342
353
|
ctx: ExtensionCommandContext,
|
|
343
354
|
config: SubagentsConfig,
|
|
344
355
|
): Promise<SubagentsConfig | undefined> {
|
|
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
|
-
|
|
356
|
+
while (true) {
|
|
357
|
+
const choice = await ctx.ui.select("Runtime setting", [
|
|
358
|
+
"Proactive injection",
|
|
359
|
+
"Agent scope",
|
|
360
|
+
"Max concurrency",
|
|
361
|
+
"Reviewer worker-fix rounds",
|
|
362
|
+
"Idle timeout",
|
|
363
|
+
]);
|
|
364
|
+
if (choice === undefined) return undefined;
|
|
365
|
+
const next = { ...config };
|
|
366
|
+
if (choice.startsWith("Proactive")) {
|
|
367
|
+
const value = await pickInjection(ctx, config.proactiveInjection);
|
|
368
|
+
if (value === undefined) continue;
|
|
369
|
+
next.proactiveInjection = value;
|
|
370
|
+
} else if (choice.startsWith("Agent scope")) {
|
|
371
|
+
const value = await pickScope(ctx, config.agentScope);
|
|
372
|
+
if (value === undefined) continue;
|
|
373
|
+
next.agentScope = value;
|
|
374
|
+
} else if (choice.startsWith("Max concurrency")) {
|
|
375
|
+
const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
|
|
376
|
+
if (value === undefined) continue;
|
|
377
|
+
next.maxConcurrency = value;
|
|
378
|
+
} else if (choice.startsWith("Reviewer")) {
|
|
379
|
+
const value = await pickCount(ctx, "Reviewer worker-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
|
|
380
|
+
if (value === undefined) continue;
|
|
381
|
+
next.maxFixRounds = value;
|
|
382
|
+
} else {
|
|
383
|
+
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
384
|
+
if (value === undefined) continue;
|
|
385
|
+
next.idleTimeoutSec = value;
|
|
386
|
+
}
|
|
387
|
+
return next;
|
|
374
388
|
}
|
|
375
|
-
return next;
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
379
|
-
|
|
380
|
-
"
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
392
|
+
while (true) {
|
|
393
|
+
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
394
|
+
"Enable/disable agents",
|
|
395
|
+
"Configure an agent (model + thinking)",
|
|
396
|
+
"Runtime settings",
|
|
397
|
+
"Full re-setup",
|
|
398
|
+
]);
|
|
399
|
+
if (choice === undefined) return;
|
|
400
|
+
if (choice.startsWith("Full")) {
|
|
401
|
+
if (await runFullSetup(ctx, configPath, config)) return;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
387
404
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
405
|
+
let next: SubagentsConfig = {
|
|
406
|
+
...config,
|
|
407
|
+
agentModels: { ...config.agentModels },
|
|
408
|
+
agentThinkingLevels: { ...config.agentThinkingLevels },
|
|
409
|
+
};
|
|
410
|
+
if (choice.startsWith("Enable")) {
|
|
411
|
+
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
412
|
+
if (enabled === undefined) continue;
|
|
413
|
+
next.enabledAgents = enabled;
|
|
414
|
+
// Newly enabling cleaner inherits the reviewer's configured model and
|
|
415
|
+
// thinking level, so the file reflects what cleaner will actually run
|
|
416
|
+
// instead of silently falling back to the current main model.
|
|
417
|
+
if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
|
|
418
|
+
if (!next.agentModels.cleaner && config.agentModels.reviewer) {
|
|
419
|
+
next.agentModels.cleaner = config.agentModels.reviewer;
|
|
420
|
+
}
|
|
421
|
+
if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
|
|
422
|
+
next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
|
|
423
|
+
}
|
|
403
424
|
}
|
|
404
|
-
|
|
405
|
-
|
|
425
|
+
// Documenter intentionally follows the faster explorer route. Fresh
|
|
426
|
+
// installs leave it unselected; enabling it later inherits any explorer
|
|
427
|
+
// overrides instead of silently choosing a stronger model.
|
|
428
|
+
if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
|
|
429
|
+
if (!next.agentModels.documenter && config.agentModels.explorer) {
|
|
430
|
+
next.agentModels.documenter = config.agentModels.explorer;
|
|
431
|
+
}
|
|
432
|
+
if (!next.agentThinkingLevels.documenter && config.agentThinkingLevels.explorer) {
|
|
433
|
+
next.agentThinkingLevels.documenter = config.agentThinkingLevels.explorer;
|
|
434
|
+
}
|
|
406
435
|
}
|
|
436
|
+
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
437
|
+
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
438
|
+
} else if (choice.startsWith("Configure")) {
|
|
439
|
+
// Per-agent loop: thinking Esc returns to that agent's model picker;
|
|
440
|
+
// model Esc returns to the agent picker; agent-picker Esc saves completed
|
|
441
|
+
// choices and returns to this settings menu.
|
|
442
|
+
let configuredAny = false;
|
|
443
|
+
while (true) {
|
|
444
|
+
const picked = await configureOneAgent(ctx, next);
|
|
445
|
+
if (!picked) break;
|
|
446
|
+
configuredAny = true;
|
|
447
|
+
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
448
|
+
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
449
|
+
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
450
|
+
}
|
|
451
|
+
if (!configuredAny) continue;
|
|
452
|
+
await saveConfig(next, configPath);
|
|
453
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
454
|
+
config = next;
|
|
455
|
+
continue;
|
|
456
|
+
} else {
|
|
457
|
+
const updated = await updateRuntimeSetting(ctx, next);
|
|
458
|
+
if (updated === undefined) continue;
|
|
459
|
+
next = updated;
|
|
407
460
|
}
|
|
408
|
-
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
409
|
-
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
410
|
-
} else if (choice.startsWith("Configure")) {
|
|
411
|
-
// Per-agent loop: model (+ thinking when the model exposes a choice), then
|
|
412
|
-
// back to the agent picker so several agents can be set in one pass. Esc
|
|
413
|
-
// at any step ends the loop; agents already configured in this pass are kept.
|
|
414
|
-
let configuredAny = false;
|
|
415
|
-
while (true) {
|
|
416
|
-
const picked = await configureOneAgent(ctx, next);
|
|
417
|
-
if (picked === undefined) break;
|
|
418
|
-
configuredAny = true;
|
|
419
|
-
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
420
|
-
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
421
|
-
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
422
|
-
}
|
|
423
|
-
if (!configuredAny) return notifyCancelled(ctx);
|
|
424
|
-
} else {
|
|
425
|
-
const updated = await updateRuntimeSetting(ctx, next);
|
|
426
|
-
if (updated === undefined) return notifyCancelled(ctx);
|
|
427
|
-
next = updated;
|
|
428
|
-
}
|
|
429
461
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
function notifyCancelled(ctx: ExtensionCommandContext): void {
|
|
435
|
-
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
462
|
+
await saveConfig(next, configPath);
|
|
463
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
436
466
|
}
|
|
437
467
|
|
|
438
468
|
/** Entry point for the /subagents-setup command. */
|
|
@@ -445,7 +475,9 @@ export async function runSetup(ctx: ExtensionCommandContext, configPath: string
|
|
|
445
475
|
const exists = await configExists(configPath);
|
|
446
476
|
const config = await loadConfig(configPath);
|
|
447
477
|
if (exists) await runMenu(ctx, configPath, config);
|
|
448
|
-
else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] })
|
|
478
|
+
else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) {
|
|
479
|
+
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
480
|
+
}
|
|
449
481
|
} catch (error) {
|
|
450
482
|
ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
|
|
451
483
|
}
|