@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/dist/agent-manager.d.ts +8 -0
  4. package/dist/agent-manager.js +87 -19
  5. package/dist/agent-runner.js +85 -66
  6. package/dist/agent-types.js +45 -27
  7. package/dist/context.js +6 -2
  8. package/dist/cross-extension-rpc.js +9 -5
  9. package/dist/custom-agents.js +18 -15
  10. package/dist/default-agents.js +4 -1
  11. package/dist/enabled-models.js +16 -11
  12. package/dist/env.js +4 -1
  13. package/dist/group-join.js +5 -1
  14. package/dist/index.js +280 -221
  15. package/dist/invocation-config.js +6 -2
  16. package/dist/memory.js +34 -24
  17. package/dist/model-resolver.js +4 -1
  18. package/dist/nico-overrides.js +20 -14
  19. package/dist/output-file.js +21 -15
  20. package/dist/prompts.js +4 -1
  21. package/dist/schedule-store.js +21 -16
  22. package/dist/schedule.js +12 -8
  23. package/dist/settings.js +23 -15
  24. package/dist/skill-loader.js +23 -20
  25. package/dist/status-note.js +4 -1
  26. package/dist/types.d.ts +1 -0
  27. package/dist/types.js +4 -1
  28. package/dist/ui/agent-widget.js +37 -23
  29. package/dist/ui/conversation-viewer.js +43 -39
  30. package/dist/ui/fleet-list.js +30 -24
  31. package/dist/ui/markdown-result.d.ts +3 -0
  32. package/dist/ui/markdown-result.js +53 -0
  33. package/dist/ui/schedule-menu.js +4 -1
  34. package/dist/ui/viewer-keys.js +10 -7
  35. package/dist/usage.js +10 -4
  36. package/dist/worktree.js +31 -26
  37. package/package.json +1 -1
  38. package/src/agent-manager.ts +72 -0
  39. package/src/agent-runner.ts +11 -3
  40. package/src/index.ts +39 -16
  41. package/src/types.ts +1 -0
  42. package/src/ui/markdown-result.ts +56 -0
  43. package/test/agent-manager-history.test.ts +84 -0
  44. package/test/ui/markdown-result.test.ts +45 -0
@@ -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
- import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
- import { getLifetimeTotal } from "../usage.js";
15
- import { getDisplayName } from "./agent-widget.js";
16
- import { ConversationViewer, VIEWPORT_HEIGHT_PCT } from "./conversation-viewer.js";
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
- export function formatFleetElapsed(ms) {
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
- export function formatFleetTokens(count) {
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
- export class FleetList {
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,3 @@
1
+ import { type Component } from "@earendil-works/pi-tui";
2
+ import type { Theme } from "./agent-widget.js";
3
+ export declare function createMarkdownResult(text: string, theme: Theme, maxLines: number): Component;
@@ -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
+ }
@@ -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
- export async function showSchedulesMenu(ctx, scheduler) {
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;
@@ -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
- import { matchesKey } from "@earendil-works/pi-tui";
9
- export function createViewerKeys(keybindings) {
10
- const matches = (data, id, fallback) => keybindings ? keybindings.matches(data, id) : matchesKey(data, fallback);
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
- export function getLifetimeTotal(u) {
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
- export function addUsage(into, delta) {
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
- export function getSessionTokens(session) {
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
- export function getSessionContextPercent(session) {
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
- import { execFileSync } from "node:child_process";
9
- import { randomUUID } from "node:crypto";
10
- import { existsSync, realpathSync } from "node:fs";
11
- import { tmpdir } from "node:os";
12
- import { join, relative } from "node:path";
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
- export function createWorktree(cwd, agentId) {
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
- export function cleanupWorktree(cwd, worktree, agentDescription) {
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
- export function pruneWorktrees(cwd) {
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.0",
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": {
@@ -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,
@@ -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
- const sessionOpts: Parameters<typeof createAgentSession>[0] & {
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?: unknown;
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 !== undefined && { modelRuntime: parentModelRuntime }),
804
+ ...(parentModelRuntime != null && {
805
+ modelRuntime: parentModelRuntime as CompatibleModelRuntime,
806
+ }),
799
807
  model,
800
808
  tools: sessionTools,
801
809
  resourceLoader: loader,