@ferris1225/pi-subagents 0.29.0 → 0.32.2

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/src/setup.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Interactive configuration wizard for /subagents-setup.
3
3
  *
4
- * Everything is selection-driven (no free-text answers): a multi-select for which
5
- * agents to enable, a per-agent single-select for model overrides (fuzzy filter +
6
- * paging), and simple menus for the injection toggle and agent scope. Config is
7
- * written to <agentDir>/pi-subagents.json.
4
+ * Everything is selection-driven (no free-text answers). Enabled agents share a
5
+ * compact primary/backup model-pool editor backed by one searchable model list;
6
+ * the remaining settings use small selection menus. Config is written to
7
+ * <agentDir>/pi-subagents.json.
8
8
  */
9
9
 
10
10
  import { stat } from "node:fs/promises";
11
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
+ import type { ExtensionCommandContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
12
+ import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
12
13
  import {
13
14
  AGENT_SCOPE_VALUES,
14
15
  BUILTIN_AGENT_NAMES,
@@ -26,11 +27,22 @@ import {
26
27
  loadConfig,
27
28
  saveConfig,
28
29
  } from "./config.ts";
29
- import { availableModelRefs, repairUnavailableModelOverrides } from "./models.ts";
30
+ import {
31
+ CURRENT_MAIN_MODEL,
32
+ applyModelPoolChoice,
33
+ availableModelRefs,
34
+ buildAgentModelPoolRows,
35
+ buildModelPickerItems,
36
+ currentModelRef,
37
+ type AgentModelPoolMaps,
38
+ type ModelPickerSlot,
39
+ type ModelPoolSlot,
40
+ } from "./models.ts";
30
41
  import { promptSelectMany, promptSelectOne } from "./ui.ts";
31
42
  import { loadBuiltinAgents } from "./agents.ts";
32
43
 
33
44
  const INHERIT = "__inherit__";
45
+ const MODEL_POOL_WIDE_MIN = 96;
34
46
 
35
47
  /** Effective per-agent default strength from builtin frontmatter (config overrides win at spawn). */
36
48
  function builtinThinkingDefaults(): Map<string, ThinkingLevel> {
@@ -76,86 +88,193 @@ async function pickEnabledAgents(
76
88
  );
77
89
  }
78
90
 
79
- /** Single-agent model pick; the inherit option drops any existing override.
80
- * `escNote` describes what Esc does at this pick (whole-wizard cancel in the
81
- * full setup vs. ending the per-agent loop in the menu). */
82
- async function pickAgentModel(
91
+ type ModelPoolEditorResult =
92
+ | { action: "edit"; agentName: string; slot: ModelPoolSlot }
93
+ | { action: "save" };
94
+
95
+ interface ModelPoolEditorStyles {
96
+ border(text: string): string;
97
+ title(text: string): string;
98
+ dim(text: string): string;
99
+ accent(text: string): string;
100
+ selected(text: string): string;
101
+ }
102
+
103
+ /** Compact overview: every enabled agent shows Primary and Backup on one row. */
104
+ class ModelPoolEditor implements Component {
105
+ private row = 0;
106
+ private slot: ModelPoolSlot = "primary";
107
+
108
+ constructor(
109
+ private readonly agentNames: readonly string[],
110
+ private readonly pools: AgentModelPoolMaps,
111
+ private readonly styles: ModelPoolEditorStyles,
112
+ private readonly tui: TUI,
113
+ private readonly keybindings: KeybindingsManager,
114
+ private readonly done: (result: ModelPoolEditorResult | undefined) => void,
115
+ initialCell?: { agentName: string; slot: ModelPoolSlot },
116
+ ) {
117
+ const initialRow = initialCell ? agentNames.indexOf(initialCell.agentName) : -1;
118
+ if (initialCell && initialRow >= 0) {
119
+ this.row = initialRow;
120
+ this.slot = initialCell.slot;
121
+ }
122
+ }
123
+
124
+ render(width: number): string[] {
125
+ const fit = (line: string): string => truncateToWidth(line, width, "");
126
+ const border = this.styles.border("─".repeat(Math.max(1, width)));
127
+ const rows = buildAgentModelPoolRows(this.agentNames, this.pools);
128
+ const lines = [
129
+ fit(border),
130
+ fit(this.styles.title("Agent model pools")),
131
+ fit(this.styles.dim("↑/↓ agent • ←/→ Primary/Backup • Enter edit/save • Esc cancel")),
132
+ fit(border),
133
+ ];
134
+ for (let index = 0; index < rows.length; index++) {
135
+ const pool = rows[index];
136
+ const active = this.row === index;
137
+ const mark = active ? this.styles.accent("❯ ") : " ";
138
+ const primary = active && this.slot === "primary"
139
+ ? this.styles.selected(`[Primary: ${pool.primary}]`)
140
+ : `Primary: ${pool.primary}`;
141
+ const backup = active && this.slot === "backup"
142
+ ? this.styles.selected(`[Backup: ${pool.backup}]`)
143
+ : `Backup: ${pool.backup}`;
144
+ if (width >= MODEL_POOL_WIDE_MIN) {
145
+ lines.push(fit(`${mark}${this.styles.accent(pool.name)} · ${primary} · ${backup}`));
146
+ } else {
147
+ // Keep both cells visible on narrow terminals; a long primary can no
148
+ // longer push Backup (including its selected state) off-screen.
149
+ lines.push(fit(`${mark}${this.styles.accent(pool.name)}`));
150
+ lines.push(fit(` ${primary}`));
151
+ lines.push(fit(` ${backup}`));
152
+ }
153
+ }
154
+ const saveActive = this.row === rows.length;
155
+ lines.push(fit(`${saveActive ? this.styles.accent("❯ ") : " "}${saveActive ? this.styles.selected("Save model pools and continue") : "Save model pools and continue"}`));
156
+ lines.push(fit(border));
157
+ return lines;
158
+ }
159
+
160
+ handleInput(data: string): void {
161
+ const lastRow = this.agentNames.length;
162
+ if (this.keybindings.matches(data, "tui.select.up")) {
163
+ this.row = this.row === 0 ? lastRow : this.row - 1;
164
+ } else if (this.keybindings.matches(data, "tui.select.down")) {
165
+ this.row = this.row === lastRow ? 0 : this.row + 1;
166
+ } else if (
167
+ this.row < lastRow &&
168
+ (this.keybindings.matches(data, "tui.editor.cursorLeft") ||
169
+ this.keybindings.matches(data, "tui.editor.cursorRight"))
170
+ ) {
171
+ this.slot = this.slot === "primary" ? "backup" : "primary";
172
+ } else if (this.keybindings.matches(data, "tui.select.confirm")) {
173
+ if (this.row === lastRow) this.done({ action: "save" });
174
+ else this.done({ action: "edit", agentName: this.agentNames[this.row], slot: this.slot });
175
+ return;
176
+ } else if (this.keybindings.matches(data, "tui.select.cancel")) {
177
+ this.done(undefined);
178
+ return;
179
+ }
180
+ this.tui.requestRender();
181
+ }
182
+
183
+ invalidate(): void {}
184
+ }
185
+
186
+ async function promptModelPoolOverview(
83
187
  ctx: ExtensionCommandContext,
84
- name: string,
85
- currentRef: string | undefined,
86
- refs: readonly string[],
87
- escNote = "cancels setup",
88
- ): Promise<string | typeof INHERIT | undefined> {
89
- const items = [
90
- {
91
- value: INHERIT,
92
- label: currentRef
93
- ? `(use main session's model — drop override "${currentRef}")`
94
- : "(use main session's current model — no override)",
95
- },
96
- ...refs.map((ref) => ({ value: ref, label: ref === currentRef ? `${ref} (current)` : ref })),
97
- ];
98
- return promptSelectOne(
99
- ctx,
100
- `Model for "${name}"`,
101
- `Type to filter • ↑/↓ • PgUp/PgDn • Enter selects • Esc ${escNote}`,
102
- items,
188
+ agentNames: readonly string[],
189
+ pools: AgentModelPoolMaps,
190
+ initialCell?: { agentName: string; slot: ModelPoolSlot },
191
+ ): Promise<ModelPoolEditorResult | undefined> {
192
+ return ctx.ui.custom<ModelPoolEditorResult | undefined>((tui, theme, keybindings, done) =>
193
+ new ModelPoolEditor(
194
+ agentNames,
195
+ pools,
196
+ {
197
+ border: (text) => theme.fg("accent", text),
198
+ title: (text) => theme.fg("accent", theme.bold(text)),
199
+ dim: (text) => theme.fg("dim", text),
200
+ accent: (text) => theme.fg("accent", text),
201
+ selected: (text) => theme.fg("accent", theme.bold(text)),
202
+ },
203
+ tui,
204
+ keybindings,
205
+ done,
206
+ initialCell,
207
+ ),
103
208
  );
104
209
  }
105
210
 
106
- /** Vision model pick for image tasks (screenshots/mockups); the inherit option
107
- * leaves it unset, so vision-flagged dispatches fall back to the main session's
108
- * current model. */
109
- async function pickVisionModel(
211
+ async function pickConfiguredModel(
110
212
  ctx: ExtensionCommandContext,
111
- currentRef: string | undefined,
112
- refs: readonly string[],
113
- ): Promise<string | typeof INHERIT | undefined> {
114
- const items = [
115
- {
116
- value: INHERIT,
117
- label: currentRef
118
- ? `(not set — vision tasks fall back to the main session's model; drop "${currentRef}")`
119
- : "(not set — vision tasks fall back to the main session's current model)",
120
- },
121
- ...refs.map((ref) => ({ value: ref, label: ref === currentRef ? `${ref} (current)` : ref })),
122
- ];
213
+ title: string,
214
+ slot: ModelPickerSlot,
215
+ configuredRef: string | undefined,
216
+ escNote: string,
217
+ ): Promise<string | undefined> {
218
+ const registry = ctx.modelRegistry as typeof ctx.modelRegistry & { getAll?: typeof ctx.modelRegistry.getAvailable };
219
+ const models = registry.getAll?.() ?? registry.getAvailable();
220
+ const items = buildModelPickerItems({
221
+ models,
222
+ availableRefs: availableModelRefs(ctx),
223
+ slot,
224
+ configuredRef,
225
+ mainRef: currentModelRef(ctx),
226
+ });
123
227
  return promptSelectOne(
124
228
  ctx,
125
- "Vision-capable model for image tasks (screenshots, mockups, designs)?",
126
- "Type to filter ↑/↓PgUp/PgDn • Enter selects • Esc cancels setup",
229
+ title,
230
+ `Type to filter by provider, model, capability, or availability ↑/↓ • Enter selects • Esc ${escNote}`,
127
231
  items,
232
+ configuredRef ?? CURRENT_MAIN_MODEL,
128
233
  );
129
234
  }
130
235
 
131
- async function pickAgentModelsAndStrength(
236
+ /** Shared by full setup and configure-one-agent. Esc in a model list returns to
237
+ * the overview; Esc in the overview cancels the whole pool edit. */
238
+ async function editModelPools(
132
239
  ctx: ExtensionCommandContext,
133
- enabledAgents: readonly string[],
134
- currentModels: Record<string, string>,
135
- currentStrengths: Record<string, ThinkingLevel>,
136
- defaultLevel: ThinkingLevel,
137
- defaults: ReadonlyMap<string, ThinkingLevel>,
138
- ): Promise<{ models: Record<string, string>; strengths: Record<string, ThinkingLevel> } | undefined> {
139
- const refs = availableModelRefs(ctx);
140
- if (refs.length === 0) {
141
- ctx.ui.notify("No Pi models are currently available; model overrides left unchanged.", "warning");
142
- return { models: { ...currentModels }, strengths: { ...currentStrengths } };
240
+ agentNames: readonly string[],
241
+ initial: AgentModelPoolMaps,
242
+ ): Promise<AgentModelPoolMaps | undefined> {
243
+ let pools: AgentModelPoolMaps = {
244
+ agentModels: { ...initial.agentModels },
245
+ agentBackupModels: { ...initial.agentBackupModels },
246
+ };
247
+ let activeCell: { agentName: string; slot: ModelPoolSlot } | undefined;
248
+ while (true) {
249
+ const action = await promptModelPoolOverview(ctx, agentNames, pools, activeCell);
250
+ if (action === undefined) return undefined;
251
+ if (action.action === "save") return pools;
252
+ activeCell = { agentName: action.agentName, slot: action.slot };
253
+ const current = action.slot === "primary"
254
+ ? pools.agentModels[action.agentName]
255
+ : pools.agentBackupModels[action.agentName];
256
+ const choice = await pickConfiguredModel(
257
+ ctx,
258
+ `${action.slot === "primary" ? "Primary" : "Backup"} model for "${action.agentName}"`,
259
+ action.slot,
260
+ current,
261
+ "returns to model pools",
262
+ );
263
+ if (choice !== undefined) pools = applyModelPoolChoice(pools, action.agentName, action.slot, choice);
143
264
  }
265
+ }
144
266
 
145
- const models: Record<string, string> = {};
146
- const strengths: Record<string, ThinkingLevel> = {};
147
- for (const name of enabledAgents) {
148
- const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs);
149
- if (modelChoice === undefined) return undefined; // Esc aborts the whole wizard
150
- if (modelChoice !== INHERIT) models[name] = modelChoice;
151
-
152
- // Convenience: the model pick is immediately followed by the strength pick,
153
- // so per-agent model + strength are configured in one pass.
154
- const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults);
155
- if (strength === undefined) return undefined; // Esc aborts the whole wizard
156
- if (strength !== INHERIT) strengths[name] = strength;
157
- }
158
- return { models, strengths };
267
+ async function pickVisionModel(
268
+ ctx: ExtensionCommandContext,
269
+ currentRef: string | undefined,
270
+ ): Promise<string | undefined> {
271
+ return pickConfiguredModel(
272
+ ctx,
273
+ "Vision primary for image tasks (then agent backup → current main model)",
274
+ "vision",
275
+ currentRef,
276
+ "cancels setup",
277
+ );
159
278
  }
160
279
 
161
280
  const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
@@ -215,48 +334,37 @@ async function pickAgentToConfigure(
215
334
  );
216
335
  }
217
336
 
218
- /**
219
- * Configure a single agent picked from the enabled set: choose its model, then
220
- * its thinking strength. Only that one agent is touched, so re-running the menu
221
- * to tweak one agent no longer walks every enabled agent. Both picks offer an
222
- * "inherit" option that drops any existing per-agent override for that field.
223
- * Resolves undefined when the user presses Esc at any step; the caller keeps
224
- * changes from agents already configured earlier in the same pass.
225
- */
337
+ /** Configure one selected agent with the same pool overview/picker used by
338
+ * full setup, then retain the existing focused thinking-strength picker. */
226
339
  async function configureOneAgent(
227
340
  ctx: ExtensionCommandContext,
228
341
  enabledAgents: readonly string[],
229
- currentModels: Record<string, string>,
342
+ currentPools: AgentModelPoolMaps,
230
343
  currentStrengths: Record<string, ThinkingLevel>,
231
344
  defaultLevel: ThinkingLevel,
232
345
  defaults: ReadonlyMap<string, ThinkingLevel>,
233
346
  ): Promise<
234
347
  | {
235
348
  name: string;
236
- model: string | typeof INHERIT;
237
- modelsAvailable: boolean;
349
+ pools: AgentModelPoolMaps;
238
350
  strength: ThinkingLevel | typeof INHERIT;
239
351
  }
240
352
  | undefined
241
353
  > {
242
354
  const name = await pickAgentToConfigure(ctx, enabledAgents);
243
- if (name === undefined) return undefined; // Esc cancels
244
-
245
- const refs = availableModelRefs(ctx);
246
- const modelsAvailable = refs.length > 0;
247
- let model: string | typeof INHERIT = INHERIT;
248
- if (!modelsAvailable) {
249
- ctx.ui.notify("No Pi models are currently available; model override left unchanged.", "warning");
250
- } else {
251
- const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs, "stops — earlier agent picks are kept");
252
- if (modelChoice === undefined) return undefined; // Esc ends the loop
253
- model = modelChoice;
254
- }
255
-
256
- const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults, "stops — earlier agent picks are kept");
257
- if (strength === undefined) return undefined; // Esc ends the loop
258
-
259
- return { name, model, modelsAvailable, strength };
355
+ if (name === undefined) return undefined;
356
+ const pools = await editModelPools(ctx, [name], currentPools);
357
+ if (pools === undefined) return undefined;
358
+ const strength = await pickAgentStrength(
359
+ ctx,
360
+ name,
361
+ currentStrengths[name],
362
+ defaultLevel,
363
+ defaults,
364
+ "stops earlier agent changes are kept",
365
+ );
366
+ if (strength === undefined) return undefined;
367
+ return { name, pools, strength };
260
368
  }
261
369
 
262
370
  async function pickThinkingLevel(
@@ -320,60 +428,42 @@ async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Pro
320
428
  return scope;
321
429
  }
322
430
 
323
- /** Replace unavailable overrides with a model usable by the current main window. */
324
- function repairStaleModels(ctx: ExtensionCommandContext, agentModels: Record<string, string>): Record<string, string> {
325
- const repair = repairUnavailableModelOverrides(ctx, agentModels);
326
- if (repair.changed) {
327
- const detail = repair.fallbackRef
328
- ? `Switched ${repair.replaced} unavailable model override(s) to ${repair.fallbackRef}.`
329
- : `Removed ${repair.removed} unavailable model override(s); no model is currently available.`;
330
- ctx.ui.notify(detail, "warning");
331
- }
332
- return repair.agentModels;
333
- }
334
-
335
- async function repairConfigModels(
336
- ctx: ExtensionCommandContext,
337
- configPath: string,
338
- config: SubagentsConfig,
339
- ): Promise<SubagentsConfig> {
340
- const repairedModels = repairUnavailableModelOverrides(ctx, config.agentModels);
341
- if (!repairedModels.changed) return config;
342
-
343
- const repaired = { ...config, agentModels: repairedModels.agentModels };
344
- try {
345
- await saveConfig(repaired, configPath);
346
- ctx.ui.notify(`Repaired unavailable model overrides in ${configPath}.`, "warning");
347
- } catch (error) {
348
- ctx.ui.notify(`Could not persist repaired model overrides: ${errorMessage(error)}`, "warning");
349
- }
350
- return repaired;
431
+ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
432
+ const keep = new Set(enabled);
433
+ return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
351
434
  }
352
435
 
353
436
  async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<void> {
354
437
  const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
355
438
  if (enabled === undefined) return notifyCancelled(ctx);
356
439
 
357
- // Global default first, so per-agent strength picks can show "inherit" against it.
358
440
  const thinkingLevel = await pickThinkingLevel(ctx, base.thinkingLevel);
359
441
  if (thinkingLevel === undefined) return notifyCancelled(ctx);
360
442
 
443
+ const pools = await editModelPools(ctx, enabled, {
444
+ agentModels: base.agentModels,
445
+ agentBackupModels: base.agentBackupModels,
446
+ });
447
+ if (pools === undefined) return notifyCancelled(ctx);
448
+
361
449
  const defaults = builtinThinkingDefaults();
362
- const picked = await pickAgentModelsAndStrength(ctx, enabled, base.agentModels, base.agentThinkingLevels, thinkingLevel, defaults);
363
- if (picked === undefined) return notifyCancelled(ctx);
364
-
365
- let nextVisionModel: string | undefined;
366
- // No models available: keep the vision model unset (vision tasks then fall
367
- // back to the main session's model) instead of showing a one-option picker.
368
- const refs = availableModelRefs(ctx);
369
- if (refs.length === 0) {
370
- ctx.ui.notify("No Pi models are currently available; vision model left unset.", "warning");
371
- } else {
372
- const visionModel = await pickVisionModel(ctx, base.visionModel, refs);
373
- if (visionModel === undefined) return notifyCancelled(ctx);
374
- if (visionModel !== INHERIT) nextVisionModel = visionModel;
450
+ const agentThinkingLevels = keepAgentEntries({ ...base.agentThinkingLevels }, enabled);
451
+ for (const agentName of enabled) {
452
+ const strength = await pickAgentStrength(
453
+ ctx,
454
+ agentName,
455
+ agentThinkingLevels[agentName],
456
+ thinkingLevel,
457
+ defaults,
458
+ );
459
+ if (strength === undefined) return notifyCancelled(ctx);
460
+ if (strength === INHERIT) delete agentThinkingLevels[agentName];
461
+ else agentThinkingLevels[agentName] = strength;
375
462
  }
376
463
 
464
+ const visionModel = await pickVisionModel(ctx, base.visionModel);
465
+ if (visionModel === undefined) return notifyCancelled(ctx);
466
+
377
467
  const injection = await pickInjection(ctx, base.proactiveInjection);
378
468
  if (injection === undefined) return notifyCancelled(ctx);
379
469
 
@@ -389,28 +479,29 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
389
479
  );
390
480
  if (maxConcurrency === undefined) return notifyCancelled(ctx);
391
481
 
392
- const maxFixRounds = await pickCount(
393
- ctx,
394
- "Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
395
- FIX_ROUNDS_STEPS,
396
- base.maxFixRounds,
397
- DEFAULT_MAX_FIX_ROUNDS,
398
- );
399
- if (maxFixRounds === undefined) return notifyCancelled(ctx);
482
+ const maxFixRounds = await pickCount(
483
+ ctx,
484
+ "Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
485
+ FIX_ROUNDS_STEPS,
486
+ base.maxFixRounds,
487
+ DEFAULT_MAX_FIX_ROUNDS,
488
+ );
489
+ if (maxFixRounds === undefined) return notifyCancelled(ctx);
400
490
 
401
- const idleTimeoutSec = await pickCount(
402
- ctx,
403
- "Idle timeout in seconds? (0 = disabled, kills a sub-agent whose output goes silent)",
404
- IDLE_TIMEOUT_STEPS,
405
- base.idleTimeoutSec,
406
- DEFAULT_IDLE_TIMEOUT_SEC,
407
- );
408
- if (idleTimeoutSec === undefined) return notifyCancelled(ctx);
491
+ const idleTimeoutSec = await pickCount(
492
+ ctx,
493
+ "Idle timeout in seconds? (0 = disabled, kills a sub-agent whose output goes silent)",
494
+ IDLE_TIMEOUT_STEPS,
495
+ base.idleTimeoutSec,
496
+ DEFAULT_IDLE_TIMEOUT_SEC,
497
+ );
498
+ if (idleTimeoutSec === undefined) return notifyCancelled(ctx);
409
499
 
410
- const next: SubagentsConfig = {
500
+ const next: SubagentsConfig = {
411
501
  enabledAgents: enabled,
412
- agentModels: repairStaleModels(ctx, picked.models),
413
- agentThinkingLevels: picked.strengths,
502
+ agentModels: keepAgentEntries(pools.agentModels, enabled),
503
+ agentBackupModels: keepAgentEntries(pools.agentBackupModels, enabled),
504
+ agentThinkingLevels,
414
505
  thinkingLevel,
415
506
  notifyOnReviewPass: base.notifyOnReviewPass,
416
507
  maxResultLines: base.maxResultLines,
@@ -421,7 +512,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
421
512
  idleTimeoutSec,
422
513
  announcedFeatures: base.announcedFeatures,
423
514
  };
424
- if (nextVisionModel !== undefined) next.visionModel = nextVisionModel;
515
+ if (visionModel !== CURRENT_MAIN_MODEL) next.visionModel = visionModel;
425
516
  await saveConfig(next, configPath);
426
517
  ctx.ui.notify(`pi-subagents configured. Saved to ${configPath}`, "info");
427
518
  }
@@ -429,7 +520,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
429
520
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
430
521
  const choice = await ctx.ui.select("pi-subagents is already configured. What would you like to change?", [
431
522
  "Enable/disable agents",
432
- "Configure an agent (model + thinking)",
523
+ "Configure an agent (model pool + thinking)",
433
524
  "Change vision model (image tasks)",
434
525
  "Toggle proactive injection",
435
526
  "Change agent scope",
@@ -442,17 +533,19 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
442
533
 
443
534
  if (choice.startsWith("Full")) return runFullSetup(ctx, configPath, config);
444
535
 
445
- let next: SubagentsConfig = { ...config, agentModels: { ...config.agentModels } };
536
+ let next: SubagentsConfig = {
537
+ ...config,
538
+ agentModels: { ...config.agentModels },
539
+ agentBackupModels: { ...config.agentBackupModels },
540
+ };
446
541
 
447
542
  if (choice.startsWith("Enable")) {
448
543
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
449
544
  if (enabled === undefined) return notifyCancelled(ctx);
450
545
  next.enabledAgents = enabled;
451
546
  } else if (choice.startsWith("Configure an agent")) {
452
- // Per-agent loop: pick one agent, then its model and thinking strength, then
453
- // return to the agent picker so several agents can be configured in one
454
- // pass. Esc at any step ends the loop; agents already configured in this
455
- // pass are kept.
547
+ // Per-agent loop: pick one agent, edit Primary + Backup together, then its
548
+ // thinking strength. Esc ends the loop; earlier saved agent changes remain.
456
549
  const defaults = builtinThinkingDefaults();
457
550
  let configuredAny = false;
458
551
  next.agentThinkingLevels = { ...config.agentThinkingLevels };
@@ -460,35 +553,30 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
460
553
  const picked = await configureOneAgent(
461
554
  ctx,
462
555
  next.enabledAgents,
463
- next.agentModels,
556
+ {
557
+ agentModels: next.agentModels,
558
+ agentBackupModels: next.agentBackupModels,
559
+ },
464
560
  next.agentThinkingLevels,
465
561
  next.thinkingLevel,
466
562
  defaults,
467
563
  );
468
- if (picked === undefined) break; // Esc ends the loop
564
+ if (picked === undefined) break;
469
565
  configuredAny = true;
470
- if (picked.modelsAvailable) {
471
- if (picked.model === INHERIT) delete next.agentModels[picked.name];
472
- else next.agentModels[picked.name] = picked.model;
473
- }
566
+ next.agentModels = picked.pools.agentModels;
567
+ next.agentBackupModels = picked.pools.agentBackupModels;
474
568
  if (picked.strength === INHERIT) delete next.agentThinkingLevels[picked.name];
475
569
  else next.agentThinkingLevels[picked.name] = picked.strength;
476
570
  }
477
571
  if (!configuredAny) return notifyCancelled(ctx);
478
- next.agentModels = repairStaleModels(ctx, next.agentModels);
479
572
  } else if (choice.startsWith("Toggle")) {
480
573
  const injection = await pickInjection(ctx, config.proactiveInjection);
481
574
  if (injection === undefined) return notifyCancelled(ctx);
482
575
  next.proactiveInjection = injection;
483
576
  } else if (choice.startsWith("Change vision")) {
484
- const refs = availableModelRefs(ctx);
485
- if (refs.length === 0) {
486
- ctx.ui.notify("No Pi models are currently available; vision model left unchanged.", "warning");
487
- return;
488
- }
489
- const visionModel = await pickVisionModel(ctx, config.visionModel, refs);
577
+ const visionModel = await pickVisionModel(ctx, config.visionModel);
490
578
  if (visionModel === undefined) return notifyCancelled(ctx);
491
- if (visionModel === INHERIT) delete next.visionModel;
579
+ if (visionModel === CURRENT_MAIN_MODEL) delete next.visionModel;
492
580
  else next.visionModel = visionModel;
493
581
  } else if (choice.startsWith("Change agent scope")) {
494
582
  const scope = await pickScope(ctx, config.agentScope);
@@ -542,8 +630,7 @@ export async function runSetup(ctx: ExtensionCommandContext, configPath: string
542
630
  }
543
631
  try {
544
632
  const exists = await configExists(configPath);
545
- const loaded = await loadConfig(configPath);
546
- const config = await repairConfigModels(ctx, configPath, loaded);
633
+ const config = await loadConfig(configPath);
547
634
  if (exists) await runMenu(ctx, configPath, config);
548
635
  else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] });
549
636
  } catch (error) {