@ferris1225/pi-subagents 4.3.4 → 4.3.5

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +59 -66
  3. package/agents/artisan.md +0 -1
  4. package/agents/steward.md +1 -2
  5. package/{src/index.ts → index.ts} +19 -19
  6. package/package.json +4 -3
  7. package/src/{config.ts → configuration/config.ts} +19 -24
  8. package/src/configuration/setup.ts +375 -0
  9. package/src/configuration/ui.ts +245 -0
  10. package/src/{agents.ts → delegation/agents.ts} +3 -3
  11. package/src/{dispatch.ts → delegation/dispatch.ts} +12 -18
  12. package/src/{prompt.ts → delegation/prompt.ts} +3 -8
  13. package/src/{background.ts → execution/background.ts} +3 -6
  14. package/src/execution/rpc-control.ts +200 -0
  15. package/src/{rpc-run.ts → execution/rpc-run.ts} +11 -199
  16. package/src/{session-fork.ts → execution/session-fork.ts} +1 -1
  17. package/src/{spawn.ts → execution/spawn.ts} +10 -8
  18. package/src/isolation/git-command.ts +147 -0
  19. package/src/{recovery.ts → isolation/recovery.ts} +1 -1
  20. package/src/{worktree.ts → isolation/worktree.ts} +10 -147
  21. package/src/{completion.ts → lifecycle/completion.ts} +2 -2
  22. package/src/{durable.ts → lifecycle/durable.ts} +3 -3
  23. package/src/{runtime.ts → lifecycle/runtime.ts} +7 -7
  24. package/src/{thread-lifecycle.ts → lifecycle/thread-lifecycle.ts} +25 -519
  25. package/src/lifecycle/thread-restore.ts +250 -0
  26. package/src/lifecycle/thread-shared.ts +269 -0
  27. package/src/{tools.ts → lifecycle/tools.ts} +8 -8
  28. package/src/{announcements.ts → presentation/announcements.ts} +4 -4
  29. package/src/{format.ts → presentation/format.ts} +3 -3
  30. package/src/{monitor.ts → presentation/monitor.ts} +2 -2
  31. package/src/{widget.ts → presentation/widget.ts} +1 -1
  32. package/agents/sentinel.md +0 -16
  33. package/src/setup.ts +0 -344
  34. package/src/ui.ts +0 -160
  35. /package/src/{models.ts → configuration/models.ts} +0 -0
  36. /package/src/{temp-hygiene.ts → isolation/temp-hygiene.ts} +0 -0
  37. /package/src/{status.ts → presentation/status.ts} +0 -0
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Interactive configuration wizard for /subagents-setup.
3
+ *
4
+ * The top-level menu exposes enabled roles, per-agent model and thinking choices,
5
+ * and a full setup pass. Everything else (agent scope, idle timeout, result lines)
6
+ * is config-file-only.
7
+ */
8
+
9
+ import { stat } from "node:fs/promises";
10
+ import type { Api, Model } from "@earendil-works/pi-ai";
11
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
12
+ import {
13
+ AGENT_PROFILES,
14
+ BUILTIN_AGENT_NAMES,
15
+ DEFAULT_CONFIG,
16
+ DEFAULT_ENABLED_AGENTS,
17
+ agentProfile,
18
+ errorMessage,
19
+ getConfigPath,
20
+ loadConfig,
21
+ roleThinkingLevel,
22
+ saveConfig,
23
+ type SubagentsConfig,
24
+ type ThinkingLevel,
25
+ } from "./config.ts";
26
+ import {
27
+ CURRENT_MAIN_MODEL,
28
+ applyAgentModelChoice,
29
+ availableModelsInScope,
30
+ buildModelPickerItems,
31
+ currentModelRef,
32
+ findModelByRef,
33
+ modelRef,
34
+ resolveThinkingLevel,
35
+ supportedThinkingLevels,
36
+ } from "./models.ts";
37
+ import { promptSelectMany, promptSelectOne } from "./ui.ts";
38
+
39
+ const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
40
+ off: "no reasoning tokens",
41
+ minimal: "minimal reasoning",
42
+ low: "light reasoning",
43
+ medium: "balanced reasoning",
44
+ high: "deep reasoning",
45
+ xhigh: "extra-deep reasoning",
46
+ max: "strongest reasoning",
47
+ };
48
+
49
+ function setupAgentNames(config: SubagentsConfig): string[] {
50
+ return [
51
+ ...new Set([
52
+ ...BUILTIN_AGENT_NAMES,
53
+ ...config.knownAgents,
54
+ ...config.enabledAgents,
55
+ ...Object.keys(config.agentModels),
56
+ ...Object.keys(config.agentThinkingLevels),
57
+ ]),
58
+ ];
59
+ }
60
+
61
+ function agentPickerItems(names: readonly string[]): Array<{ value: string; label: string; description: string }> {
62
+ return names.map((name) => {
63
+ const profile = agentProfile(name);
64
+ return {
65
+ value: name,
66
+ label: profile ? name : `${name} (custom)`,
67
+ description: profile ? `${profile.summary} — ${profile.remark}` : "custom agent",
68
+ };
69
+ });
70
+ }
71
+
72
+ function moduleLabel(name: string): string {
73
+ const profile = agentProfile(name);
74
+ return profile ? `${name} — ${profile.summary}` : `${name} (custom)`;
75
+ }
76
+
77
+ async function configExists(configPath: string): Promise<boolean> {
78
+ try {
79
+ await stat(configPath);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ async function pickEnabledAgents(
87
+ ctx: ExtensionCommandContext,
88
+ config: SubagentsConfig,
89
+ ): Promise<string[] | undefined> {
90
+ return promptSelectMany(
91
+ ctx,
92
+ "Which agents should run?",
93
+ "Each line is a role and its job. Space toggles • Enter confirms • Esc back",
94
+ agentPickerItems(setupAgentNames(config)),
95
+ config.enabledAgents,
96
+ );
97
+ }
98
+
99
+ async function pickConfiguredModel(
100
+ ctx: ExtensionCommandContext,
101
+ title: string,
102
+ configuredRef: string | undefined,
103
+ escNote: string,
104
+ ): Promise<string | undefined> {
105
+ const models = availableModelsInScope(ctx);
106
+ const items = buildModelPickerItems({
107
+ models,
108
+ configuredRef,
109
+ mainRef: currentModelRef(ctx),
110
+ });
111
+ return promptSelectOne(
112
+ ctx,
113
+ title,
114
+ `Type to filter by provider, model, or capability • ↑/↓ • Enter selects • Esc ${escNote}`,
115
+ items,
116
+ configuredRef ?? CURRENT_MAIN_MODEL,
117
+ );
118
+ }
119
+
120
+ async function pickAgentModel(
121
+ ctx: ExtensionCommandContext,
122
+ agentName: string,
123
+ agentModels: Readonly<Record<string, string>>,
124
+ escNote = "cancels this setup pass",
125
+ ): Promise<string | undefined> {
126
+ const profile = agentProfile(agentName);
127
+ const duty = profile ? ` — ${profile.summary}` : "";
128
+ return pickConfiguredModel(
129
+ ctx,
130
+ `Model for ${agentName}${duty}?`,
131
+ agentModels[agentName],
132
+ escNote,
133
+ );
134
+ }
135
+
136
+ function effectiveModelForChoice(
137
+ ctx: ExtensionCommandContext,
138
+ agentName: string,
139
+ choice: string,
140
+ agentModels: Readonly<Record<string, string>>,
141
+ ): Model<Api> | undefined {
142
+ const selectedModels = applyAgentModelChoice({ ...agentModels }, agentName, choice);
143
+ const ref = selectedModels[agentName];
144
+ return findModelByRef(availableModelsInScope(ctx), ref) ?? ctx.model;
145
+ }
146
+
147
+ /** The role default is marked; picking it clears a stored override. */
148
+ async function pickAgentStrength(
149
+ ctx: ExtensionCommandContext,
150
+ agentName: string,
151
+ model: Model<Api> | undefined,
152
+ current: ThinkingLevel | undefined,
153
+ escNote = "cancels this setup pass",
154
+ ): Promise<ThinkingLevel | undefined> {
155
+ const supported = supportedThinkingLevels(model);
156
+ const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName));
157
+ if (supported.length <= 1) return roleDefault;
158
+
159
+ const currentEffective = current ? resolveThinkingLevel(model, current) : roleDefault;
160
+ const modelName = model ? modelRef(model) : "current main model";
161
+ const options = supported.map((level) => {
162
+ const tags = [
163
+ level === roleDefault ? "role default" : "",
164
+ current !== undefined && currentEffective === level ? "current" : "",
165
+ ].filter(Boolean);
166
+ return {
167
+ value: level,
168
+ label: `${level} — ${THINKING_LEVEL_HINTS[level]}${tags.length ? ` (${tags.join(", ")})` : ""}`,
169
+ };
170
+ });
171
+ return promptSelectOne(
172
+ ctx,
173
+ `Thinking for ${agentName}?`,
174
+ `${agentName} defaults to ${roleDefault} on ${modelName} • Enter selects • Esc ${escNote}`,
175
+ options,
176
+ currentEffective,
177
+ ) as Promise<ThinkingLevel | undefined>;
178
+ }
179
+
180
+ async function pickAgentToConfigure(
181
+ ctx: ExtensionCommandContext,
182
+ enabledAgents: readonly string[],
183
+ ): Promise<string | undefined> {
184
+ if (enabledAgents.length === 0) {
185
+ ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
186
+ return undefined;
187
+ }
188
+ return promptSelectOne(
189
+ ctx,
190
+ "Configure which agent?",
191
+ "Name, then what it owns • ↑/↓ • Enter selects • Esc returns to settings",
192
+ enabledAgents.map((name) => {
193
+ const profile = agentProfile(name);
194
+ return {
195
+ value: name,
196
+ label: moduleLabel(name),
197
+ description: profile?.remark,
198
+ };
199
+ }),
200
+ );
201
+ }
202
+
203
+ interface ConfiguredAgentChoice {
204
+ name: string;
205
+ model: string;
206
+ strength: ThinkingLevel;
207
+ }
208
+
209
+ async function configureOneAgent(
210
+ ctx: ExtensionCommandContext,
211
+ config: SubagentsConfig,
212
+ ): Promise<ConfiguredAgentChoice | undefined> {
213
+ while (true) {
214
+ const name = await pickAgentToConfigure(ctx, config.enabledAgents);
215
+ if (name === undefined) return undefined;
216
+ const profile = agentProfile(name);
217
+ if (profile) ctx.ui.notify(`${name}: ${profile.remark}`, "info");
218
+
219
+ while (true) {
220
+ const modelChoice = await pickAgentModel(
221
+ ctx,
222
+ name,
223
+ config.agentModels,
224
+ "returns to agent selection",
225
+ );
226
+ if (modelChoice === undefined) break;
227
+ const model = effectiveModelForChoice(ctx, name, modelChoice, config.agentModels);
228
+ const strength = await pickAgentStrength(
229
+ ctx,
230
+ name,
231
+ model,
232
+ config.agentThinkingLevels[name],
233
+ "returns to model selection",
234
+ );
235
+ if (strength === undefined) continue;
236
+ return { name, model: modelChoice, strength };
237
+ }
238
+ }
239
+ }
240
+
241
+ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
242
+ const keep = new Set(enabled);
243
+ return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
244
+ }
245
+
246
+ function applyThinkingChoice(
247
+ levels: Record<string, ThinkingLevel>,
248
+ agentName: string,
249
+ strength: ThinkingLevel,
250
+ model: Model<Api> | undefined,
251
+ ): Record<string, ThinkingLevel> {
252
+ const next = { ...levels };
253
+ const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName));
254
+ if (strength === roleDefault) delete next[agentName];
255
+ else next[agentName] = resolveThinkingLevel(model, strength);
256
+ return next;
257
+ }
258
+
259
+ async function introduceSetup(ctx: ExtensionCommandContext): Promise<boolean> {
260
+ const lines = BUILTIN_AGENT_NAMES.map((name) => {
261
+ const profile = AGENT_PROFILES[name];
262
+ return `${name} — ${profile.summary}. ${profile.remark}`;
263
+ });
264
+ const defaults = BUILTIN_AGENT_NAMES.map((name) => `${name} ${roleThinkingLevel(name)}`).join(", ");
265
+ ctx.ui.notify(
266
+ `pi-subagents: ${lines.join(" ")} Pick a model for each role next. Thinking defaults per role (${defaults}); change it on a role when you want.`,
267
+ "info",
268
+ );
269
+ const choice = await ctx.ui.select("How to configure pi-subagents", [
270
+ "Continue — pick a model for each role (thinking has a role default you can change later)",
271
+ ]);
272
+ return choice !== undefined;
273
+ }
274
+
275
+ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
276
+ if (!(await introduceSetup(ctx))) return false;
277
+
278
+ const enabled = await pickEnabledAgents(ctx, base);
279
+ if (enabled === undefined) return false;
280
+
281
+ let agentModels = keepAgentEntries(base.agentModels, enabled);
282
+ for (const agentName of enabled) {
283
+ const profile = agentProfile(agentName);
284
+ if (profile) ctx.ui.notify(`${agentName}: ${profile.remark}`, "info");
285
+ const choice = await pickAgentModel(ctx, agentName, agentModels);
286
+ if (choice === undefined) return false;
287
+ agentModels = applyAgentModelChoice(agentModels, agentName, choice);
288
+ }
289
+
290
+ const next: SubagentsConfig = {
291
+ enabledAgents: enabled,
292
+ knownAgents: setupAgentNames(base),
293
+ agentModels,
294
+ agentThinkingLevels: keepAgentEntries(base.agentThinkingLevels, enabled),
295
+ maxResultLines: base.maxResultLines,
296
+ agentScope: base.agentScope,
297
+ idleTimeoutSec: base.idleTimeoutSec,
298
+ };
299
+ await saveConfig(next, configPath);
300
+ ctx.ui.notify(
301
+ `pi-subagents saved to ${configPath}. Role thinking defaults apply; open Configure an agent to change one.`,
302
+ "info",
303
+ );
304
+ return true;
305
+ }
306
+
307
+ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
308
+ while (true) {
309
+ const choice = await ctx.ui.select("pi-subagents settings", [
310
+ "Enable/disable agents — choose which roles are available",
311
+ "Configure an agent — model and thinking, with its job on the row",
312
+ "Full re-setup — walk through the team and pick models again",
313
+ ]);
314
+ if (choice === undefined) return;
315
+ if (choice.startsWith("Full")) {
316
+ if (await runFullSetup(ctx, configPath, config)) return;
317
+ continue;
318
+ }
319
+
320
+ let next: SubagentsConfig = {
321
+ ...config,
322
+ agentModels: { ...config.agentModels },
323
+ agentThinkingLevels: { ...config.agentThinkingLevels },
324
+ };
325
+ if (choice.startsWith("Enable")) {
326
+ const enabled = await pickEnabledAgents(ctx, config);
327
+ if (enabled === undefined) continue;
328
+ next.enabledAgents = enabled;
329
+ next.agentModels = keepAgentEntries(next.agentModels, enabled);
330
+ next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
331
+ } else {
332
+ let configuredAny = false;
333
+ while (true) {
334
+ const picked = await configureOneAgent(ctx, next);
335
+ if (!picked) break;
336
+ configuredAny = true;
337
+ next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
338
+ const model = effectiveModelForChoice(ctx, picked.name, picked.model, next.agentModels);
339
+ next.agentThinkingLevels = applyThinkingChoice(
340
+ next.agentThinkingLevels,
341
+ picked.name,
342
+ picked.strength,
343
+ model,
344
+ );
345
+ }
346
+ if (!configuredAny) continue;
347
+ await saveConfig(next, configPath);
348
+ ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
349
+ config = next;
350
+ continue;
351
+ }
352
+
353
+ await saveConfig(next, configPath);
354
+ ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
355
+ return;
356
+ }
357
+ }
358
+
359
+ /** Entry point for the /subagents-setup command. */
360
+ export async function runSetup(ctx: ExtensionCommandContext, configPath: string = getConfigPath()): Promise<void> {
361
+ if (ctx.mode !== "tui") {
362
+ ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
363
+ return;
364
+ }
365
+ try {
366
+ const exists = await configExists(configPath);
367
+ const config = await loadConfig(configPath);
368
+ if (exists) await runMenu(ctx, configPath, config);
369
+ else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) {
370
+ ctx.ui.notify("pi-subagents setup cancelled.", "info");
371
+ }
372
+ } catch (error) {
373
+ ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
374
+ }
375
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * TUI pickers for /subagents-setup, built on @earendil-works/pi-tui.
3
+ *
4
+ * A single self-contained `Picker` component powers both selectors:
5
+ * - single-select (model picker): type to fuzzy-filter, arrows to move,
6
+ * PageUp/PageDown to page, Enter to choose, Esc to cancel.
7
+ * - multi-select (module picker): same navigation, Space toggles a checkbox,
8
+ * Enter confirms the selection set.
9
+ *
10
+ * pi-tui's built-in SelectList only handles up/down/confirm/cancel (no paging),
11
+ * so we render the list ourselves and use the injected keybinding manager. Every line
12
+ * is passed through truncateToWidth() — pi hard-crashes if a rendered line is
13
+ * wider than the terminal.
14
+ */
15
+
16
+ import {
17
+ fuzzyFilter,
18
+ truncateToWidth,
19
+ type Component,
20
+ type Focusable,
21
+ type KeybindingsManager,
22
+ type SelectItem,
23
+ type TUI,
24
+ } from "@earendil-works/pi-tui";
25
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
26
+
27
+ /** The slice of the extension context the pickers need (mode + ui). */
28
+ type PickerContext = Pick<ExtensionCommandContext, "mode" | "ui">;
29
+
30
+ /** Rows shown at once; longer lists are reached with PageUp/PageDown. */
31
+ const PAGE_SIZE = 8;
32
+
33
+ interface PickerStyles {
34
+ border: (t: string) => string;
35
+ title: (t: string) => string;
36
+ hint: (t: string) => string;
37
+ cursorMark: (t: string) => string;
38
+ selectedLabel: (t: string) => string;
39
+ label: (t: string) => string;
40
+ dim: (t: string) => string;
41
+ checked: (t: string) => string;
42
+ unchecked: (t: string) => string;
43
+ filterEcho: (t: string) => string;
44
+ }
45
+
46
+ type PickerItem = SelectItem;
47
+
48
+ function pickerItemSearchText(item: PickerItem): string {
49
+ return `${item.value} ${item.label} ${item.description ?? ""}`;
50
+ }
51
+
52
+ interface PickerCallbacks {
53
+ /** single-select: fired with the highlighted value on Enter. */
54
+ onSelect?: (value: string) => void;
55
+ /** multi-select: fired with the full chosen set on Enter. */
56
+ onConfirm?: (values: string[]) => void;
57
+ onCancel: () => void;
58
+ }
59
+
60
+ class Picker implements Component, Focusable {
61
+ private _focused = false;
62
+ private query = "";
63
+ private cursor = 0;
64
+ private filtered: PickerItem[];
65
+
66
+ constructor(
67
+ private readonly items: PickerItem[],
68
+ private readonly multi: boolean,
69
+ private readonly selected: Set<string>,
70
+ private readonly styles: PickerStyles,
71
+ private readonly headerLines: string[],
72
+ private readonly tui: TUI,
73
+ private readonly keybindings: KeybindingsManager,
74
+ private readonly cb: PickerCallbacks,
75
+ initialValue?: string,
76
+ ) {
77
+ this.filtered = items;
78
+ const initialIndex = initialValue === undefined ? -1 : items.findIndex((item) => item.value === initialValue);
79
+ if (initialIndex >= 0) this.cursor = initialIndex;
80
+ }
81
+
82
+ get focused(): boolean {
83
+ return this._focused;
84
+ }
85
+ set focused(value: boolean) {
86
+ this._focused = value;
87
+ }
88
+
89
+ private recompute(): void {
90
+ const q = this.query.trim();
91
+ this.filtered = q ? fuzzyFilter(this.items, q, pickerItemSearchText) : this.items;
92
+ this.cursor = Math.max(0, Math.min(this.cursor, this.filtered.length - 1));
93
+ }
94
+
95
+ render(width: number): string[] {
96
+ const s = this.styles;
97
+ const fit = (line: string): string => truncateToWidth(line, width, "");
98
+ const border = fit(s.border("─".repeat(Math.max(1, width))));
99
+
100
+ const lines: string[] = [border];
101
+ for (const h of this.headerLines) lines.push(fit(h));
102
+ lines.push(fit(this.query ? s.filterEcho(`filter: ${this.query}`) : s.dim("filter: (type to narrow)")));
103
+ lines.push(border);
104
+
105
+ if (this.filtered.length === 0) {
106
+ lines.push(fit(s.dim(" (no matches)")));
107
+ } else {
108
+ const start = Math.max(
109
+ 0,
110
+ Math.min(this.cursor - Math.floor(PAGE_SIZE / 2), this.filtered.length - PAGE_SIZE),
111
+ );
112
+ const visible = this.filtered.slice(start, start + PAGE_SIZE);
113
+ for (let i = 0; i < visible.length; i++) {
114
+ const item = visible[i];
115
+ const isCursor = start + i === this.cursor;
116
+ const mark = isCursor ? s.cursorMark("❯ ") : " ";
117
+ const label = isCursor ? s.selectedLabel(item.label) : s.label(item.label);
118
+ const description = item.description ? s.dim(` — ${item.description}`) : "";
119
+ const line = this.multi
120
+ ? mark + (this.selected.has(item.value) ? s.checked("[x] ") : s.unchecked("[ ] ")) + label + description
121
+ : mark + label + description;
122
+ lines.push(fit(line));
123
+ }
124
+ const more = this.filtered.length > PAGE_SIZE ? " ↑/↓ move • PgUp/PgDn page" : "";
125
+ lines.push(fit(s.dim(` (${this.cursor + 1}/${this.filtered.length})${more}`)));
126
+ }
127
+
128
+ lines.push(border);
129
+ return lines;
130
+ }
131
+
132
+ handleInput(data: string): void {
133
+ const kb = this.keybindings;
134
+ if (kb.matches(data, "tui.select.up")) {
135
+ if (this.filtered.length > 0) this.cursor = this.cursor === 0 ? this.filtered.length - 1 : this.cursor - 1;
136
+ } else if (kb.matches(data, "tui.select.down")) {
137
+ if (this.filtered.length > 0) this.cursor = this.cursor === this.filtered.length - 1 ? 0 : this.cursor + 1;
138
+ } else if (kb.matches(data, "tui.select.pageUp")) {
139
+ this.cursor = Math.max(0, this.cursor - PAGE_SIZE);
140
+ } else if (kb.matches(data, "tui.select.pageDown")) {
141
+ this.cursor = Math.min(Math.max(0, this.filtered.length - 1), this.cursor + PAGE_SIZE);
142
+ } else if (kb.matches(data, "tui.select.confirm")) {
143
+ if (this.multi) this.cb.onConfirm?.([...this.selected]);
144
+ else {
145
+ const item = this.filtered[this.cursor];
146
+ if (item) this.cb.onSelect?.(item.value);
147
+ }
148
+ return;
149
+ } else if (kb.matches(data, "tui.select.cancel")) {
150
+ this.cb.onCancel();
151
+ return;
152
+ } else if (data === "\x7f" || data === "\b") {
153
+ this.query = this.query.slice(0, -1);
154
+ this.cursor = 0;
155
+ this.recompute();
156
+ } else if (data === " ") {
157
+ if (this.multi) {
158
+ const item = this.filtered[this.cursor];
159
+ if (item) {
160
+ if (this.selected.has(item.value)) this.selected.delete(item.value);
161
+ else this.selected.add(item.value);
162
+ }
163
+ } else {
164
+ this.query += data;
165
+ this.cursor = 0;
166
+ this.recompute();
167
+ }
168
+ } else if (isPrintable(data)) {
169
+ this.query += data;
170
+ this.cursor = 0;
171
+ this.recompute();
172
+ }
173
+ this.tui.requestRender();
174
+ }
175
+
176
+ invalidate(): void {}
177
+ }
178
+
179
+ function isPrintable(data: string): boolean {
180
+ if (data.length === 0) return false;
181
+ // Reject ESC-led escape sequences and other control characters.
182
+ return data.charCodeAt(0) >= 0x20;
183
+ }
184
+
185
+ /** Build style functions from the pi theme. */
186
+ function makeStyles(theme: Theme): PickerStyles {
187
+ return {
188
+ border: (t) => theme.fg("accent", t),
189
+ title: (t) => theme.fg("accent", theme.bold(t)),
190
+ hint: (t) => theme.fg("dim", t),
191
+ cursorMark: (t) => theme.fg("accent", t),
192
+ selectedLabel: (t) => theme.fg("accent", theme.bold(t)),
193
+ label: (t) => t,
194
+ dim: (t) => theme.fg("dim", t),
195
+ checked: (t) => theme.fg("accent", t),
196
+ unchecked: (t) => theme.fg("dim", t),
197
+ filterEcho: (t) => theme.fg("accent", t),
198
+ };
199
+ }
200
+
201
+ function requireTui(ctx: PickerContext): boolean {
202
+ if (ctx.mode !== "tui") {
203
+ ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
204
+ return false;
205
+ }
206
+ return true;
207
+ }
208
+
209
+ /** Single-select with fuzzy filter + paging. Resolves undefined on Esc. */
210
+ export function promptSelectOne(
211
+ ctx: PickerContext,
212
+ title: string,
213
+ hint: string,
214
+ items: PickerItem[],
215
+ initialValue?: string,
216
+ ): Promise<string | undefined> {
217
+ if (!requireTui(ctx)) return Promise.resolve(undefined);
218
+ return ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
219
+ const styles = makeStyles(theme);
220
+ const header = [styles.title(title), styles.hint(hint)];
221
+ return new Picker(items, false, new Set<string>(), styles, header, tui, keybindings, {
222
+ onSelect: (value) => done(value),
223
+ onCancel: () => done(undefined),
224
+ }, initialValue);
225
+ });
226
+ }
227
+
228
+ /** Multi-select with fuzzy filter + paging. Resolves undefined on Esc. */
229
+ export function promptSelectMany(
230
+ ctx: PickerContext,
231
+ title: string,
232
+ hint: string,
233
+ items: PickerItem[],
234
+ initialSelected: readonly string[],
235
+ ): Promise<string[] | undefined> {
236
+ if (!requireTui(ctx)) return Promise.resolve(undefined);
237
+ return ctx.ui.custom<string[] | undefined>((tui, theme, keybindings, done) => {
238
+ const styles = makeStyles(theme);
239
+ const header = [styles.title(title), styles.hint(hint)];
240
+ return new Picker(items, true, new Set<string>(initialSelected), styles, header, tui, keybindings, {
241
+ onConfirm: (values) => done(values),
242
+ onCancel: () => done(undefined),
243
+ });
244
+ });
245
+ }
@@ -15,8 +15,8 @@ import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "no
15
15
  import { dirname, join } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
