@gotgenes/pi-subagents 6.4.0 → 6.6.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/src/settings.ts CHANGED
@@ -16,16 +16,134 @@ export interface SubagentsSettings {
16
16
  graceTurns?: number;
17
17
  }
18
18
 
19
- /** Setter hooks used by applySettings to wire persisted values into in-memory state. */
20
- export interface SettingsAppliers {
21
- setMaxConcurrent: (n: number) => void;
22
- setDefaultMaxTurns: (n: number) => void;
23
- setGraceTurns: (n: number) => void;
24
- }
25
19
 
26
20
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
27
21
  export type SettingsEmit = (event: string, payload: unknown) => void;
28
22
 
23
+ const DEFAULT_MAX_CONCURRENT = 4;
24
+ const DEFAULT_GRACE_TURNS = 5;
25
+
26
+ /**
27
+ * Owns all three in-memory settings values and their load/save/persist cycle.
28
+ * Replaces the scattered free-function + SettingsAppliers callback pattern.
29
+ */
30
+ export class SettingsManager {
31
+ private _defaultMaxTurns: number | undefined = undefined;
32
+ private _graceTurns: number = DEFAULT_GRACE_TURNS;
33
+ private _maxConcurrent: number = DEFAULT_MAX_CONCURRENT;
34
+
35
+ private readonly emit: SettingsEmit;
36
+ private readonly cwd: string;
37
+ private readonly onMaxConcurrentChanged: (() => void) | undefined;
38
+
39
+ constructor(deps: { emit: SettingsEmit; cwd: string; onMaxConcurrentChanged?: () => void }) {
40
+ this.emit = deps.emit;
41
+ this.cwd = deps.cwd;
42
+ this.onMaxConcurrentChanged = deps.onMaxConcurrentChanged;
43
+ }
44
+
45
+ // ── defaultMaxTurns: 0 or undefined → unlimited (undefined); else max(1, n) ──
46
+
47
+ get defaultMaxTurns(): number | undefined {
48
+ return this._defaultMaxTurns;
49
+ }
50
+
51
+ set defaultMaxTurns(n: number | undefined) {
52
+ if (n == null || n === 0) {
53
+ this._defaultMaxTurns = undefined;
54
+ } else {
55
+ this._defaultMaxTurns = Math.max(1, n);
56
+ }
57
+ }
58
+
59
+ // ── graceTurns: minimum 1 ──
60
+
61
+ get graceTurns(): number {
62
+ return this._graceTurns;
63
+ }
64
+
65
+ set graceTurns(n: number) {
66
+ this._graceTurns = Math.max(1, n);
67
+ }
68
+
69
+ // ── maxConcurrent: minimum 1 ──
70
+
71
+ get maxConcurrent(): number {
72
+ return this._maxConcurrent;
73
+ }
74
+
75
+ set maxConcurrent(n: number) {
76
+ this._maxConcurrent = Math.max(1, n);
77
+ }
78
+
79
+ // ── Lifecycle methods ──
80
+
81
+ /**
82
+ * Load merged settings (global + project), apply to in-memory values,
83
+ * and emit the `subagents:settings_loaded` lifecycle event.
84
+ * Returns the raw loaded settings object.
85
+ */
86
+ load(): SubagentsSettings {
87
+ const settings = loadSettings(this.cwd);
88
+ if (typeof settings.maxConcurrent === "number") this.maxConcurrent = settings.maxConcurrent;
89
+ if (typeof settings.defaultMaxTurns === "number") this.defaultMaxTurns = settings.defaultMaxTurns;
90
+ if (typeof settings.graceTurns === "number") this.graceTurns = settings.graceTurns;
91
+ this.emit("subagents:settings_loaded", { settings });
92
+ return settings;
93
+ }
94
+
95
+ /**
96
+ * Snapshot current in-memory values for persistence.
97
+ * `defaultMaxTurns` uses 0 as the on-disk marker for unlimited (undefined).
98
+ */
99
+ snapshot(): { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number } {
100
+ return {
101
+ maxConcurrent: this._maxConcurrent,
102
+ defaultMaxTurns: this._defaultMaxTurns ?? 0,
103
+ graceTurns: this._graceTurns,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Set maxConcurrent, notify interested parties, persist, and return the toast.
109
+ * Owns the full consequence chain so callers just say what they want.
110
+ */
111
+ applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" } {
112
+ this.maxConcurrent = n; // setter normalizes: max(1, n)
113
+ this.onMaxConcurrentChanged?.();
114
+ return this.saveAndNotify(`Max concurrency set to ${this.maxConcurrent}`);
115
+ }
116
+
117
+ /**
118
+ * Set defaultMaxTurns, persist, and return the toast.
119
+ * Pass 0 for unlimited (maps to undefined internally).
120
+ */
121
+ applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" } {
122
+ this.defaultMaxTurns = n === 0 ? undefined : n; // setter normalizes further
123
+ const label = this.defaultMaxTurns == null ? "unlimited" : String(this.defaultMaxTurns);
124
+ return this.saveAndNotify(`Default max turns set to ${label}`);
125
+ }
126
+
127
+ /**
128
+ * Set graceTurns, persist, and return the toast.
129
+ */
130
+ applyGraceTurns(n: number): { message: string; level: "info" | "warning" } {
131
+ this.graceTurns = n; // setter normalizes: max(1, n)
132
+ return this.saveAndNotify(`Grace turns set to ${this.graceTurns}`);
133
+ }
134
+
135
+ /**
136
+ * Persist the current snapshot, emit `subagents:settings_changed`,
137
+ * and return the toast the UI should display.
138
+ */
139
+ saveAndNotify(successMsg: string): { message: string; level: "info" | "warning" } {
140
+ const snap = this.snapshot();
141
+ const persisted = saveSettings(snap, this.cwd);
142
+ this.emit("subagents:settings_changed", { settings: snap, persisted });
143
+ return persistToastFor(successMsg, persisted);
144
+ }
145
+ }
146
+
29
147
  // Sanity ceilings — prevent hand-edited configs from asking for values that
