@esso0428/pi-subagents 0.15.0 → 0.15.1

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.
@@ -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.1",
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": {
@@ -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,
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  type UICtx,
49
49
  } from "./ui/agent-widget.js";
50
50
  import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
51
+ import { createMarkdownResult } from "./ui/markdown-result.js";
51
52
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
52
53
  import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
53
54
 
@@ -240,6 +241,9 @@ function buildDetails(
240
241
  };
241
242
  }
242
243
 
244
+ /** Maximum expanded notification body retained in message details. */
245
+ const MAX_NOTIFICATION_RESULT_TEXT_LENGTH = 20_000;
246
+
243
247
  /** Build notification details for the custom message renderer. */
244
248
  function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
245
249
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
@@ -255,6 +259,11 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
255
259
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
256
260
  outputFile: record.outputFile,
257
261
  error: record.error,
262
+ resultText: record.result
263
+ ? record.result.length > MAX_NOTIFICATION_RESULT_TEXT_LENGTH
264
+ ? record.result.slice(0, MAX_NOTIFICATION_RESULT_TEXT_LENGTH) + "…"
265
+ : record.result
266
+ : undefined,
258
267
  resultPreview: record.result
259
268
  ? record.result.length > resultMaxLen
260
269
  ? record.result.slice(0, resultMaxLen) + "…"
@@ -271,7 +280,7 @@ export default function (pi: ExtensionAPI) {
271
280
  const d = message.details;
272
281
  if (!d) return undefined;
273
282
 
274
- function renderOne(d: NotificationDetails): string {
283
+ function renderOne(d: NotificationDetails): string | Container {
275
284
  const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
276
285
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
277
286
  const statusText = isError ? d.status
@@ -291,25 +300,34 @@ export default function (pi: ExtensionAPI) {
291
300
  line += "\n " + parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
292
301
  }
293
302
 
294
- // Line 3: result preview (collapsed) or full (expanded)
295
- if (expanded) {
296
- const lines = d.resultPreview.split("\n").slice(0, 30);
297
- for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
298
- } else {
303
+ if (!expanded) {
304
+ // Collapsed previews intentionally remain compact plain text.
299
305
  const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
300
306
  line += "\n " + theme.fg("dim", `⎿ ${preview}`);
307
+ if (d.outputFile) {
308
+ line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
309
+ }
310
+ return line;
301
311
  }
302
312
 
303
- // Line 4: output file link (if present)
313
+ const container = new Container();
314
+ container.addChild(new Text(line, 0, 0));
315
+ container.addChild(createMarkdownResult(d.resultText ?? d.resultPreview, theme, 30));
304
316
  if (d.outputFile) {
305
- line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
317
+ container.addChild(new Text(theme.fg("muted", ` transcript: ${d.outputFile}`), 0, 0));
306
318
  }
307
-
308
- return line;
319
+ return container;
309
320
  }
310
321
 
311
322
  const all = [d, ...(d.others ?? [])];
312
- return new Text(all.map(renderOne).join("\n"), 0, 0);
323
+ if (!expanded) return new Text(all.map(renderOne).join("\n"), 0, 0);
324
+
325
+ const container = new Container();
326
+ all.forEach((item, index) => {
327
+ if (index > 0) container.addChild(new Spacer(1));
328
+ container.addChild(renderOne(item) as Container);
329
+ });
330
+ return container;
313
331
  }
314
332
  );
315
333
 
@@ -1024,15 +1042,15 @@ Terse command-style prompts produce shallow, generic work.
1024
1042
 
1025
1043
  if (expanded) {
1026
1044
  const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
1045
+ const container = new Container();
1046
+ container.addChild(new Text(line, 0, 0));
1027
1047
  if (resultText) {
1028
- const lines = resultText.split("\n").slice(0, 50);
1029
- for (const l of lines) {
1030
- line += "\n" + theme.fg("dim", ` ${l}`);
1031
- }
1048
+ container.addChild(createMarkdownResult(resultText, theme, 50));
1032
1049
  if (resultText.split("\n").length > 50) {
1033
- line += "\n" + theme.fg("muted", " ... (use get_subagent_result with verbose for full output)");
1050
+ container.addChild(new Text(theme.fg("muted", " ... (use get_subagent_result with verbose for full output)"), 0, 0));
1034
1051
  }
1035
1052
  }
1053
+ return container;
1036
1054
  } else {
1037
1055
  const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
1038
1056
  line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
package/src/types.ts CHANGED
@@ -154,6 +154,7 @@ export interface NotificationDetails {
154
154
  outputFile?: string;
155
155
  error?: string;
156
156
  resultPreview: string;
157
+ resultText?: string;
157
158
  /** Additional agents in a group notification. */
158
159
  others?: NotificationDetails[];
159
160
  }