@ferris1225/pi-subagents 0.20.0 → 0.22.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 CHANGED
@@ -347,6 +347,15 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
347
347
  `thinkingLevel` is set first and applies as the final fallback. `notifyOnReviewPass` and
348
348
  `maxResultLines` are edited directly in `pi-subagents.json`.
349
349
 
350
+ When the config already exists, re-running `/subagents-setup` opens a menu whose
351
+ **Configure an agent (model + thinking)** entry lets you pick one agent and set just its
352
+ model and thinking strength — so changing a single agent no longer walks every enabled
353
+ agent. After one agent's model + strength picks, the wizard returns to the agent picker
354
+ so several agents can be configured in one pass; Esc at any step ends the pass and keeps
355
+ every agent already configured. **Change default thinking strength** sets only the global
356
+ fallback. The rest of the menu toggles injection, scope, concurrency, fix rounds, and
357
+ idle timeout; **Full re-setup** re-runs the whole first-time wizard.
358
+
350
359
  ```json
351
360
  {
352
361
  "enabledAgents": ["explore", "worker", "reviewer"],
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "@ferris1225/pi-subagents",
3
- "version": "0.20.0",
4
- "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
- "type": "module",
6
- "license": "MIT",
7
- "publishConfig": {
8
- "access": "public"
9
- },
10
- "keywords": [
11
- "pi-package",
12
- "pi-extension",
13
- "subagent",
14
- "sub-agent",
15
- "delegation"
16
- ],
17
- "files": [
18
- "src",
19
- "agents",
20
- "README.md",
21
- "LICENSE"
22
- ],
23
- "pi": {
24
- "extensions": [
25
- "./src/index.ts"
26
- ]
27
- },
28
- "scripts": {
29
- "check": "tsc --noEmit",
30
- "test": "vitest run tests",
31
- "prepack": "npm run check && npm test"
32
- },
33
- "peerDependencies": {
34
- "@earendil-works/pi-agent-core": ">=0.80.6",
35
- "@earendil-works/pi-ai": ">=0.80.6",
36
- "@earendil-works/pi-coding-agent": ">=0.80.6",
37
- "@earendil-works/pi-tui": ">=0.80.6",
38
- "typebox": "*"
39
- },
40
- "devDependencies": {
41
- "@earendil-works/pi-agent-core": "^0.83.0",
42
- "@earendil-works/pi-ai": "^0.83.0",
43
- "@earendil-works/pi-coding-agent": "^0.83.0",
44
- "@earendil-works/pi-tui": "^0.83.0",
45
- "@types/node": "^22.10.0",
46
- "typebox": "^1.3.9",
47
- "typescript": "^5.9.0",
48
- "vitest": "^4.1.0"
49
- },
50
- "engines": {
51
- "node": ">=22.19.0"
52
- }
53
- }
1
+ {
2
+ "name": "@ferris1225/pi-subagents",
3
+ "version": "0.22.0",
4
+ "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "keywords": [
11
+ "pi-package",
12
+ "pi-extension",
13
+ "subagent",
14
+ "sub-agent",
15
+ "delegation"
16
+ ],
17
+ "files": [
18
+ "src",
19
+ "agents",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./src/index.ts"
26
+ ]
27
+ },
28
+ "scripts": {
29
+ "check": "tsc --noEmit",
30
+ "test": "vitest run tests",
31
+ "prepack": "npm run check && npm test"
32
+ },
33
+ "peerDependencies": {
34
+ "@earendil-works/pi-agent-core": ">=0.80.6",
35
+ "@earendil-works/pi-ai": ">=0.80.6",
36
+ "@earendil-works/pi-coding-agent": ">=0.80.6",
37
+ "@earendil-works/pi-tui": ">=0.80.6",
38
+ "typebox": "*"
39
+ },
40
+ "devDependencies": {
41
+ "@earendil-works/pi-agent-core": "^0.83.0",
42
+ "@earendil-works/pi-ai": "^0.83.0",
43
+ "@earendil-works/pi-coding-agent": "^0.83.0",
44
+ "@earendil-works/pi-tui": "^0.83.0",
45
+ "@types/node": "^22.10.0",
46
+ "typebox": "^1.3.9",
47
+ "typescript": "^5.9.0",
48
+ "vitest": "^4.1.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=22.19.0"
52
+ }
53
+ }
package/src/index.ts CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  import { buildFixTaskBrief, buildReReviewBrief, shouldTriggerFixLoop } from "./fixloop.ts";
46
46
  import {
47
47
  activityStateLabel,
48
- compactModelRef,
48
+ compactLine,
49
49
  deriveActivityState,
50
50
  formatElapsed,
51
51
  formatTaskSummary,
@@ -795,11 +795,12 @@ export default function (pi: ExtensionAPI): void {
795
795
  const title = formatTaskSummary(r.task, Math.max(16, Math.floor(width * 0.65)), true);
796
796
  const left = `${head}${icon} ${theme.fg("dim", `#${r.id}`)} ${theme.bold(name)}`;
797
797
 
798
- // Right side: compact model (no provider prefix), token usage (in/out +
798
+ // Right side: full model ref (provider/model), token usage (in/out +
799
799
  // cache read/write), tool count, elapsed, and the soft activity-state
800
- // annotation (idle / long-running). Always visible rightAlign clips
801
- // the title on overflow, never this.
802
- const model = compactModelRef(r.model);
800
+ // annotation (idle / long-running). Trailing the header with a single
801
+ // " · " chain keeps the row compact (no center gap); compactLine
802
+ // clips on overflow, never the right side on its own.
803
+ const model = r.model ?? "?";
803
804
  const usage = formatUsageCompact(r.usage);
804
805
  const tools = r.toolCount ? `${r.toolCount} tool${r.toolCount === 1 ? "" : "s"}` : "";
805
806
  const elapsed = formatElapsed(r, now);
@@ -808,10 +809,10 @@ export default function (pi: ExtensionAPI): void {
808
809
  // the other states (ready / done / stopped) so they are unambiguous.
809
810
  if (r.status !== "running") metaParts.push(statusLabel(r.status));
810
811
  const state = deriveActivityState(r, now);
811
- const stateNote = state ? ` · ${activityStateLabel(state)}` : "";
812
- const note = r.annotation ? ` · ${r.annotation}` : "";
813
- const right = theme.fg("dim", `${metaParts.join(" · ")}${stateNote}${note}`);
814
- lines.push(rightAlign(left, right, width));
812
+ if (state) metaParts.push(activityStateLabel(state));
813
+ if (r.annotation) metaParts.push(r.annotation);
814
+ const right = metaParts.length ? theme.fg("dim", ` · ${metaParts.join(" · ")}`) : "";
815
+ lines.push(compactLine(left, right, width));
815
816
 
816
817
  // Task summary sits one indent below the header, on its own line.
817
818
  lines.push(rightAlign(`${head} ${theme.fg("accent", title)}`, "", width));
package/src/monitor.ts CHANGED
@@ -297,6 +297,16 @@ export function rightAlign(left: string, right: string, width: number): string {
297
297
  return truncateToWidth(`${leftClipped}${" ".repeat(gap)}${right}`, width);
298
298
  }
299
299
 
300
+ /** Concatenate `left` and `right` (no center padding) and clip the combined line
301
+ * to `width`. Unlike {@link rightAlign}, the right side trails the left side
302
+ * instead of being pinned to the far edge, so a short header leaves no gap in
303
+ * the middle. `right` should carry its own leading separator (e.g. ` \u00b7 `);
304
+ * pass an empty string when there is nothing to append. Styled strings are
305
+ * measured by display width. */
306
+ export function compactLine(left: string, right: string, width: number): string {
307
+ return truncateToWidth(`${left}${right}`, width);
308
+ }
309
+
300
310
  /** Max length of the argument target inside a formatted activity line. */
301
311
  export const ACTIVITY_TARGET_MAX = 60;
302
312
 
package/src/setup.ts CHANGED
@@ -76,6 +76,33 @@ async function pickEnabledAgents(
76
76
  );
77
77
  }
78
78
 
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(
83
+ 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,
103
+ );
104
+ }
105
+
79
106
  async function pickAgentModelsAndStrength(
80
107
  ctx: ExtensionCommandContext,
81
108
  enabledAgents: readonly string[],
@@ -93,24 +120,9 @@ async function pickAgentModelsAndStrength(
93
120
  const models: Record<string, string> = {};
94
121
  const strengths: Record<string, ThinkingLevel> = {};
95
122
  for (const name of enabledAgents) {
96
- const currentRef = currentModels[name];
97
- const items = [
98
- {
99
- value: INHERIT,
100
- label: currentRef
101
- ? `(use main session's model — drop override "${currentRef}")`
102
- : "(use main session's current model — no override)",
103
- },
104
- ...refs.map((ref) => ({ value: ref, label: ref === currentRef ? `${ref} (current)` : ref })),
105
- ];
106
- const choice = await promptSelectOne(
107
- ctx,
108
- `Model for "${name}"`,
109
- "Type to filter • ↑/↓ • PgUp/PgDn • Enter selects • Esc cancels setup",
110
- items,
111
- );
112
- if (choice === undefined) return undefined; // Esc aborts the whole wizard
113
- if (choice !== INHERIT) models[name] = choice;
123
+ const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs);
124
+ if (modelChoice === undefined) return undefined; // Esc aborts the whole wizard
125
+ if (modelChoice !== INHERIT) models[name] = modelChoice;
114
126
 