30
148
  // make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
31
149
  // that any realistic power-user setting passes through.
@@ -107,13 +225,6 @@ export function saveSettings(s: SubagentsSettings, cwd: string = process.cwd()):
107
225
  }
108
226
  }
109
227
 
110
- /** Apply persisted settings to the in-memory state via caller-supplied setters. */
111
- export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers): void {
112
- if (typeof s.maxConcurrent === "number") appliers.setMaxConcurrent(s.maxConcurrent);
113
- if (typeof s.defaultMaxTurns === "number") appliers.setDefaultMaxTurns(s.defaultMaxTurns);
114
- if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
115
- }
116
-
117
228
  /**
118
229
  * Format the user-facing toast for a settings mutation. Pure function —
119
230
  * routes the success/failure of `saveSettings` into the right message + level
@@ -127,36 +238,3 @@ export function persistToastFor(
127
238
  ? { message: successMsg, level: "info" }
128
239
  : { message: `${successMsg} (session only; failed to persist)`, level: "warning" };
129
240
  }
130
-
131
- /**
132
- * Load merged settings, apply them to in-memory state, and emit the
133
- * `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
134
- * callers can log/inspect. Extension init wires this once.
135
- */
136
- export function applyAndEmitLoaded(
137
- appliers: SettingsAppliers,
138
- emit: SettingsEmit,
139
- cwd: string = process.cwd(),
140
- ): SubagentsSettings {
141
- const settings = loadSettings(cwd);
142
- applySettings(settings, appliers);
143
- emit("subagents:settings_loaded", { settings });
144
- return settings;
145
- }
146
-
147
- /**
148
- * Persist a settings snapshot, emit the `subagents:settings_changed` event
149
- * (regardless of persist outcome so listeners see the in-memory change), and
150
- * return the toast the UI should display. Event payload carries the `persisted`
151
- * flag so listeners can react to write failures.
152
- */
153
- export function saveAndEmitChanged(
154
- snapshot: SubagentsSettings,
155
- successMsg: string,
156
- emit: SettingsEmit,
157
- cwd: string = process.cwd(),
158
- ): { message: string; level: "info" | "warning" } {
159
- const persisted = saveSettings(snapshot, cwd);
160
- emit("subagents:settings_changed", { settings: snapshot, persisted });
161
- return persistToastFor(successMsg, persisted);
162
- }
@@ -112,8 +112,8 @@ export interface AgentToolDeps {
112
112
  typeListText: string;
113
113
  availableTypesText: string;
114
114
  agentDir: string;
115
- /** Returns the runtime default max turns (undefined = unlimited). */
116
- getDefaultMaxTurns: () => number | undefined;
115
+ /** Narrow settings accessor — only the default max turns is needed here. */
116
+ settings: { readonly defaultMaxTurns: number | undefined };
117
117
  }
