pi-subagents-lite 1.3.0 → 1.4.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.
Files changed (53) hide show
  1. package/README.md +184 -235
  2. package/package.json +1 -1
  3. package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
  4. package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
  5. package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
  6. package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
  7. package/src/agents/agent-types.ts +339 -0
  8. package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
  9. package/src/{output-file.ts → agents/output-file.ts} +68 -1
  10. package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
  11. package/src/agents/types.ts +54 -0
  12. package/src/{usage.ts → agents/usage.ts} +7 -0
  13. package/src/{config-io.ts → config/config-io.ts} +20 -3
  14. package/src/config/config-store.ts +472 -0
  15. package/src/config/types.ts +26 -0
  16. package/src/events.ts +185 -0
  17. package/src/index.ts +8 -281
  18. package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
  19. package/src/{model-selector.ts → models/model-selector.ts} +1 -1
  20. package/src/{context.ts → prompt/context.ts} +1 -1
  21. package/src/prompt/prompts.ts +180 -0
  22. package/src/prompt/skill-loader.ts +195 -0
  23. package/src/registration.ts +101 -0
  24. package/src/shell.ts +101 -0
  25. package/src/spawn/spawn-coordinator.ts +232 -0
  26. package/src/status-note.ts +10 -0
  27. package/src/types.ts +47 -71
  28. package/src/ui/agent-widget.ts +61 -49
  29. package/src/{format.ts → ui/format.ts} +64 -26
  30. package/src/ui/menu/helpers.ts +93 -0
  31. package/src/ui/menu/menu-concurrency.ts +192 -0
  32. package/src/ui/menu/menu-debug.ts +125 -0
  33. package/src/ui/menu/menu-model-settings.ts +208 -0
  34. package/src/ui/menu/menu-running-agents.ts +224 -0
  35. package/src/ui/menu/menu-spawn-options.ts +87 -0
  36. package/src/ui/menu/menu-spawn-wizard.ts +418 -0
  37. package/src/ui/menu/menu-system-prompt.ts +109 -0
  38. package/src/ui/menu/menu-widget-settings.ts +130 -0
  39. package/src/ui/menu/menus.ts +101 -0
  40. package/src/ui/menu/submenus/confirm.ts +47 -0
  41. package/src/ui/menu/submenus/model-select.ts +70 -0
  42. package/src/ui/menu/submenus/numeric-input.ts +98 -0
  43. package/src/ui/menu/wrappers/settings-list.ts +205 -0
  44. package/src/{renderer.ts → ui/renderer.ts} +7 -6
  45. package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
  46. package/src/ui/types.ts +11 -0
  47. package/src/agent-types.ts +0 -184
  48. package/src/config-mutator.ts +0 -183
  49. package/src/menus.ts +0 -1333
  50. package/src/prompts.ts +0 -94
  51. package/src/skill-loader.ts +0 -178
  52. package/src/state.ts +0 -83
  53. /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * config-store.ts — Deep module owning persisted config + per-session overrides.
