@ferris1225/pi-subagents 0.20.0 → 0.21.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,13 @@ 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. **Change default thinking strength** sets only the global fallback. The rest of the
354
+ menu toggles injection, scope, concurrency, fix rounds, and idle timeout; **Full re-setup**
355
+ re-runs the whole first-time wizard.
356
+
350
357
  ```json
351
358
  {
352
359
  "enabledAgents": ["explore", "worker", "reviewer"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
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
5
  "type": "module",
6
6
  "license": "MIT",
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,30 @@ async function pickEnabledAgents(
76
76
  );
77
77
  }
78
78
 
79
+ /** Single-agent model pick; the inherit option drops any existing override. */
80
+ async function pickAgentModel(
81
+ ctx: ExtensionCommandContext,
82
+ name: string,
83
+ currentRef: string | undefined,
84
+ refs: readonly string[],
85
+ ): Promise<string | typeof INHERIT | undefined> {
86
+ const items = [
87
+ {
88
+ value: INHERIT,
89
+ label: currentRef
90
+ ? `(use main session's model — drop override "${currentRef}")`
91
+ : "(use main session's current model — no override)",
92
+ },
93
+ ...refs.map((ref) => ({ value: ref, label: ref === currentRef ? `${ref} (current)` : ref })),
94
+ ];
95
+ return promptSelectOne(
96
+ ctx,
97
+ `Model for "${name}"`,
98
+ "Type to filter • ↑/↓ • PgUp/PgDn • Enter selects • Esc cancels setup",
99
+ items,
100
+ );
101
+ }
102
+
79
103
  async function pickAgentModelsAndStrength(
80
104
  ctx: ExtensionCommandContext,
81
105
  enabledAgents: readonly string[],
@@ -93,24 +117,9 @@ async function pickAgentModelsAndStrength(
93
117
  const models: Record<string, string> = {};
94
118
  const strengths: Record<string, ThinkingLevel> = {};
95
119
  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;
120
+ const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs);
121
+ if (modelChoice === undefined) return undefined; // Esc aborts the whole wizard
122
+ if (modelChoice !== INHERIT) models[name] = modelChoice;
114
123
 
115
124
  // Convenience: the model pick is immediately followed by the strength pick,
116
125
  // so per-agent model + strength are configured in one pass.
@@ -157,21 +166,65 @@ async function pickAgentStrength(
157
166
  return choice === INHERIT ? INHERIT : (choice as ThinkingLevel);
158
167
  }
159
168
 
160
- /** Strength picks for every enabled agent (inherit keeps the effective default). */
161
- async function pickAgentStrengths(
169
+ /** Pick one enabled agent to reconfigure. Resolves undefined on Esc. */
170
+ async function pickAgentToConfigure(
162
171
  ctx: ExtensionCommandContext,
163
172
  enabledAgents: readonly string[],
173
+ ): Promise<string | undefined> {
174
+ if (enabledAgents.length === 0) {
175
+ ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
176
+ return undefined;
177
+ }
178
+ const items = enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) }));
179
+ return promptSelectOne(
180
+ ctx,
181
+ "Configure which agent?",
182
+ "Type to filter • ↑/↓ • PgUp/PgDn • Enter selects • Esc cancels",
183
+ items,
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Configure a single agent picked from the enabled set: choose its model, then
189
+ * its thinking strength. Only that one agent is touched, so re-running the menu
190
+ * to tweak one agent no longer walks every enabled agent. Both picks offer an
191
+ * "inherit" option that drops any existing per-agent override for that field.
192
+ * Resolves undefined when the user cancels (Esc) at any step.
193
+ */
194
+ async function configureOneAgent(
195
+ ctx: ExtensionCommandContext,
196
+ enabledAgents: readonly string[],
197
+ currentModels: Record<string, string>,
164
198
  currentStrengths: Record<string, ThinkingLevel>,
165
199
  defaultLevel: ThinkingLevel,
166
200
  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;
201
+ ): Promise<
202
+ | {
203
+ name: string;
204
+ model: string | typeof INHERIT;
205
+ modelsAvailable: boolean;
206
+ strength: ThinkingLevel | typeof INHERIT;
207
+ }
208
+ | undefined
209
+ > {
210
+ const name = await pickAgentToConfigure(ctx, enabledAgents);
211
+ if (name === undefined) return undefined; // Esc cancels
212
+
213
+ const refs = availableModelRefs(ctx);
214
+ const modelsAvailable = refs.length > 0;
215
+ let model: string | typeof INHERIT = INHERIT;
216
+ if (!modelsAvailable) {
217
+ ctx.ui.notify("No Pi models are currently available; model override left unchanged.", "warning");
218
+ } else {
219
+ const modelChoice = await pickAgentModel(ctx, name, currentModels[name], refs);
220
+ if (modelChoice === undefined) return undefined; // Esc cancels setup
221
+ model = modelChoice;
173
222
  }
174
- return strengths;
223
+
224
+ const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults);
225
+ if (strength === undefined) return undefined; // Esc cancels setup
226
+
227
+ return { name, model, modelsAvailable, strength };
175
228
  }
176
229
 
177
230
  async function pickThinkingLevel(
@@ -330,8 +383,8 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
330
383
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
331
384
  const choice = await ctx.ui.select("pi-subagents is already configured. What would you like to change?", [
332
385
  "Enable/disable agents",
333
- "Change agent models",
334
- "Change thinking strength",
386
+ "Configure an agent (model + thinking)",
387
+ "Change default thinking strength",
335
388
  "Toggle proactive injection",
336
389
  "Change agent scope",
337
390
  "Change max concurrent sub-agents",
@@ -349,9 +402,11 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
349
402
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
350
403
  if (enabled === undefined) return notifyCancelled(ctx);
351
404
  next.enabledAgents = enabled;
352
- } else if (choice.startsWith("Change agent models")) {
405
+ } else if (choice.startsWith("Configure an agent")) {
406
+ // Per-agent: pick one agent, then its model and thinking strength. Only that
407
+ // agent is touched, so tweaking one no longer walks every enabled agent.
353
408
  const defaults = builtinThinkingDefaults();
354
- const picked = await pickAgentModelsAndStrength(
409
+ const picked = await configureOneAgent(
355
410
  ctx,
356
411
  config.enabledAgents,
357
412
  config.agentModels,
@@ -360,16 +415,19 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
360
415
  defaults,
361
416
  );
362
417
  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
+ next.agentModels = { ...config.agentModels };
419
+ next.agentThinkingLevels = { ...config.agentThinkingLevels };
420
+ if (picked.modelsAvailable) {
421
+ if (picked.model === INHERIT) delete next.agentModels[picked.name];
422
+ else next.agentModels[picked.name] = picked.model;
423
+ next.agentModels = repairStaleModels(ctx, next.agentModels);
424
+ }
425
+ if (picked.strength === INHERIT) delete next.agentThinkingLevels[picked.name];
426
+ else next.agentThinkingLevels[picked.name] = picked.strength;
427
+ } else if (choice.startsWith("Change default thinking")) {
367
428
  const thinkingLevel = await pickThinkingLevel(ctx, config.thinkingLevel);
368
429
  if (thinkingLevel === undefined) return notifyCancelled(ctx);
369
430
  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
431
  } else if (choice.startsWith("Toggle")) {
374
432
  const injection = await pickInjection(ctx, config.proactiveInjection);
375
433
  if (injection === undefined) return notifyCancelled(ctx);