@gotgenes/pi-subagents 6.16.1 → 6.16.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.
@@ -1,7 +1,6 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { AgentSpawnConfig } from "../agent-manager.js";
3
1
  import { AgentTypeRegistry } from "../agent-types.js";
4
2
  import type { ModelRegistry } from "../model-resolver.js";
3
+ import type { ParentSnapshot } from "../parent-snapshot.js";
5
4
  import type { AgentConfig, AgentRecord } from "../types.js";
6
5
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
7
6
  import { createAgentConfigEditor } from "./agent-config-editor.js";
@@ -16,7 +15,12 @@ export interface AgentMenuManager {
16
15
  listAgents: () => AgentRecord[];
17
16
  getRecord: (id: string) => AgentRecord | undefined;
18
17
  /** Used by generate wizard to spawn an agent that writes the .md file. */
19
- spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
18
+ spawnAndWait: (
19
+ parentSnapshot: ParentSnapshot,
20
+ type: string,
21
+ prompt: string,
22
+ opts: { description: string; maxTurns: number },
23
+ ) => Promise<AgentRecord>;
20
24
  }
21
25
 
22
26
  /** Narrow settings interface required by the agent menu. */
@@ -52,35 +56,58 @@ export interface AgentMenuDeps {
52
56
 
53
57
  // ---- Narrow UI context types ----
54
58
 
59
+ /** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
60
+ export interface MenuUI {
61
+ select(title: string, options: string[]): Promise<string | undefined>;
62
+ confirm(title: string, message: string): Promise<boolean>;
63
+ input(title: string, defaultValue?: string): Promise<string | undefined>;
64
+ notify(message: string, level: "info" | "warning" | "error"): void;
65
+ editor(title: string, content: string): Promise<string | undefined>;
66
+ custom<R>(component: any, options?: any): Promise<R>;
67
+ }
68
+
55
69
  // ---- Factory ----
56
70
 
57
71
  /**
58
72
  * Create the `/agents` command handler.
59
73
  * Returns a function suitable for `pi.registerCommand("agents", { handler })`.
60
74
  */
61
- export function createAgentsMenuHandler(deps: AgentMenuDeps) {
62
- const editor = createAgentConfigEditor({
63
- fileOps: deps.fileOps,
64
- registry: deps.registry,
65
- personalAgentsDir: deps.personalAgentsDir,
66
- projectAgentsDir: deps.projectAgentsDir,
67
- });
75
+ export function createAgentsMenuHandler({
76
+ manager,
77
+ registry,
78
+ agentActivity,
79
+ getModelLabel,
80
+ settings,
81
+ fileOps,
82
+ personalAgentsDir,
83
+ projectAgentsDir,
84
+ }: AgentMenuDeps) {
85
+ const editor = createAgentConfigEditor(
86
+ fileOps,
87
+ registry,
88
+ personalAgentsDir,
89
+ projectAgentsDir,
90
+ );
68
91
 
69
92
  const wizard = createAgentCreationWizard({
70
- fileOps: deps.fileOps,
71
- manager: deps.manager,
72
- registry: deps.registry,
73
- personalAgentsDir: deps.personalAgentsDir,
74
- projectAgentsDir: deps.projectAgentsDir,
93
+ fileOps,
94
+ manager,
95
+ registry,
96
+ personalAgentsDir,
97
+ projectAgentsDir,
75
98
  });
76
99
 
77
- async function showAgentsMenu(ctx: ExtensionContext) {
78
- deps.registry.reload();
79
- const allNames = deps.registry.getAllTypes();
100
+ async function showAgentsMenu(
101
+ ui: MenuUI,
102
+ modelRegistry: ModelRegistry,
103
+ parentSnapshot: ParentSnapshot,
104
+ ) {
105
+ registry.reload();
106
+ const allNames = registry.getAllTypes();
80
107
 
81
108
  const options: string[] = [];
82
109
 
83
- const agents = deps.manager.listAgents();
110
+ const agents = manager.listAgents();
84
111
  if (agents.length > 0) {
85
112
  const running = agents.filter(
86
113
  (a) => a.status === "running" || a.status === "queued",
@@ -108,30 +135,30 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
108
135
  : "";
109
136
 
110
137
  if (noAgentsMsg) {
111
- ctx.ui.notify(noAgentsMsg, "info");
138
+ ui.notify(noAgentsMsg, "info");
112
139
  }
113
140
 
114
- const choice = await ctx.ui.select("Agents", options);
141
+ const choice = await ui.select("Agents", options);
115
142
  if (!choice) return;
116
143
 
117
144
  if (choice.startsWith("Running agents (")) {
118
- await showRunningAgents(ctx);
119
- await showAgentsMenu(ctx);
145
+ await showRunningAgents(ui);
146
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
120
147
  } else if (choice.startsWith("Agent types (")) {
121
- await showAllAgentsList(ctx);
122
- await showAgentsMenu(ctx);
148
+ await showAllAgentsList(ui, modelRegistry);
149
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
123
150
  } else if (choice === "Create new agent") {
124
- await wizard.showCreateWizard(ctx);
151
+ await wizard.showCreateWizard(ui, parentSnapshot);
125
152
  } else if (choice === "Settings") {
126
- await showSettings(ctx);
127
- await showAgentsMenu(ctx);
153
+ await showSettings(ui);
154
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
128
155
  }
129
156
  }
130
157
 
131
- async function showAllAgentsList(ctx: ExtensionContext) {
132
- const allNames = deps.registry.getAllTypes();
158
+ async function showAllAgentsList(ui: MenuUI, modelRegistry: ModelRegistry) {
159
+ const allNames = registry.getAllTypes();
133
160
  if (allNames.length === 0) {
134
- ctx.ui.notify("No agents.", "info");
161
+ ui.notify("No agents.", "info");
135
162
  return;
136
163
  }
137
164
 
@@ -144,9 +171,9 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
144
171
  };
145
172
 
146
173
  const entries = allNames.map((name) => {
147
- const cfg = deps.registry.resolveAgentConfig(name);
174
+ const cfg = registry.resolveAgentConfig(name);
148
175
  const disabled = cfg.enabled === false;
149
- const model = deps.getModelLabel(name, ctx.modelRegistry);
176
+ const model = getModelLabel(name, modelRegistry);
150
177
  const indicator = sourceIndicator(cfg);
151
178
  const prefix = `${indicator}${name} · ${model}`;
152
179
  const desc = disabled ? "(disabled)" : cfg.description;
@@ -155,10 +182,12 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
155
182
  const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
156
183
 
157
184
  const hasCustom = allNames.some((n) => {
158
- const c = deps.registry.resolveAgentConfig(n);
185
+ const c = registry.resolveAgentConfig(n);
159
186
  return !c.isDefault && c.enabled !== false;
160
187
  });
161
- const hasDisabled = allNames.some((n) => deps.registry.resolveAgentConfig(n).enabled === false);
188
+ const hasDisabled = allNames.some(
189
+ (n) => registry.resolveAgentConfig(n).enabled === false,
190
+ );
162
191
  const legendParts: string[] = [];
163
192
  if (hasCustom) legendParts.push("• = project ◦ = global");
164
193
  if (hasDisabled) legendParts.push("✕ = disabled");
@@ -169,47 +198,47 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
169
198
  );
170
199
  if (legend) options.push(legend);
171
200
 
172
- const choice = await ctx.ui.select("Agent types", options);
201
+ const choice = await ui.select("Agent types", options);
173
202
  if (!choice) return;
174
203
 
175
204
  const agentName = choice
176
205
  .split(" · ")[0]
177
206
  .replace(/^[•◦✕\s]+/, "")
178
207
  .trim();
179
- if (deps.registry.resolveType(agentName) != null) {
180
- await editor.showAgentDetail(ctx, agentName);
181
- await showAllAgentsList(ctx);
208
+ if (registry.resolveType(agentName) != null) {
209
+ await editor.showAgentDetail(ui, agentName);
210
+ await showAllAgentsList(ui, modelRegistry);
182
211
  }
183
212
  }
184
213
 
185
- async function showRunningAgents(ctx: ExtensionContext) {
186
- const agents = deps.manager.listAgents();
214
+ async function showRunningAgents(ui: MenuUI) {
215
+ const agents = manager.listAgents();
187
216
  if (agents.length === 0) {
188
- ctx.ui.notify("No agents.", "info");
217
+ ui.notify("No agents.", "info");
189
218
  return;
190
219
  }
191
220
 
192
221
  const options = agents.map((a) => {
193
- const dn = getDisplayName(a.type, deps.registry);
222
+ const dn = getDisplayName(a.type, registry);
194
223
  const dur = formatDuration(a.startedAt, a.completedAt);
195
224
  return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
196
225
  });
197
226
 
198
- const choice = await ctx.ui.select("Running agents", options);
227
+ const choice = await ui.select("Running agents", options);
199
228
  if (!choice) return;
200
229
 
201
230
  const idx = options.indexOf(choice);
202
231
  if (idx < 0) return;
203
232
  const record = agents[idx];
204
233
 
205
- await viewAgentConversation(ctx, record);
206
- await showRunningAgents(ctx);
234
+ await viewAgentConversation(ui, record);
235
+ await showRunningAgents(ui);
207
236
  }
208
237
 
209
- async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
238
+ async function viewAgentConversation(ui: MenuUI, record: AgentRecord) {
210
239
  const session = record.session;
211
240
  if (!session) {
212
- ctx.ui.notify(
241
+ ui.notify(
213
242
  `Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
214
243
  "info",
215
244
  );
@@ -219,11 +248,19 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
219
248
  const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
220
249
  "./conversation-viewer.js"
221
250
  );
222
- const activity = deps.agentActivity.get(record.id);
251
+ const activity = agentActivity.get(record.id);
223
252
 
224
- await ctx.ui.custom<undefined>(
253
+ await ui.custom<undefined>(
225
254
  (tui: any, theme: any, _keybindings: any, done: any) => {
226
- return new ConversationViewer({ tui, session, record, activity, theme, done, registry: deps.registry });
255
+ return new ConversationViewer({
256
+ tui,
257
+ session,
258
+ record,
259
+ activity,
260
+ theme,
261
+ done,
262
+ registry,
263
+ });
227
264
  },
228
265
  {
229
266
  overlay: true,
@@ -236,61 +273,68 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
236
273
  );
237
274
  }
238
275
 
239
- async function showSettings(ctx: ExtensionContext) {
240
- const choice = await ctx.ui.select("Settings", [
241
- `Max concurrency (current: ${deps.settings.maxConcurrent})`,
242
- `Default max turns (current: ${deps.settings.defaultMaxTurns ?? "unlimited"})`,
243
- `Grace turns (current: ${deps.settings.graceTurns})`,
276
+ async function showSettings(ui: MenuUI) {
277
+ const choice = await ui.select("Settings", [
278
+ `Max concurrency (current: ${settings.maxConcurrent})`,
279
+ `Default max turns (current: ${settings.defaultMaxTurns ?? "unlimited"})`,
280
+ `Grace turns (current: ${settings.graceTurns})`,
244
281
  ]);
245
282
  if (!choice) return;
246
283
 
247
284
  if (choice.startsWith("Max concurrency")) {
248
- const val = await ctx.ui.input(
285
+ const val = await ui.input(
249
286
  "Max concurrent background agents",
250
- String(deps.settings.maxConcurrent),
287
+ String(settings.maxConcurrent),
251
288
  );
252
289
  if (val) {
253
290
  const n = parseInt(val, 10);
254
291
  if (n >= 1) {
255
- const toast = deps.settings.applyMaxConcurrent(n);
256
- ctx.ui.notify(toast.message, toast.level);
292
+ const toast = settings.applyMaxConcurrent(n);
293
+ ui.notify(toast.message, toast.level);
257
294
  } else {
258
- ctx.ui.notify("Must be a positive integer.", "warning");
295
+ ui.notify("Must be a positive integer.", "warning");
259
296
  }
260
297
  }
261
298
  } else if (choice.startsWith("Default max turns")) {
262
- const val = await ctx.ui.input(
299
+ const val = await ui.input(
263
300
  "Default max turns before wrap-up (0 = unlimited)",
264
- String(deps.settings.defaultMaxTurns ?? 0),
301
+ String(settings.defaultMaxTurns ?? 0),
265
302
  );
266
303
  if (val) {
267
304
  const n = parseInt(val, 10);
268
305
  if (n >= 0) {
269
- const toast = deps.settings.applyDefaultMaxTurns(n);
270
- ctx.ui.notify(toast.message, toast.level);
306
+ const toast = settings.applyDefaultMaxTurns(n);
307
+ ui.notify(toast.message, toast.level);
271
308
  } else {
272
- ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
309
+ ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
273
310
  }
274
311
  }
275
312
  } else if (choice.startsWith("Grace turns")) {
276
- const val = await ctx.ui.input(
313
+ const val = await ui.input(
277
314
  "Grace turns after wrap-up steer",
278
- String(deps.settings.graceTurns),
315
+ String(settings.graceTurns),
279
316
  );
280
317
  if (val) {
281
318
  const n = parseInt(val, 10);
282
319
  if (n >= 1) {
283
- const toast = deps.settings.applyGraceTurns(n);
284
- ctx.ui.notify(toast.message, toast.level);
320
+ const toast = settings.applyGraceTurns(n);
321
+ ui.notify(toast.message, toast.level);
285
322
  } else {
286
- ctx.ui.notify("Must be a positive integer.", "warning");
323
+ ui.notify("Must be a positive integer.", "warning");
287
324
  }
288
325
  }
289
326
  }
290
327
  }
291
328
 
292
- // Return the handler function
293
- return async (ctx: ExtensionContext) => {
294
- await showAgentsMenu(ctx);
329
+ return async ({
330
+ ui,
331
+ modelRegistry,
332
+ parentSnapshot,
333
+ }: {
334
+ ui: MenuUI;
335
+ modelRegistry: ModelRegistry;
336
+ parentSnapshot: ParentSnapshot;
337
+ }) => {
338
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
295
339
  };
296
340
  }