18
- import { type AgentScope } from "./config.ts";
19
- import type { IsolationMode } from "./worktree.ts";
18
+ import { type AgentScope } from "../configuration/config.ts";
19
+ import type { IsolationMode } from "../isolation/worktree.ts";
20
20
 
21
21
  export type AgentSource = "builtin" | "user" | "project";
22
22
 
@@ -113,7 +113,7 @@ export function isWriteCapableAgent(
113
113
 
114
114
  const here = dirname(fileURLToPath(import.meta.url));
115
115
  /** <package>/agents — the agents shipped with this extension. */
116
- export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
116
+ export const BUILTIN_AGENTS_DIR = join(here, "..", "..", "agents");
117
117
 
118
118
  /** Agents shipped with the package and surfaced by the setup overlay. */
119
119
  export function loadBuiltinAgents(): AgentConfig[] {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The `subagent` tool: dispatches enabled scout, artisan, steward, sentinel,
3
- * and custom roles as isolated pi child processes, single or parallel.
2
+ * The `subagent` tool: dispatches enabled built-in and custom roles as
3
+ * isolated pi child processes, single or parallel.
4
4
  * Owns the public dispatch contract and
5
5
  * per-run status tracking. Stable thread generations, final integration, and
6
6
  * completion ownership live in thread-lifecycle.ts.
@@ -9,11 +9,10 @@
9
9
  import { StringEnum, type Usage } from "@earendil-works/pi-ai";
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { Text } from "@earendil-works/pi-tui";
12
- import { join, resolve } from "node:path";
13
12
  import { Type } from "typebox";
14
- import { discoverAgents, resolveAgentTools, type AgentConfig } from "./agents.ts";
15
- import { loadConfig } from "./config.ts";
16
- import { formatCompletionBlock, formatUsage, queuedResult } from "./format.ts";
13
+ import { discoverAgents } from "./agents.ts";
14
+ import { loadConfig } from "../configuration/config.ts";
15
+ import { formatCompletionBlock, formatUsage } from "../presentation/format.ts";
17
16
  import {
18
17
  formatTaskSummary,
19
18
  formatToolActivity,
@@ -21,32 +20,27 @@ import {
21
20
  statusIcon,
22
21
  statusLabel,
23
22
  sumUsage,
24
- type RunView,
25
23
  type RunWaitReason,
26
- } from "./monitor.ts";
24
+ } from "../presentation/monitor.ts";
27
25
  import { formatPhaseLeaseReceipt } from "./prompt.ts";
28
- import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
29
- import { persistThreadCheckpoint } from "./thread-lifecycle.ts";
26
+ import type { SubagentRuntime, SubagentThread } from "../lifecycle/runtime.ts";
27
+ import { createBackgroundDispatcher } from "../lifecycle/thread-lifecycle.ts";
30
28
  import {
31
- getProjectRoot,
32
29
  getResultOutput,
33
30
  isFailedResult,
34
- runSingleAgentWithMainFallback,
35
31
  type SingleResult,
36
32
  type SubagentDetails,
37
33
  type SubagentLiveEvent,
38
34
  type UsageStats,
39
- } from "./spawn.ts";
35
+ } from "../execution/spawn.ts";
40
36
  import {
41
- createBackgroundDispatcher,
42
37
  isWorktreeCapableAgent,
38
+ persistThreadCheckpoint,
43
39
  projectResultsRoot,
44
- resolveDispatchModelRoute,
45
40
  runInManagedRepositoryLane,
46
- withWorktreeSystemPrompt,
47
41
  type DispatchEnvironment,
48
- } from "./thread-lifecycle.ts";
49
- import type { IsolationMode } from "./worktree.ts";
42
+ } from "../lifecycle/thread-shared.ts";
43
+ import type { IsolationMode } from "../isolation/worktree.ts";
50
44
 
51
45
  export { isWorktreeCapableAgent, runInManagedRepositoryLane };
52
46