@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.
@@ -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: "prove and apply safe cleanup cuts (full tools)",
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 cancels",
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 ends this pass",
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
- /** One agent: model, then thinking if the model exposes a choice. Esc at any
198
- * step ends the caller's pass; earlier agents in that pass stay applied. */
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
- name: string;
205
- model: string;
206
- strength: ThinkingLevel | typeof AUTO_THINKING;
207
- }
208
- | undefined
209
- > {
210
- const name = await pickAgentToConfigure(ctx, config.enabledAgents);
211
- if (name === undefined) return undefined;
212
- const modelChoice = await pickAgentModel(
213
- ctx,
214
- name,
215
- config.agentModels[name],
216
- "stops earlier agent changes are kept",
217
- );
218
- if (modelChoice === undefined) return undefined;
219
- const model = effectiveModelForChoice(ctx, modelChoice);
220
- const strength = await pickAgentStrength(
221
- ctx,
222
- name,
223
- model,
224
- config.agentThinkingLevels[name],
225
- actualAgentThinkingDefault(ctx, config, name),
226
- "stops — earlier agent changes are kept",
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<void> {
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 notifyCancelled(ctx);
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 notifyCancelled(ctx);
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 notifyCancelled(ctx);
299
+ if (injection === undefined) return false;
294
300
  const scope = await pickScope(ctx, base.agentScope);
295
- if (scope === undefined) return notifyCancelled(ctx);
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 notifyCancelled(ctx);
309
+ if (maxConcurrency === undefined) return false;
304
310
  const maxFixRounds = await pickCount(
305
311
  ctx,
306
- "Reviewer auto-fix rounds? (0 = main agent handles fixes)",
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 notifyCancelled(ctx);
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 notifyCancelled(ctx);
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 the cleaner
334
- // default-enable upgrade as processed so the saved list is kept as-is.
335
- announcedFeatures: [...new Set([...base.announcedFeatures, CLEANER_DEFAULTED_FEATURE])],
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
- const choice = await ctx.ui.select("Runtime setting", [
346
- "Proactive injection",
347
- "Agent scope",
348
- "Max concurrency",
349
- "Reviewer auto-fix rounds",
350
- "Idle timeout",
351
- ]);
352
- if (choice === undefined) return undefined;
353
- const next = { ...config };
354
- if (choice.startsWith("Proactive")) {
355
- const value = await pickInjection(ctx, config.proactiveInjection);
356
- if (value === undefined) return undefined;
357
- next.proactiveInjection = value;
358
- } else if (choice.startsWith("Agent scope")) {
359
- const value = await pickScope(ctx, config.agentScope);
360
- if (value === undefined) return undefined;
361
- next.agentScope = value;
362
- } else if (choice.startsWith("Max concurrency")) {
363
- const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
364
- if (value === undefined) return undefined;
365
- next.maxConcurrency = value;
366
- } else if (choice.startsWith("Reviewer")) {
367
- const value = await pickCount(ctx, "Reviewer auto-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
368
- if (value === undefined) return undefined;
369
- next.maxFixRounds = value;
370
- } else {
371
- const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
372
- if (value === undefined) return undefined;
373
- next.idleTimeoutSec = value;
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
- const choice = await ctx.ui.select("pi-subagents settings", [
380
- "Enable/disable agents",
381
- "Configure an agent (model + thinking)",
382
- "Runtime settings",
383
- "Full re-setup",
384
- ]);
385
- if (choice === undefined) return notifyCancelled(ctx);
386
- if (choice.startsWith("Full")) return runFullSetup(ctx, configPath, config);
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
- let next: SubagentsConfig = {
389
- ...config,
390
- agentModels: { ...config.agentModels },
391
- agentThinkingLevels: { ...config.agentThinkingLevels },
392
- };
393
- if (choice.startsWith("Enable")) {
394
- const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
395
- if (enabled === undefined) return notifyCancelled(ctx);
396
- next.enabledAgents = enabled;
397
- // Newly enabling cleaner inherits the reviewer's configured model and
398
- // thinking level, so the file reflects what cleaner will actually run
399
- // instead of silently falling back to the current main model.
400
- if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
401
- if (!next.agentModels.cleaner && config.agentModels.reviewer) {
402
- next.agentModels.cleaner = config.agentModels.reviewer;
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
- if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
405
- next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
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
- await saveConfig(next, configPath);
431
- ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
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
  }