3
+ *
4
+ * Absorbs config-io.ts, config-mutator.ts, and the config/widget-sync half of
5
+ * state.ts. See docs/adr/0004-composition-root-over-shared-state.md.
6
+ *
7
+ * - Reads return defaults baked in (no `?? 6` at call sites).
8
+ * - Each persisted mutate method is mutate + persist + its side effect, so a
9
+ * side effect cannot be forgotten.
10
+ * - Widget/manager are injected after construction (they're created lazily).
11
+ *
12
+ * Lifecycle: per-session. `reload()` re-reads disk + resets session overrides
13
+ * at session_start. `dispose()` drops deps at session_shutdown.
14
+ */
15
+
16
+ import type { SubagentsConfig, SessionModelOverrides } from "../models/model-precedence.js";
17
+ import { resolveModel } from "../models/model-precedence.js";
18
+ import type { AgentWidget } from "../ui/agent-widget.js";
19
+ import type { AgentManager } from "../agents/agent-manager.js";
20
+ import { CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
21
+ import type { SystemPromptMode } from "../agents/types.js";
22
+ import type { ThinkingLevel } from "../types.js";
23
+ import { DEFAULT_CONFIG, loadConfig, saveConfigAtomic } from "./config-io.js";
24
+
25
+ /** Valid values for systemPromptMode — checked once at module load. */
26
+ const VALID_SYSTEM_PROMPT_MODES = new Set<string>(["replace", "inherit", "custom"]);
27
+
28
+ /** Injected persistence adapter. Swap for an in-memory adapter in tests. */
29
+ export interface ConfigIO {
30
+ load(): SubagentsConfig;
31
+ save(config: SubagentsConfig): void;
32
+ }
33
+
34
+ /** Production adapter wrapping the real config file. */
35
+ export const fileConfigIO: ConfigIO = {
36
+ load: () => loadConfig(),
37
+ save: (c) => saveConfigAtomic(c),
38
+ };
39
+
40
+ /** Agent settings with all scalar defaults resolved. Model fields stay nullable. */
41
+ export interface ResolvedAgentSettings {
42
+ /** null = inherit parent. Kept nullable to preserve resolveModel's null-skip. */
43
+ readonly defaultModel: string | null;
44
+ readonly forceBackground: boolean;
45
+ readonly showCost: boolean;
46
+ readonly graceTurns: number;
47
+ readonly widgetMaxLines: number;
48
+ readonly widgetMaxLinesCompact: number;
49
+ readonly widgetCompact: boolean;
50
+ readonly widgetShortcut: boolean;
51
+ readonly widgetDescLengthFull: number;
52
+ readonly widgetDescLengthCompact: number;
53
+ /** System prompt mode: replace (default), inherit parent, or custom file. */
54
+ readonly systemPromptMode: SystemPromptMode;
55
+ /** Whether to include AGENTS.md context files in the subagent system prompt. */
56
+ readonly includeContextFiles: boolean;
57
+ /** Default thinking level for spawned agents. Undefined = inherit from agent config. */
58
+ readonly defaultThinking: ThinkingLevel | undefined;
59
+ /** Default max turns for spawned agents. Undefined = unlimited. */
60
+ readonly defaultMaxTurns: number | undefined;
61
+ /** Global default for skills loading: true (load all) or false (none). */
62
+ readonly loadSkillsImplicitly: boolean;
63
+ /** Global default for extensions loading: true (load all) or false (none). */
64
+ readonly loadExtensionsImplicitly: boolean;
65
+ /** Whether to skip built-in default agents at registration. */
66
+ readonly disableDefaultAgents: boolean;
67
+ /** Whether to show toolUses count in widget stats line. */
68
+ readonly showTools: boolean;
69
+ /** Whether to show turn count in widget stats line. */
70
+ readonly showTurns: boolean;
71
+ /** Whether to show input tokens in widget stats line. */
72
+ readonly showInput: boolean;
73
+ /** Whether to show output tokens in widget stats line. */
74
+ readonly showOutput: boolean;
75
+ /** Whether to show context percent and compactions in widget stats line. */
76
+ readonly showContext: boolean;
77
+ /** Whether to show elapsed time in widget stats line. */
78
+ readonly showTime: boolean;
79
+ }
80
+
81
+ /** Side-effect targets, injected after construction. */
82
+ export interface ConfigStoreDeps {
83
+ widget?: AgentWidget;
84
+ manager?: AgentManager;
85
+ }
86
+
87
+ export class ConfigStore {
88
+ private config: SubagentsConfig;
89
+ private sessionOverrides: SessionModelOverrides = { default: null };
90
+ private sessionShowCost: boolean | undefined;
91
+ private widget?: AgentWidget;
92
+ private manager?: AgentManager;
93
+ /** Previous tool-expansion state, for ctrl+o compact sync. */
94
+ private lastToolsExpanded: boolean | undefined;
95
+
96
+ constructor(private readonly io: ConfigIO = fileConfigIO) {
97
+ this.config = this.io.load();
98
+ }
99
+
100
+ // ── Reads ──────────────────────────────────────────────────────
101
+
102
+ /** Whether a session-level showCost override is active. */
103
+ get hasSessionShowCost(): boolean {
104
+ return this.sessionShowCost !== undefined;
105
+ }
106
+
107
+ get agent(): ResolvedAgentSettings {
108
+ const a = this.config.agent;
109
+ const widgetMaxLines = a.widgetMaxLines ?? DEFAULT_CONFIG.agent.widgetMaxLines ?? 12;
110
+ const widgetMaxLinesCompact = a.widgetMaxLinesCompact ?? Math.floor(widgetMaxLines / 2);
111
+ const widgetCompact = a.widgetCompact === true;
112
+ const widgetShortcut = a.widgetShortcut === true;
113
+ const widgetDescLengthFull = a.widgetDescLengthFull ?? DEFAULT_CONFIG.agent.widgetDescLengthFull ?? 50;
114
+ const widgetDescLengthCompact = a.widgetDescLengthCompact ?? DEFAULT_CONFIG.agent.widgetDescLengthCompact ?? 30;
115
+ const rawMode = a.systemPromptMode;
116
+ const systemPromptMode = VALID_SYSTEM_PROMPT_MODES.has(rawMode as string) ? rawMode as SystemPromptMode : "replace";
117
+ const includeContextFiles = a.includeContextFiles ?? DEFAULT_CONFIG.agent.includeContextFiles ?? true;
118
+
119
+ return {
120
+ defaultModel: a.default ?? null,
121
+ forceBackground: a.forceBackground === true,
122
+ showCost: this.sessionShowCost ?? (a.showCost === true),
123
+ graceTurns: a.graceTurns ?? DEFAULT_CONFIG.agent.graceTurns ?? 6,
124
+ widgetMaxLines,
125
+ widgetMaxLinesCompact,
126
+ widgetCompact,
127
+ widgetShortcut,
128
+ widgetDescLengthFull,
129
+ widgetDescLengthCompact,
130
+ systemPromptMode,
131
+ includeContextFiles,
132
+ defaultThinking: a.defaultThinking as ThinkingLevel | undefined,
133
+ defaultMaxTurns: a.defaultMaxTurns,
134
+ loadSkillsImplicitly: a.loadSkillsImplicitly !== false,
135
+ loadExtensionsImplicitly: a.loadExtensionsImplicitly !== false,
136
+ disableDefaultAgents: a.disableDefaultAgents === true,
137
+ showTools: a.showTools !== false,
138
+ showTurns: a.showTurns !== false,
139
+ showInput: a.showInput !== false,
140
+ showOutput: a.showOutput !== false,
141
+ showContext: a.showContext !== false,
142
+ showTime: a.showTime !== false,
143
+ };
144
+ }
145
+
146
+ get concurrency(): {
147
+ default: number;
148
+ providers: Record<string, number>;
149
+ models: Record<string, number>;
150
+ } {
151
+ return {
152
+ default: this.config.concurrency.default,
153
+ providers: this.config.concurrency.providers ?? {},
154
+ models: this.config.concurrency.models ?? {},
155
+ };
156
+ }
157
+
158
+ get sessionDefaultModel(): string | null {
159
+ return this.sessionOverrides.default ?? null;
160
+ }
161
+
162
+ sessionModelOverride(type: string): string | null {
163
+ return this.sessionOverrides[type] ?? null;
164
+ }
165
+
166
+ /** Raw agent config incl. dynamic per-type model keys (for menu display). */
167
+ agentConfigSnapshot(): Readonly<SubagentsConfig["agent"]> {
168
+ return this.config.agent;
169
+ }
170
+
171
+ /**
172
+ * Resolve the effective model for a spawn, hiding resolveModel's option
173
+ * assembly. Precedence: session per-type → session default → config per-type
174
+ * → config default → agentConfig (frontmatter) → parentModelId.
175
+ */
176
+ modelFor(type: string, parentModelId: string, agentConfig?: { model?: string }): string {
177
+ return resolveModel({
178
+ subagentType: type,
179
+ agentConfig,
180
+ config: this.config,
181
+ parentModelId,
182
+ sessionOverrides: this.sessionOverrides,
183
+ });
184
+ }
185
+
186
+ // ── Mutations ──────────────────────────────────────────────────
187
+ // Each persisted method = mutate + persist (+ side effect). Session methods
188
+ // are in-memory only: never persisted, no side effects.
189
+
190
+ readonly mutate = {
191
+ agent: {
192
+ setDefaultModel: (value: string | null): void => {
193
+ this.config.agent.default = value;
194
+ this.persist();
195
+ },
196
+ setModelOverride: (type: string, value: string | null): void => {
197
+ this.config.agent[type] = value;
198
+ this.persist();
199
+ },
200
+ clearModelOverride: (type: string): void => {
201
+ delete this.config.agent[type];
202
+ this.persist();
203
+ },
204
+ /** Clear all per-type model overrides, preserving non-model settings. */
205
+ clearAllModelOverrides: (): void => {
206
+ const preserved: Record<string, unknown> = {};
207
+ for (const key of CONFIG_AGENT_NON_MODEL_KEYS) {
208
+ const val = this.config.agent[key];
209
+ if (val != null || key === "default" || key === "forceBackground") {
210
+ preserved[key] = val;
211
+ }
212
+ }
213
+ this.config.agent = preserved as SubagentsConfig["agent"];
214
+ this.persist();
215
+ this.syncWidgetSettings();
216
+ },
217
+ setForceBackground: (enabled: boolean): void => {
218
+ this.config.agent.forceBackground = enabled;
219
+ this.persist();
220
+ },
221
+ setShowCost: (enabled: boolean): void => {
222
+ this.config.agent.showCost = enabled;
223
+ this.sessionShowCost = undefined;
224
+ this.persist();
225
+ this.widget?.setShowCost(enabled);
226
+ this.syncWidgetStatsVisibility();
227
+ },
228
+ setGraceTurns: (n: number): void => {
229
+ this.config.agent.graceTurns = n;
230
+ this.persist();
231
+ },
232
+ setSystemPromptMode: (mode: SystemPromptMode): void => {
233
+ this.config.agent.systemPromptMode = mode;
234
+ this.persist();
235
+ },
236
+ setIncludeContextFiles: (enabled: boolean): void => {
237
+ this.config.agent.includeContextFiles = enabled;
238
+ this.persist();
239
+ },
240
+ setDefaultThinking: (level: ThinkingLevel | undefined): void => {
241
+ if (level === undefined) {
242
+ delete this.config.agent.defaultThinking;
243
+ } else {
244
+ this.config.agent.defaultThinking = level;
245
+ }
246
+ this.persist();
247
+ },
248
+ setDefaultMaxTurns: (n: number | undefined): void => {
249
+ if (n === undefined) {
250
+ delete this.config.agent.defaultMaxTurns;
251
+ } else {
252
+ this.config.agent.defaultMaxTurns = n;
253
+ }
254
+ this.persist();
255
+ },
256
+ setLoadSkillsImplicitly: (value: boolean): void => {
257
+ this.config.agent.loadSkillsImplicitly = value;
258
+ this.persist();
259
+ },
260
+ setLoadExtensionsImplicitly: (value: boolean): void => {
261
+ this.config.agent.loadExtensionsImplicitly = value;
262
+ this.persist();
263
+ },
264
+ setDisableDefaultAgents: (value: boolean): void => {
265
+ this.config.agent.disableDefaultAgents = value;
266
+ this.persist();
267
+ },
268
+ setShowTools: (enabled: boolean) => this.setAgentVisibility("showTools", enabled),
269
+ setShowTurns: (enabled: boolean) => this.setAgentVisibility("showTurns", enabled),
270
+ setShowInput: (enabled: boolean) => this.setAgentVisibility("showInput", enabled),
271
+ setShowOutput: (enabled: boolean) => this.setAgentVisibility("showOutput", enabled),
272
+ setShowContext: (enabled: boolean) => this.setAgentVisibility("showContext", enabled),
273
+ setShowTime: (enabled: boolean) => this.setAgentVisibility("showTime", enabled),
274
+ },
275
+ widget: {
276
+ setCompact: (enabled: boolean): void => {
277
+ this.config.agent.widgetCompact = enabled;
278
+ this.persist();
279
+ this.syncWidgetSettings();
280
+ },
281
+ setMaxLines: (lines: number): void => {
282
+ this.config.agent.widgetMaxLines = lines;
283
+ if (this.config.agent.widgetMaxLinesCompact === undefined) {
284
+ this.config.agent.widgetMaxLinesCompact = Math.floor(lines / 2);
285
+ }
286
+ this.persist();
287
+ this.syncWidgetSettings();
288
+ },
289
+ setMaxLinesCompact: (lines: number): void => {
290
+ this.config.agent.widgetMaxLinesCompact = lines;
291
+ this.persist();
292
+ this.syncWidgetSettings();
293
+ },
294
+ setDescLengthFull: (n: number): void => {
295
+ this.config.agent.widgetDescLengthFull = n;
296
+ this.persist();
297
+ this.syncWidgetSettings();
298
+ },
299
+ setDescLengthCompact: (n: number): void => {
300
+ this.config.agent.widgetDescLengthCompact = n;
301
+ this.persist();
302
+ this.syncWidgetSettings();
303
+ },
304
+ // Note: persists only. Does NOT syncWidgetSettings — matches the existing
305
+ // behavior, where toggling the shortcut takes effect on next reload rather
306
+ // than immediately. Flagged for a follow-up (the other three widget
307
+ // setters do sync).
308
+ setShortcut: (enabled: boolean): void => {
309
+ this.config.agent.widgetShortcut = enabled;
310
+ this.persist();
311
+ },
312
+ },
313
+ concurrency: {
314
+ setDefault: (n: number): void => {
315
+ this.config.concurrency.default = n;
316
+ this.persist();
317
+ this.applyConcurrency();
318
+ },
319
+ setProvider: (key: string, n: number): void => {
320
+ this.config.concurrency.providers = { ...(this.config.concurrency.providers ?? {}), [key]: n };
321
+ this.persist();
322
+ this.applyConcurrency();
323
+ },
324
+ setModel: (key: string, n: number): void => {
325
+ this.config.concurrency.models = { ...(this.config.concurrency.models ?? {}), [key]: n };
326
+ this.persist();
327
+ this.applyConcurrency();
328
+ },
329
+ removeProvider: (key: string): void => {
330
+ if (this.config.concurrency.providers) delete this.config.concurrency.providers[key];
331
+ this.persist();
332
+ this.applyConcurrency();
333
+ },
334
+ removeModel: (key: string): void => {
335
+ if (this.config.concurrency.models) delete this.config.concurrency.models[key];
336
+ this.persist();
337
+ this.applyConcurrency();
338
+ },
339
+ reset: (): void => {
340
+ this.config.concurrency = { ...DEFAULT_CONFIG.concurrency };
341
+ this.persist();
342
+ this.applyConcurrency();
343
+ },
344
+ },
345
+ session: {
346
+ /** Set a session model override for a type (or "default"). Not persisted. */
347
+ setOverride: (type: string, model: string): void => {
348
+ this.sessionOverrides[type] = model;
349
+ },
350
+ clearOverride: (type: string): void => {
351
+ delete this.sessionOverrides[type];
352
+ },
353
+ clearAll: (): void => {
354
+ this.sessionOverrides = { default: null };
355
+ },
356
+ /** Set a session showCost override. Not persisted. */
357
+ setShowCost: (enabled: boolean): void => {
358
+ this.sessionShowCost = enabled;
359
+ this.widget?.setShowCost(enabled);
360
+ this.syncWidgetStatsVisibility();
361
+ },
362
+ /** Clear session showCost override, reverting to config value. */
363
+ clearShowCost: (): void => {
364
+ this.sessionShowCost = undefined;
365
+ this.widget?.setShowCost(this.config.agent.showCost === true);
366
+ this.syncWidgetStatsVisibility();
367
+ },
368
+ },
369
+ };
370
+
371
+ // ── ctrl+o compact sync (absorbs syncCompactFromToolsExpanded) ──
372
+
373
+ /**
374
+ * Toggle widget compact mode when tool expansion changes (ctrl+o), gated on
375
+ * widgetShortcut. No-op when widgetCompact is forced on. Only acts on actual
376
+ * state transitions (not every call).
377
+ */
378
+ notifyToolsExpanded(expanded: boolean): void {
379
+ if (this.config.agent.widgetShortcut !== true) {
380
+ this.lastToolsExpanded = expanded;
381
+ return;
382
+ }
383
+ if (this.config.agent.widgetCompact === true) {
384
+ this.lastToolsExpanded = expanded;
385
+ return;
386
+ }
387
+ if (this.lastToolsExpanded !== undefined && this.lastToolsExpanded !== expanded) {
388
+ this.widget?.setCompactMode(!expanded);
389
+ }
390
+ this.lastToolsExpanded = expanded;
391
+ }
392
+
393
+ // ── Lifecycle ──────────────────────────────────────────────────
394
+
395
+ /** Re-read disk, reset session overrides + toggle state, re-sync deps. Called at session_start. */
396
+ reload(): void {
397
+ this.config = this.io.load();
398
+ this.sessionOverrides = { default: null };
399
+ this.sessionShowCost = undefined;
400
+ this.lastToolsExpanded = undefined;
401
+ this.syncAllDeps();
402
+ }
403
+
404
+ /** Inject side-effect targets. Re-syncs whatever deps are present (lazy widget/manager). */
405
+ setDeps(deps: ConfigStoreDeps): void {
406
+ if (deps.widget !== undefined) this.widget = deps.widget;
407
+ if (deps.manager !== undefined) this.manager = deps.manager;
408
+ this.syncAllDeps();
409
+ }
410
+
411
+ /** Drop deps at session_shutdown. The widget/manager are disposed by the composition root. */
412
+ dispose(): void {
413
+ this.widget = undefined;
414
+ this.manager = undefined;
415
+ }
416
+
417
+ // ── Private helpers ────────────────────────────────────────────
418
+
419
+ private persist(): void {
420
+ this.io.save(this.config);
421
+ }
422
+
423
+ /** Push widget display settings (compact, shortcut, max lines) to the widget. */
424
+ private syncWidgetSettings(): void {
425
+ const w = this.widget;
426
+ if (!w) return;
427
+ const a = this.agent;
428
+ w.setForceCompact(a.widgetCompact);
429
+ w.setWidgetShortcut(a.widgetShortcut);
430
+ w.setMaxLines(a.widgetMaxLines);
431
+ w.setMaxLinesCompact(a.widgetMaxLinesCompact);
432
+ w.setDescLengthFull(a.widgetDescLengthFull);
433
+ w.setDescLengthCompact(a.widgetDescLengthCompact);
434
+ }
435
+
436
+ /** Push stats visibility flags to the widget. */
437
+ private syncWidgetStatsVisibility(): void {
438
+ const w = this.widget;
439
+ if (!w) return;
440
+ const a = this.agent;
441
+ w.setStatsVisibility({
442
+ showTools: a.showTools,
443
+ showTurns: a.showTurns,
444
+ showInput: a.showInput,
445
+ showOutput: a.showOutput,
446
+ showContext: a.showContext,
447
+ showCost: a.showCost,
448
+ showTime: a.showTime,
449
+ });
450
+ }
451
+
452
+ /** Update a widget stats visibility flag: mutate config → persist → sync widget. */
453
+ private setAgentVisibility(key: "showTools" | "showTurns" | "showInput" | "showOutput" | "showContext" | "showTime", value: boolean): void {
454
+ this.config.agent[key] = value;
455
+ this.persist();
456
+ this.syncWidgetStatsVisibility();
457
+ }
458
+
459
+ private applyConcurrency(): void {
460
+ this.manager?.setConcurrency(this.config.concurrency);
461
+ }
462
+
463
+ /** Full re-sync of all present deps. Used by reload/setDeps. */
464
+ private syncAllDeps(): void {
465
+ if (this.widget) {
466
+ this.widget.setShowCost(this.agent.showCost);
467
+ this.syncWidgetSettings();
468
+ this.syncWidgetStatsVisibility();
469
+ }
470
+ this.applyConcurrency();
471
+ }
472
+ }
@@ -0,0 +1,26 @@
1
+ /** Non-model keys in config.agent — preserved when clearing all overrides. */
2
+ export const CONFIG_AGENT_NON_MODEL_KEYS = [
3
+ "default",
4
+ "forceBackground",
5
+ "graceTurns",
6
+ "showCost",
7
+ "showTools",
8
+ "showTurns",
9
+ "showInput",
10
+ "showOutput",
11
+ "showContext",
12
+ "showTime",
13
+ "widgetMaxLines",
14
+ "widgetMaxLinesCompact",
15
+ "widgetDescLengthFull",
16
+ "widgetDescLengthCompact",
17
+ "widgetCompact",
18
+ "widgetShortcut",
19
+ "systemPromptMode",
20
+ "includeContextFiles",
21
+ "defaultThinking",
22
+ "defaultMaxTurns",
23
+ "loadSkillsImplicitly",
24
+ "loadExtensionsImplicitly",
25
+ "disableDefaultAgents",
26
+ ];
package/src/events.ts ADDED
@@ -0,0 +1,185 @@
1
+ import * as path from "node:path";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { DEFAULT_AGENTS } from "./agents/default-agents.js";
4
+ import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agents/agent-types.js";
5
+ import { scanAgentFilesInDir, mergeAgents } from "./agents/agent-discovery.js";
6
+ import { AgentManager } from "./agents/agent-manager.js";
7
+ import { AgentWidget, type UICtx } from "./ui/agent-widget.js";
8
+ import { SpawnCoordinator } from "./spawn/spawn-coordinator.js";
9
+ import { toolCallListener } from "./agents/tool-execution.js";
10
+ import { registerAgentTool } from "./registration.js";
11
+ import {
12
+ getPiInstance,
13
+ getManager,
14
+ getWidget,
15
+ getCoordinator,
16
+ getStore,
17
+ setSessionCtx,
18
+ setManager,
19
+ setWidget,
20
+ setCoordinator,
21
+ } from "./shell.js";
22
+
23
+ // ============================================================================
24
+ // Config loader — session_start handler logic
25
+ // ============================================================================
26
+
27
+ /**
28
+ * Ensure the manager and widget singletons exist.
29
+ * Idempotent — safe to call on every session_start.
30
+ */
31
+ export function ensureManagerAndWidget(): void {
32
+ const currentManager = getManager();
33
+ const currentWidget = getWidget();
34
+
35
+ // Create manager if missing
36
+ if (!currentManager) {
37
+ // Coordinator will be created after manager, so use a placeholder onComplete
38
+ // that we'll replace once coordinator is created.
39
+ const newManager = new AgentManager(
40
+ undefined, // onComplete wired below
41
+ getStore().concurrency as unknown as ConstructorParameters<typeof AgentManager>[1],
42
+ );
43
+ setManager(newManager);
44
+ // Sync the manager as a config side-effect target (concurrency setters call setConcurrency).
45
+ getStore().setDeps({ manager: newManager });
46
+
47
+ // Now create coordinator with the real manager
48
+ const coordinator = new SpawnCoordinator(newManager, getPiInstance());
49
+ setCoordinator(coordinator);
50
+
51
+ // Wire the manager's onComplete to the coordinator
52
+ newManager.setOnComplete((record) => {
53
+ // Delegate completion side-effects to coordinator
54
+ coordinator.onAgentComplete(record);
55
+
56
+ // Mark finished and update widget
57
+ getWidget()?.markFinished(record.id);
58
+ getWidget()?.update();
59
+ });
60
+ }
61
+
62
+ // Create widget if missing (uses existing or newly created manager)
63
+ if (!currentWidget) {
64
+ const newWidget = new AgentWidget(
65
+ getManager()!,
66
+ (id: string) => getCoordinator()?.liveView(id),
67
+ );
68
+ setWidget(newWidget);
69
+ // Sync the widget as a config side-effect target. setDeps re-syncs showCost +
70
+ // all widget display settings from current config (absorbs the old
71
+ // newWidget.setShowCost(...) + syncWidgetSettings() calls).
72
+ getStore().setDeps({ widget: newWidget });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Scan agent files from user and project directories, merge with defaults,
78
+ * and register into the type registry.
79
+ */
80
+ export async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
81
+ const homeDir = process.env.HOME || "";
82
+ const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
83
+ const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
84
+
85
+ // Store scan dirs for on-demand discovery (agents added during the session)
86
+ setAgentScanDirs(userAgentDir, projectAgentDir);
87
+
88
+ const disableDefaults = getStore().agent.disableDefaultAgents;
89
+
90
+ const [userAgents, projectAgents] = await Promise.all([
91
+ scanAgentFilesInDir(userAgentDir, "user"),
92
+ scanAgentFilesInDir(projectAgentDir, "project"),
93
+ ]);
94
+
95
+ // Merge with defaults (skip defaults when disableDefaultAgents is on)
96
+ const defaults = disableDefaults ? new Map() : DEFAULT_AGENTS;
97
+ const merged = mergeAgents(defaults, userAgents, projectAgents);
98
+
99
+ // Register into the type registry (skip re-adding defaults)
100
+ registerAgents(merged, { disableDefaultAgents: disableDefaults });
101
+ }
102
+
103
+ export async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
104
+ // ConfigStore is authoritative for config + session overrides + widget/manager
105
+ // side effects.
106
+ getStore().reload();
107
+ ensureManagerAndWidget();
108
+ await scanAndRegisterAgents(ctx);
109
+ }
110
+
111
+ // ============================================================================
112
+ // Event listener setup
113
+ // ============================================================================
114
+
115
+ /** Register all pi.on() event listeners. */
116
+ export function setupEventListeners(pi: ExtensionAPI): void {
117
+ pi.on("tool_call", toolCallListener);
118
+
119
+ pi.on("tool_execution_start", async (_event, ctx) => {
120
+ // Set UI context on first tool execution
121
+ if (!getWidget()) {
122
+ ensureManagerAndWidget();
123
+ }
124
+ getWidget()?.setUICtx(ctx.ui as unknown as UICtx);
125
+ getWidget()?.onTurnStart();
126
+ });
127
+
128
+
129
+ // session_start — load config, scan agents, register into registry,
130
+ // then re-register Agent tool with dynamic agent type enum
131
+ // Listen for ctrl+o keypress to sync compact mode (push-based, no polling)
132
+ let unregisterTerminalInput: (() => void) | undefined;
133
+
134
+ pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
135
+ setSessionCtx(ctx);
136
+ await loadConfigAndRegisterAgents(ctx);
137
+ // Re-register with updated agent type list (now includes user/project agents)
138
+ registerAgentTool(pi);
139
+ // Register ctrl+o listener
140
+ if (ctx.hasUI && !unregisterTerminalInput) {
141
+ unregisterTerminalInput = ctx.ui.onTerminalInput((data: string) => {
142
+ // ctrl+o = 0x0F (15) — toggles tool expansion
143
+ if (data === "\u000f") {
144
+ // Read state after a tick to let the built-in handler process it first
145
+ setTimeout(() => {
146
+ const ui = ctx.ui as unknown as { getToolsExpanded?: () => boolean };
147
+ const expanded = ui.getToolsExpanded?.();
148
+ if (expanded !== undefined) {
149
+ // Widget render hint (tool row state), then config-gated compact toggle.
150
+ getWidget()?.notifyToolsExpansionChanged(expanded);
151
+ getStore().notifyToolsExpanded(expanded);
152
+ }
153
+ }, 0);
154
+ }
155
+ return undefined; // Don't consume the input
156
+ });
157
+ }
158
+ // Sync compact mode with initial tool expansion state
159
+ getStore().notifyToolsExpanded(false);
160
+ });
161
+
162
+ // session_shutdown — abort all, dispose manager
163
+ pi.on("session_shutdown", async (_event: unknown, ctx: ExtensionContext) => {
164
+ // Warn if agents were killed
165
+ const currentManager = getManager();
166
+ if (currentManager) {
167
+ const records = currentManager.listAgents();
168
+ const active = records.filter(r => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
169
+ if (active.length > 0 && ctx.hasUI) {
170
+ ctx.ui.notify(`${active.length} agent(s) killed by reload`, "warning");
171
+ }
172
+ }
173
+ // Dispose coordinator, store, widget, then manager
174
+ getCoordinator()?.dispose();
175
+ setCoordinator(null);
176
+ getStore().dispose();
177
+ getWidget()?.dispose();
178
+ setWidget(null);
179
+ const mgr = getManager();
180
+ if (mgr) {
181
+ await mgr.dispose();
182
+ setManager(null);
183
+ }
184
+ });
185
+ }