pi-subagents-lite 0.2.0 → 0.3.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 +222 -36
- package/package.json +3 -1
- package/src/agent-discovery.ts +36 -45
- package/src/agent-manager.ts +101 -87
- package/src/agent-runner.ts +40 -49
- package/src/agent-types.ts +15 -37
- package/src/config-io.ts +40 -0
- package/src/context.ts +80 -1
- package/src/index.ts +105 -1117
- package/src/menus.ts +866 -0
- package/src/model-precedence.ts +46 -36
- package/src/model-selector.ts +19 -19
- package/src/output-file.ts +123 -33
- package/src/prompts.ts +2 -2
- package/src/result-viewer.ts +166 -37
- package/src/skill-loader.ts +1 -1
- package/src/stop-agent-tool.ts +76 -0
- package/src/tool-execution.ts +361 -0
- package/src/types.ts +16 -1
- package/src/ui/agent-widget.ts +98 -91
- package/src/usage.ts +12 -4
- package/src/utils.ts +53 -4
package/src/index.ts
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* - Loaded from ~/.pi/agent/subagents-lite.json at session_start
|
|
15
15
|
* - Module-level __config cache; tool_call reads from cache
|
|
16
16
|
* - Config mutations update cache + atomic write to disk
|
|
17
|
-
* - Migrates subagent-model-defaults.json on first load
|
|
18
17
|
*
|
|
19
18
|
* Commands:
|
|
20
19
|
* - /agents: Management menu with 5 sub-menus
|
|
@@ -27,781 +26,49 @@
|
|
|
27
26
|
|
|
28
27
|
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
29
28
|
import { Type } from "@sinclair/typebox";
|
|
30
|
-
import * as fs from "node:fs";
|
|
31
29
|
import * as path from "node:path";
|
|
32
30
|
import type {
|
|
33
31
|
ExtensionAPI,
|
|
34
32
|
ExtensionCommandContext,
|
|
35
33
|
ExtensionContext,
|
|
36
|
-
ToolCallEvent,
|
|
37
34
|
} from "@earendil-works/pi-coding-agent";
|
|
38
|
-
import type {
|
|
39
|
-
import
|
|
40
|
-
import {
|
|
41
|
-
import { resolveType, getAgentConfig, registerAgents, getAvailableTypes, getAllTypes } from "./agent-types.js";
|
|
35
|
+
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
36
|
+
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
37
|
+
import { registerAgents, getAvailableTypes } from "./agent-types.js";
|
|
42
38
|
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
39
|
import { AgentManager } from "./agent-manager.js";
|
|
48
|
-
import type
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
40
|
+
import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type UICtx } from "./ui/agent-widget.js";
|
|
41
|
+
import { showAgentsMainMenu } from "./menus.js";
|
|
42
|
+
import { loadConfig } from "./config-io.js";
|
|
43
|
+
import { executeAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
|
|
44
|
+
import { executeStopAgentTool } from "./stop-agent-tool.js";
|
|
51
45
|
|
|
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
46
|
// ============================================================================
|
|
59
47
|
// Module-level state
|
|
60
48
|
// ============================================================================
|
|
61
49
|
|
|
50
|
+
/** Session-only model overrides — not persisted, cleared on session_start. */
|
|
51
|
+
export let sessionOverrides: SessionModelOverrides = { default: null };
|
|
52
|
+
|
|
62
53
|
/** Config cache — loaded at session_start, updated by /agents menu mutations. */
|
|
63
|
-
let __config: SubagentsConfig = {
|
|
64
|
-
agent: { default: null },
|
|
54
|
+
export let __config: SubagentsConfig = {
|
|
55
|
+
agent: { default: null, forceBackground: false },
|
|
65
56
|
concurrency: { default: 4 },
|
|
66
57
|
};
|
|
67
58
|
|
|
68
59
|
/** Agent manager singleton — module-level, no globalThis access. */
|
|
69
|
-
let manager: AgentManager;
|
|
60
|
+
export let manager: AgentManager;
|
|
70
61
|
|
|
71
|
-
/** Live activity state per agent, keyed by agent ID. Read by AgentWidget
|
|
72
|
-
const agentActivity = new Map<string, AgentActivity>();
|
|
62
|
+
/** Live activity state per agent, keyed by agent ID. Read by AgentWidget and tool-execution. */
|
|
63
|
+
export const agentActivity = new Map<string, AgentActivity>();
|
|
73
64
|
|
|
74
|
-
/** Live TUI widget showing running/completed agents above the editor. */
|
|
75
|
-
let widget: AgentWidget | undefined;
|
|
65
|
+
/** Live TUI widget showing running/completed agents above the editor. Used by tool-execution. */
|
|
66
|
+
export let widget: AgentWidget | undefined;
|
|
76
67
|
|
|
77
68
|
/** 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
|
-
}
|
|
69
|
+
export let piInstance: ExtensionAPI;
|
|
172
70
|
|
|
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
71
|
|
|
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
72
|
|
|
806
73
|
// ============================================================================
|
|
807
74
|
// Config loader — session_start handler logic
|
|
@@ -814,16 +81,11 @@ async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
814
81
|
function ensureManagerAndWidget(): void {
|
|
815
82
|
if (manager) return;
|
|
816
83
|
|
|
817
|
-
const concurrencyConfig = {
|
|
818
|
-
default: __config.concurrency.default,
|
|
819
|
-
providers: __config.concurrency.providers ?? {},
|
|
820
|
-
models: __config.concurrency.models ?? {},
|
|
821
|
-
};
|
|
822
84
|
manager = new AgentManager(
|
|
823
85
|
(record) => {
|
|
824
86
|
// Only nudge for background (async) agents — sync agents already returned via tool result
|
|
825
87
|
if (backgroundAgentIds.has(record.id)) {
|
|
826
|
-
scheduleNudge(record.id
|
|
88
|
+
scheduleNudge(record.id);
|
|
827
89
|
backgroundAgentIds.delete(record.id);
|
|
828
90
|
}
|
|
829
91
|
|
|
@@ -835,7 +97,7 @@ function ensureManagerAndWidget(): void {
|
|
|
835
97
|
// Remove from live activity tracking
|
|
836
98
|
agentActivity.delete(record.id);
|
|
837
99
|
},
|
|
838
|
-
|
|
100
|
+
__config.concurrency,
|
|
839
101
|
);
|
|
840
102
|
|
|
841
103
|
// Create/replace widget tied to this manager instance
|
|
@@ -858,8 +120,6 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
|
858
120
|
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
859
121
|
]);
|
|
860
122
|
|
|
861
|
-
const { DEFAULT_AGENTS } = await import("./default-agents.js");
|
|
862
|
-
|
|
863
123
|
// Merge with defaults
|
|
864
124
|
const merged = mergeAgents(DEFAULT_AGENTS, userAgents, projectAgents);
|
|
865
125
|
|
|
@@ -868,308 +128,51 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
|
868
128
|
}
|
|
869
129
|
|
|
870
130
|
async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
871
|
-
// Load config (with migration if needed)
|
|
872
131
|
__config = loadConfig();
|
|
873
|
-
|
|
874
|
-
// Ensure manager exists
|
|
875
132
|
ensureManagerAndWidget();
|
|
876
|
-
|
|
877
|
-
// Scan agent files and register
|
|
878
133
|
await scanAndRegisterAgents(ctx);
|
|
879
134
|
}
|
|
880
135
|
|
|
881
136
|
// ============================================================================
|
|
882
|
-
//
|
|
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
|
|
137
|
+
// UI helpers — stats card rendering (shared by renderResult and message renderer)
|
|
934
138
|
// ============================================================================
|
|
935
139
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
|
|
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);
|
|
140
|
+
/** Build the stats line for an agent result card. Used by both renderers. */
|
|
141
|
+
function buildStatsLine(d: Record<string, unknown>, theme: any): string {
|
|
142
|
+
const parts = buildStatsParts({
|
|
143
|
+
toolUses: (d.toolUses as number) ?? 0,
|
|
144
|
+
turnCount: d.turnCount as number | undefined,
|
|
145
|
+
maxTurns: d.maxTurns as number | undefined,
|
|
146
|
+
tokens: (d.tokens as number) ?? 0,
|
|
147
|
+
contextPercent: d.contextPercent as number | null,
|
|
148
|
+
compactions: (d.compactions as number) ?? 0,
|
|
149
|
+
}, theme);
|
|
150
|
+
parts.push(formatMs(d.durationMs as number));
|
|
151
|
+
return parts.join("·");
|
|
988
152
|
}
|
|
989
153
|
|
|
990
154
|
// ============================================================================
|
|
991
|
-
//
|
|
155
|
+
// Agent tool registration helper — dynamic enum for agent types
|
|
992
156
|
// ============================================================================
|
|
993
157
|
|
|
994
158
|
/**
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
159
|
+
* Register (or re-register) the Agent tool with current agent types.
|
|
160
|
+
* At init time only defaults exist; call again from session_start after
|
|
161
|
+
* user/project agents are loaded to update the enum.
|
|
998
162
|
*/
|
|
999
|
-
function
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
|
163
|
+
function registerAgentTool(pi: ExtensionAPI): void {
|
|
164
|
+
const types = getAvailableTypes();
|
|
165
|
+
const agentParam = types.length > 0
|
|
166
|
+
? Type.Optional(Type.Union(types.map(t => Type.Literal(t))))
|
|
167
|
+
: Type.Optional(Type.String());
|
|
1163
168
|
pi.registerTool({
|
|
1164
169
|
name: "Agent",
|
|
1165
170
|
label: "Agent",
|
|
1166
171
|
description: ".",
|
|
1167
|
-
// No promptSnippet, no promptGuidelines
|
|
1168
172
|
parameters: Type.Object({
|
|
1169
173
|
prompt: Type.String(),
|
|
1170
174
|
description: Type.String(),
|
|
1171
|
-
agent:
|
|
1172
|
-
thinking: Type.Optional(Type.String()),
|
|
175
|
+
agent: agentParam,
|
|
1173
176
|
run_in_background: Type.Optional(Type.Boolean()),
|
|
1174
177
|
resume: Type.Optional(Type.String()),
|
|
1175
178
|
}),
|
|
@@ -1178,7 +181,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1178
181
|
renderCall(args, theme) {
|
|
1179
182
|
const typeName = getDisplayName((args.agent as string) || "");
|
|
1180
183
|
const label = typeName || "Agent";
|
|
1181
|
-
|
|
184
|
+
let text = `▸ ${theme.fg("accent", theme.bold(label))}`;
|
|
185
|
+
|
|
186
|
+
// Show model in parens when it differs from the parent model
|
|
187
|
+
// _modelOverride is injected by toolCallListener when the resolved
|
|
188
|
+
// model differs from the session's parent model
|
|
189
|
+
const a = args as Record<string, unknown>;
|
|
190
|
+
const modelOverride = a._modelOverride as string | undefined;
|
|
191
|
+
if (modelOverride) {
|
|
192
|
+
text += ` (${modelOverride})`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Text(text, 0, 0);
|
|
1182
196
|
},
|
|
1183
197
|
|
|
1184
198
|
renderResult(result, options, theme) {
|
|
@@ -1187,113 +201,95 @@ export default function (pi: ExtensionAPI) {
|
|
|
1187
201
|
const d = result.details as Record<string, unknown> | undefined;
|
|
1188
202
|
const isError = !!(result as any).isError;
|
|
1189
203
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1190
|
-
|
|
1191
|
-
const typeName = getDisplayName((d?.type as string) || "");
|
|
1192
204
|
const desc = (d?.description as string) || "";
|
|
1193
205
|
|
|
1194
206
|
if (d && d.turnCount != null) {
|
|
1195
|
-
|
|
1196
|
-
|
|
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)}`;
|
|
207
|
+
const statsLine = buildStatsLine(d, theme);
|
|
208
|
+
let lines = `${icon} ${statsLine}\n ${theme.fg("text", desc)}`;
|
|
1216
209
|
if (expanded && text) {
|
|
1217
210
|
lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
|
|
1218
211
|
}
|
|
1219
212
|
return new Text(lines, 0, 0);
|
|
1220
213
|
}
|
|
1221
214
|
|
|
1222
|
-
// Minimal card
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
return new Text(
|
|
215
|
+
// Minimal card — type name already shown by renderCall
|
|
216
|
+
// For background spawns (no stats), use space placeholder — agent isn't done yet
|
|
217
|
+
const isBackground = text.includes("running in background") || text.includes("queued");
|
|
218
|
+
const prefix = isBackground ? " " : `${icon} `;
|
|
219
|
+
if (desc) {
|
|
220
|
+
return new Text(`${prefix}${theme.fg("text", desc)}`, 0, 0);
|
|
1228
221
|
}
|
|
1229
222
|
|
|
1230
|
-
return new Text(`${
|
|
223
|
+
return new Text(`${prefix}${theme.fg("dim", text)}`, 0, 0);
|
|
1231
224
|
},
|
|
1232
225
|
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Extension factory
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
export default function (pi: ExtensionAPI) {
|
|
233
|
+
// Store pi for execute callbacks
|
|
234
|
+
piInstance = pi;
|
|
1233
235
|
|
|
1234
236
|
// ========================================================================
|
|
1235
|
-
//
|
|
237
|
+
// Tool registration (stealth schemas — at init time)
|
|
1236
238
|
// ========================================================================
|
|
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
239
|
|
|
240
|
+
// Agent tool — stealth schema with dynamic agent type enum
|
|
241
|
+
registerAgentTool(pi);
|
|
242
|
+
|
|
243
|
+
// StopAgent tool — stealth schema, stop a running agent by ID
|
|
244
|
+
pi.registerTool({
|
|
245
|
+
name: "StopAgent",
|
|
246
|
+
label: "StopAgent",
|
|
247
|
+
description: ".",
|
|
248
|
+
parameters: Type.Object({
|
|
249
|
+
agent_id: Type.String(),
|
|
250
|
+
}),
|
|
251
|
+
execute: executeStopAgentTool,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Message renderer — subagent-result (background agent completion)
|
|
1240
255
|
pi.registerMessageRenderer("subagent-result", (message, options, theme) => {
|
|
1241
256
|
const { expanded } = options as { expanded?: boolean };
|
|
1242
257
|
const d = message.details as Record<string, unknown> | undefined;
|
|
1243
258
|
const text = (message.content as string)?.trim() || "";
|
|
1244
259
|
|
|
1245
|
-
// Build the content inside the purple card
|
|
1246
260
|
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));
|
|
261
|
+
inner.addChild(new Text(theme.fg("customMessageLabel", "Subagent Result"), 0, 0));
|
|
1251
262
|
inner.addChild(new Spacer(1));
|
|
1252
263
|
|
|
1253
264
|
if (d && d.turnCount != null) {
|
|
1254
|
-
// Rich stats card — matching the foreground Agent tool renderResult
|
|
1255
265
|
const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
|
|
1256
266
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1257
267
|
const typeName = getDisplayName((d.type as string) || "");
|
|
268
|
+
const modelName = d.modelName as string | undefined;
|
|
1258
269
|
const desc = (d.description as string) || "";
|
|
1259
270
|
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
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)}`;
|
|
271
|
+
const namePart = modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
|
|
272
|
+
const statsLine = buildStatsLine(d, theme);
|
|
273
|
+
let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
1280
274
|
if ((d.outputFile as string)) {
|
|
1281
275
|
headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
1282
276
|
}
|
|
1283
277
|
inner.addChild(new Text(headerLine, 0, 0));
|
|
1284
278
|
|
|
1285
|
-
// Result text — only when expanded (collapsible)
|
|
1286
279
|
if (expanded && text) {
|
|
1287
280
|
inner.addChild(new Spacer(1));
|
|
1288
281
|
const resultLines = text.split("\n").map(l => ` ${l}`).join("\n");
|
|
1289
282
|
inner.addChild(new Text(resultLines, 0, 0));
|
|
1290
283
|
}
|
|
1291
284
|
} else {
|
|
1292
|
-
// Minimal card — no stats (shouldn't happen, but handle gracefully)
|
|
1293
285
|
const typeName = getDisplayName((d?.type as string) || "");
|
|
286
|
+
const modelName = d?.modelName as string | undefined;
|
|
1294
287
|
const desc = (d?.description as string) || "";
|
|
1295
288
|
let line = `${theme.fg("success", "✓")}`;
|
|
1296
|
-
if (typeName)
|
|
289
|
+
if (typeName) {
|
|
290
|
+
const namePart = modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
|
|
291
|
+
line += ` ${namePart}`;
|
|
292
|
+
}
|
|
1297
293
|
if (desc) line += `\n ${theme.fg("text", desc)}`;
|
|
1298
294
|
if (d?.outputFile) {
|
|
1299
295
|
line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
@@ -1301,7 +297,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1301
297
|
inner.addChild(new Text(line, 0, 0));
|
|
1302
298
|
}
|
|
1303
299
|
|
|
1304
|
-
// Wrap in purple card matching default CustomMessageComponent styling
|
|
1305
300
|
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
1306
301
|
box.addChild(inner);
|
|
1307
302
|
|
|
@@ -1312,10 +307,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1312
307
|
return outer;
|
|
1313
308
|
});
|
|
1314
309
|
|
|
1315
|
-
// ========================================================================
|
|
1316
310
|
// Command registration
|
|
1317
|
-
// ========================================================================
|
|
1318
|
-
|
|
1319
311
|
pi.registerCommand("agents", {
|
|
1320
312
|
description: "Manage subagents: agent briefing, model settings, concurrency, running agents, agent types",
|
|
1321
313
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -1324,29 +316,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1324
316
|
},
|
|
1325
317
|
});
|
|
1326
318
|
|
|
1327
|
-
// ========================================================================
|
|
1328
319
|
// Event listeners
|
|
1329
|
-
// ========================================================================
|
|
1330
|
-
|
|
1331
|
-
// tool_call listener — inject model into Agent tool calls
|
|
1332
320
|
pi.on("tool_call", toolCallListener);
|
|
1333
321
|
|
|
1334
|
-
// Grab UI context for widget rendering on first tool execution each session,
|
|
1335
|
-
// and advance finished-agent linger state on each turn.
|
|
1336
322
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
1337
323
|
widget?.setUICtx(ctx.ui as unknown as UICtx);
|
|
1338
324
|
widget?.onTurnStart();
|
|
1339
325
|
});
|
|
1340
326
|
|
|
1341
|
-
// session_start — load config, scan agents, register into registry
|
|
327
|
+
// session_start — load config, scan agents, register into registry,
|
|
328
|
+
// then re-register Agent tool with dynamic agent type enum
|
|
1342
329
|
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
330
|
+
sessionOverrides = { default: null };
|
|
1343
331
|
agentActivity.clear();
|
|
1344
332
|
await loadConfigAndRegisterAgents(ctx);
|
|
333
|
+
// Re-register with updated agent type list (now includes user/project agents)
|
|
334
|
+
registerAgentTool(pi);
|
|
1345
335
|
});
|
|
1346
336
|
|
|
1347
|
-
// session_shutdown — clean up
|
|
1348
337
|
pi.on("session_shutdown", async (_event: unknown) => {
|
|
1349
|
-
// Dispose widget before manager
|
|
1350
338
|
widget?.dispose();
|
|
1351
339
|
widget = undefined;
|
|
1352
340
|
if (manager) {
|