@esso0428/pi-subagents 0.15.4 → 0.15.6
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/CHANGELOG.md +4 -17
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +18 -0
- package/dist/agent-history-list.js +68 -0
- package/dist/agent-history.d.ts +1 -1
- package/dist/agent-history.js +31 -34
- package/dist/agent-manager.d.ts +22 -8
- package/dist/agent-manager.js +100 -93
- package/dist/agent-runner.js +67 -86
- package/dist/agent-types.js +27 -45
- package/dist/context.js +2 -6
- package/dist/cross-extension-rpc.js +5 -9
- package/dist/custom-agents.js +15 -18
- package/dist/default-agents.js +1 -4
- package/dist/enabled-models.js +11 -16
- package/dist/env.js +1 -4
- package/dist/group-join.js +1 -5
- package/dist/index.js +273 -284
- package/dist/invocation-config.js +2 -6
- package/dist/memory.js +24 -34
- package/dist/model-resolver.js +1 -4
- package/dist/nico-overrides.js +14 -20
- package/dist/output-file.js +16 -22
- package/dist/prompts.js +1 -4
- package/dist/schedule-store.js +16 -21
- package/dist/schedule.js +8 -12
- package/dist/settings.js +15 -23
- package/dist/skill-loader.js +20 -23
- package/dist/status-note.js +1 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.js +1 -4
- package/dist/ui/agent-widget.d.ts +1 -5
- package/dist/ui/agent-widget.js +25 -49
- package/dist/ui/conversation-blocks.d.ts +18 -0
- package/dist/ui/conversation-blocks.js +214 -0
- package/dist/ui/conversation-nvim.d.ts +7 -0
- package/dist/ui/conversation-nvim.js +116 -0
- package/dist/ui/conversation-search.d.ts +38 -0
- package/dist/ui/conversation-search.js +123 -0
- package/dist/ui/conversation-viewer.d.ts +55 -19
- package/dist/ui/conversation-viewer.js +398 -215
- package/dist/ui/fleet-list.d.ts +23 -11
- package/dist/ui/fleet-list.js +85 -78
- package/dist/ui/schedule-menu.js +1 -4
- package/dist/ui/viewer-keys.js +7 -10
- package/dist/usage.js +4 -10
- package/dist/worktree.js +26 -31
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +108 -81
- package/src/agent-runner.ts +5 -13
- package/src/index.ts +84 -85
- package/src/output-file.ts +1 -1
- package/src/types.ts +0 -1
- package/src/ui/agent-widget.ts +1 -8
- package/src/ui/conversation-blocks.ts +270 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-viewer.ts +376 -205
- package/src/ui/fleet-list.ts +73 -52
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/ui/conversation-blocks.test.ts +72 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +132 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
|
|
4
3
|
*
|
|
@@ -10,34 +9,31 @@
|
|
|
10
9
|
* Commands:
|
|
11
10
|
* /agents — Interactive agent management menu
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const markdown_result_js_1 = require("./ui/markdown-result.js");
|
|
39
|
-
const schedule_menu_js_1 = require("./ui/schedule-menu.js");
|
|
40
|
-
const usage_js_1 = require("./usage.js");
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { defineTool, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Container, Key, matchesKey, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { Type } from "@sinclair/typebox";
|
|
17
|
+
import { AgentManager } from "./agent-manager.js";
|
|
18
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
|
+
import { applyNicoOverrides, BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
20
|
+
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
21
|
+
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
|
+
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
23
|
+
import { GroupJoinManager } from "./group-join.js";
|
|
24
|
+
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
25
|
+
import { resolveModel } from "./model-resolver.js";
|
|
26
|
+
import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory } from "./agent-history.js";
|
|
27
|
+
import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
|
|
28
|
+
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
29
|
+
import { SubagentScheduler } from "./schedule.js";
|
|
30
|
+
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
31
|
+
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
32
|
+
import { getStatusNote } from "./status-note.js";
|
|
33
|
+
import { AgentWidget, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
34
|
+
import { FleetList } from "./ui/fleet-list.js";
|
|
35
|
+
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
36
|
+
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
41
37
|
// ---- Shared helpers ----
|
|
42
38
|
/** Tool execute return value for a text response. */
|
|
43
39
|
function textResult(msg, details) {
|
|
@@ -75,16 +71,16 @@ function abortable(promise, signal) {
|
|
|
75
71
|
});
|
|
76
72
|
});
|
|
77
73
|
}
|
|
78
|
-
function renderRunningAgentStatus(frame, statsText, activity, theme) {
|
|
79
|
-
const container = new
|
|
80
|
-
container.addChild(new
|
|
81
|
-
container.addChild(new
|
|
74
|
+
export function renderRunningAgentStatus(frame, statsText, activity, theme) {
|
|
75
|
+
const container = new Container();
|
|
76
|
+
container.addChild(new Text(theme.fg("accent", frame) + (statsText ? " " + statsText : ""), 0, 0));
|
|
77
|
+
container.addChild(new Text(theme.fg("dim", ` ⎿ ${activity}`), 0, 0));
|
|
82
78
|
return container;
|
|
83
79
|
}
|
|
84
80
|
/** Format an agent's lifetime token total, or "" when zero. */
|
|
85
81
|
function formatLifetimeTokens(o) {
|
|
86
|
-
const t =
|
|
87
|
-
return t > 0 ?
|
|
82
|
+
const t = getLifetimeTotal(o.lifetimeUsage);
|
|
83
|
+
return t > 0 ? formatTokens(t) : "";
|
|
88
84
|
}
|
|
89
85
|
/**
|
|
90
86
|
* Create an AgentActivity state and spawn callbacks for tracking tool usage.
|
|
@@ -128,7 +124,7 @@ function createActivityTracker(maxTurns, onStreamUpdate) {
|
|
|
128
124
|
state.session = session;
|
|
129
125
|
},
|
|
130
126
|
onAssistantUsage: (usage) => {
|
|
131
|
-
|
|
127
|
+
addUsage(state.lifetimeUsage, usage);
|
|
132
128
|
onStreamUpdate?.();
|
|
133
129
|
},
|
|
134
130
|
};
|
|
@@ -169,8 +165,8 @@ function escapeXml(s) {
|
|
|
169
165
|
function formatTaskNotification(record, resultMaxLen) {
|
|
170
166
|
const status = getStatusLabel(record.status, record.error);
|
|
171
167
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
172
|
-
const totalTokens =
|
|
173
|
-
const contextPercent =
|
|
168
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
169
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
174
170
|
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
175
171
|
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
176
172
|
const resultPreview = record.result
|
|
@@ -184,7 +180,7 @@ function formatTaskNotification(record, resultMaxLen) {
|
|
|
184
180
|
record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
|
|
185
181
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
186
182
|
`<status>${escapeXml(status)}</status>`,
|
|
187
|
-
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${
|
|
183
|
+
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
188
184
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
189
185
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
190
186
|
`</task-notification>`,
|
|
@@ -205,11 +201,9 @@ function buildDetails(base, record, activity, overrides) {
|
|
|
205
201
|
...overrides,
|
|
206
202
|
};
|
|
207
203
|
}
|
|
208
|
-
/** Maximum expanded notification body retained in message details. */
|
|
209
|
-
const MAX_NOTIFICATION_RESULT_TEXT_LENGTH = 20_000;
|
|
210
204
|
/** Build notification details for the custom message renderer. */
|
|
211
205
|
function buildNotificationDetails(record, resultMaxLen, activity) {
|
|
212
|
-
const totalTokens =
|
|
206
|
+
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
213
207
|
return {
|
|
214
208
|
id: record.id,
|
|
215
209
|
description: record.description,
|
|
@@ -221,11 +215,6 @@ function buildNotificationDetails(record, resultMaxLen, activity) {
|
|
|
221
215
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
222
216
|
outputFile: record.outputFile,
|
|
223
217
|
error: record.error,
|
|
224
|
-
resultText: record.result
|
|
225
|
-
? record.result.length > MAX_NOTIFICATION_RESULT_TEXT_LENGTH
|
|
226
|
-
? record.result.slice(0, MAX_NOTIFICATION_RESULT_TEXT_LENGTH) + "…"
|
|
227
|
-
: record.result
|
|
228
|
-
: undefined,
|
|
229
218
|
resultPreview: record.result
|
|
230
219
|
? record.result.length > resultMaxLen
|
|
231
220
|
? record.result.slice(0, resultMaxLen) + "…"
|
|
@@ -233,7 +222,7 @@ function buildNotificationDetails(record, resultMaxLen, activity) {
|
|
|
233
222
|
: "No output.",
|
|
234
223
|
};
|
|
235
224
|
}
|
|
236
|
-
function
|
|
225
|
+
export default function (pi) {
|
|
237
226
|
// ---- Register custom notification renderer ----
|
|
238
227
|
pi.registerMessageRenderer("subagent-notification", (message, { expanded }, theme) => {
|
|
239
228
|
const d = message.details;
|
|
@@ -250,49 +239,40 @@ function default_1(pi) {
|
|
|
250
239
|
// Line 2: stats
|
|
251
240
|
const parts = [];
|
|
252
241
|
if (d.turnCount > 0)
|
|
253
|
-
parts.push(
|
|
242
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
254
243
|
if (d.toolUses > 0)
|
|
255
244
|
parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
256
245
|
if (d.totalTokens > 0)
|
|
257
|
-
parts.push(
|
|
246
|
+
parts.push(formatTokens(d.totalTokens));
|
|
258
247
|
if (d.durationMs > 0)
|
|
259
|
-
parts.push(
|
|
248
|
+
parts.push(formatMs(d.durationMs));
|
|
260
249
|
if (parts.length) {
|
|
261
250
|
line += "\n " + parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
262
251
|
}
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
// Line 3: result preview (collapsed) or full (expanded)
|
|
253
|
+
if (expanded) {
|
|
254
|
+
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
255
|
+
for (const l of lines)
|
|
256
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
265
259
|
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
266
260
|
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
267
|
-
if (d.outputFile) {
|
|
268
|
-
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
269
|
-
}
|
|
270
|
-
return line;
|
|
271
261
|
}
|
|
272
|
-
|
|
273
|
-
container.addChild(new pi_tui_1.Text(line, 0, 0));
|
|
274
|
-
container.addChild((0, markdown_result_js_1.createMarkdownResult)(d.resultText ?? d.resultPreview, theme, 30));
|
|
262
|
+
// Line 4: output file link (if present)
|
|
275
263
|
if (d.outputFile) {
|
|
276
|
-
|
|
264
|
+
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
277
265
|
}
|
|
278
|
-
return
|
|
266
|
+
return line;
|
|
279
267
|
}
|
|
280
268
|
const all = [d, ...(d.others ?? [])];
|
|
281
|
-
|
|
282
|
-
return new pi_tui_1.Text(all.map(renderOne).join("\n"), 0, 0);
|
|
283
|
-
const container = new pi_tui_1.Container();
|
|
284
|
-
all.forEach((item, index) => {
|
|
285
|
-
if (index > 0)
|
|
286
|
-
container.addChild(new pi_tui_1.Spacer(1));
|
|
287
|
-
container.addChild(renderOne(item));
|
|
288
|
-
});
|
|
289
|
-
return container;
|
|
269
|
+
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
290
270
|
});
|
|
291
271
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
292
272
|
const reloadCustomAgents = () => {
|
|
293
|
-
const userAgents =
|
|
294
|
-
|
|
295
|
-
|
|
273
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
274
|
+
registerAgents(userAgents);
|
|
275
|
+
applyNicoOverrides();
|
|
296
276
|
};
|
|
297
277
|
// Initial load
|
|
298
278
|
reloadCustomAgents();
|
|
@@ -344,7 +324,7 @@ function default_1(pi) {
|
|
|
344
324
|
widget.update();
|
|
345
325
|
}
|
|
346
326
|
// ---- Group join manager ----
|
|
347
|
-
const groupJoin = new
|
|
327
|
+
const groupJoin = new GroupJoinManager((records, partial) => {
|
|
348
328
|
for (const r of records) {
|
|
349
329
|
agentActivity.delete(r.id);
|
|
350
330
|
widget.markFinished(r.id);
|
|
@@ -384,7 +364,7 @@ function default_1(pi) {
|
|
|
384
364
|
// tokens is omitted when nothing was ever produced (e.g. agent errored before
|
|
385
365
|
// any message_end fired), preserving prior payload shape.
|
|
386
366
|
const u = record.lifetimeUsage;
|
|
387
|
-
const total =
|
|
367
|
+
const total = getLifetimeTotal(u);
|
|
388
368
|
const tokens = total > 0
|
|
389
369
|
? { input: u.input, output: u.output, total }
|
|
390
370
|
: undefined;
|
|
@@ -401,7 +381,7 @@ function default_1(pi) {
|
|
|
401
381
|
};
|
|
402
382
|
}
|
|
403
383
|
// Background completion: route through group join or send individual nudge
|
|
404
|
-
const manager = new
|
|
384
|
+
const manager = new AgentManager((record) => {
|
|
405
385
|
// Emit lifecycle event based on terminal status
|
|
406
386
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
407
387
|
const eventData = buildEventData(record);
|
|
@@ -411,9 +391,7 @@ function default_1(pi) {
|
|
|
411
391
|
else {
|
|
412
392
|
pi.events.emit("subagents:completed", eventData);
|
|
413
393
|
}
|
|
414
|
-
// Persist final record for cross-extension history reconstruction
|
|
415
|
-
// the transcript body in a project-local file rather than inflating the
|
|
416
|
-
// parent session JSONL; only its project-relative locator is persisted.
|
|
394
|
+
// Persist final record for cross-extension history reconstruction
|
|
417
395
|
pi.appendEntry("subagents:record", {
|
|
418
396
|
id: record.id, type: record.type, description: record.description,
|
|
419
397
|
status: record.status, result: record.result, error: record.error,
|
|
@@ -493,14 +471,14 @@ function default_1(pi) {
|
|
|
493
471
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
494
472
|
// is available. Mirrors pi-chonky-tasks's session-scoped task store —
|
|
495
473
|
// schedules reset on /new, restore on /resume.
|
|
496
|
-
const scheduler = new
|
|
474
|
+
const scheduler = new SubagentScheduler();
|
|
497
475
|
function startScheduler(ctx) {
|
|
498
476
|
try {
|
|
499
477
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
500
478
|
if (!sessionId)
|
|
501
479
|
return; // sessionId not yet available — try again on next event
|
|
502
|
-
const path =
|
|
503
|
-
const store = new
|
|
480
|
+
const path = resolveStorePath(ctx.cwd, sessionId);
|
|
481
|
+
const store = new ScheduleStore(path);
|
|
504
482
|
scheduler.start(pi, ctx, manager, store);
|
|
505
483
|
pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
|
|
506
484
|
}
|
|
@@ -515,27 +493,15 @@ function default_1(pi) {
|
|
|
515
493
|
// bound session_start, so a filtered-out activation never advertises (#142).
|
|
516
494
|
pi.on("session_start", async (_event, ctx) => {
|
|
517
495
|
currentCtx = ctx;
|
|
518
|
-
try {
|
|
519
|
-
(0, agent_history_js_1.ensureSubagentsGitignore)(ctx.cwd);
|
|
520
|
-
}
|
|
521
|
-
catch (err) {
|
|
522
|
-
console.warn("[pi-subagents] Failed to protect project-local history:", err);
|
|
523
|
-
}
|
|
524
496
|
manager.clearCompleted(true);
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
.filter((entry) => entry?.type === "custom" && entry
|
|
528
|
-
.map((entry) => entry.data);
|
|
529
|
-
manager.restoreCompleted(historicalRecords);
|
|
530
|
-
// Bind the UI here as well as on tool execution so rehydrated terminal
|
|
531
|
-
// records are immediately visible and openable from FleetView after reload.
|
|
532
|
-
widget.setUICtx(ctx.ui);
|
|
533
|
-
fleet.setUICtx(ctx.ui);
|
|
534
|
-
fleet.update();
|
|
497
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
498
|
+
manager.restoreCompleted(branch
|
|
499
|
+
.filter((entry) => entry?.type === "custom" && entry?.customType === "subagents:record")
|
|
500
|
+
.map((entry) => entry.data));
|
|
535
501
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
536
502
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
537
503
|
if (!rpcHandle) {
|
|
538
|
-
rpcHandle =
|
|
504
|
+
rpcHandle = registerRpcHandlers({
|
|
539
505
|
events: pi.events,
|
|
540
506
|
pi,
|
|
541
507
|
getCtx: () => currentCtx,
|
|
@@ -581,10 +547,10 @@ function default_1(pi) {
|
|
|
581
547
|
// everything else; "off" = hide the widget entirely. Read live at render time.
|
|
582
548
|
let widgetMode = "background";
|
|
583
549
|
function getWidgetMode() { return widgetMode; }
|
|
584
|
-
const widget = new
|
|
550
|
+
const widget = new AgentWidget(manager, agentActivity, getWidgetMode);
|
|
585
551
|
function setWidgetMode(m) { widgetMode = m; widget.update(); }
|
|
586
552
|
// Claude Code-style FleetView: navigable list of main + subagents below the editor.
|
|
587
|
-
const fleet = new
|
|
553
|
+
const fleet = new FleetList(manager, agentActivity, () => currentCtx?.cwd, pi, () => currentCtx);
|
|
588
554
|
let fleetViewEnabled = true;
|
|
589
555
|
function isFleetViewEnabled() { return fleetViewEnabled; }
|
|
590
556
|
function setFleetViewEnabled(b) { fleetViewEnabled = b; fleet.setEnabled(b); }
|
|
@@ -625,7 +591,7 @@ function default_1(pi) {
|
|
|
625
591
|
// State lives in agent-types.ts (isDefaultsDisabled) because registerAgents
|
|
626
592
|
// needs it; this wrapper just re-registers after flipping it.
|
|
627
593
|
function setDisableDefaultAgents(b) {
|
|
628
|
-
|
|
594
|
+
setDefaultsDisabled(b);
|
|
629
595
|
reloadCustomAgents(); // re-register with new setting
|
|
630
596
|
}
|
|
631
597
|
// ---- Agent tool description mode ----
|
|
@@ -688,15 +654,15 @@ function default_1(pi) {
|
|
|
688
654
|
const tools = cfg?.builtinToolNames;
|
|
689
655
|
if (!tools || tools.length === 0)
|
|
690
656
|
return "*";
|
|
691
|
-
const isFullSet = tools.length ===
|
|
692
|
-
&&
|
|
657
|
+
const isFullSet = tools.length === BUILTIN_TOOL_NAMES.length
|
|
658
|
+
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
693
659
|
return isFullSet ? "*" : tools.join(", ");
|
|
694
660
|
};
|
|
695
661
|
/** Build the full type list text dynamically from available agents only. */
|
|
696
662
|
const buildTypeListText = () => {
|
|
697
|
-
const available =
|
|
663
|
+
const available = getAvailableTypes();
|
|
698
664
|
return available.map((name) => {
|
|
699
|
-
const cfg =
|
|
665
|
+
const cfg = getAgentConfig(name);
|
|
700
666
|
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
701
667
|
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
702
668
|
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
|
|
@@ -708,8 +674,8 @@ function default_1(pi) {
|
|
|
708
674
|
return (match ? match[0] : text).replace(/\s+/g, " ").trim();
|
|
709
675
|
};
|
|
710
676
|
/** Compact type list: one line per agent, first sentence only. */
|
|
711
|
-
const buildCompactTypeListText = () =>
|
|
712
|
-
const cfg =
|
|
677
|
+
const buildCompactTypeListText = () => getAvailableTypes().map((name) => {
|
|
678
|
+
const cfg = getAgentConfig(name);
|
|
713
679
|
return `- ${name}: ${firstSentence(cfg?.description ?? name)} (Tools: ${formatToolsSuffix(cfg)})`;
|
|
714
680
|
}).join("\n");
|
|
715
681
|
/** Derive a short model label from a model string. */
|
|
@@ -722,10 +688,10 @@ function default_1(pi) {
|
|
|
722
688
|
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
723
689
|
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
724
690
|
// to stderr and falls back to defaults.
|
|
725
|
-
|
|
691
|
+
applyAndEmitLoaded({
|
|
726
692
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
727
|
-
setDefaultMaxTurns
|
|
728
|
-
setGraceTurns
|
|
693
|
+
setDefaultMaxTurns,
|
|
694
|
+
setGraceTurns,
|
|
729
695
|
setDefaultJoinMode,
|
|
730
696
|
setSchedulingEnabled,
|
|
731
697
|
setScopeModels: setScopeModelsEnabled,
|
|
@@ -742,7 +708,7 @@ function default_1(pi) {
|
|
|
742
708
|
// preserves Type.Object's inference when present and produces a
|
|
743
709
|
// `schedule`-free schema when absent — zero LLM-context cost in disabled mode.
|
|
744
710
|
const scheduleParamShape = {
|
|
745
|
-
schedule:
|
|
711
|
+
schedule: Type.Optional(Type.String({
|
|
746
712
|
description: 'Opt-in only — fire later instead of now. Omit to run immediately (the default, almost always correct). ' +
|
|
747
713
|
'Formats: 6-field cron ("0 0 9 * * 1" = 9am Mon), interval ("5m"/"1h"), one-shot ("+10m" or ISO). ' +
|
|
748
714
|
'Forces run_in_background; incompatible with inherit_context and resume. Returns job ID.',
|
|
@@ -758,7 +724,7 @@ function default_1(pi) {
|
|
|
758
724
|
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
759
725
|
${buildCompactTypeListText()}
|
|
760
726
|
|
|
761
|
-
Custom agents: .pi/agents/<name>.md (project) or ${
|
|
727
|
+
Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
|
|
762
728
|
|
|
763
729
|
Notes:
|
|
764
730
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
@@ -771,7 +737,7 @@ Notes:
|
|
|
771
737
|
Available agent types and the tools they have access to:
|
|
772
738
|
${buildTypeListText()}
|
|
773
739
|
|
|
774
|
-
Custom agents can be defined in .pi/agents/<name>.md (project) or ${
|
|
740
|
+
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
|
|
775
741
|
|
|
776
742
|
When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
|
|
777
743
|
|
|
@@ -816,7 +782,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
816
782
|
const vars = {
|
|
817
783
|
typeList: buildTypeListText,
|
|
818
784
|
compactTypeList: buildCompactTypeListText,
|
|
819
|
-
agentDir:
|
|
785
|
+
agentDir: getAgentDir,
|
|
820
786
|
scheduleGuideline: () => scheduleGuideline,
|
|
821
787
|
};
|
|
822
788
|
// Replacement callback (not a string) — agent descriptions may contain `$&` etc.
|
|
@@ -829,13 +795,13 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
829
795
|
};
|
|
830
796
|
const loadCustomToolDescription = () => {
|
|
831
797
|
for (const path of [
|
|
832
|
-
|
|
833
|
-
|
|
798
|
+
join(process.cwd(), ".pi", "agent-tool-description.md"),
|
|
799
|
+
join(getAgentDir(), "agent-tool-description.md"),
|
|
834
800
|
]) {
|
|
835
801
|
try {
|
|
836
|
-
if (!
|
|
802
|
+
if (!existsSync(path))
|
|
837
803
|
continue;
|
|
838
|
-
const text =
|
|
804
|
+
const text = readFileSync(path, "utf-8").trim();
|
|
839
805
|
if (text)
|
|
840
806
|
return renderToolDescriptionTemplate(text);
|
|
841
807
|
console.warn(`[pi-subagents] ${path} is empty — ignoring`);
|
|
@@ -858,8 +824,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
858
824
|
}
|
|
859
825
|
return fullAgentToolDescription;
|
|
860
826
|
})();
|
|
861
|
-
pi.registerTool(
|
|
862
|
-
name:
|
|
827
|
+
pi.registerTool(defineTool({
|
|
828
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
863
829
|
label: "Agent",
|
|
864
830
|
description: agentToolDescription,
|
|
865
831
|
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
@@ -869,54 +835,54 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
869
835
|
"When an agent runs in the background, you will be notified on completion — do not poll or sleep waiting for it. Continue with other work instead.",
|
|
870
836
|
"Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
|
|
871
837
|
],
|
|
872
|
-
parameters:
|
|
873
|
-
prompt:
|
|
838
|
+
parameters: Type.Object({
|
|
839
|
+
prompt: Type.String({
|
|
874
840
|
description: "The task for the agent to perform.",
|
|
875
841
|
}),
|
|
876
|
-
description:
|
|
842
|
+
description: Type.String({
|
|
877
843
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
878
844
|
}),
|
|
879
|
-
subagent_type:
|
|
880
|
-
description: `The type of specialized agent to use. Available types: ${
|
|
845
|
+
subagent_type: Type.String({
|
|
846
|
+
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
|
|
881
847
|
}),
|
|
882
|
-
model:
|
|
848
|
+
model: Type.Optional(Type.String({
|
|
883
849
|
description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
884
850
|
})),
|
|
885
|
-
thinking:
|
|
851
|
+
thinking: Type.Optional(Type.String({
|
|
886
852
|
description: `Thinking level: ${THINKING_LEVELS.join(", ")}. Overrides agent default.`,
|
|
887
853
|
})),
|
|
888
|
-
max_turns:
|
|
854
|
+
max_turns: Type.Optional(Type.Number({
|
|
889
855
|
description: "Maximum number of agentic turns before stopping. Omit for unlimited (default).",
|
|
890
856
|
minimum: 1,
|
|
891
857
|
})),
|
|
892
|
-
run_in_background:
|
|
858
|
+
run_in_background: Type.Optional(Type.Boolean({
|
|
893
859
|
description: "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
894
860
|
})),
|
|
895
|
-
resume:
|
|
861
|
+
resume: Type.Optional(Type.String({
|
|
896
862
|
description: "Optional agent ID to resume from. Continues from previous context.",
|
|
897
863
|
})),
|
|
898
|
-
isolated:
|
|
864
|
+
isolated: Type.Optional(Type.Boolean({
|
|
899
865
|
description: "If true, agent gets no extension/MCP tools — only built-in tools.",
|
|
900
866
|
})),
|
|
901
|
-
inherit_context:
|
|
867
|
+
inherit_context: Type.Optional(Type.Boolean({
|
|
902
868
|
description: "If true, fork parent conversation into the agent. Default: false (fresh context).",
|
|
903
869
|
})),
|
|
904
|
-
isolation:
|
|
870
|
+
isolation: Type.Optional(Type.Literal("worktree", {
|
|
905
871
|
description: 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
906
872
|
})),
|
|
907
873
|
...scheduleParam,
|
|
908
874
|
}),
|
|
909
875
|
// ---- Custom rendering: Claude Code style ----
|
|
910
876
|
renderCall(args, theme) {
|
|
911
|
-
const displayName = args.subagent_type ?
|
|
877
|
+
const displayName = args.subagent_type ? getDisplayName(args.subagent_type) : "Agent";
|
|
912
878
|
const desc = args.description ?? "";
|
|
913
|
-
return new
|
|
879
|
+
return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
|
|
914
880
|
},
|
|
915
881
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
916
882
|
const details = result.details;
|
|
917
883
|
if (!details) {
|
|
918
884
|
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
919
|
-
return new
|
|
885
|
+
return new Text(text, 0, 0);
|
|
920
886
|
}
|
|
921
887
|
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
922
888
|
const stats = (d) => {
|
|
@@ -926,27 +892,27 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
926
892
|
if (d.tags)
|
|
927
893
|
parts.push(...d.tags);
|
|
928
894
|
if (d.turnCount != null && d.turnCount > 0) {
|
|
929
|
-
parts.push(
|
|
895
|
+
parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
930
896
|
}
|
|
931
897
|
if (d.toolUses > 0)
|
|
932
898
|
parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
933
899
|
if (d.tokens)
|
|
934
900
|
parts.push(d.tokens);
|
|
935
|
-
return parts.map(p =>
|
|
901
|
+
return parts.map(p => fgPreservingNestedStyles(theme, "dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
936
902
|
};
|
|
937
903
|
// ---- While running (streaming) ----
|
|
938
904
|
if (isPartial || details.status === "running") {
|
|
939
|
-
const frame =
|
|
905
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
940
906
|
const s = stats(details);
|
|
941
907
|
return renderRunningAgentStatus(frame, s, details.activity ?? "thinking…", theme);
|
|
942
908
|
}
|
|
943
909
|
// ---- Background agent launched ----
|
|
944
910
|
if (details.status === "background") {
|
|
945
|
-
return new
|
|
911
|
+
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
|
|
946
912
|
}
|
|
947
913
|
// ---- Completed / Steered ----
|
|
948
914
|
if (details.status === "completed" || details.status === "steered") {
|
|
949
|
-
const duration =
|
|
915
|
+
const duration = formatMs(details.durationMs);
|
|
950
916
|
const isSteered = details.status === "steered";
|
|
951
917
|
const icon = isSteered ? theme.fg("warning", "✓") : theme.fg("success", "✓");
|
|
952
918
|
const s = stats(details);
|
|
@@ -954,28 +920,28 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
954
920
|
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
955
921
|
if (expanded) {
|
|
956
922
|
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
957
|
-
const container = new pi_tui_1.Container();
|
|
958
|
-
container.addChild(new pi_tui_1.Text(line, 0, 0));
|
|
959
923
|
if (resultText) {
|
|
960
|
-
|
|
924
|
+
const lines = resultText.split("\n").slice(0, 50);
|
|
925
|
+
for (const l of lines) {
|
|
926
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
927
|
+
}
|
|
961
928
|
if (resultText.split("\n").length > 50) {
|
|
962
|
-
|
|
929
|
+
line += "\n" + theme.fg("muted", " ... (use get_subagent_result with verbose for full output)");
|
|
963
930
|
}
|
|
964
931
|
}
|
|
965
|
-
return container;
|
|
966
932
|
}
|
|
967
933
|
else {
|
|
968
934
|
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
969
935
|
line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
|
|
970
936
|
}
|
|
971
|
-
return new
|
|
937
|
+
return new Text(line, 0, 0);
|
|
972
938
|
}
|
|
973
939
|
// ---- Stopped (user-initiated abort) ----
|
|
974
940
|
if (details.status === "stopped") {
|
|
975
941
|
const s = stats(details);
|
|
976
942
|
let line = theme.fg("dim", "■") + (s ? " " + s : "");
|
|
977
943
|
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
978
|
-
return new
|
|
944
|
+
return new Text(line, 0, 0);
|
|
979
945
|
}
|
|
980
946
|
// ---- Error / Aborted (hard max_turns) ----
|
|
981
947
|
const s = stats(details);
|
|
@@ -986,7 +952,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
986
952
|
else {
|
|
987
953
|
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
988
954
|
}
|
|
989
|
-
return new
|
|
955
|
+
return new Text(line, 0, 0);
|
|
990
956
|
},
|
|
991
957
|
// ---- Execute ----
|
|
992
958
|
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
@@ -995,17 +961,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
995
961
|
// Reload custom agents so new project/global .md files are picked up without restart
|
|
996
962
|
reloadCustomAgents();
|
|
997
963
|
const rawType = params.subagent_type;
|
|
998
|
-
const resolved =
|
|
964
|
+
const resolved = resolveType(rawType);
|
|
999
965
|
const subagentType = resolved ?? "general-purpose";
|
|
1000
966
|
const fellBack = resolved === undefined;
|
|
1001
|
-
const displayName =
|
|
967
|
+
const displayName = getDisplayName(subagentType);
|
|
1002
968
|
// Get agent config (if any)
|
|
1003
|
-
const customConfig =
|
|
1004
|
-
const resolvedConfig =
|
|
969
|
+
const customConfig = getAgentConfig(subagentType);
|
|
970
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
1005
971
|
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
1006
972
|
let model = ctx.model;
|
|
1007
973
|
if (resolvedConfig.modelInput) {
|
|
1008
|
-
const resolved =
|
|
974
|
+
const resolved = resolveModel(resolvedConfig.modelInput, ctx.modelRegistry);
|
|
1009
975
|
if (typeof resolved === "string") {
|
|
1010
976
|
if (resolvedConfig.modelFromParams)
|
|
1011
977
|
return textResult(resolved);
|
|
@@ -1025,8 +991,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1025
991
|
// user authored/installed this agent or chose the parent's model; trust it).
|
|
1026
992
|
// See SubagentsSettings.scopeModels docstring for the full policy.
|
|
1027
993
|
if (isScopeModelsEnabled() && model) {
|
|
1028
|
-
const allowed =
|
|
1029
|
-
if (allowed && !
|
|
994
|
+
const allowed = resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd);
|
|
995
|
+
if (allowed && !isModelInScope(model, allowed)) {
|
|
1030
996
|
if (resolvedConfig.modelFromParams) {
|
|
1031
997
|
const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
|
|
1032
998
|
return textResult(`Model not in scope: "${resolvedConfig.modelInput}".\n\n` +
|
|
@@ -1052,20 +1018,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1052
1018
|
const attachTranscript = (rec, agentId) => {
|
|
1053
1019
|
if (!rec || !outputTranscript)
|
|
1054
1020
|
return;
|
|
1055
|
-
rec.outputFile =
|
|
1056
|
-
|
|
1057
|
-
// Keep the existing /tmp output for compatibility, while also writing a
|
|
1058
|
-
// durable project-local copy for reload-safe, read-only history viewing.
|
|
1021
|
+
rec.outputFile = createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId());
|
|
1022
|
+
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
1059
1023
|
try {
|
|
1060
|
-
rec.historyFile =
|
|
1061
|
-
rec.transcriptPath =
|
|
1062
|
-
|
|
1024
|
+
rec.historyFile = createAgentHistoryPath(ctx.cwd, agentId);
|
|
1025
|
+
rec.transcriptPath = agentHistoryLocator(ctx.cwd, rec.historyFile);
|
|
1026
|
+
writeInitialEntry(rec.historyFile, agentId, params.prompt, ctx.cwd);
|
|
1063
1027
|
}
|
|
1064
1028
|
catch (err) {
|
|
1065
|
-
// History is best effort; never disable the existing output transcript.
|
|
1066
1029
|
rec.historyFile = undefined;
|
|
1067
1030
|
rec.transcriptPath = undefined;
|
|
1068
|
-
|
|
1031
|
+
ctx.ui.notify(`Could not create durable transcript for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
1069
1032
|
}
|
|
1070
1033
|
};
|
|
1071
1034
|
const parentModelId = ctx.model?.id;
|
|
@@ -1073,21 +1036,21 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1073
1036
|
const modelName = effectiveModelId && effectiveModelId !== parentModelId
|
|
1074
1037
|
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
1075
1038
|
: undefined;
|
|
1076
|
-
const effectiveMaxTurns =
|
|
1039
|
+
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
1077
1040
|
const agentInvocation = {
|
|
1078
1041
|
modelName,
|
|
1079
1042
|
thinking,
|
|
1080
1043
|
// Explicit value only — the default fallback would just add noise.
|
|
1081
1044
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
1082
|
-
maxTurns:
|
|
1045
|
+
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
1083
1046
|
isolated,
|
|
1084
1047
|
inheritContext,
|
|
1085
1048
|
runInBackground,
|
|
1086
1049
|
isolation,
|
|
1087
1050
|
};
|
|
1088
1051
|
// Tool-result render shows the mode label too; viewer's header already does.
|
|
1089
|
-
const modeLabel =
|
|
1090
|
-
const { tags: invocationTags } =
|
|
1052
|
+
const modeLabel = getPromptModeLabel(subagentType);
|
|
1053
|
+
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
1091
1054
|
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
1092
1055
|
const detailBase = {
|
|
1093
1056
|
displayName,
|
|
@@ -1167,7 +1130,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1167
1130
|
origBgOnSession(session);
|
|
1168
1131
|
const rec = manager.getRecord(id);
|
|
1169
1132
|
if (rec?.outputFile) {
|
|
1170
|
-
rec.outputCleanup =
|
|
1133
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
|
|
1171
1134
|
}
|
|
1172
1135
|
};
|
|
1173
1136
|
try {
|
|
@@ -1189,7 +1152,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1189
1152
|
}
|
|
1190
1153
|
// Set output file + join mode synchronously after spawn, before the
|
|
1191
1154
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
1192
|
-
const joinMode =
|
|
1155
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
1193
1156
|
const record = manager.getRecord(id);
|
|
1194
1157
|
if (record && joinMode) {
|
|
1195
1158
|
record.joinMode = joinMode;
|
|
@@ -1244,8 +1207,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1244
1207
|
maxTurns: fgState.maxTurns,
|
|
1245
1208
|
durationMs: Date.now() - startedAt,
|
|
1246
1209
|
status: "running",
|
|
1247
|
-
activity:
|
|
1248
|
-
spinnerFrame: spinnerFrame %
|
|
1210
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
1211
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
1249
1212
|
};
|
|
1250
1213
|
onUpdate?.({
|
|
1251
1214
|
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
@@ -1273,7 +1236,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1273
1236
|
if (fgId) {
|
|
1274
1237
|
const rec = manager.getRecord(fgId);
|
|
1275
1238
|
if (rec?.outputFile) {
|
|
1276
|
-
rec.outputCleanup =
|
|
1239
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, fgId, ctx.cwd, rec.historyFile);
|
|
1277
1240
|
}
|
|
1278
1241
|
}
|
|
1279
1242
|
};
|
|
@@ -1321,7 +1284,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1321
1284
|
// "general-purpose" may itself be unregistered (defaults disabled, no
|
|
1322
1285
|
// user override) — getConfig then uses the hardcoded fallback config.
|
|
1323
1286
|
const fallbackNote = fellBack
|
|
1324
|
-
? `Note: Unknown agent type "${rawType}" — using ${
|
|
1287
|
+
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1325
1288
|
: "";
|
|
1326
1289
|
if (record.status === "error") {
|
|
1327
1290
|
// Error headline + any partial output the run produced before failing.
|
|
@@ -1331,24 +1294,24 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1331
1294
|
const statsParts = [`${record.toolUses} tool uses`];
|
|
1332
1295
|
if (tokenText)
|
|
1333
1296
|
statsParts.push(tokenText);
|
|
1334
|
-
return textResult(`${fallbackNote}Agent completed in ${
|
|
1297
|
+
return textResult(`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
1335
1298
|
(record.result?.trim() || "No output."), details);
|
|
1336
1299
|
},
|
|
1337
1300
|
}));
|
|
1338
1301
|
// ---- get_subagent_result tool ----
|
|
1339
|
-
pi.registerTool(
|
|
1340
|
-
name:
|
|
1302
|
+
pi.registerTool(defineTool({
|
|
1303
|
+
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
1341
1304
|
label: "Get Agent Result",
|
|
1342
1305
|
description: "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
1343
1306
|
promptSnippet: "Check status and retrieve results from a background agent",
|
|
1344
|
-
parameters:
|
|
1345
|
-
agent_id:
|
|
1307
|
+
parameters: Type.Object({
|
|
1308
|
+
agent_id: Type.String({
|
|
1346
1309
|
description: "The agent ID to check.",
|
|
1347
1310
|
}),
|
|
1348
|
-
wait:
|
|
1311
|
+
wait: Type.Optional(Type.Boolean({
|
|
1349
1312
|
description: "If true, wait for the agent to complete before returning. Default: false.",
|
|
1350
1313
|
})),
|
|
1351
|
-
verbose:
|
|
1314
|
+
verbose: Type.Optional(Type.Boolean({
|
|
1352
1315
|
description: "If true, include the agent's full conversation (messages + tool calls). Default: false.",
|
|
1353
1316
|
})),
|
|
1354
1317
|
}),
|
|
@@ -1369,10 +1332,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1369
1332
|
if (record.promise)
|
|
1370
1333
|
await abortable(record.promise, signal);
|
|
1371
1334
|
}
|
|
1372
|
-
const displayName =
|
|
1373
|
-
const duration =
|
|
1335
|
+
const displayName = getDisplayName(record.type);
|
|
1336
|
+
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
1374
1337
|
const tokens = formatLifetimeTokens(record);
|
|
1375
|
-
const contextPercent =
|
|
1338
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
1376
1339
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
1377
1340
|
if (tokens)
|
|
1378
1341
|
statsParts.push(tokens);
|
|
@@ -1382,7 +1345,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1382
1345
|
statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
1383
1346
|
statsParts.push(`Duration: ${duration}`);
|
|
1384
1347
|
let output = `Agent: ${record.id}\n` +
|
|
1385
|
-
`Type: ${displayName} | Status: ${record.status}${
|
|
1348
|
+
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
1386
1349
|
`Description: ${record.description}\n\n`;
|
|
1387
1350
|
if (record.status === "running") {
|
|
1388
1351
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
@@ -1400,7 +1363,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1400
1363
|
}
|
|
1401
1364
|
// Verbose: include full conversation
|
|
1402
1365
|
if (params.verbose && record.session) {
|
|
1403
|
-
const conversation =
|
|
1366
|
+
const conversation = getAgentConversation(record.session);
|
|
1404
1367
|
if (conversation) {
|
|
1405
1368
|
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
1406
1369
|
}
|
|
@@ -1409,17 +1372,17 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1409
1372
|
},
|
|
1410
1373
|
}));
|
|
1411
1374
|
// ---- steer_subagent tool ----
|
|
1412
|
-
pi.registerTool(
|
|
1413
|
-
name:
|
|
1375
|
+
pi.registerTool(defineTool({
|
|
1376
|
+
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
1414
1377
|
label: "Steer Agent",
|
|
1415
1378
|
description: "Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
1416
1379
|
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
1417
1380
|
promptSnippet: "Send a steering message to redirect a running background agent",
|
|
1418
|
-
parameters:
|
|
1419
|
-
agent_id:
|
|
1381
|
+
parameters: Type.Object({
|
|
1382
|
+
agent_id: Type.String({
|
|
1420
1383
|
description: "The agent ID to steer (must be currently running).",
|
|
1421
1384
|
}),
|
|
1422
|
-
message:
|
|
1385
|
+
message: Type.String({
|
|
1423
1386
|
description: "The steering message to send. This will appear as a user message in the agent's conversation.",
|
|
1424
1387
|
}),
|
|
1425
1388
|
}),
|
|
@@ -1440,10 +1403,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1440
1403
|
return textResult(`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`);
|
|
1441
1404
|
}
|
|
1442
1405
|
try {
|
|
1443
|
-
await
|
|
1406
|
+
await steerAgent(record.session, params.message);
|
|
1444
1407
|
pi.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
1445
1408
|
const tokens = formatLifetimeTokens(record);
|
|
1446
|
-
const contextPercent =
|
|
1409
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
1447
1410
|
const stateParts = [];
|
|
1448
1411
|
if (tokens)
|
|
1449
1412
|
stateParts.push(tokens);
|
|
@@ -1461,30 +1424,30 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1461
1424
|
},
|
|
1462
1425
|
}));
|
|
1463
1426
|
// ---- /agents interactive menu ----
|
|
1464
|
-
const projectAgentsDir = () =>
|
|
1465
|
-
const workspaceAgentsDir = () =>
|
|
1466
|
-
const personalAgentsDir = () =>
|
|
1427
|
+
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
1428
|
+
const workspaceAgentsDir = () => join(process.cwd(), ".agents", "agents");
|
|
1429
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
1467
1430
|
/** Find the file path of a custom agent by name, in discovery-precedence order (project, workspace, then global). */
|
|
1468
1431
|
function findAgentFile(name) {
|
|
1469
|
-
const projectPath =
|
|
1470
|
-
if (
|
|
1432
|
+
const projectPath = join(projectAgentsDir(), `${name}.md`);
|
|
1433
|
+
if (existsSync(projectPath))
|
|
1471
1434
|
return { path: projectPath, location: "project" };
|
|
1472
|
-
const workspacePath =
|
|
1473
|
-
if (
|
|
1435
|
+
const workspacePath = join(workspaceAgentsDir(), `${name}.md`);
|
|
1436
|
+
if (existsSync(workspacePath))
|
|
1474
1437
|
return { path: workspacePath, location: "workspace" };
|
|
1475
|
-
const personalPath =
|
|
1476
|
-
if (
|
|
1438
|
+
const personalPath = join(personalAgentsDir(), `${name}.md`);
|
|
1439
|
+
if (existsSync(personalPath))
|
|
1477
1440
|
return { path: personalPath, location: "personal" };
|
|
1478
1441
|
return undefined;
|
|
1479
1442
|
}
|
|
1480
1443
|
function getModelLabel(type, registry) {
|
|
1481
|
-
const cfg =
|
|
1444
|
+
const cfg = getAgentConfig(type);
|
|
1482
1445
|
if (!cfg?.model)
|
|
1483
1446
|
return "inherit"; // no model configured → really inherits parent
|
|
1484
1447
|
const label = getModelLabelFromConfig(cfg.model);
|
|
1485
1448
|
if (!registry)
|
|
1486
1449
|
return label;
|
|
1487
|
-
const resolved =
|
|
1450
|
+
const resolved = resolveModel(cfg.model, registry);
|
|
1488
1451
|
// Configured but unresolvable: the runtime silently falls back to the parent
|
|
1489
1452
|
// model, so flag it (and the fallback) rather than hiding the config.
|
|
1490
1453
|
if (typeof resolved === "string")
|
|
@@ -1500,16 +1463,13 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1500
1463
|
}
|
|
1501
1464
|
async function showAgentsMenu(ctx) {
|
|
1502
1465
|
reloadCustomAgents();
|
|
1503
|
-
const allNames =
|
|
1466
|
+
const allNames = getAllTypes();
|
|
1504
1467
|
// Build select options
|
|
1505
1468
|
const options = [];
|
|
1506
|
-
//
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const done = agents.filter(a => a.status === "completed" || a.status === "steered").length;
|
|
1511
|
-
options.push(`Running agents (${agents.length}) — ${running} running, ${done} done`);
|
|
1512
|
-
}
|
|
1469
|
+
// Keep active agents and terminal history in separate menu entries.
|
|
1470
|
+
const records = manager.listAgents();
|
|
1471
|
+
const { active, history } = splitAgentRecords(records, ctx.cwd);
|
|
1472
|
+
options.push(...buildAgentStatusMenuEntries(records, ctx.cwd));
|
|
1513
1473
|
// Agent types list
|
|
1514
1474
|
if (allNames.length > 0) {
|
|
1515
1475
|
options.push(`Agent types (${allNames.length})`);
|
|
@@ -1522,7 +1482,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1522
1482
|
// Actions
|
|
1523
1483
|
options.push("Create new agent");
|
|
1524
1484
|
options.push("Settings");
|
|
1525
|
-
const noAgentsMsg = allNames.length === 0 &&
|
|
1485
|
+
const noAgentsMsg = allNames.length === 0 && active.length === 0 && history.length === 0
|
|
1526
1486
|
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
1527
1487
|
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
1528
1488
|
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
@@ -1537,12 +1497,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1537
1497
|
await showRunningAgents(ctx);
|
|
1538
1498
|
await showAgentsMenu(ctx);
|
|
1539
1499
|
}
|
|
1500
|
+
else if (choice.startsWith("Agent history (")) {
|
|
1501
|
+
await showAgentHistory(ctx);
|
|
1502
|
+
await showAgentsMenu(ctx);
|
|
1503
|
+
}
|
|
1540
1504
|
else if (choice.startsWith("Agent types (")) {
|
|
1541
1505
|
await showAllAgentsList(ctx);
|
|
1542
1506
|
await showAgentsMenu(ctx);
|
|
1543
1507
|
}
|
|
1544
1508
|
else if (choice.startsWith("Scheduled jobs (")) {
|
|
1545
|
-
await
|
|
1509
|
+
await showSchedulesMenu(ctx, scheduler);
|
|
1546
1510
|
await showAgentsMenu(ctx);
|
|
1547
1511
|
}
|
|
1548
1512
|
else if (choice === "Create new agent") {
|
|
@@ -1554,7 +1518,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1554
1518
|
}
|
|
1555
1519
|
}
|
|
1556
1520
|
async function showAllAgentsList(ctx) {
|
|
1557
|
-
const allNames =
|
|
1521
|
+
const allNames = getAllTypes();
|
|
1558
1522
|
if (allNames.length === 0) {
|
|
1559
1523
|
ctx.ui.notify("No agents.", "info");
|
|
1560
1524
|
return;
|
|
@@ -1575,7 +1539,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1575
1539
|
// full description renders below the highlighted row via SettingsList,
|
|
1576
1540
|
// exactly like the Settings menu — so long descriptions never wrap the list.
|
|
1577
1541
|
const items = allNames.map(name => {
|
|
1578
|
-
const cfg =
|
|
1542
|
+
const cfg = getAgentConfig(name);
|
|
1579
1543
|
const disabled = cfg?.enabled === false;
|
|
1580
1544
|
const model = getModelLabel(name, ctx.modelRegistry);
|
|
1581
1545
|
return {
|
|
@@ -1588,22 +1552,22 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1588
1552
|
values: [model],
|
|
1589
1553
|
};
|
|
1590
1554
|
});
|
|
1591
|
-
const hasCustom = allNames.some(n => { const c =
|
|
1592
|
-
const hasDisabled = allNames.some(n =>
|
|
1555
|
+
const hasCustom = allNames.some(n => { const c = getAgentConfig(n); return c && !c.isDefault && c.enabled !== false; });
|
|
1556
|
+
const hasDisabled = allNames.some(n => getAgentConfig(n)?.enabled === false);
|
|
1593
1557
|
const legendParts = [];
|
|
1594
1558
|
if (hasCustom)
|
|
1595
1559
|
legendParts.push("• = project ◦ = global");
|
|
1596
1560
|
if (hasDisabled)
|
|
1597
1561
|
legendParts.push("✕ = disabled");
|
|
1598
1562
|
const selected = await ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
1599
|
-
const slTheme =
|
|
1600
|
-
const list = new
|
|
1563
|
+
const slTheme = getSettingsListTheme();
|
|
1564
|
+
const list = new SettingsList(items, Math.min(items.length, 12), slTheme, id => done(id), // Enter/Space on a row → return that agent's name
|
|
1601
1565
|
() => done(undefined));
|
|
1602
|
-
const container = new
|
|
1603
|
-
container.addChild(new
|
|
1566
|
+
const container = new Container();
|
|
1567
|
+
container.addChild(new Text("Agent types", 0, 0));
|
|
1604
1568
|
if (legendParts.length)
|
|
1605
|
-
container.addChild(new
|
|
1606
|
-
container.addChild(new
|
|
1569
|
+
container.addChild(new Text(slTheme.hint(legendParts.join(" ")), 0, 0));
|
|
1570
|
+
container.addChild(new Spacer(1));
|
|
1607
1571
|
container.addChild(list);
|
|
1608
1572
|
return {
|
|
1609
1573
|
render: (w) => container.render(w),
|
|
@@ -1611,20 +1575,20 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1611
1575
|
handleInput: (data) => list.handleInput?.(data),
|
|
1612
1576
|
};
|
|
1613
1577
|
});
|
|
1614
|
-
if (selected &&
|
|
1578
|
+
if (selected && getAgentConfig(selected)) {
|
|
1615
1579
|
await showAgentDetail(ctx, selected);
|
|
1616
1580
|
await showAllAgentsList(ctx);
|
|
1617
1581
|
}
|
|
1618
1582
|
}
|
|
1619
1583
|
async function showRunningAgents(ctx) {
|
|
1620
|
-
const agents = manager.listAgents()
|
|
1584
|
+
const { active: agents } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
1621
1585
|
if (agents.length === 0) {
|
|
1622
1586
|
ctx.ui.notify("No agents.", "info");
|
|
1623
1587
|
return;
|
|
1624
1588
|
}
|
|
1625
1589
|
const options = agents.map(a => {
|
|
1626
|
-
const dn =
|
|
1627
|
-
const dur =
|
|
1590
|
+
const dn = getDisplayName(a.type);
|
|
1591
|
+
const dur = formatDuration(a.startedAt, a.completedAt);
|
|
1628
1592
|
return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
|
|
1629
1593
|
});
|
|
1630
1594
|
const choice = await ctx.ui.select("Running agents", options);
|
|
@@ -1635,41 +1599,66 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1635
1599
|
if (idx < 0)
|
|
1636
1600
|
return;
|
|
1637
1601
|
const record = agents[idx];
|
|
1638
|
-
await viewAgentConversation(ctx, record);
|
|
1602
|
+
await viewAgentConversation(ctx, record, "live");
|
|
1639
1603
|
// Back-navigation: re-show the list
|
|
1640
1604
|
await showRunningAgents(ctx);
|
|
1641
1605
|
}
|
|
1642
|
-
async function
|
|
1643
|
-
|
|
1644
|
-
|
|
1606
|
+
async function showAgentHistory(ctx) {
|
|
1607
|
+
const { history } = splitAgentRecords(manager.listAgents(), ctx.cwd);
|
|
1608
|
+
if (history.length === 0) {
|
|
1609
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const options = history.map(record => formatAgentHistoryOption(record, Date.now()));
|
|
1613
|
+
const choice = await ctx.ui.select("Agent history", options);
|
|
1614
|
+
if (!choice)
|
|
1615
|
+
return;
|
|
1616
|
+
const record = history[options.indexOf(choice)];
|
|
1617
|
+
if (!record)
|
|
1618
|
+
return;
|
|
1619
|
+
await viewAgentConversation(ctx, record, "history");
|
|
1620
|
+
}
|
|
1621
|
+
async function viewAgentConversation(ctx, record, mode) {
|
|
1622
|
+
if (mode === "live" && !canOpenActiveAgent(record)) {
|
|
1623
|
+
ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no history available.`, "info");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (mode === "history" && !canOpenAgentHistory(record, ctx.cwd)) {
|
|
1627
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1645
1628
|
return;
|
|
1646
1629
|
}
|
|
1647
1630
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT, createStaticConversationSource } = await import("./ui/conversation-viewer.js");
|
|
1648
|
-
const session =
|
|
1649
|
-
?
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1631
|
+
const session = mode === "live"
|
|
1632
|
+
? record.session
|
|
1633
|
+
: (() => {
|
|
1634
|
+
const messages = record.transcriptPath
|
|
1635
|
+
? readAgentHistory(ctx.cwd, record.transcriptPath)
|
|
1636
|
+
: undefined;
|
|
1637
|
+
return messages
|
|
1638
|
+
? createStaticConversationSource(messages)
|
|
1639
|
+
: record.session
|
|
1640
|
+
? createStaticConversationSource(record.session.messages)
|
|
1641
|
+
: undefined;
|
|
1642
|
+
})();
|
|
1654
1643
|
if (!session) {
|
|
1655
|
-
ctx.ui.notify(
|
|
1644
|
+
ctx.ui.notify("No agent history.", "info");
|
|
1656
1645
|
return;
|
|
1657
1646
|
}
|
|
1658
1647
|
const activity = agentActivity.get(record.id);
|
|
1659
|
-
const isLive =
|
|
1648
|
+
const isLive = mode === "live";
|
|
1660
1649
|
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
1661
1650
|
return new ConversationViewer(tui, session, record, activity, theme, done, isLive ? () => {
|
|
1662
1651
|
if (manager.abort(record.id)) {
|
|
1663
1652
|
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1664
1653
|
}
|
|
1665
|
-
} : undefined, keybindings, isLive ? (message) => manager.steer(record.id, message) : undefined);
|
|
1654
|
+
} : undefined, keybindings, isLive ? (message) => manager.steer(record.id, message) : undefined, mode === "history" ? { pi, ctx, readOnly: true } : { pi, ctx });
|
|
1666
1655
|
}, {
|
|
1667
1656
|
overlay: true,
|
|
1668
1657
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
1669
1658
|
});
|
|
1670
1659
|
}
|
|
1671
1660
|
async function showAgentDetail(ctx, name) {
|
|
1672
|
-
const cfg =
|
|
1661
|
+
const cfg = getAgentConfig(name);
|
|
1673
1662
|
if (!cfg) {
|
|
1674
1663
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
1675
1664
|
return;
|
|
@@ -1700,7 +1689,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1700
1689
|
if (!choice || choice === "Back")
|
|
1701
1690
|
return;
|
|
1702
1691
|
if (choice === "Edit" && file) {
|
|
1703
|
-
const content =
|
|
1692
|
+
const content = readFileSync(file.path, "utf-8");
|
|
1704
1693
|
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
1705
1694
|
if (edited !== undefined && edited !== content) {
|
|
1706
1695
|
const { writeFileSync } = await import("node:fs");
|
|
@@ -1713,7 +1702,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1713
1702
|
if (file) {
|
|
1714
1703
|
const confirmed = await ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`);
|
|
1715
1704
|
if (confirmed) {
|
|
1716
|
-
|
|
1705
|
+
unlinkSync(file.path);
|
|
1717
1706
|
reloadCustomAgents();
|
|
1718
1707
|
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
1719
1708
|
}
|
|
@@ -1722,7 +1711,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1722
1711
|
else if (choice === "Reset to default" && file) {
|
|
1723
1712
|
const confirmed = await ctx.ui.confirm("Reset to default", `Delete override ${file.path} and restore embedded default?`);
|
|
1724
1713
|
if (confirmed) {
|
|
1725
|
-
|
|
1714
|
+
unlinkSync(file.path);
|
|
1726
1715
|
reloadCustomAgents();
|
|
1727
1716
|
ctx.ui.notify(`Restored default ${name}`, "info");
|
|
1728
1717
|
}
|
|
@@ -1746,9 +1735,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1746
1735
|
if (!location)
|
|
1747
1736
|
return;
|
|
1748
1737
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1749
|
-
|
|
1750
|
-
const targetPath =
|
|
1751
|
-
if (
|
|
1738
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1739
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1740
|
+
if (existsSync(targetPath)) {
|
|
1752
1741
|
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1753
1742
|
if (!overwrite)
|
|
1754
1743
|
return;
|
|
@@ -1801,7 +1790,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1801
1790
|
const file = findAgentFile(name);
|
|
1802
1791
|
if (file) {
|
|
1803
1792
|
// Existing file — set enabled: false in frontmatter (idempotent)
|
|
1804
|
-
const content =
|
|
1793
|
+
const content = readFileSync(file.path, "utf-8");
|
|
1805
1794
|
if (content.includes("\nenabled: false\n")) {
|
|
1806
1795
|
ctx.ui.notify(`${name} is already disabled.`, "info");
|
|
1807
1796
|
return;
|
|
@@ -1821,8 +1810,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1821
1810
|
if (!location)
|
|
1822
1811
|
return;
|
|
1823
1812
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1824
|
-
|
|
1825
|
-
const targetPath =
|
|
1813
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1814
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1826
1815
|
const { writeFileSync } = await import("node:fs");
|
|
1827
1816
|
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
1828
1817
|
reloadCustomAgents();
|
|
@@ -1833,12 +1822,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1833
1822
|
const file = findAgentFile(name);
|
|
1834
1823
|
if (!file)
|
|
1835
1824
|
return;
|
|
1836
|
-
const content =
|
|
1825
|
+
const content = readFileSync(file.path, "utf-8");
|
|
1837
1826
|
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
1838
1827
|
const { writeFileSync } = await import("node:fs");
|
|
1839
1828
|
// If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
|
|
1840
1829
|
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
1841
|
-
|
|
1830
|
+
unlinkSync(file.path);
|
|
1842
1831
|
reloadCustomAgents();
|
|
1843
1832
|
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
1844
1833
|
}
|
|
@@ -1876,9 +1865,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1876
1865
|
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
1877
1866
|
if (!name)
|
|
1878
1867
|
return;
|
|
1879
|
-
|
|
1880
|
-
const targetPath =
|
|
1881
|
-
if (
|
|
1868
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1869
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1870
|
+
if (existsSync(targetPath)) {
|
|
1882
1871
|
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1883
1872
|
if (!overwrite)
|
|
1884
1873
|
return;
|
|
@@ -1932,7 +1921,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1932
1921
|
return;
|
|
1933
1922
|
}
|
|
1934
1923
|
reloadCustomAgents();
|
|
1935
|
-
if (
|
|
1924
|
+
if (existsSync(targetPath)) {
|
|
1936
1925
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1937
1926
|
}
|
|
1938
1927
|
else {
|
|
@@ -1954,7 +1943,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1954
1943
|
return;
|
|
1955
1944
|
let tools;
|
|
1956
1945
|
if (toolChoice === "all") {
|
|
1957
|
-
tools =
|
|
1946
|
+
tools = BUILTIN_TOOL_NAMES.join(", ");
|
|
1958
1947
|
}
|
|
1959
1948
|
else if (toolChoice === "none") {
|
|
1960
1949
|
tools = "none";
|
|
@@ -1963,7 +1952,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1963
1952
|
tools = "read, bash, grep, find, ls";
|
|
1964
1953
|
}
|
|
1965
1954
|
else {
|
|
1966
|
-
const customTools = await ctx.ui.input("Tools (comma-separated)",
|
|
1955
|
+
const customTools = await ctx.ui.input("Tools (comma-separated)", BUILTIN_TOOL_NAMES.join(", "));
|
|
1967
1956
|
if (!customTools)
|
|
1968
1957
|
return;
|
|
1969
1958
|
tools = customTools;
|
|
@@ -2011,9 +2000,9 @@ prompt_mode: replace
|
|
|
2011
2000
|
|
|
2012
2001
|
${systemPrompt}
|
|
2013
2002
|
`;
|
|
2014
|
-
|
|
2015
|
-
const targetPath =
|
|
2016
|
-
if (
|
|
2003
|
+
mkdirSync(targetDir, { recursive: true });
|
|
2004
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
2005
|
+
if (existsSync(targetPath)) {
|
|
2017
2006
|
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
2018
2007
|
if (!overwrite)
|
|
2019
2008
|
return;
|
|
@@ -2028,12 +2017,12 @@ ${systemPrompt}
|
|
|
2028
2017
|
maxConcurrent: manager.getMaxConcurrent(),
|
|
2029
2018
|
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
2030
2019
|
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
2031
|
-
defaultMaxTurns:
|
|
2032
|
-
graceTurns:
|
|
2020
|
+
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
2021
|
+
graceTurns: getGraceTurns(),
|
|
2033
2022
|
defaultJoinMode: getDefaultJoinMode(),
|
|
2034
2023
|
schedulingEnabled: isSchedulingEnabled(),
|
|
2035
2024
|
scopeModels: isScopeModelsEnabled(),
|
|
2036
|
-
disableDefaultAgents:
|
|
2025
|
+
disableDefaultAgents: isDefaultsDisabled(),
|
|
2037
2026
|
toolDescriptionMode: getToolDescriptionMode(),
|
|
2038
2027
|
fleetView: isFleetViewEnabled(),
|
|
2039
2028
|
widgetMode: getWidgetMode(),
|
|
@@ -2044,8 +2033,8 @@ ${systemPrompt}
|
|
|
2044
2033
|
async function showSettings(ctx) {
|
|
2045
2034
|
function buildItems() {
|
|
2046
2035
|
const mc = manager.getMaxConcurrent();
|
|
2047
|
-
const dmt =
|
|
2048
|
-
const gt =
|
|
2036
|
+
const dmt = getDefaultMaxTurns() ?? 0;
|
|
2037
|
+
const gt = getGraceTurns();
|
|
2049
2038
|
return [
|
|
2050
2039
|
{
|
|
2051
2040
|
id: "maxConcurrent",
|
|
@@ -2093,7 +2082,7 @@ ${systemPrompt}
|
|
|
2093
2082
|
id: "disableDefaultAgents",
|
|
2094
2083
|
label: "Disable defaults",
|
|
2095
2084
|
description: "Hide built-in agents (general-purpose, Explore, Plan) — custom agents are unaffected",
|
|
2096
|
-
currentValue:
|
|
2085
|
+
currentValue: isDefaultsDisabled() ? "on" : "off",
|
|
2097
2086
|
values: ["on", "off"],
|
|
2098
2087
|
},
|
|
2099
2088
|
{
|
|
@@ -2137,18 +2126,18 @@ ${systemPrompt}
|
|
|
2137
2126
|
else if (id === "defaultMaxTurns") {
|
|
2138
2127
|
const n = parseInt(value, 10);
|
|
2139
2128
|
if (n === 0) {
|
|
2140
|
-
|
|
2129
|
+
setDefaultMaxTurns(undefined);
|
|
2141
2130
|
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
2142
2131
|
}
|
|
2143
2132
|
else if (n >= 1) {
|
|
2144
|
-
|
|
2133
|
+
setDefaultMaxTurns(n);
|
|
2145
2134
|
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
2146
2135
|
}
|
|
2147
2136
|
}
|
|
2148
2137
|
else if (id === "graceTurns") {
|
|
2149
2138
|
const n = parseInt(value, 10);
|
|
2150
2139
|
if (n >= 1) {
|
|
2151
|
-
|
|
2140
|
+
setGraceTurns(n);
|
|
2152
2141
|
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
2153
2142
|
}
|
|
2154
2143
|
}
|
|
@@ -2203,26 +2192,26 @@ ${systemPrompt}
|
|
|
2203
2192
|
let currentIndex = 0;
|
|
2204
2193
|
const result = await ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
2205
2194
|
const items = buildItems();
|
|
2206
|
-
list = new
|
|
2195
|
+
list = new SettingsList(items, items.length + 2, getSettingsListTheme(), (id, newValue) => {
|
|
2207
2196
|
applyValue(id, newValue);
|
|
2208
2197
|
}, () => done(undefined));
|
|
2209
|
-
const container = new
|
|
2210
|
-
container.addChild(new
|
|
2211
|
-
container.addChild(new
|
|
2198
|
+
const container = new Container();
|
|
2199
|
+
container.addChild(new Text("⚙ Subagent Settings", 0, 0));
|
|
2200
|
+
container.addChild(new Spacer(1));
|
|
2212
2201
|
container.addChild(list);
|
|
2213
2202
|
return {
|
|
2214
2203
|
render: (w) => container.render(w),
|
|
2215
2204
|
invalidate: () => container.invalidate(),
|
|
2216
2205
|
handleInput: (data) => {
|
|
2217
2206
|
// Track navigation so Enter knows the current field
|
|
2218
|
-
if (
|
|
2207
|
+
if (matchesKey(data, "up")) {
|
|
2219
2208
|
currentIndex = Math.max(0, currentIndex - 1);
|
|
2220
2209
|
}
|
|
2221
|
-
else if (
|
|
2210
|
+
else if (matchesKey(data, "down")) {
|
|
2222
2211
|
currentIndex = Math.min(items.length - 1, currentIndex + 1);
|
|
2223
2212
|
}
|
|
2224
2213
|
// Enter on numeric field → close and prompt for typed input
|
|
2225
|
-
if (
|
|
2214
|
+
if (matchesKey(data, Key.enter) && NUMERIC_IDS.has(items[currentIndex].id)) {
|
|
2226
2215
|
done(items[currentIndex].id);
|
|
2227
2216
|
return;
|
|
2228
2217
|
}
|
|
@@ -2235,8 +2224,8 @@ ${systemPrompt}
|
|
|
2235
2224
|
const current = result === "maxConcurrent"
|
|
2236
2225
|
? String(manager.getMaxConcurrent())
|
|
2237
2226
|
: result === "defaultMaxTurns"
|
|
2238
|
-
? String(
|
|
2239
|
-
: String(
|
|
2227
|
+
? String(getDefaultMaxTurns() ?? 0)
|
|
2228
|
+
: String(getGraceTurns());
|
|
2240
2229
|
const label = result === "maxConcurrent"
|
|
2241
2230
|
? "Max concurrency (1+)"
|
|
2242
2231
|
: result === "defaultMaxTurns"
|
|
@@ -2263,7 +2252,7 @@ ${systemPrompt}
|
|
|
2263
2252
|
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
2264
2253
|
// of outcome so listeners see the in-memory change.
|
|
2265
2254
|
function notifyApplied(ctx, successMsg) {
|
|
2266
|
-
const { message, level } =
|
|
2255
|
+
const { message, level } = saveAndEmitChanged(snapshotSettings(), successMsg, (event, payload) => pi.events.emit(event, payload));
|
|
2267
2256
|
ctx.ui.notify(message, level);
|
|
2268
2257
|
}
|
|
2269
2258
|
pi.registerCommand("agents", {
|