115
127
  // Convenience: the model pick is immediately followed by the strength pick,
116
128
  // so per-agent model + strength are configured in one pass.
@@ -131,13 +143,16 @@ const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
131
143
  max: "strongest reasoning",
132
144
  };
133
145
 
134
- /** Single strength pick for one agent; the inherit option keeps the effective default. */
146
+ /** Single strength pick for one agent; the inherit option keeps the effective default.
147
+ * `escNote` describes what Esc does at this pick (whole-wizard cancel in the
148
+ * full setup vs. ending the per-agent loop in the menu). */
135
149
  async function pickAgentStrength(
136
150
  ctx: ExtensionCommandContext,
137
151
  agentName: string,
138
152
  current: ThinkingLevel | undefined,
139
153
  defaultLevel: ThinkingLevel,
140
154
  defaults: ReadonlyMap<string, ThinkingLevel>,
155
+ escNote = "cancels setup",
141
156
  ): Promise<ThinkingLevel | typeof INHERIT | undefined> {
142
157
  const options = THINKING_LEVEL_VALUES.map((level) => ({
143
158
  value: level,
@@ -150,28 +165,73 @@ async function pickAgentStrength(
150
165
  const choice = await promptSelectOne(
151
166
  ctx,
152
167
  `Thinking strength for "${agentName}"?`,
153
- "Type to filter • ↑/↓ • Enter selects • Esc cancels setup",
168
+ `Type to filter • ↑/↓ • Enter selects • Esc ${escNote}`,
154
169
  [{ value: INHERIT, label: inheritLabel }, ...options],
155
170
  );
156
171
  if (choice === undefined) return undefined;
157
172
  return choice === INHERIT ? INHERIT : (choice as ThinkingLevel);
158
173
  }
159
174
 
160
- /** Strength picks for every enabled agent (inherit keeps the effective default). */
161
- async function pickAgentStrengths(
175
+ /** Pick one enabled agent to reconfigure. Resolves undefined on Esc. */
176
+ async function pickAgentToConfigure(
162
177
  ctx: ExtensionCommandContext,
163
178
  enabledAgents: readonly string[],
179
+ ): Promise<string | undefined> {
180
+ if (enabledAgents.length === 0) {
181
+ ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
182
+ return undefined;
183
+ }
184
+ const items = enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) }));
185
+ return promptSelectOne(
186
+ ctx,
187
+ "Configure which agent?",
188
+ "Type to filter • ↑/↓ • PgUp/PgDn • Enter selects • Esc stops",
189
+ items,
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Configure a single agent picked from the enabled set: choose its model, then
195
+ * its thinking strength. Only that one agent is touched, so re-running the menu
196
+ * to tweak one agent no longer walks every enabled agent. Both picks offer an
197
+ * "inherit" option that drops any existing per-agent override for that field.
198
+ * Resolves undefined when the user presses Esc at any step; the caller keeps
199
+ * changes from agents already configured earlier in the same pass.
200
+ */
201
+ async function configureOneAgent(
202
+ ctx: ExtensionCommandContext,
203
+ enabledAgents: readonly string[],
204
+ currentModels: Record<string, string>,
164
205
  currentStrengths: Record<string, ThinkingLevel>,
165
206
  defaultLevel: ThinkingLevel,
166
207
  defaults: ReadonlyMap<string, ThinkingLevel>,
167
- ): Promise<Record<string, ThinkingLevel> | undefined> {
168
- const strengths: Record<string, ThinkingLevel> = {};
169
- for (const name of enabledAgents) {
170
- const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults);
171
- if (strength === undefined) return undefined; // Esc aborts
172
- if (strength !== INHERIT) strengths[name] = strength;
208
+ ): Promise<
209
+ | {
210
+ name: string;
211
+ model: string | typeof INHERIT;
212
+ modelsAvailable: boolean;
213
+ strength: ThinkingLevel | typeof INHERIT;
214
+ }
215
+ | undefined
216
+ > {
217
+ const name = await pickAgentToConfigure(ctx, enabledAgents);
218
+ if (name === undefined) return undefined; // Esc cancels
219
+
220
+ const refs = availableModelRefs(ctx);
221
+ const modelsAvailable = refs.length > 0;
222
+ let model: string | typeof INHERIT = INHERIT;
223
+ if (!modelsAvailable) {
224
+ ctx.ui.notify("No Pi models are currently available; model override left unchanged.", "warning");
225
+ } else {
226
+ const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs, "stops — earlier agent picks are kept");
227
+ if (modelChoice === undefined) return undefined; // Esc ends the loop
228
+ model = modelChoice;
173
229
  }
174
- return strengths;
230
+
231
+ const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults, "stops — earlier agent picks are kept");
232
+ if (strength === undefined) return undefined; // Esc ends the loop
233
+
234
+ return { name, model, modelsAvailable, strength };
175
235
  }
176
236
 
177
237
  async function pickThinkingLevel(
@@ -330,8 +390,8 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
330
390
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
331
391
  const choice = await ctx.ui.select("pi-subagents is already configured. What would you like to change?", [
332
392
  "Enable/disable agents",
333
- "Change agent models",
334
- "Change thinking strength",
393
+ "Configure an agent (model + thinking)",
394
+ "Change default thinking strength",
335
395
  "Toggle proactive injection",
336
396
  "Change agent scope",
337
397
  "Change max concurrent sub-agents",
@@ -349,27 +409,38 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
349
409
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
350
410
  if (enabled === undefined) return notifyCancelled(ctx);
351
411
  next.enabledAgents = enabled;
352
- } else if (choice.startsWith("Change agent models")) {
412
+ } else if (choice.startsWith("Configure an agent")) {
413
+ // Per-agent loop: pick one agent, then its model and thinking strength, then
414
+ // return to the agent picker so several agents can be configured in one
415
+ // pass. Esc at any step ends the loop; agents already configured in this
416
+ // pass are kept.
353
417
  const defaults = builtinThinkingDefaults();
354
- const picked = await pickAgentModelsAndStrength(
355
- ctx,
356
- config.enabledAgents,
357
- config.agentModels,
358
- config.agentThinkingLevels,
359
- config.thinkingLevel,
360
- defaults,
361
- );
362
- if (picked === undefined) return notifyCancelled(ctx);
363
- next.agentModels = repairStaleModels(ctx, picked.models);
364
- next.agentThinkingLevels = picked.strengths;
365
- } else if (choice.startsWith("Change thinking")) {
366
- // Global first so per-agent "inherit" labels reflect the value that will be stored.
418
+ let configuredAny = false;
419
+ next.agentThinkingLevels = { ...config.agentThinkingLevels };
420
+ while (true) {
421
+ const picked = await configureOneAgent(
422
+ ctx,
423
+ next.enabledAgents,
424
+ next.agentModels,
425
+ next.agentThinkingLevels,
426
+ next.thinkingLevel,
427
+ defaults,
428
+ );
429
+ if (picked === undefined) break; // Esc ends the loop
430
+ configuredAny = true;
431
+ if (picked.modelsAvailable) {
432
+ if (picked.model === INHERIT) delete next.agentModels[picked.name];
433
+ else next.agentModels[picked.name] = picked.model;
434
+ }
435
+ if (picked.strength === INHERIT) delete next.agentThinkingLevels[picked.name];
436
+ else next.agentThinkingLevels[picked.name] = picked.strength;
437
+ }
438
+ if (!configuredAny) return notifyCancelled(ctx);
439
+ next.agentModels = repairStaleModels(ctx, next.agentModels);
440
+ } else if (choice.startsWith("Change default thinking")) {
367
441
  const thinkingLevel = await pickThinkingLevel(ctx, config.thinkingLevel);
368
442
  if (thinkingLevel === undefined) return notifyCancelled(ctx);
369
443
  next.thinkingLevel = thinkingLevel;
370
- const strengths = await pickAgentStrengths(ctx, config.enabledAgents, config.agentThinkingLevels, thinkingLevel, builtinThinkingDefaults());
371
- if (strengths === undefined) return notifyCancelled(ctx);
372
- next.agentThinkingLevels = strengths;
373
444
  } else if (choice.startsWith("Toggle")) {
374
445
  const injection = await pickInjection(ctx, config.proactiveInjection);
375
446
  if (injection === undefined) return notifyCancelled(ctx);