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.
- package/README.md +184 -235
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
- package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -281
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /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
|
+
}
|