@esso0428/pi-subagents 0.15.0 → 0.15.2
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 +10 -0
- package/README.md +2 -2
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +87 -19
- package/dist/agent-runner.js +85 -66
- package/dist/agent-types.js +45 -27
- package/dist/context.js +6 -2
- package/dist/cross-extension-rpc.js +9 -5
- package/dist/custom-agents.js +18 -15
- package/dist/default-agents.js +4 -1
- package/dist/enabled-models.js +16 -11
- package/dist/env.js +4 -1
- package/dist/group-join.js +5 -1
- package/dist/index.js +280 -221
- package/dist/invocation-config.js +6 -2
- package/dist/memory.js +34 -24
- package/dist/model-resolver.js +4 -1
- package/dist/nico-overrides.js +20 -14
- package/dist/output-file.js +21 -15
- package/dist/prompts.js +4 -1
- package/dist/schedule-store.js +21 -16
- package/dist/schedule.js +12 -8
- package/dist/settings.js +23 -15
- package/dist/skill-loader.js +23 -20
- package/dist/status-note.js +4 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js +4 -1
- package/dist/ui/agent-widget.js +37 -23
- package/dist/ui/conversation-viewer.js +43 -39
- package/dist/ui/fleet-list.js +30 -24
- package/dist/ui/markdown-result.d.ts +3 -0
- package/dist/ui/markdown-result.js +53 -0
- package/dist/ui/schedule-menu.js +4 -1
- package/dist/ui/viewer-keys.js +10 -7
- package/dist/usage.js +10 -4
- package/dist/worktree.js +31 -26
- package/package.json +1 -1
- package/src/agent-manager.ts +72 -0
- package/src/agent-runner.ts +11 -3
- package/src/index.ts +39 -16
- package/src/types.ts +1 -0
- package/src/ui/markdown-result.ts +56 -0
- package/test/agent-manager-history.test.ts +84 -0
- package/test/ui/markdown-result.test.ts +45 -0
package/dist/ui/fleet-list.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
|
|
3
4
|
*
|
|
@@ -10,10 +11,14 @@
|
|
|
10
11
|
* handling goes through `onTerminalInput` — which fires before the focused editor and
|
|
11
12
|
* can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FleetList = void 0;
|
|
16
|
+
exports.formatFleetElapsed = formatFleetElapsed;
|
|
17
|
+
exports.formatFleetTokens = formatFleetTokens;
|
|
18
|
+
const pi_tui_1 = require("@earendil-works/pi-tui");
|
|
19
|
+
const usage_js_1 = require("../usage.js");
|
|
20
|
+
const agent_widget_js_1 = require("./agent-widget.js");
|
|
21
|
+
const conversation_viewer_js_1 = require("./conversation-viewer.js");
|
|
17
22
|
/** Widget key for the below-editor fleet list. */
|
|
18
23
|
const FLEET_KEY = "fleet";
|
|
19
24
|
/** Max agent rows shown at once; extras collapse into a "↓ N more" indicator. */
|
|
@@ -23,11 +28,11 @@ const TICK_MS = 200;
|
|
|
23
28
|
/** How long a finished agent lingers in the list before it drops out. */
|
|
24
29
|
const FINISHED_LINGER_MS = 4000;
|
|
25
30
|
/** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
|
|
26
|
-
|
|
31
|
+
function formatFleetElapsed(ms) {
|
|
27
32
|
return `${Math.max(0, Math.round(ms / 1000))}s`;
|
|
28
33
|
}
|
|
29
34
|
/** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
|
|
30
|
-
|
|
35
|
+
function formatFleetTokens(count) {
|
|
31
36
|
let compact;
|
|
32
37
|
if (count >= 1_000_000)
|
|
33
38
|
compact = `${(count / 1_000_000).toFixed(1)}M`;
|
|
@@ -43,13 +48,13 @@ export function formatFleetTokens(count) {
|
|
|
43
48
|
* desync pi's line-diff → flicker) even on a terminal too narrow for the stats.
|
|
44
49
|
*/
|
|
45
50
|
function rightAlign(left, right, width) {
|
|
46
|
-
const rightW = visibleWidth(right);
|
|
51
|
+
const rightW = (0, pi_tui_1.visibleWidth)(right);
|
|
47
52
|
const maxLeft = Math.max(0, width - rightW - 1);
|
|
48
|
-
const leftClamped = truncateToWidth(left, maxLeft);
|
|
49
|
-
const gap = Math.max(1, width - visibleWidth(leftClamped) - rightW);
|
|
50
|
-
return truncateToWidth(leftClamped + " ".repeat(gap) + right, width);
|
|
53
|
+
const leftClamped = (0, pi_tui_1.truncateToWidth)(left, maxLeft);
|
|
54
|
+
const gap = Math.max(1, width - (0, pi_tui_1.visibleWidth)(leftClamped) - rightW);
|
|
55
|
+
return (0, pi_tui_1.truncateToWidth)(leftClamped + " ".repeat(gap) + right, width);
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
class FleetList {
|
|
53
58
|
manager;
|
|
54
59
|
agentActivity;
|
|
55
60
|
ui;
|
|
@@ -190,7 +195,7 @@ export class FleetList {
|
|
|
190
195
|
// Input listeners receive BOTH key-press and key-release (the kitty protocol
|
|
191
196
|
// emits both, and matchesKey matches either) — act on press only, or every
|
|
192
197
|
// tap would move/fire twice. Repeats still pass through for held-key nav.
|
|
193
|
-
if (isKeyRelease(data))
|
|
198
|
+
if ((0, pi_tui_1.isKeyRelease)(data))
|
|
194
199
|
return undefined;
|
|
195
200
|
// While an overlay is open, let it own all input.
|
|
196
201
|
if (this.viewerClose)
|
|
@@ -206,7 +211,7 @@ export class FleetList {
|
|
|
206
211
|
}
|
|
207
212
|
if (!this.active) {
|
|
208
213
|
// Activate: ↓ or ← at an empty prompt moves focus into the list.
|
|
209
|
-
const isActivator = matchesKey(data, "down") || matchesKey(data, "left");
|
|
214
|
+
const isActivator = (0, pi_tui_1.matchesKey)(data, "down") || (0, pi_tui_1.matchesKey)(data, "left");
|
|
210
215
|
if (isActivator && this.agentRecords().length > 0 && this.ui.getEditorText() === "") {
|
|
211
216
|
this.active = true;
|
|
212
217
|
this.selectedIndex = 0;
|
|
@@ -216,13 +221,13 @@ export class FleetList {
|
|
|
216
221
|
return undefined;
|
|
217
222
|
}
|
|
218
223
|
// Active — arrows navigate, Enter opens, Esc / Up-past-top exits.
|
|
219
|
-
if (matchesKey(data, "down")) {
|
|
224
|
+
if ((0, pi_tui_1.matchesKey)(data, "down")) {
|
|
220
225
|
const max = this.roster().length - 1;
|
|
221
226
|
this.selectedIndex = Math.min(max, this.selectedIndex + 1);
|
|
222
227
|
this.update();
|
|
223
228
|
return { consume: true };
|
|
224
229
|
}
|
|
225
|
-
if (matchesKey(data, "up")) {
|
|
230
|
+
if ((0, pi_tui_1.matchesKey)(data, "up")) {
|
|
226
231
|
if (this.selectedIndex === 0) {
|
|
227
232
|
this.deactivate();
|
|
228
233
|
return { consume: true };
|
|
@@ -231,11 +236,11 @@ export class FleetList {
|
|
|
231
236
|
this.update();
|
|
232
237
|
return { consume: true };
|
|
233
238
|
}
|
|
234
|
-
if (matchesKey(data, "escape")) {
|
|
239
|
+
if ((0, pi_tui_1.matchesKey)(data, "escape")) {
|
|
235
240
|
this.deactivate();
|
|
236
241
|
return { consume: true };
|
|
237
242
|
}
|
|
238
|
-
if (matchesKey(data, Key.enter)) {
|
|
243
|
+
if ((0, pi_tui_1.matchesKey)(data, pi_tui_1.Key.enter)) {
|
|
239
244
|
this.openSelected();
|
|
240
245
|
return { consume: true };
|
|
241
246
|
}
|
|
@@ -253,7 +258,7 @@ export class FleetList {
|
|
|
253
258
|
*/
|
|
254
259
|
editorHasFocus() {
|
|
255
260
|
const focused = this.tui?.focusedComponent;
|
|
256
|
-
return focused == null || focused instanceof Editor;
|
|
261
|
+
return focused == null || focused instanceof pi_tui_1.Editor;
|
|
257
262
|
}
|
|
258
263
|
deactivate() {
|
|
259
264
|
this.active = false;
|
|
@@ -279,13 +284,13 @@ export class FleetList {
|
|
|
279
284
|
this.viewingAgentId = record.id;
|
|
280
285
|
void this.ui.custom((tui, theme, keybindings, done) => {
|
|
281
286
|
this.viewerClose = () => done(undefined);
|
|
282
|
-
return new ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
287
|
+
return new conversation_viewer_js_1.ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
283
288
|
if (this.manager.abort(record.id))
|
|
284
289
|
this.ui?.notify(`Stopped "${record.description}".`, "info");
|
|
285
290
|
}, keybindings, (message) => this.manager.steer(record.id, message));
|
|
286
291
|
}, {
|
|
287
292
|
overlay: true,
|
|
288
|
-
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
293
|
+
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${conversation_viewer_js_1.VIEWPORT_HEIGHT_PCT}%` },
|
|
289
294
|
}).then(() => this.clearViewer(), () => this.clearViewer());
|
|
290
295
|
}
|
|
291
296
|
/** Reset overlay state and return to the list (on close, auto-close, or error). */
|
|
@@ -315,9 +320,9 @@ export class FleetList {
|
|
|
315
320
|
? "↑↓ select · enter view · esc back"
|
|
316
321
|
: "esc to interrupt · ← for agents · ↓ to manage";
|
|
317
322
|
const lines = [];
|
|
318
|
-
lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
|
|
323
|
+
lines.push((0, pi_tui_1.truncateToWidth)(" " + theme.fg("dim", hint), width));
|
|
319
324
|
lines.push("");
|
|
320
|
-
lines.push(truncateToWidth(` ${this.bullet(0, sel, theme)} main`, width));
|
|
325
|
+
lines.push((0, pi_tui_1.truncateToWidth)(` ${this.bullet(0, sel, theme)} main`, width));
|
|
321
326
|
// Window the agent rows so the selected one stays visible.
|
|
322
327
|
const visible = Math.min(MAX_AGENT_ROWS, agents.length);
|
|
323
328
|
const selAgent = Math.max(0, sel - 1);
|
|
@@ -336,10 +341,11 @@ export class FleetList {
|
|
|
336
341
|
return rosterIndex === sel ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
337
342
|
}
|
|
338
343
|
renderAgentRow(rosterIndex, sel, record, width, theme) {
|
|
339
|
-
const left = ` ${this.bullet(rosterIndex, sel, theme)} ${theme.fg("muted", getDisplayName(record.type))} ${record.description}`;
|
|
340
|
-
const tokens = getLifetimeTotal(this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage);
|
|
344
|
+
const left = ` ${this.bullet(rosterIndex, sel, theme)} ${theme.fg("muted", (0, agent_widget_js_1.getDisplayName)(record.type))} ${record.description}`;
|
|
345
|
+
const tokens = (0, usage_js_1.getLifetimeTotal)(this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage);
|
|
341
346
|
const elapsedMs = (record.completedAt ?? Date.now()) - record.startedAt; // freezes once finished
|
|
342
347
|
const right = theme.fg("dim", `${formatFleetElapsed(elapsedMs)} · ${formatFleetTokens(tokens)}`);
|
|
343
348
|
return rightAlign(left, right, width);
|
|
344
349
|
}
|
|
345
350
|
}
|
|
351
|
+
exports.FleetList = FleetList;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMarkdownResult = createMarkdownResult;
|
|
4
|
+
const pi_coding_agent_1 = require("@earendil-works/pi-coding-agent");
|
|
5
|
+
const pi_tui_1 = require("@earendil-works/pi-tui");
|
|
6
|
+
const TRUNCATION_MARKER = "… (more output available)";
|
|
7
|
+
/**
|
|
8
|
+
* Render a child result as Markdown while keeping notification output bounded.
|
|
9
|
+
*
|
|
10
|
+
* Markdown is rendered at paint time so the TUI's current width controls wrapping.
|
|
11
|
+
* If the host's Markdown implementation is unavailable or throws, the original
|
|
12
|
+
* text is rendered through Text instead of making the result disappear.
|
|
13
|
+
*/
|
|
14
|
+
class CappedMarkdownResult {
|
|
15
|
+
text;
|
|
16
|
+
theme;
|
|
17
|
+
maxLines;
|
|
18
|
+
markdown;
|
|
19
|
+
constructor(text, theme, maxLines) {
|
|
20
|
+
this.text = text;
|
|
21
|
+
this.theme = theme;
|
|
22
|
+
this.maxLines = Math.max(0, Math.floor(maxLines));
|
|
23
|
+
try {
|
|
24
|
+
this.markdown = new pi_tui_1.Markdown(text, 0, 0, (0, pi_coding_agent_1.getMarkdownTheme)());
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// A mismatched host Pi version should still leave the child output visible.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
invalidate() {
|
|
31
|
+
this.markdown?.invalidate();
|
|
32
|
+
}
|
|
33
|
+
render(width) {
|
|
34
|
+
let lines;
|
|
35
|
+
try {
|
|
36
|
+
lines = this.markdown?.render(width) ?? new pi_tui_1.Text(this.text, 0, 0).render(width);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
lines = new pi_tui_1.Text(this.text, 0, 0).render(width);
|
|
40
|
+
}
|
|
41
|
+
if (lines.length <= this.maxLines)
|
|
42
|
+
return lines;
|
|
43
|
+
if (this.maxLines === 0)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
...lines.slice(0, this.maxLines - 1),
|
|
47
|
+
this.theme.fg("muted", TRUNCATION_MARKER),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function createMarkdownResult(text, theme, maxLines) {
|
|
52
|
+
return new CappedMarkdownResult(text, theme, maxLines);
|
|
53
|
+
}
|
package/dist/ui/schedule-menu.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* schedule-menu.ts — `/agents → Scheduled jobs` submenu.
|
|
3
4
|
*
|
|
@@ -7,6 +8,8 @@
|
|
|
7
8
|
* "I scheduled something dumb, get rid of it"). Add management surfaces here
|
|
8
9
|
* if real demand emerges.
|
|
9
10
|
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.showSchedulesMenu = showSchedulesMenu;
|
|
10
13
|
/** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
|
|
11
14
|
function relTime(iso, now = Date.now()) {
|
|
12
15
|
if (!iso)
|
|
@@ -69,7 +72,7 @@ function formatDetails(j, scheduler) {
|
|
|
69
72
|
* List scheduled jobs; selecting one opens a cancel-confirm with details.
|
|
70
73
|
* Returns when the user backs out or after a cancellation.
|
|
71
74
|
*/
|
|
72
|
-
|
|
75
|
+
async function showSchedulesMenu(ctx, scheduler) {
|
|
73
76
|
if (!scheduler.isActive()) {
|
|
74
77
|
ctx.ui.notify("Scheduler is not active in this session.", "warning");
|
|
75
78
|
return;
|
package/dist/ui/viewer-keys.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* viewer-keys.ts — Scroll key matchers for the conversation viewer.
|
|
3
4
|
*
|
|
@@ -5,13 +6,15 @@
|
|
|
5
6
|
* manager, falling back to the previous hardcoded keys otherwise. The viewer's
|
|
6
7
|
* k/j and shift+arrow aliases always work alongside whatever is bound.
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.createViewerKeys = createViewerKeys;
|
|
11
|
+
const pi_tui_1 = require("@earendil-works/pi-tui");
|
|
12
|
+
function createViewerKeys(keybindings) {
|
|
13
|
+
const matches = (data, id, fallback) => keybindings ? keybindings.matches(data, id) : (0, pi_tui_1.matchesKey)(data, fallback);
|
|
11
14
|
return {
|
|
12
|
-
scrollUp: (data) => matches(data, "tui.select.up", "up") || matchesKey(data, "k"),
|
|
13
|
-
scrollDown: (data) => matches(data, "tui.select.down", "down") || matchesKey(data, "j"),
|
|
14
|
-
pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || matchesKey(data, "shift+up"),
|
|
15
|
-
pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || matchesKey(data, "shift+down"),
|
|
15
|
+
scrollUp: (data) => matches(data, "tui.select.up", "up") || (0, pi_tui_1.matchesKey)(data, "k"),
|
|
16
|
+
scrollDown: (data) => matches(data, "tui.select.down", "down") || (0, pi_tui_1.matchesKey)(data, "j"),
|
|
17
|
+
pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || (0, pi_tui_1.matchesKey)(data, "shift+up"),
|
|
18
|
+
pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || (0, pi_tui_1.matchesKey)(data, "shift+down"),
|
|
16
19
|
};
|
|
17
20
|
}
|
package/dist/usage.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.getLifetimeTotal = getLifetimeTotal;
|
|
5
|
+
exports.addUsage = addUsage;
|
|
6
|
+
exports.getSessionTokens = getSessionTokens;
|
|
7
|
+
exports.getSessionContextPercent = getSessionContextPercent;
|
|
2
8
|
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
3
|
-
|
|
9
|
+
function getLifetimeTotal(u) {
|
|
4
10
|
return u ? u.input + u.output + u.cacheWrite : 0;
|
|
5
11
|
}
|
|
6
12
|
/** Add a usage delta into a target accumulator (mutates target). */
|
|
7
|
-
|
|
13
|
+
function addUsage(into, delta) {
|
|
8
14
|
into.input += delta.input;
|
|
9
15
|
into.output += delta.output;
|
|
10
16
|
into.cacheWrite += delta.cacheWrite;
|
|
@@ -22,7 +28,7 @@ export function addUsage(into, delta) {
|
|
|
22
28
|
* and so counts the cumulative cached prefix N times across N turns
|
|
23
29
|
* (issue #38).
|
|
24
30
|
*/
|
|
25
|
-
|
|
31
|
+
function getSessionTokens(session) {
|
|
26
32
|
if (!session)
|
|
27
33
|
return 0;
|
|
28
34
|
try {
|
|
@@ -37,7 +43,7 @@ export function getSessionTokens(session) {
|
|
|
37
43
|
* Context-window utilization (0–100), or null when unavailable
|
|
38
44
|
* (no model contextWindow, or post-compaction before the next response).
|
|
39
45
|
*/
|
|
40
|
-
|
|
46
|
+
function getSessionContextPercent(session) {
|
|
41
47
|
if (!session)
|
|
42
48
|
return null;
|
|
43
49
|
try {
|
package/dist/worktree.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* worktree.ts — Git worktree isolation for agents.
|
|
3
4
|
*
|
|
@@ -5,47 +6,51 @@
|
|
|
5
6
|
* On completion, if no changes were made, the worktree is cleaned up.
|
|
6
7
|
* If changes exist, a branch is created and returned in the result.
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.createWorktree = createWorktree;
|
|
11
|
+
exports.cleanupWorktree = cleanupWorktree;
|
|
12
|
+
exports.pruneWorktrees = pruneWorktrees;
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
13
18
|
/**
|
|
14
19
|
* Create a temporary git worktree for an agent.
|
|
15
20
|
* Returns the worktree path, or undefined if not in a git repo.
|
|
16
21
|
*/
|
|
17
|
-
|
|
22
|
+
function createWorktree(cwd, agentId) {
|
|
18
23
|
// Verify we're in a git repo with at least one commit (HEAD must exist)
|
|
19
24
|
let baseSha;
|
|
20
25
|
let subdir;
|
|
21
26
|
try {
|
|
22
|
-
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
23
|
-
baseSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 })
|
|
27
|
+
(0, node_child_process_1.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
28
|
+
baseSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 })
|
|
24
29
|
.toString()
|
|
25
30
|
.trim();
|
|
26
31
|
// Where cwd sits inside the repo ("" at the root): the agent must work at
|
|
27
32
|
// the same subdirectory inside the copy, or a monorepo-package cwd would
|
|
28
33
|
// silently widen to the whole repo. realpath both sides — git emits
|
|
29
34
|
// resolved paths while cwd may arrive through a symlink (macOS /tmp).
|
|
30
|
-
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: "pipe", timeout: 5000 })
|
|
35
|
+
const topLevel = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: "pipe", timeout: 5000 })
|
|
31
36
|
.toString()
|
|
32
37
|
.trim();
|
|
33
|
-
subdir = relative(realpathSync(topLevel), realpathSync(cwd));
|
|
38
|
+
subdir = (0, node_path_1.relative)((0, node_fs_1.realpathSync)(topLevel), (0, node_fs_1.realpathSync)(cwd));
|
|
34
39
|
}
|
|
35
40
|
catch {
|
|
36
41
|
return undefined;
|
|
37
42
|
}
|
|
38
43
|
const branch = `pi-agent-${agentId}`;
|
|
39
|
-
const suffix = randomUUID().slice(0, 8);
|
|
40
|
-
const worktreePath = join(tmpdir(), `pi-agent-${agentId}-${suffix}`);
|
|
44
|
+
const suffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
|
|
45
|
+
const worktreePath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `pi-agent-${agentId}-${suffix}`);
|
|
41
46
|
try {
|
|
42
47
|
// Create detached worktree at HEAD
|
|
43
|
-
execFileSync("git", ["worktree", "add", "--detach", worktreePath, "HEAD"], {
|
|
48
|
+
(0, node_child_process_1.execFileSync)("git", ["worktree", "add", "--detach", worktreePath, "HEAD"], {
|
|
44
49
|
cwd,
|
|
45
50
|
stdio: "pipe",
|
|
46
51
|
timeout: 30000,
|
|
47
52
|
});
|
|
48
|
-
return { path: worktreePath, branch, baseSha, workPath: subdir ? join(worktreePath, subdir) : worktreePath };
|
|
53
|
+
return { path: worktreePath, branch, baseSha, workPath: subdir ? (0, node_path_1.join)(worktreePath, subdir) : worktreePath };
|
|
49
54
|
}
|
|
50
55
|
catch {
|
|
51
56
|
// If worktree creation fails, return undefined (agent runs in normal cwd)
|
|
@@ -57,31 +62,31 @@ export function createWorktree(cwd, agentId) {
|
|
|
57
62
|
* - If no changes: remove worktree entirely.
|
|
58
63
|
* - If changes exist: create a branch, commit changes, return branch info.
|
|
59
64
|
*/
|
|
60
|
-
|
|
61
|
-
if (!existsSync(worktree.path)) {
|
|
65
|
+
function cleanupWorktree(cwd, worktree, agentDescription) {
|
|
66
|
+
if (!(0, node_fs_1.existsSync)(worktree.path)) {
|
|
62
67
|
return { hasChanges: false };
|
|
63
68
|
}
|
|
64
69
|
try {
|
|
65
70
|
// Check for uncommitted changes in the worktree
|
|
66
|
-
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
71
|
+
const status = (0, node_child_process_1.execFileSync)("git", ["status", "--porcelain"], {
|
|
67
72
|
cwd: worktree.path,
|
|
68
73
|
stdio: "pipe",
|
|
69
74
|
timeout: 10000,
|
|
70
75
|
}).toString().trim();
|
|
71
76
|
if (status) {
|
|
72
77
|
// Changes exist — stage, commit, and create a branch
|
|
73
|
-
execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
|
|
78
|
+
(0, node_child_process_1.execFileSync)("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
|
|
74
79
|
// Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
|
|
75
80
|
const safeDesc = agentDescription.slice(0, 200);
|
|
76
81
|
const commitMsg = `pi-agent: ${safeDesc}`;
|
|
77
|
-
execFileSync("git", ["commit", "--no-verify", "-m", commitMsg], {
|
|
82
|
+
(0, node_child_process_1.execFileSync)("git", ["commit", "--no-verify", "-m", commitMsg], {
|
|
78
83
|
cwd: worktree.path,
|
|
79
84
|
stdio: "pipe",
|
|
80
85
|
timeout: 10000,
|
|
81
86
|
});
|
|
82
87
|
}
|
|
83
88
|
else {
|
|
84
|
-
const currentSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
89
|
+
const currentSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
85
90
|
cwd: worktree.path,
|
|
86
91
|
stdio: "pipe",
|
|
87
92
|
timeout: 5000,
|
|
@@ -96,7 +101,7 @@ export function cleanupWorktree(cwd, worktree, agentDescription) {
|
|
|
96
101
|
// If the branch already exists, append a suffix to avoid overwriting previous work.
|
|
97
102
|
let branchName = worktree.branch;
|
|
98
103
|
try {
|
|
99
|
-
execFileSync("git", ["branch", branchName], {
|
|
104
|
+
(0, node_child_process_1.execFileSync)("git", ["branch", branchName], {
|
|
100
105
|
cwd: worktree.path,
|
|
101
106
|
stdio: "pipe",
|
|
102
107
|
timeout: 5000,
|
|
@@ -105,7 +110,7 @@ export function cleanupWorktree(cwd, worktree, agentDescription) {
|
|
|
105
110
|
catch {
|
|
106
111
|
// Branch already exists — use a unique suffix
|
|
107
112
|
branchName = `${worktree.branch}-${Date.now()}`;
|
|
108
|
-
execFileSync("git", ["branch", branchName], {
|
|
113
|
+
(0, node_child_process_1.execFileSync)("git", ["branch", branchName], {
|
|
109
114
|
cwd: worktree.path,
|
|
110
115
|
stdio: "pipe",
|
|
111
116
|
timeout: 5000,
|
|
@@ -135,7 +140,7 @@ export function cleanupWorktree(cwd, worktree, agentDescription) {
|
|
|
135
140
|
*/
|
|
136
141
|
function removeWorktree(cwd, worktreePath) {
|
|
137
142
|
try {
|
|
138
|
-
execFileSync("git", ["worktree", "remove", "--force", worktreePath], {
|
|
143
|
+
(0, node_child_process_1.execFileSync)("git", ["worktree", "remove", "--force", worktreePath], {
|
|
139
144
|
cwd,
|
|
140
145
|
stdio: "pipe",
|
|
141
146
|
timeout: 10000,
|
|
@@ -144,7 +149,7 @@ function removeWorktree(cwd, worktreePath) {
|
|
|
144
149
|
catch {
|
|
145
150
|
// If git worktree remove fails, try pruning
|
|
146
151
|
try {
|
|
147
|
-
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
152
|
+
(0, node_child_process_1.execFileSync)("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
148
153
|
}
|
|
149
154
|
catch { /* ignore */ }
|
|
150
155
|
}
|
|
@@ -152,9 +157,9 @@ function removeWorktree(cwd, worktreePath) {
|
|
|
152
157
|
/**
|
|
153
158
|
* Prune any orphaned worktrees (crash recovery).
|
|
154
159
|
*/
|
|
155
|
-
|
|
160
|
+
function pruneWorktrees(cwd) {
|
|
156
161
|
try {
|
|
157
|
-
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
162
|
+
(0, node_child_process_1.execFileSync)("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
158
163
|
}
|
|
159
164
|
catch { /* ignore */ }
|
|
160
165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
|
|
5
5
|
"author": "ESSO0428",
|
|
6
6
|
"repository": {
|
package/src/agent-manager.ts
CHANGED
|
@@ -23,6 +23,13 @@ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; toke
|
|
|
23
23
|
|
|
24
24
|
/** Default max concurrent background agents. */
|
|
25
25
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
26
|
+
const TERMINAL_STATUSES = new Set<AgentRecord["status"]>([
|
|
27
|
+
"completed",
|
|
28
|
+
"steered",
|
|
29
|
+
"aborted",
|
|
30
|
+
"stopped",
|
|
31
|
+
"error",
|
|
32
|
+
]);
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
|
|
@@ -97,6 +104,37 @@ interface SpawnOptions {
|
|
|
97
104
|
onCompaction?: (info: CompactionInfo) => void;
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
const RESTORABLE_STATUSES = new Set<AgentRecord["status"]>([
|
|
108
|
+
"completed",
|
|
109
|
+
"steered",
|
|
110
|
+
"aborted",
|
|
111
|
+
"stopped",
|
|
112
|
+
"error",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
type PersistedAgentRecord = Pick<
|
|
116
|
+
AgentRecord,
|
|
117
|
+
"id" | "type" | "description" | "status" | "result" | "error" | "startedAt" | "completedAt"
|
|
118
|
+
>;
|
|
119
|
+
|
|
120
|
+
function isRestorableRecord(value: unknown): value is PersistedAgentRecord {
|
|
121
|
+
if (!value || typeof value !== "object") return false;
|
|
122
|
+
const record = value as Record<string, unknown>;
|
|
123
|
+
return (
|
|
124
|
+
typeof record.id === "string" &&
|
|
125
|
+
record.id.length > 0 &&
|
|
126
|
+
typeof record.type === "string" &&
|
|
127
|
+
typeof record.description === "string" &&
|
|
128
|
+
RESTORABLE_STATUSES.has(record.status as AgentRecord["status"]) &&
|
|
129
|
+
typeof record.startedAt === "number" &&
|
|
130
|
+
Number.isFinite(record.startedAt) &&
|
|
131
|
+
typeof record.completedAt === "number" &&
|
|
132
|
+
Number.isFinite(record.completedAt) &&
|
|
133
|
+
(record.result === undefined || typeof record.result === "string") &&
|
|
134
|
+
(record.error === undefined || typeof record.error === "string")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
100
138
|
export class AgentManager {
|
|
101
139
|
private agents = new Map<string, AgentRecord>();
|
|
102
140
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
@@ -510,6 +548,40 @@ export class AgentManager {
|
|
|
510
548
|
return this.agents.get(id);
|
|
511
549
|
}
|
|
512
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Restore terminal records persisted in a parent session.
|
|
553
|
+
*
|
|
554
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
555
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
556
|
+
* extension data and may have been written by an older version.
|
|
557
|
+
*/
|
|
558
|
+
restoreCompleted(records: readonly unknown[]): void {
|
|
559
|
+
const restoredIds = new Set<string>();
|
|
560
|
+
// getBranch() is chronological; newest persisted state wins on duplicate IDs.
|
|
561
|
+
for (const value of [...records].reverse()) {
|
|
562
|
+
if (!isRestorableRecord(value)) continue;
|
|
563
|
+
if (restoredIds.has(value.id) || this.agents.has(value.id)) continue;
|
|
564
|
+
restoredIds.add(value.id);
|
|
565
|
+
|
|
566
|
+
this.agents.set(value.id, {
|
|
567
|
+
id: value.id,
|
|
568
|
+
type: value.type,
|
|
569
|
+
description: value.description,
|
|
570
|
+
status: value.status,
|
|
571
|
+
result: value.result,
|
|
572
|
+
error: value.error,
|
|
573
|
+
toolUses: 0,
|
|
574
|
+
startedAt: value.startedAt,
|
|
575
|
+
completedAt: value.completedAt,
|
|
576
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
577
|
+
compactionCount: 0,
|
|
578
|
+
// Historical records have no inline tool surface and should remain
|
|
579
|
+
// visible in the background widget.
|
|
580
|
+
isBackground: true,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
513
585
|
listAgents(): AgentRecord[] {
|
|
514
586
|
return [...this.agents.values()].sort(
|
|
515
587
|
(a, b) => b.startedAt - a.startedAt,
|
package/src/agent-runner.ts
CHANGED
|
@@ -786,16 +786,24 @@ export async function runAgent(
|
|
|
786
786
|
// modelRuntime, but ExtensionContext still exposes only the registry facade.
|
|
787
787
|
// Pass both so the full supported Pi range retains the parent's providers.
|
|
788
788
|
const parentModelRuntime = (ctx.modelRegistry as unknown as { runtime?: unknown }).runtime;
|
|
789
|
-
|
|
789
|
+
type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
790
|
+
// `modelRuntime` was added in Pi 0.80.8. Infer it when available, while
|
|
791
|
+
// falling back to unknown for older Pi declarations.
|
|
792
|
+
type CompatibleModelRuntime = CreateAgentSessionOptions extends {
|
|
793
|
+
modelRuntime?: infer Runtime;
|
|
794
|
+
} ? Runtime : unknown;
|
|
795
|
+
const sessionOpts: CreateAgentSessionOptions & {
|
|
790
796
|
modelRegistry: ExtensionContext["modelRegistry"];
|
|
791
|
-
modelRuntime?:
|
|
797
|
+
modelRuntime?: CompatibleModelRuntime;
|
|
792
798
|
} = {
|
|
793
799
|
cwd: effectiveCwd,
|
|
794
800
|
agentDir,
|
|
795
801
|
sessionManager,
|
|
796
802
|
settingsManager,
|
|
797
803
|
modelRegistry: ctx.modelRegistry,
|
|
798
|
-
...(parentModelRuntime
|
|
804
|
+
...(parentModelRuntime != null && {
|
|
805
|
+
modelRuntime: parentModelRuntime as CompatibleModelRuntime,
|
|
806
|
+
}),
|
|
799
807
|
model,
|
|
800
808
|
tools: sessionTools,
|
|
801
809
|
resourceLoader: loader,
|