118
118
 
119
119
  // ---- Factory ----
@@ -364,7 +364,7 @@ Guidelines:
364
364
  ? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
365
365
  : undefined;
366
366
  const effectiveMaxTurns = normalizeMaxTurns(
367
- resolvedConfig.maxTurns ?? deps.getDefaultMaxTurns(),
367
+ resolvedConfig.maxTurns ?? deps.settings.defaultMaxTurns,
368
368
  );
369
369
  const agentInvocation: AgentInvocation = {
370
370
  modelName,
@@ -20,8 +20,16 @@ export interface AgentMenuManager {
20
20
  getRecord: (id: string) => AgentRecord | undefined;
21
21
  /** Used by generate wizard to spawn an agent that writes the .md file. */
22
22
  spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
23
- getMaxConcurrent: () => number;
24
- setMaxConcurrent: (n: number) => void;
23
+ }
24
+
25
+ /** Narrow settings interface required by the agent menu. */
26
+ export interface AgentMenuSettings {
27
+ readonly maxConcurrent: number;
28
+ readonly defaultMaxTurns: number | undefined;
29
+ readonly graceTurns: number;
30
+ applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
31
+ applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
32
+ applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
25
33
  }
26
34
 
27
35
  export interface AgentMenuDeps {
@@ -30,24 +38,11 @@ export interface AgentMenuDeps {
30
38
  agentActivity: Map<string, AgentActivity>;
31
39
  /** Resolve model label for a given agent type + registry. */
32
40
  getModelLabel: (type: string, registry?: ModelRegistry) => string;
33
- /** Snapshot current settings for persistence. */
34
- snapshotSettings: () => { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number };
35
- /** Save settings and return a notification result. */
36
- saveSettings: (
37
- settings: { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number },
38
- successMsg: string,
39
- ) => { message: string; level: string };
41
+ /** Settings manager owns in-memory values and persistence. */
42
+ settings: AgentMenuSettings;
40
43
  emitEvent: (name: string, data: unknown) => void;
41
44
  personalAgentsDir: string;
42
45
  projectAgentsDir: string;
43
- /** Returns the runtime default max turns (undefined = unlimited). */
44
- getDefaultMaxTurns: () => number | undefined;
45
- /** Returns the runtime grace turns value. */
46
- getGraceTurns: () => number;
47
- /** Updates the runtime default max turns (undefined = unlimited). */
48
- setDefaultMaxTurns: (n: number | undefined) => void;
49
- /** Updates the runtime grace turns value (minimum 1). */
50
- setGraceTurns: (n: number) => void;
51
46
  }
52
47
 
53
48
  // ---- Narrow UI context types ----
@@ -608,22 +603,22 @@ ${systemPrompt}
608
603
 
609
604
  async function showSettings(ctx: ExtensionContext) {
610
605
  const choice = await ctx.ui.select("Settings", [
611
- `Max concurrency (current: ${deps.manager.getMaxConcurrent()})`,
612
- `Default max turns (current: ${deps.getDefaultMaxTurns() ?? "unlimited"})`,
613
- `Grace turns (current: ${deps.getGraceTurns()})`,
606
+ `Max concurrency (current: ${deps.settings.maxConcurrent})`,
607
+ `Default max turns (current: ${deps.settings.defaultMaxTurns ?? "unlimited"})`,
608
+ `Grace turns (current: ${deps.settings.graceTurns})`,
614
609
  ]);
615
610
  if (!choice) return;
616
611
 
617
612
  if (choice.startsWith("Max concurrency")) {
618
613
  const val = await ctx.ui.input(
619
614
  "Max concurrent background agents",
620
- String(deps.manager.getMaxConcurrent()),
615
+ String(deps.settings.maxConcurrent),
621
616
  );
622
617
  if (val) {
623
618
  const n = parseInt(val, 10);
624
619
  if (n >= 1) {
625
- deps.manager.setMaxConcurrent(n);
626
- notifyApplied(ctx, `Max concurrency set to ${n}`);
620
+ const toast = deps.settings.applyMaxConcurrent(n);
621
+ ctx.ui.notify(toast.message, toast.level);
627
622
  } else {
628
623
  ctx.ui.notify("Must be a positive integer.", "warning");
629
624
  }
@@ -631,16 +626,13 @@ ${systemPrompt}
631
626
  } else if (choice.startsWith("Default max turns")) {
632
627
  const val = await ctx.ui.input(
633
628
  "Default max turns before wrap-up (0 = unlimited)",
634
- String(deps.getDefaultMaxTurns() ?? 0),
629
+ String(deps.settings.defaultMaxTurns ?? 0),
635
630
  );
636
631
  if (val) {
637
632
  const n = parseInt(val, 10);
638
- if (n === 0) {
639
- deps.setDefaultMaxTurns(undefined);
640
- notifyApplied(ctx, "Default max turns set to unlimited");
641
- } else if (n >= 1) {
642
- deps.setDefaultMaxTurns(n);
643
- notifyApplied(ctx, `Default max turns set to ${n}`);
633
+ if (n >= 0) {
634
+ const toast = deps.settings.applyDefaultMaxTurns(n);
635
+ ctx.ui.notify(toast.message, toast.level);
644
636
  } else {
645
637
  ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
646
638
  }
@@ -648,13 +640,13 @@ ${systemPrompt}
648
640
  } else if (choice.startsWith("Grace turns")) {
649
641
  const val = await ctx.ui.input(
650
642
  "Grace turns after wrap-up steer",
651
- String(deps.getGraceTurns()),
643
+ String(deps.settings.graceTurns),
652
644
  );
653
645
  if (val) {
654
646
  const n = parseInt(val, 10);
655
647
  if (n >= 1) {
656
- deps.setGraceTurns(n);
657
- notifyApplied(ctx, `Grace turns set to ${n}`);
648
+ const toast = deps.settings.applyGraceTurns(n);
649
+ ctx.ui.notify(toast.message, toast.level);
658
650
  } else {
659
651
  ctx.ui.notify("Must be a positive integer.", "warning");
660
652
  }
@@ -662,11 +654,6 @@ ${systemPrompt}
662
654
  }
663
655
  }
664
656
 
665
- function notifyApplied(ctx: ExtensionContext, successMsg: string) {
666
- const { message, level } = deps.saveSettings(deps.snapshotSettings(), successMsg);
667
- ctx.ui.notify(message, level as "info" | "warning" | "error");
668
- }
669
-
670
657
  // Return the handler function
671
658
  return async (ctx: ExtensionContext) => {
672
659
  await showAgentsMenu(ctx);