pi-subagents-lite 0.2.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/LICENSE +21 -0
- package/README.md +82 -0
- package/package.json +52 -0
- package/src/agent-discovery.ts +412 -0
- package/src/agent-manager.ts +545 -0
- package/src/agent-runner.ts +435 -0
- package/src/agent-types.ts +140 -0
- package/src/context.ts +13 -0
- package/src/default-agents.ts +67 -0
- package/src/index.ts +1356 -0
- package/src/model-precedence.ts +71 -0
- package/src/model-selector.ts +271 -0
- package/src/output-file.ts +176 -0
- package/src/prompts.ts +61 -0
- package/src/result-viewer.ts +218 -0
- package/src/skill-loader.ts +104 -0
- package/src/types.ts +96 -0
- package/src/ui/agent-widget.ts +666 -0
- package/src/usage.ts +39 -0
- package/src/utils.ts +40 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — Local subagents extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Registers tools, commands, and event listeners at init time.
|
|
5
|
+
*
|
|
6
|
+
* Stealth tool registration:
|
|
7
|
+
* - All tools register at extension init (not runtime)
|
|
8
|
+
* - description: "." (single character — tells LLM nothing)
|
|
9
|
+
* - No promptSnippet, no promptGuidelines
|
|
10
|
+
* - Parameters without .description()
|
|
11
|
+
* - Model parameter removed from schema — injected via tool_call listener
|
|
12
|
+
*
|
|
13
|
+
* Config:
|
|
14
|
+
* - Loaded from ~/.pi/agent/subagents-lite.json at session_start
|
|
15
|
+
* - Module-level __config cache; tool_call reads from cache
|
|
16
|
+
* - Config mutations update cache + atomic write to disk
|
|
17
|
+
* - Migrates subagent-model-defaults.json on first load
|
|
18
|
+
*
|
|
19
|
+
* Commands:
|
|
20
|
+
* - /agents: Management menu with 5 sub-menus
|
|
21
|
+
*
|
|
22
|
+
* Events:
|
|
23
|
+
* - tool_call: Inject model into Agent tool calls
|
|
24
|
+
* - session_start: Load config, register agents, initialise manager
|
|
25
|
+
* - session_shutdown: Abort all, dispose manager
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
29
|
+
import { Type } from "@sinclair/typebox";
|
|
30
|
+
import * as fs from "node:fs";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
import type {
|
|
33
|
+
ExtensionAPI,
|
|
34
|
+
ExtensionCommandContext,
|
|
35
|
+
ExtensionContext,
|
|
36
|
+
ToolCallEvent,
|
|
37
|
+
} from "@earendil-works/pi-coding-agent";
|
|
38
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
39
|
+
import type { SubagentsConfig } from "./model-precedence.js";
|
|
40
|
+
import { resolveModel } from "./model-precedence.js";
|
|
41
|
+
import { resolveType, getAgentConfig, registerAgents, getAvailableTypes, getAllTypes } from "./agent-types.js";
|
|
42
|
+
import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
|
|
43
|
+
import { steerAgent } from "./agent-runner.js";
|
|
44
|
+
import type { AgentRecord, ThinkingLevel } from "./types.js";
|
|
45
|
+
import { ModelSelectorDialog, type ModelOption } from "./model-selector.js";
|
|
46
|
+
import { ResultViewer } from "./result-viewer.js";
|
|
47
|
+
import { AgentManager } from "./agent-manager.js";
|
|
48
|
+
import type { SpawnOptions as AgentManagerSpawnOptions } from "./agent-manager.js";
|
|
49
|
+
import { AgentWidget, formatTurns, formatMs, formatSessionTokens, getDisplayName, type AgentActivity, type UICtx } from "./ui/agent-widget.js";
|
|
50
|
+
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Constants
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
const CONFIG_DIR = path.join(process.env.HOME || "", ".pi", "agent");
|
|
57
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Module-level state
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/** Config cache — loaded at session_start, updated by /agents menu mutations. */
|
|
63
|
+
let __config: SubagentsConfig = {
|
|
64
|
+
agent: { default: null },
|
|
65
|
+
concurrency: { default: 4 },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Agent manager singleton — module-level, no globalThis access. */
|
|
69
|
+
let manager: AgentManager;
|
|
70
|
+
|
|
71
|
+
/** Live activity state per agent, keyed by agent ID. Read by AgentWidget for rendering. */
|
|
72
|
+
const agentActivity = new Map<string, AgentActivity>();
|
|
73
|
+
|
|
74
|
+
/** Live TUI widget showing running/completed agents above the editor. */
|
|
75
|
+
let widget: AgentWidget | undefined;
|
|
76
|
+
|
|
77
|
+
/** ExtensionAPI reference — stored at init for execute callbacks. */
|
|
78
|
+
let piInstance: ExtensionAPI;
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Nudge scheduling (200ms hold to batch completion notifications)
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
/** Agent IDs that were spawned as background — only these trigger a nudge on completion. */
|
|
85
|
+
const backgroundAgentIds = new Set<string>();
|
|
86
|
+
|
|
87
|
+
const pendingNudges = new Set<string>();
|
|
88
|
+
let nudgeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
89
|
+
|
|
90
|
+
function scheduleNudge(agentId: string, record: AgentRecord): void {
|
|
91
|
+
pendingNudges.add(agentId);
|
|
92
|
+
|
|
93
|
+
if (nudgeTimer) return;
|
|
94
|
+
|
|
95
|
+
nudgeTimer = setTimeout(() => {
|
|
96
|
+
nudgeTimer = null;
|
|
97
|
+
const batch = [...pendingNudges];
|
|
98
|
+
pendingNudges.clear();
|
|
99
|
+
|
|
100
|
+
for (const id of batch) {
|
|
101
|
+
emitIndividualNudge(id, manager?.getRecord(id));
|
|
102
|
+
}
|
|
103
|
+
}, 200);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emitIndividualNudge(agentId: string, record?: AgentRecord): void {
|
|
107
|
+
if (!record) return;
|
|
108
|
+
|
|
109
|
+
// Stats go in details only (rendered by the UI message renderer).
|
|
110
|
+
// Content is just the result text — the model only sees this.
|
|
111
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
112
|
+
const elapsedMs = record.completedAt
|
|
113
|
+
? record.completedAt - record.startedAt
|
|
114
|
+
: 0;
|
|
115
|
+
|
|
116
|
+
const details: Record<string, unknown> = {
|
|
117
|
+
type: record.type,
|
|
118
|
+
description: record.description,
|
|
119
|
+
status: record.status,
|
|
120
|
+
outputFile: record.outputFile,
|
|
121
|
+
turnCount: record.turnCount ?? agentActivity.get(agentId)?.turnCount,
|
|
122
|
+
maxTurns: record.maxTurns,
|
|
123
|
+
toolUses: record.toolUses,
|
|
124
|
+
tokens: totalTokens,
|
|
125
|
+
contextPercent: getSessionContextPercent(record.session),
|
|
126
|
+
durationMs: elapsedMs,
|
|
127
|
+
compactions: record.compactionCount,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Deliver the result directly to the session so the model sees it.
|
|
131
|
+
piInstance.sendMessage(
|
|
132
|
+
{
|
|
133
|
+
customType: "subagent-result",
|
|
134
|
+
content: record.result ?? "",
|
|
135
|
+
details,
|
|
136
|
+
display: true,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
deliverAs: "steer",
|
|
140
|
+
triggerTurn: true,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Tool result helpers
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
/** Shortcut for a successful tool result. */
|
|
150
|
+
function successResult(text: string, details?: Record<string, unknown>) {
|
|
151
|
+
return { content: [{ type: "text", text }], details };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Shortcut for an error tool result. */
|
|
155
|
+
function errorResult(text: string, details?: Record<string, unknown>) {
|
|
156
|
+
return { content: [{ type: "text", text }], isError: true as const, details };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Helpers
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse a "provider/model-id" string into { provider, modelId }.
|
|
165
|
+
* Returns null if the format is invalid.
|
|
166
|
+
*/
|
|
167
|
+
function parseModelKey(modelStr: string): { provider: string; modelId: string } | null {
|
|
168
|
+
const slashIdx = modelStr.indexOf("/");
|
|
169
|
+
if (slashIdx <= 0) return null;
|
|
170
|
+
return { provider: modelStr.slice(0, slashIdx), modelId: modelStr.slice(slashIdx + 1) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build ModelOption[] from raw "provider/model-id" strings.
|
|
175
|
+
* Includes "(inherits parent)" as the first option.
|
|
176
|
+
*/
|
|
177
|
+
function buildModelOptions(rawOptions: string[]): ModelOption[] {
|
|
178
|
+
const items: ModelOption[] = [
|
|
179
|
+
{ value: "(inherits parent)", label: "(inherits parent)", provider: "" },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const opt of rawOptions) {
|
|
183
|
+
const parsed = parseModelKey(opt);
|
|
184
|
+
if (!parsed) continue;
|
|
185
|
+
items.push({ value: opt, label: parsed.modelId, provider: parsed.provider });
|
|
186
|
+
}
|
|
187
|
+
return items;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Config persistence (atomic writes)
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
function loadConfig(): SubagentsConfig {
|
|
195
|
+
try {
|
|
196
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
197
|
+
return JSON.parse(raw) as SubagentsConfig;
|
|
198
|
+
} catch {
|
|
199
|
+
// File doesn't exist or is invalid — return defaults
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
agent: { default: null },
|
|
204
|
+
concurrency: { default: 4 },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function saveConfigAtomic(config: SubagentsConfig): void {
|
|
209
|
+
const tmpPath = CONFIG_PATH + ".tmp";
|
|
210
|
+
try {
|
|
211
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
212
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
213
|
+
fs.renameSync(tmpPath, CONFIG_PATH);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`[subagents] Failed to save config: ${err}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Show the ModelSelectorDialog and return the chosen model string, or null.
|
|
221
|
+
*/
|
|
222
|
+
async function promptModelSelection(
|
|
223
|
+
ctx: ExtensionCommandContext,
|
|
224
|
+
modelOptions: string[],
|
|
225
|
+
currentValue: string,
|
|
226
|
+
): Promise<string | null> {
|
|
227
|
+
return ctx.ui.custom<string | null>(
|
|
228
|
+
(tui, theme, _kb, done) => {
|
|
229
|
+
const opts = buildModelOptions(modelOptions);
|
|
230
|
+
return new ModelSelectorDialog(opts, currentValue, {
|
|
231
|
+
onSelect: (m) => done(m),
|
|
232
|
+
onCancel: () => done(null),
|
|
233
|
+
}, theme);
|
|
234
|
+
}, // no overlay — renders inline below editor, matching pi's model selector look and feel
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Show a select menu and dispatch the chosen action.
|
|
240
|
+
* Pattern used by model settings, concurrency settings, and running agents menus.
|
|
241
|
+
*/
|
|
242
|
+
async function runMenu(
|
|
243
|
+
ctx: ExtensionCommandContext,
|
|
244
|
+
title: string,
|
|
245
|
+
items: string[],
|
|
246
|
+
actions: Array<() => Promise<void>>,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
const choice = await ctx.ui.select(title, items);
|
|
249
|
+
if (choice === undefined) return;
|
|
250
|
+
const idx = items.indexOf(choice);
|
|
251
|
+
if (idx >= 0 && idx < actions.length) {
|
|
252
|
+
await actions[idx]();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// /agents command handler
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
260
|
+
async function showAgentsMainMenu(
|
|
261
|
+
ctx: ExtensionCommandContext,
|
|
262
|
+
modelOptions: string[],
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
const menuItems = [
|
|
265
|
+
"1. Model settings — Set global default and per-type model overrides",
|
|
266
|
+
"2. Concurrency settings — Set per-model slot limits",
|
|
267
|
+
"3. Running agents — List running/queued agents",
|
|
268
|
+
"4. Agent types — List available agent types and their configs",
|
|
269
|
+
"5. Agent briefing — Send agent types/capabilities info to LLM (Optional, if having issues)",
|
|
270
|
+
"",
|
|
271
|
+
"Press Escape to close",
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// Loop so sub-menus navigate back to root; only Escape at root closes
|
|
275
|
+
while (true) {
|
|
276
|
+
const choice = await ctx.ui.select("Subagents Management", menuItems);
|
|
277
|
+
if (choice === undefined || choice === "Press Escape to close") return;
|
|
278
|
+
|
|
279
|
+
if (choice.startsWith("1.")) {
|
|
280
|
+
await showModelSettingsMenu(ctx, modelOptions);
|
|
281
|
+
} else if (choice.startsWith("2.")) {
|
|
282
|
+
await showConcurrencySettingsMenu(ctx, modelOptions);
|
|
283
|
+
} else if (choice.startsWith("3.")) {
|
|
284
|
+
await showRunningAgentsMenu(ctx);
|
|
285
|
+
} else if (choice.startsWith("4.")) {
|
|
286
|
+
await showAgentTypes(ctx);
|
|
287
|
+
} else if (choice.startsWith("5.")) {
|
|
288
|
+
await handleAgentBriefing(ctx);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void> {
|
|
294
|
+
const types = getAvailableTypes();
|
|
295
|
+
const agents = types.map((t) => ({ name: t, config: getAgentConfig(t) }));
|
|
296
|
+
|
|
297
|
+
const lines: string[] = [
|
|
298
|
+
"# Agent Types and Capabilities\n",
|
|
299
|
+
"The following agent types are available. Use the `agent` parameter to select one.\n",
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
for (const { name, config } of agents) {
|
|
303
|
+
if (!config) continue;
|
|
304
|
+
lines.push(`## ${config.displayName ?? name}`);
|
|
305
|
+
lines.push(config.description);
|
|
306
|
+
lines.push("");
|
|
307
|
+
|
|
308
|
+
if (config.builtinToolNames) {
|
|
309
|
+
lines.push(`**Tools:** ${config.builtinToolNames.join(", ")}`);
|
|
310
|
+
}
|
|
311
|
+
if (config.model) {
|
|
312
|
+
lines.push(`**Default model:** ${config.model}`);
|
|
313
|
+
}
|
|
314
|
+
if (config.maxTurns) {
|
|
315
|
+
lines.push(`**Max turns:** ${config.maxTurns}`);
|
|
316
|
+
}
|
|
317
|
+
lines.push("");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Parameter descriptions
|
|
321
|
+
lines.push("## Agent Tool Parameters\n");
|
|
322
|
+
lines.push("| Parameter | Description |");
|
|
323
|
+
lines.push("|-----------|-------------|");
|
|
324
|
+
lines.push("| `prompt` | The task for the agent (required) |");
|
|
325
|
+
lines.push("| `description` | One-line summary of what the agent should do (required) |");
|
|
326
|
+
lines.push("| `agent` | Which agent type to use (default: general-purpose) |");
|
|
327
|
+
lines.push("| `thinking` | Optional thinking mode override (e.g., `high`, `medium`, `low`, `off`) |");
|
|
328
|
+
lines.push("| `run_in_background` | When `true`, result is auto-delivered — do NOT poll. Continue working while waiting. |");
|
|
329
|
+
lines.push("| `resume` | Agent ID to resume from; when set, `prompt` is appended to the previous conversation |");
|
|
330
|
+
lines.push("");
|
|
331
|
+
|
|
332
|
+
// Usage guidelines
|
|
333
|
+
lines.push("## Usage Guidelines\n");
|
|
334
|
+
lines.push("- Agents start fresh with their config — they do NOT inherit the parent conversation");
|
|
335
|
+
lines.push("- For parallel tasks, spawn multiple `run_in_background: true` agents in one turn");
|
|
336
|
+
lines.push(" → Results are auto-delivered — do NOT poll, the result will arrive when ready");
|
|
337
|
+
lines.push("- Use `resume` to continue an incomplete agent's conversation");
|
|
338
|
+
piInstance.sendUserMessage(lines.join("\n"));
|
|
339
|
+
ctx.ui.notify("Agent briefing sent to LLM", "info");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function showModelSettingsMenu(
|
|
343
|
+
ctx: ExtensionCommandContext,
|
|
344
|
+
modelOptions: string[],
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
// Loop so actions stay in this menu; only Back/Escape leaves
|
|
347
|
+
while (true) {
|
|
348
|
+
const items: string[] = [];
|
|
349
|
+
const actions: Array<() => Promise<void>> = [];
|
|
350
|
+
|
|
351
|
+
// Global default
|
|
352
|
+
const globalLabel = __config.agent.default
|
|
353
|
+
? `Global default model · ${__config.agent.default}`
|
|
354
|
+
: "Global default model · (inherits parent)";
|
|
355
|
+
items.push(globalLabel);
|
|
356
|
+
actions.push(async () => {
|
|
357
|
+
const chosen = await promptModelSelection(
|
|
358
|
+
ctx, modelOptions, __config.agent.default ?? "(inherits parent)",
|
|
359
|
+
);
|
|
360
|
+
if (chosen === null) return;
|
|
361
|
+
|
|
362
|
+
const updated = { ...__config };
|
|
363
|
+
updated.agent = { ...updated.agent };
|
|
364
|
+
updated.agent.default = chosen === "(inherits parent)" ? null : chosen;
|
|
365
|
+
__config = updated;
|
|
366
|
+
saveConfigAtomic(updated);
|
|
367
|
+
ctx.ui.notify(
|
|
368
|
+
chosen === "(inherits parent)"
|
|
369
|
+
? "Global default cleared — agents inherit parent model"
|
|
370
|
+
: `Global default model set to ${chosen}`,
|
|
371
|
+
"info",
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
items.push("─── per-type overrides ───");
|
|
376
|
+
actions.push(async () => {}); // separator
|
|
377
|
+
|
|
378
|
+
// Per-type overrides
|
|
379
|
+
const types = getAllTypes();
|
|
380
|
+
for (const typeName of types) {
|
|
381
|
+
const cfg = getAgentConfig(typeName);
|
|
382
|
+
const currentOverride = __config.agent[typeName];
|
|
383
|
+
const displayModel = currentOverride
|
|
384
|
+
? currentOverride
|
|
385
|
+
: (cfg?.model ?? __config.agent.default ?? "(inherits parent)");
|
|
386
|
+
const frontmatterHint = currentOverride && cfg?.model ? ` → ${cfg.model}` : "";
|
|
387
|
+
items.push(`${typeName} · ${displayModel}${frontmatterHint}`);
|
|
388
|
+
|
|
389
|
+
actions.push(async () => {
|
|
390
|
+
const currentDisplay = __config.agent[typeName] ?? cfg?.model ?? __config.agent.default ?? "(inherits parent)";
|
|
391
|
+
const chosen = await promptModelSelection(ctx, modelOptions, currentDisplay);
|
|
392
|
+
if (chosen === null) return;
|
|
393
|
+
|
|
394
|
+
const updated = { ...__config };
|
|
395
|
+
updated.agent = { ...updated.agent };
|
|
396
|
+
updated.agent[typeName] = chosen === "(inherits parent)" ? null : chosen;
|
|
397
|
+
__config = updated;
|
|
398
|
+
saveConfigAtomic(updated);
|
|
399
|
+
ctx.ui.notify(
|
|
400
|
+
chosen === "(inherits parent)"
|
|
401
|
+
? `${typeName} inherits parent model`
|
|
402
|
+
: `${typeName} model set to ${chosen}`,
|
|
403
|
+
"info",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Clear all overrides
|
|
409
|
+
items.push("Clear all overrides");
|
|
410
|
+
actions.push(async () => {
|
|
411
|
+
const hasOverrides = Object.entries(__config.agent).some(
|
|
412
|
+
([k, v]) => k !== "default" && v != null,
|
|
413
|
+
);
|
|
414
|
+
if (!hasOverrides && __config.agent.default === null) {
|
|
415
|
+
ctx.ui.notify("No overrides to clear", "info");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const updated = { ...__config };
|
|
419
|
+
updated.agent = { default: __config.agent.default };
|
|
420
|
+
__config = updated;
|
|
421
|
+
saveConfigAtomic(updated);
|
|
422
|
+
ctx.ui.notify("All model overrides cleared", "info");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Append blank spacer + "Back" as the last items
|
|
426
|
+
items.push("");
|
|
427
|
+
actions.push(async () => {});
|
|
428
|
+
items.push("Back");
|
|
429
|
+
actions.push(async () => {});
|
|
430
|
+
|
|
431
|
+
const choice = await ctx.ui.select("Model Settings", items);
|
|
432
|
+
if (choice === undefined || choice === "Back") return;
|
|
433
|
+
const idx = items.indexOf(choice);
|
|
434
|
+
if (idx >= 0 && idx < actions.length) {
|
|
435
|
+
await actions[idx]();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function showConcurrencySettingsMenu(
|
|
441
|
+
ctx: ExtensionCommandContext,
|
|
442
|
+
modelOptions: string[],
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
// Loop so actions stay in this menu; only Back/Escape leaves
|
|
445
|
+
while (true) {
|
|
446
|
+
const items: string[] = [];
|
|
447
|
+
const actions: Array<() => Promise<void>> = [];
|
|
448
|
+
|
|
449
|
+
// Global default
|
|
450
|
+
items.push(`Default concurrency limit · ${__config.concurrency.default}`);
|
|
451
|
+
actions.push(async () => {
|
|
452
|
+
const input = await ctx.ui.input(
|
|
453
|
+
"Default concurrency limit",
|
|
454
|
+
String(__config.concurrency.default),
|
|
455
|
+
);
|
|
456
|
+
if (input === undefined) return;
|
|
457
|
+
const parsed = parseInt(input.trim(), 10);
|
|
458
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
459
|
+
ctx.ui.notify("Invalid value — must be a number ≥ 1", "error");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const updated = { ...__config };
|
|
463
|
+
updated.concurrency = { ...updated.concurrency, default: parsed };
|
|
464
|
+
__config = updated;
|
|
465
|
+
saveConfigAtomic(updated);
|
|
466
|
+
ctx.ui.notify(`Default concurrency limit set to ${parsed}`, "info");
|
|
467
|
+
manager?.setConcurrency({
|
|
468
|
+
default: __config.concurrency.default,
|
|
469
|
+
providers: __config.concurrency.providers ?? {},
|
|
470
|
+
models: __config.concurrency.models ?? {},
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Extract unique providers from model options
|
|
475
|
+
const providers = [...new Set(modelOptions.map((m) => m.split("/")[0]))].sort();
|
|
476
|
+
|
|
477
|
+
// Per-provider limits
|
|
478
|
+
const providerLimits = __config.concurrency.providers ?? {};
|
|
479
|
+
const configuredProviders = Object.keys(providerLimits);
|
|
480
|
+
if (configuredProviders.length > 0) {
|
|
481
|
+
items.push("─── per-provider limits ───");
|
|
482
|
+
actions.push(async () => {}); // separator
|
|
483
|
+
|
|
484
|
+
for (const provider of configuredProviders) {
|
|
485
|
+
items.push(`${provider} · ${providerLimits[provider]} slots`);
|
|
486
|
+
actions.push(async () => {
|
|
487
|
+
const input = await ctx.ui.input(
|
|
488
|
+
`Concurrency slots for ${provider}`,
|
|
489
|
+
String(providerLimits[provider]),
|
|
490
|
+
);
|
|
491
|
+
if (input === undefined) return;
|
|
492
|
+
const parsed = parseInt(input.trim(), 10);
|
|
493
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
494
|
+
ctx.ui.notify("Invalid value — must be a number ≥ 1", "error");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const updated = { ...__config };
|
|
498
|
+
updated.concurrency.providers = { ...providerLimits, [provider]: parsed };
|
|
499
|
+
__config = updated;
|
|
500
|
+
saveConfigAtomic(updated);
|
|
501
|
+
ctx.ui.notify(`${provider} concurrency set to ${parsed}`, "info");
|
|
502
|
+
manager?.setConcurrency({
|
|
503
|
+
default: __config.concurrency.default,
|
|
504
|
+
providers: __config.concurrency.providers ?? {},
|
|
505
|
+
models: __config.concurrency.models ?? {},
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Add per-provider limit
|
|
512
|
+
items.push("Add per-provider limit...");
|
|
513
|
+
actions.push(async () => {
|
|
514
|
+
const currentProviders = __config.concurrency.providers ?? {};
|
|
515
|
+
const provider = await ctx.ui.select("Select provider", providers);
|
|
516
|
+
if (provider === undefined) return;
|
|
517
|
+
const input = await ctx.ui.input("Concurrency slots", "1");
|
|
518
|
+
if (input === undefined) return;
|
|
519
|
+
const parsed = parseInt(input.trim(), 10);
|
|
520
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
521
|
+
ctx.ui.notify("Invalid value — must be a number ≥ 1", "error");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const updated = { ...__config };
|
|
525
|
+
updated.concurrency.providers = { ...currentProviders, [provider]: parsed };
|
|
526
|
+
__config = updated;
|
|
527
|
+
saveConfigAtomic(updated);
|
|
528
|
+
ctx.ui.notify(`${provider} concurrency set to ${parsed}`, "info");
|
|
529
|
+
manager?.setConcurrency({
|
|
530
|
+
default: __config.concurrency.default,
|
|
531
|
+
providers: __config.concurrency.providers ?? {},
|
|
532
|
+
models: __config.concurrency.models ?? {},
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Per-model limits
|
|
537
|
+
const models = __config.concurrency.models ?? {};
|
|
538
|
+
const modelKeys = Object.keys(models);
|
|
539
|
+
if (modelKeys.length > 0) {
|
|
540
|
+
items.push("─── per-model limits ───");
|
|
541
|
+
actions.push(async () => {}); // separator
|
|
542
|
+
|
|
543
|
+
for (const modelKey of modelKeys) {
|
|
544
|
+
items.push(`${modelKey} · ${models[modelKey]} slots`);
|
|
545
|
+
actions.push(async () => {
|
|
546
|
+
const input = await ctx.ui.input(
|
|
547
|
+
`Concurrency slots for ${modelKey}`,
|
|
548
|
+
String(models[modelKey]),
|
|
549
|
+
);
|
|
550
|
+
if (input === undefined) return;
|
|
551
|
+
const parsed = parseInt(input.trim(), 10);
|
|
552
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
553
|
+
ctx.ui.notify("Invalid value — must be a number ≥ 1", "error");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const updated = { ...__config };
|
|
557
|
+
updated.concurrency.models = { ...models, [modelKey]: parsed };
|
|
558
|
+
__config = updated;
|
|
559
|
+
saveConfigAtomic(updated);
|
|
560
|
+
ctx.ui.notify(`${modelKey} concurrency set to ${parsed}`, "info");
|
|
561
|
+
manager?.setConcurrency({
|
|
562
|
+
default: __config.concurrency.default,
|
|
563
|
+
providers: __config.concurrency.providers ?? {},
|
|
564
|
+
models: __config.concurrency.models ?? {},
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Add per-model limit
|
|
571
|
+
items.push("Add per-model limit...");
|
|
572
|
+
actions.push(async () => {
|
|
573
|
+
const currentModels = __config.concurrency.models ?? {};
|
|
574
|
+
const modelKey = await promptModelSelection(
|
|
575
|
+
ctx,
|
|
576
|
+
modelOptions,
|
|
577
|
+
__config.agent.default ?? "(inherits parent)",
|
|
578
|
+
);
|
|
579
|
+
if (modelKey === null) return;
|
|
580
|
+
const input = await ctx.ui.input("Concurrency slots", "1");
|
|
581
|
+
if (input === undefined) return;
|
|
582
|
+
const parsed = parseInt(input.trim(), 10);
|
|
583
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
584
|
+
ctx.ui.notify("Invalid value — must be a number ≥ 1", "error");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const updated = { ...__config };
|
|
588
|
+
updated.concurrency.models = { ...currentModels, [modelKey.trim()]: parsed };
|
|
589
|
+
__config = updated;
|
|
590
|
+
saveConfigAtomic(updated);
|
|
591
|
+
ctx.ui.notify(`${modelKey.trim()} concurrency set to ${parsed}`, "info");
|
|
592
|
+
manager?.setConcurrency({
|
|
593
|
+
default: __config.concurrency.default,
|
|
594
|
+
providers: __config.concurrency.providers ?? {},
|
|
595
|
+
models: __config.concurrency.models ?? {},
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Append blank spacer + "Back" as the last items
|
|
600
|
+
items.push("");
|
|
601
|
+
actions.push(async () => {});
|
|
602
|
+
items.push("Back");
|
|
603
|
+
actions.push(async () => {});
|
|
604
|
+
|
|
605
|
+
const choice = await ctx.ui.select("Concurrency Settings", items);
|
|
606
|
+
if (choice === undefined || choice === "Back") return;
|
|
607
|
+
const idx = items.indexOf(choice);
|
|
608
|
+
if (idx >= 0 && idx < actions.length) {
|
|
609
|
+
await actions[idx]();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function showRunningAgentsMenu(
|
|
615
|
+
ctx: ExtensionCommandContext,
|
|
616
|
+
): Promise<void> {
|
|
617
|
+
// Loop so sub-actions navigate back to this menu; only Escape closes
|
|
618
|
+
while (true) {
|
|
619
|
+
const records = manager?.listAgents() ?? [];
|
|
620
|
+
const running = records.filter((r) => r.status === "running" || r.status === "queued");
|
|
621
|
+
|
|
622
|
+
if (records.length === 0) {
|
|
623
|
+
ctx.ui.notify("No agents have been spawned this session", "info");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const items: string[] = [];
|
|
628
|
+
const actions: Array<() => Promise<void>> = [];
|
|
629
|
+
|
|
630
|
+
for (const record of records) {
|
|
631
|
+
const elapsed = Math.round((Date.now() - record.startedAt) / 1000);
|
|
632
|
+
const statusIcon = record.status === "running" ? "▶" :
|
|
633
|
+
record.status === "completed" ? "✓" :
|
|
634
|
+
record.status === "queued" ? "⏳" :
|
|
635
|
+
record.status === "error" ? "✗" : "•";
|
|
636
|
+
items.push(
|
|
637
|
+
`${statusIcon} ${record.id.slice(0, 8)} ${record.type} ${record.status} ${elapsed}s`,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
actions.push(async () => {
|
|
641
|
+
await showAgentActions(ctx, record);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (running.length > 0) {
|
|
646
|
+
items.push("─── actions ───");
|
|
647
|
+
actions.push(async () => {}); // separator
|
|
648
|
+
|
|
649
|
+
items.push(`Stop ${running.length} running agent(s)`);
|
|
650
|
+
actions.push(async () => {
|
|
651
|
+
for (const record of running) {
|
|
652
|
+
manager?.abort(record.id);
|
|
653
|
+
}
|
|
654
|
+
ctx.ui.notify(`Stopped ${running.length} agent(s)`, "info");
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Append blank spacer + "Back" as the last items
|
|
659
|
+
items.push("");
|
|
660
|
+
actions.push(async () => {});
|
|
661
|
+
items.push("Back");
|
|
662
|
+
actions.push(async () => {});
|
|
663
|
+
|
|
664
|
+
const choice = await ctx.ui.select("Running Agents", items);
|
|
665
|
+
if (choice === undefined || choice === "Back") return;
|
|
666
|
+
const idx = items.indexOf(choice);
|
|
667
|
+
if (idx >= 0 && idx < actions.length) {
|
|
668
|
+
await actions[idx]();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Send a steer message to a specific agent. Used by the per-agent action menu.
|
|
675
|
+
*/
|
|
676
|
+
async function steerAgentById(
|
|
677
|
+
agentId: string,
|
|
678
|
+
ctx: ExtensionCommandContext,
|
|
679
|
+
): Promise<void> {
|
|
680
|
+
const record = manager?.getRecord(agentId);
|
|
681
|
+
if (!record) {
|
|
682
|
+
ctx.ui.notify("Agent not found", "error");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const message = await ctx.ui.input(`Steer ${record.type}`);
|
|
687
|
+
if (!message?.trim()) return;
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
if (!record.session) {
|
|
691
|
+
if (!record.pendingSteers) {
|
|
692
|
+
record.pendingSteers = [];
|
|
693
|
+
}
|
|
694
|
+
record.pendingSteers.push(message.trim());
|
|
695
|
+
ctx.ui.notify(`Steer message queued for ${record.id.slice(0, 8)}…`, "info");
|
|
696
|
+
} else {
|
|
697
|
+
await steerAgent(record.session, message.trim());
|
|
698
|
+
ctx.ui.notify(`Steer sent to ${record.id.slice(0, 8)}…`, "info");
|
|
699
|
+
}
|
|
700
|
+
} catch (err) {
|
|
701
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
702
|
+
ctx.ui.notify(`Steer failed: ${msg}`, "error");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Sub-menu with actions for a single agent. Replaces the old showAgentDetail
|
|
708
|
+
* notify popup — clicking an agent in the running agents menu opens actions.
|
|
709
|
+
*/
|
|
710
|
+
async function showAgentActions(
|
|
711
|
+
ctx: ExtensionCommandContext,
|
|
712
|
+
record: AgentRecord,
|
|
713
|
+
): Promise<void> {
|
|
714
|
+
const items: string[] = [];
|
|
715
|
+
const actions: Array<() => Promise<void>> = [];
|
|
716
|
+
|
|
717
|
+
const isRunning = record.status === "running" || record.status === "queued";
|
|
718
|
+
const hasResult = !!record.result && record.result.length > 0;
|
|
719
|
+
const hasError = !!record.error && record.error.length > 0;
|
|
720
|
+
|
|
721
|
+
if (isRunning) {
|
|
722
|
+
items.push("Steer");
|
|
723
|
+
actions.push(async () => {
|
|
724
|
+
await steerAgentById(record.id, ctx);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
items.push("Stop");
|
|
728
|
+
actions.push(async () => {
|
|
729
|
+
manager?.abort(record.id);
|
|
730
|
+
ctx.ui.notify(`Stopped ${record.id.slice(0, 8)}`, "info");
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (hasResult) {
|
|
735
|
+
items.push("View result");
|
|
736
|
+
actions.push(async () => {
|
|
737
|
+
await ctx.ui.custom<void>(
|
|
738
|
+
(tui, theme, _kb, done) =>
|
|
739
|
+
new ResultViewer(
|
|
740
|
+
`${getDisplayName(record.type)} · ${record.id.slice(0, 8)}`,
|
|
741
|
+
record.result!,
|
|
742
|
+
{ onClose: () => done() },
|
|
743
|
+
theme,
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (hasError) {
|
|
750
|
+
items.push("View error");
|
|
751
|
+
actions.push(async () => {
|
|
752
|
+
await ctx.ui.custom<void>(
|
|
753
|
+
(tui, theme, _kb, done) =>
|
|
754
|
+
new ResultViewer(
|
|
755
|
+
`${getDisplayName(record.type)} · Error`,
|
|
756
|
+
record.error!,
|
|
757
|
+
{ onClose: () => done() },
|
|
758
|
+
theme,
|
|
759
|
+
),
|
|
760
|
+
);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (items.length === 0) {
|
|
765
|
+
ctx.ui.notify(`Agent ${record.id.slice(0, 8)} — no actions available`, "info");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Append blank spacer + "Back" as the last items
|
|
770
|
+
items.push("");
|
|
771
|
+
actions.push(async () => {});
|
|
772
|
+
items.push("Back");
|
|
773
|
+
actions.push(async () => {});
|
|
774
|
+
|
|
775
|
+
await runMenu(ctx, `Agent ${record.id.slice(0, 8)}`, items, actions);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
|
|
779
|
+
const types = getAllTypes();
|
|
780
|
+
if (types.length === 0) {
|
|
781
|
+
ctx.ui.notify("No agent types available", "info");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const lines: string[] = ["Available agent types:\n"];
|
|
786
|
+
for (const name of types) {
|
|
787
|
+
const cfg = getAgentConfig(name);
|
|
788
|
+
if (!cfg) continue;
|
|
789
|
+
const disabled = cfg.enabled === false ? " [DISABLED]" : "";
|
|
790
|
+
const model = cfg.model ? ` Model: ${cfg.model}` : "";
|
|
791
|
+
const tools = cfg.builtinToolNames
|
|
792
|
+
? ` Tools: ${cfg.builtinToolNames.join(", ")}`
|
|
793
|
+
: " Tools: all built-in tools";
|
|
794
|
+
const source = cfg.source ? ` Source: ${cfg.source}` : "";
|
|
795
|
+
lines.push(` ${name}${disabled}`);
|
|
796
|
+
lines.push(` ${cfg.description}`);
|
|
797
|
+
if (model) lines.push(model);
|
|
798
|
+
lines.push(tools);
|
|
799
|
+
if (source) lines.push(source);
|
|
800
|
+
lines.push("");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// Config loader — session_start handler logic
|
|
808
|
+
// ============================================================================
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Ensure the manager and widget singletons exist.
|
|
812
|
+
* Idempotent — safe to call on every session_start.
|
|
813
|
+
*/
|
|
814
|
+
function ensureManagerAndWidget(): void {
|
|
815
|
+
if (manager) return;
|
|
816
|
+
|
|
817
|
+
const concurrencyConfig = {
|
|
818
|
+
default: __config.concurrency.default,
|
|
819
|
+
providers: __config.concurrency.providers ?? {},
|
|
820
|
+
models: __config.concurrency.models ?? {},
|
|
821
|
+
};
|
|
822
|
+
manager = new AgentManager(
|
|
823
|
+
(record) => {
|
|
824
|
+
// Only nudge for background (async) agents — sync agents already returned via tool result
|
|
825
|
+
if (backgroundAgentIds.has(record.id)) {
|
|
826
|
+
scheduleNudge(record.id, record);
|
|
827
|
+
backgroundAgentIds.delete(record.id);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Mark finished and update widget BEFORE deleting activity —
|
|
831
|
+
// renderFinishedLine reads activity for turn count, tokens, etc.
|
|
832
|
+
widget?.markFinished(record.id);
|
|
833
|
+
widget?.update();
|
|
834
|
+
|
|
835
|
+
// Remove from live activity tracking
|
|
836
|
+
agentActivity.delete(record.id);
|
|
837
|
+
},
|
|
838
|
+
concurrencyConfig,
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// Create/replace widget tied to this manager instance
|
|
842
|
+
if (!widget) {
|
|
843
|
+
widget = new AgentWidget(manager, agentActivity);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Scan agent files from user and project directories, merge with defaults,
|
|
849
|
+
* and register into the type registry.
|
|
850
|
+
*/
|
|
851
|
+
async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
852
|
+
const homeDir = process.env.HOME || "";
|
|
853
|
+
const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
|
|
854
|
+
const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
|
|
855
|
+
|
|
856
|
+
const [userAgents, projectAgents] = await Promise.all([
|
|
857
|
+
scanAgentFilesInDir(userAgentDir, "user"),
|
|
858
|
+
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
859
|
+
]);
|
|
860
|
+
|
|
861
|
+
const { DEFAULT_AGENTS } = await import("./default-agents.js");
|
|
862
|
+
|
|
863
|
+
// Merge with defaults
|
|
864
|
+
const merged = mergeAgents(DEFAULT_AGENTS, userAgents, projectAgents);
|
|
865
|
+
|
|
866
|
+
// Register into the type registry
|
|
867
|
+
registerAgents(merged);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
871
|
+
// Load config (with migration if needed)
|
|
872
|
+
__config = loadConfig();
|
|
873
|
+
|
|
874
|
+
// Ensure manager exists
|
|
875
|
+
ensureManagerAndWidget();
|
|
876
|
+
|
|
877
|
+
// Scan agent files and register
|
|
878
|
+
await scanAndRegisterAgents(ctx);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ============================================================================
|
|
882
|
+
// Activity tracking — bridge between spawn callbacks and widget renderer
|
|
883
|
+
// ============================================================================
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Create an AgentActivity state and spawn callbacks for tracking tool usage.
|
|
887
|
+
* Used by both foreground and background paths to avoid duplication.
|
|
888
|
+
*/
|
|
889
|
+
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
890
|
+
const state: AgentActivity = {
|
|
891
|
+
activeTools: new Map(),
|
|
892
|
+
toolUses: 0,
|
|
893
|
+
turnCount: 1,
|
|
894
|
+
maxTurns,
|
|
895
|
+
responseText: "",
|
|
896
|
+
session: undefined,
|
|
897
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const callbacks = {
|
|
901
|
+
onToolActivity: (activity: { type: "start" | "end"; toolName: string }) => {
|
|
902
|
+
if (activity.type === "start") {
|
|
903
|
+
state.activeTools.set(activity.toolName + "_" + Date.now(), activity.toolName);
|
|
904
|
+
} else {
|
|
905
|
+
for (const [key, name] of state.activeTools) {
|
|
906
|
+
if (name === activity.toolName) { state.activeTools.delete(key); break; }
|
|
907
|
+
}
|
|
908
|
+
state.toolUses++;
|
|
909
|
+
}
|
|
910
|
+
onStreamUpdate?.();
|
|
911
|
+
},
|
|
912
|
+
onTextDelta: (_delta: string, fullText: string) => {
|
|
913
|
+
state.responseText = fullText;
|
|
914
|
+
onStreamUpdate?.();
|
|
915
|
+
},
|
|
916
|
+
onTurnEnd: (turnCount: number) => {
|
|
917
|
+
state.turnCount = turnCount;
|
|
918
|
+
onStreamUpdate?.();
|
|
919
|
+
},
|
|
920
|
+
onSessionCreated: (session: unknown) => {
|
|
921
|
+
state.session = session as Parameters<typeof getSessionContextPercent>[0];
|
|
922
|
+
},
|
|
923
|
+
onAssistantUsage: (usage: { input: number; output: number; cacheWrite: number }) => {
|
|
924
|
+
addUsage(state.lifetimeUsage, usage);
|
|
925
|
+
onStreamUpdate?.();
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
return { state, callbacks };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ============================================================================
|
|
933
|
+
// Tool execute handlers
|
|
934
|
+
// ============================================================================
|
|
935
|
+
|
|
936
|
+
// These are wired in the registerTool calls below.
|
|
937
|
+
// We define them as functions here for clarity.
|
|
938
|
+
|
|
939
|
+
async function executeAgentTool(
|
|
940
|
+
_toolCallId: string,
|
|
941
|
+
params: Record<string, unknown>,
|
|
942
|
+
_signal: AbortSignal | undefined,
|
|
943
|
+
_onUpdate: ((update: any) => void) | undefined,
|
|
944
|
+
ctx: ExtensionContext,
|
|
945
|
+
): Promise<any> {
|
|
946
|
+
// Resolve type — default to general-purpose when not specified
|
|
947
|
+
const type = (params.agent as string) || "general-purpose";
|
|
948
|
+
const resolvedType = resolveType(type);
|
|
949
|
+
if (!resolvedType) {
|
|
950
|
+
return errorResult(`Unknown agent type: ${type}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const prompt = params.prompt as string;
|
|
954
|
+
const description = params.description as string;
|
|
955
|
+
const resume = params.resume as string | undefined;
|
|
956
|
+
const runInBackground = params.run_in_background as boolean | undefined;
|
|
957
|
+
const isolated = params.isolated as boolean | undefined;
|
|
958
|
+
const maxTurns = params.max_turns as number | undefined;
|
|
959
|
+
const thinking = params.thinking as string | undefined;
|
|
960
|
+
|
|
961
|
+
// Model is injected by tool_call listener — use it directly
|
|
962
|
+
const modelStr = params.model as string | undefined;
|
|
963
|
+
|
|
964
|
+
// Resolve model string to Model object
|
|
965
|
+
const model = resolveModelString(modelStr, ctx);
|
|
966
|
+
|
|
967
|
+
// Compute modelKey for concurrency pool lookup
|
|
968
|
+
const modelKey = model ? `${model.provider}/${model.id}` : undefined;
|
|
969
|
+
|
|
970
|
+
if (resume) {
|
|
971
|
+
return executeResumeAgent(resume, prompt);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const spawnOptions: AgentManagerSpawnOptions = {
|
|
975
|
+
description,
|
|
976
|
+
model,
|
|
977
|
+
maxTurns,
|
|
978
|
+
isolated,
|
|
979
|
+
thinkingLevel: thinking as ThinkingLevel | undefined,
|
|
980
|
+
modelKey,
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
if (runInBackground) {
|
|
984
|
+
return executeSpawnBackground(resolvedType, prompt, ctx, spawnOptions);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return executeSpawnForeground(resolvedType, prompt, ctx, spawnOptions);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ============================================================================
|
|
991
|
+
// Model string resolution
|
|
992
|
+
// ============================================================================
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Parse a "provider/model-id" string into a Model object.
|
|
996
|
+
* Falls back to ctx.model if the string lacks a provider or the registry
|
|
997
|
+
* can't find the model.
|
|
998
|
+
*/
|
|
999
|
+
function resolveModelString(
|
|
1000
|
+
modelStr: string | undefined,
|
|
1001
|
+
ctx: ExtensionContext,
|
|
1002
|
+
): Model<any> | undefined {
|
|
1003
|
+
if (!modelStr) return undefined;
|
|
1004
|
+
|
|
1005
|
+
const parsed = parseModelKey(modelStr);
|
|
1006
|
+
if (!parsed) return ctx.model;
|
|
1007
|
+
|
|
1008
|
+
return ctx.modelRegistry?.find(parsed.provider, parsed.modelId) ?? ctx.model;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ============================================================================
|
|
1012
|
+
// Sub-handlers for executeAgentTool
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
|
|
1015
|
+
async function executeResumeAgent(
|
|
1016
|
+
resume: string,
|
|
1017
|
+
prompt: string,
|
|
1018
|
+
): Promise<any> {
|
|
1019
|
+
const record = await manager.resume(resume, prompt);
|
|
1020
|
+
if (!record) {
|
|
1021
|
+
return errorResult(`Agent not found: ${resume}`);
|
|
1022
|
+
}
|
|
1023
|
+
return successResult(record.result ?? "");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function executeSpawnBackground(
|
|
1027
|
+
resolvedType: string,
|
|
1028
|
+
prompt: string,
|
|
1029
|
+
ctx: ExtensionContext,
|
|
1030
|
+
spawnOptions: AgentManagerSpawnOptions,
|
|
1031
|
+
): Promise<any> {
|
|
1032
|
+
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(
|
|
1033
|
+
spawnOptions.maxTurns,
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
const agentId = manager.spawn(piInstance, ctx, resolvedType, prompt, {
|
|
1037
|
+
...spawnOptions,
|
|
1038
|
+
isBackground: true,
|
|
1039
|
+
...bgCallbacks,
|
|
1040
|
+
});
|
|
1041
|
+
backgroundAgentIds.add(agentId);
|
|
1042
|
+
agentActivity.set(agentId, bgState);
|
|
1043
|
+
widget?.ensureTimer();
|
|
1044
|
+
widget?.update();
|
|
1045
|
+
|
|
1046
|
+
const record = manager.getRecord(agentId);
|
|
1047
|
+
if (!record) {
|
|
1048
|
+
return errorResult("Failed to create agent");
|
|
1049
|
+
}
|
|
1050
|
+
const bgDetails: Record<string, unknown> = { type: resolvedType, description: spawnOptions.description };
|
|
1051
|
+
if (record.status === "queued") {
|
|
1052
|
+
return successResult(`[Agent queued] Concurrency limit reached. It will start automatically when a slot frees up. Do NOT poll — you will be notified when ready.
|
|
1053
|
+
|
|
1054
|
+
Agent ID: ${agentId}`, bgDetails);
|
|
1055
|
+
}
|
|
1056
|
+
return successResult(
|
|
1057
|
+
`[Agent started in background] Do NOT poll — the result will be delivered to you automatically when it completes. Continue with other work while waiting.\n\nAgent ID: ${agentId}`,
|
|
1058
|
+
bgDetails,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function executeSpawnForeground(
|
|
1063
|
+
resolvedType: string,
|
|
1064
|
+
prompt: string,
|
|
1065
|
+
ctx: ExtensionContext,
|
|
1066
|
+
spawnOptions: AgentManagerSpawnOptions,
|
|
1067
|
+
): Promise<any> {
|
|
1068
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(
|
|
1069
|
+
spawnOptions.maxTurns,
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
// Capture agent ID when session is created
|
|
1073
|
+
let fgId: string | undefined;
|
|
1074
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
1075
|
+
fgCallbacks.onSessionCreated = (session) => {
|
|
1076
|
+
origOnSession(session);
|
|
1077
|
+
for (const a of manager!.listAgents()) {
|
|
1078
|
+
if (a.session === session) {
|
|
1079
|
+
fgId = a.id;
|
|
1080
|
+
agentActivity.set(a.id, fgState);
|
|
1081
|
+
widget?.ensureTimer();
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const { isBackground: _isBackground, ...spawnOpts } = spawnOptions;
|
|
1088
|
+
const record = await manager.spawnAndWait(piInstance, ctx, resolvedType, prompt, {
|
|
1089
|
+
...spawnOpts,
|
|
1090
|
+
...fgCallbacks,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// Clean up foreground agent from widget
|
|
1094
|
+
if (fgId) {
|
|
1095
|
+
agentActivity.delete(fgId);
|
|
1096
|
+
widget?.markFinished(fgId);
|
|
1097
|
+
widget?.update();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Build raw stats for the reply card — formatted in renderResult with theme
|
|
1101
|
+
const elapsedMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
1102
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
1103
|
+
const stats = {
|
|
1104
|
+
type: resolvedType,
|
|
1105
|
+
turnCount: fgState.turnCount,
|
|
1106
|
+
maxTurns: fgState.maxTurns,
|
|
1107
|
+
toolUses: record.toolUses,
|
|
1108
|
+
tokens: totalTokens,
|
|
1109
|
+
contextPercent: getSessionContextPercent(fgState.session),
|
|
1110
|
+
durationMs: elapsedMs,
|
|
1111
|
+
description: spawnOptions.description,
|
|
1112
|
+
compactions: record.compactionCount,
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
if (record.status === "error") {
|
|
1116
|
+
return errorResult(`Agent failed: ${record.error || "unknown error"}`, stats as any);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return successResult(record.result ?? "", stats as any);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ============================================================================
|
|
1123
|
+
// Tool_call listener — inject model into Agent tool calls
|
|
1124
|
+
// ============================================================================
|
|
1125
|
+
|
|
1126
|
+
async function toolCallListener(
|
|
1127
|
+
event: ToolCallEvent,
|
|
1128
|
+
ctx: ExtensionContext,
|
|
1129
|
+
): Promise<void> {
|
|
1130
|
+
// Only handle Agent tool calls
|
|
1131
|
+
if (event.toolName !== "Agent") return;
|
|
1132
|
+
|
|
1133
|
+
const input = event.input;
|
|
1134
|
+
const subagentType = input.agent as string | undefined;
|
|
1135
|
+
const agentConfig = subagentType ? getAgentConfig(subagentType) : undefined;
|
|
1136
|
+
|
|
1137
|
+
// Resolve effective model using precedence chain
|
|
1138
|
+
const effectiveModel = resolveModel(
|
|
1139
|
+
subagentType ?? "general-purpose",
|
|
1140
|
+
agentConfig,
|
|
1141
|
+
__config,
|
|
1142
|
+
ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "",
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
if (effectiveModel) {
|
|
1146
|
+
input.model = effectiveModel;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ============================================================================
|
|
1151
|
+
// Extension factory
|
|
1152
|
+
// ============================================================================
|
|
1153
|
+
|
|
1154
|
+
export default function (pi: ExtensionAPI) {
|
|
1155
|
+
// Store pi for execute callbacks
|
|
1156
|
+
piInstance = pi;
|
|
1157
|
+
|
|
1158
|
+
// ========================================================================
|
|
1159
|
+
// Tool registration (stealth schemas — at init time)
|
|
1160
|
+
// ========================================================================
|
|
1161
|
+
|
|
1162
|
+
// Agent tool — stealth schema
|
|
1163
|
+
pi.registerTool({
|
|
1164
|
+
name: "Agent",
|
|
1165
|
+
label: "Agent",
|
|
1166
|
+
description: ".",
|
|
1167
|
+
// No promptSnippet, no promptGuidelines
|
|
1168
|
+
parameters: Type.Object({
|
|
1169
|
+
prompt: Type.String(),
|
|
1170
|
+
description: Type.String(),
|
|
1171
|
+
agent: Type.Optional(Type.String()),
|
|
1172
|
+
thinking: Type.Optional(Type.String()),
|
|
1173
|
+
run_in_background: Type.Optional(Type.Boolean()),
|
|
1174
|
+
resume: Type.Optional(Type.String()),
|
|
1175
|
+
}),
|
|
1176
|
+
execute: executeAgentTool,
|
|
1177
|
+
|
|
1178
|
+
renderCall(args, theme) {
|
|
1179
|
+
const typeName = getDisplayName((args.agent as string) || "");
|
|
1180
|
+
const label = typeName || "Agent";
|
|
1181
|
+
return new Text("▸ " + theme.fg("accent", theme.bold(label)), 0, 0);
|
|
1182
|
+
},
|
|
1183
|
+
|
|
1184
|
+
renderResult(result, options, theme) {
|
|
1185
|
+
const { expanded } = options as { expanded?: boolean };
|
|
1186
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
1187
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
1188
|
+
const isError = !!(result as any).isError;
|
|
1189
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1190
|
+
|
|
1191
|
+
const typeName = getDisplayName((d?.type as string) || "");
|
|
1192
|
+
const desc = (d?.description as string) || "";
|
|
1193
|
+
|
|
1194
|
+
if (d && d.turnCount != null) {
|
|
1195
|
+
// Rich stats card — format with theme (matching pi-subagents style)
|
|
1196
|
+
const parts: string[] = [];
|
|
1197
|
+
if ((d.toolUses as number) > 0) {
|
|
1198
|
+
parts.push(`${d.toolUses}🛠 `);
|
|
1199
|
+
}
|
|
1200
|
+
if (d.turnCount != null && (d.turnCount as number) > 0) {
|
|
1201
|
+
parts.push(formatTurns(d.turnCount as number, d.maxTurns as number | undefined));
|
|
1202
|
+
}
|
|
1203
|
+
if ((d.tokens as number) > 0) {
|
|
1204
|
+
const tokenText = formatSessionTokens(
|
|
1205
|
+
d.tokens as number,
|
|
1206
|
+
d.contextPercent as number | null,
|
|
1207
|
+
theme,
|
|
1208
|
+
(d.compactions as number) ?? 0,
|
|
1209
|
+
);
|
|
1210
|
+
parts.push(tokenText);
|
|
1211
|
+
}
|
|
1212
|
+
parts.push(formatMs(d.durationMs as number));
|
|
1213
|
+
|
|
1214
|
+
const statsLine = parts.join("·");
|
|
1215
|
+
let lines = `${icon} ${theme.bold(typeName)}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
1216
|
+
if (expanded && text) {
|
|
1217
|
+
lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
|
|
1218
|
+
}
|
|
1219
|
+
return new Text(lines, 0, 0);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Minimal card when we have type/description but no stats (e.g. background spawn)
|
|
1223
|
+
if (typeName || desc) {
|
|
1224
|
+
let lines = `${icon}`;
|
|
1225
|
+
if (typeName) lines += ` ${theme.bold(typeName)}`;
|
|
1226
|
+
if (desc) lines += `\n ${theme.fg("text", desc)}`;
|
|
1227
|
+
return new Text(lines, 0, 0);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return new Text(`${icon} ${theme.fg("dim", text)}`, 0, 0);
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// ========================================================================
|
|
1235
|
+
// Message renderer — subagent-result (background agent completion)
|
|
1236
|
+
// ========================================================================
|
|
1237
|
+
// Renders a collapsible stats card matching the foreground Agent tool card.
|
|
1238
|
+
// Stats come from `details` (UI-only), content is just the result text.
|
|
1239
|
+
|
|
1240
|
+
pi.registerMessageRenderer("subagent-result", (message, options, theme) => {
|
|
1241
|
+
const { expanded } = options as { expanded?: boolean };
|
|
1242
|
+
const d = message.details as Record<string, unknown> | undefined;
|
|
1243
|
+
const text = (message.content as string)?.trim() || "";
|
|
1244
|
+
|
|
1245
|
+
// Build the content inside the purple card
|
|
1246
|
+
const inner = new Container();
|
|
1247
|
+
|
|
1248
|
+
// Title — matches default CustomMessageComponent style
|
|
1249
|
+
const titleText = theme.fg("customMessageLabel", `[subagent-result]`);
|
|
1250
|
+
inner.addChild(new Text(titleText, 0, 0));
|
|
1251
|
+
inner.addChild(new Spacer(1));
|
|
1252
|
+
|
|
1253
|
+
if (d && d.turnCount != null) {
|
|
1254
|
+
// Rich stats card — matching the foreground Agent tool renderResult
|
|
1255
|
+
const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
|
|
1256
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1257
|
+
const typeName = getDisplayName((d.type as string) || "");
|
|
1258
|
+
const desc = (d.description as string) || "";
|
|
1259
|
+
|
|
1260
|
+
const parts: string[] = [];
|
|
1261
|
+
if ((d.toolUses as number) > 0) {
|
|
1262
|
+
parts.push(`${d.toolUses}🛠 `);
|
|
1263
|
+
}
|
|
1264
|
+
if ((d.turnCount as number) > 0) {
|
|
1265
|
+
parts.push(formatTurns(d.turnCount as number, d.maxTurns as number | undefined));
|
|
1266
|
+
}
|
|
1267
|
+
if ((d.tokens as number) > 0) {
|
|
1268
|
+
const tokenText = formatSessionTokens(
|
|
1269
|
+
d.tokens as number,
|
|
1270
|
+
d.contextPercent as number | null,
|
|
1271
|
+
theme,
|
|
1272
|
+
(d.compactions as number) ?? 0,
|
|
1273
|
+
);
|
|
1274
|
+
parts.push(tokenText);
|
|
1275
|
+
}
|
|
1276
|
+
parts.push(formatMs(d.durationMs as number));
|
|
1277
|
+
|
|
1278
|
+
const statsLine = parts.join("·");
|
|
1279
|
+
let headerLine = `${icon} ${theme.bold(typeName)}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
1280
|
+
if ((d.outputFile as string)) {
|
|
1281
|
+
headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
1282
|
+
}
|
|
1283
|
+
inner.addChild(new Text(headerLine, 0, 0));
|
|
1284
|
+
|
|
1285
|
+
// Result text — only when expanded (collapsible)
|
|
1286
|
+
if (expanded && text) {
|
|
1287
|
+
inner.addChild(new Spacer(1));
|
|
1288
|
+
const resultLines = text.split("\n").map(l => ` ${l}`).join("\n");
|
|
1289
|
+
inner.addChild(new Text(resultLines, 0, 0));
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
// Minimal card — no stats (shouldn't happen, but handle gracefully)
|
|
1293
|
+
const typeName = getDisplayName((d?.type as string) || "");
|
|
1294
|
+
const desc = (d?.description as string) || "";
|
|
1295
|
+
let line = `${theme.fg("success", "✓")}`;
|
|
1296
|
+
if (typeName) line += ` ${theme.bold(typeName)}`;
|
|
1297
|
+
if (desc) line += `\n ${theme.fg("text", desc)}`;
|
|
1298
|
+
if (d?.outputFile) {
|
|
1299
|
+
line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
1300
|
+
}
|
|
1301
|
+
inner.addChild(new Text(line, 0, 0));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Wrap in purple card matching default CustomMessageComponent styling
|
|
1305
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
1306
|
+
box.addChild(inner);
|
|
1307
|
+
|
|
1308
|
+
const outer = new Container();
|
|
1309
|
+
outer.addChild(new Spacer(1));
|
|
1310
|
+
outer.addChild(box);
|
|
1311
|
+
outer.addChild(new Spacer(1));
|
|
1312
|
+
return outer;
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// ========================================================================
|
|
1316
|
+
// Command registration
|
|
1317
|
+
// ========================================================================
|
|
1318
|
+
|
|
1319
|
+
pi.registerCommand("agents", {
|
|
1320
|
+
description: "Manage subagents: agent briefing, model settings, concurrency, running agents, agent types",
|
|
1321
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
1322
|
+
const modelOptions = ctx.modelRegistry.getAvailable().map((m) => `${m.provider}/${m.id}`);
|
|
1323
|
+
await showAgentsMainMenu(ctx, modelOptions);
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// ========================================================================
|
|
1328
|
+
// Event listeners
|
|
1329
|
+
// ========================================================================
|
|
1330
|
+
|
|
1331
|
+
// tool_call listener — inject model into Agent tool calls
|
|
1332
|
+
pi.on("tool_call", toolCallListener);
|
|
1333
|
+
|
|
1334
|
+
// Grab UI context for widget rendering on first tool execution each session,
|
|
1335
|
+
// and advance finished-agent linger state on each turn.
|
|
1336
|
+
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
1337
|
+
widget?.setUICtx(ctx.ui as unknown as UICtx);
|
|
1338
|
+
widget?.onTurnStart();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// session_start — load config, scan agents, register into registry
|
|
1342
|
+
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
1343
|
+
agentActivity.clear();
|
|
1344
|
+
await loadConfigAndRegisterAgents(ctx);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// session_shutdown — clean up
|
|
1348
|
+
pi.on("session_shutdown", async (_event: unknown) => {
|
|
1349
|
+
// Dispose widget before manager
|
|
1350
|
+
widget?.dispose();
|
|
1351
|
+
widget = undefined;
|
|
1352
|
+
if (manager) {
|
|
1353
|
+
await manager.dispose();
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
}
|