@yusukeshib/pi-babysit 0.6.7 → 0.6.9

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 (3) hide show
  1. package/README.md +13 -6
  2. package/index.ts +62 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,12 +81,19 @@ backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
81
81
  A widget below the editor separates session **kind** from task **state** at a glance:
82
82
  summary counts use `RUNNING` / `IDLE`, and every row is labeled
83
83
  `PROCESS RUNNING`, `AGENT RUNNING`, or `AGENT IDLE`. In fullscreen mode, the
84
- `SESSIONS(n) < 1/4 >` list combines running, idle, finished, and failed sessions
85
- (newest first, five per page); the fraction shows the current/total page. Click the arrows to page or anywhere else on the header to collapse
86
- and expand the list. Click a row to toggle between its latest line and latest 20
87
- available output lines. A session with no available output shows an explicit
88
- placeholder when expanded. Mouse clicks require fullscreen mode; `/babysit`
89
- works in regular mode. The displayed `age` is total session age, not time spent
84
+ `babysits(n running)` list shows live sessions (including idle agents) by default.
85
+ Click the header to toggle to `babysits(n all)`, which also shows finished and failed
86
+ sessions, and click again to return to running-only. Both modes show newest first,
87
+ ten per page; `< 1/4 >` shows the current/total page when needed. Click the
88
+ arrows to page. Collapsed rows show the complete command for processes and the
89
+ complete task for agents, wrapping long text across lines rather than abbreviating
90
+ it. Click an agent row for the task,
91
+ its RPC worker launch argv (bold, readable text rather than a copy-paste shell
92
+ command), and up to 20 output lines. Opening another row closes the previous
93
+ one. A session with no available output shows an explicit placeholder when
94
+ expanded. Mouse clicks
95
+ require fullscreen mode; `/babysit` works in regular mode. The displayed `age`
96
+ is total session age, not time spent
90
97
  in the current state. Transcript details
91
98
  from tool results, snapshots, and completion notifications collapse or expand
92
99
  with Pi's tool-output toggle (`Ctrl+O` by default).
package/index.ts CHANGED
@@ -43,7 +43,7 @@ import type {
43
43
  ToolDefinition,
44
44
  } from "@earendil-works/pi-coding-agent";
45
45
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
46
- import { Box, Markdown, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
46
+ import { Box, Markdown, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
47
47
  import { Type, type TSchema } from "typebox";
48
48
  import { StringEnum } from "@earendil-works/pi-ai";
49
49
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents";
@@ -463,6 +463,7 @@ interface Meta {
463
463
  startedAt?: number;
464
464
  // subagent
465
465
  task?: string;
466
+ launchCommand?: string;
466
467
  promptOffset?: number;
467
468
  model?: string;
468
469
  tools?: string[];
@@ -1239,6 +1240,12 @@ function escapeCommandForDisplay(command: string): string {
1239
1240
  );
1240
1241
  }
1241
1242
 
1243
+ export function readableLaunchCommand(command: string): string {
1244
+ // Older sessions quoted every argv element, including safe paths and flags.
1245
+ if (!command.startsWith("'") || !command.endsWith("'") || !command.includes("' '")) return command;
1246
+ return command.slice(1, -1).split("' '").map((arg) => arg.replace(/'\\''/g, "'")).join(" ");
1247
+ }
1248
+
1242
1249
  export function summarizeNotificationCommand(command: string | undefined): string {
1243
1250
  const preview = escapeCommandForDisplay((command ?? "?").trim()) || "?";
1244
1251
  const bytes = Buffer.from(preview, "utf8");
@@ -2154,6 +2161,7 @@ async function spawnSubagent(
2154
2161
  PI_BIN,
2155
2162
  ...piArgs,
2156
2163
  );
2164
+ const launchCommand = bsArgs.slice(bsArgs.indexOf("--") + 1).join(" ");
2157
2165
 
2158
2166
  let r: Awaited<ReturnType<typeof bs>>;
2159
2167
  try {
@@ -2187,6 +2195,7 @@ async function spawnSubagent(
2187
2195
  kind: "subagent",
2188
2196
  name: opts.name ?? id,
2189
2197
  task: opts.task,
2198
+ launchCommand,
2190
2199
  notified: true,
2191
2200
  depth: opts.depth,
2192
2201
  maxDepth: opts.maxDepth,
@@ -2274,6 +2283,7 @@ async function spawnSubagent(
2274
2283
  kind: "subagent",
2275
2284
  name: opts.name ?? id,
2276
2285
  task: opts.task,
2286
+ launchCommand,
2277
2287
  promptOffset: resp.offset,
2278
2288
  model: resolvedModel,
2279
2289
  tools,
@@ -2360,10 +2370,10 @@ function renderWidgetSessionHeader(
2360
2370
  const WIDGET_COLLAPSED_TAIL_LINES = 1;
2361
2371
  const WIDGET_EXPANDED_TAIL_LINES = 20;
2362
2372
  const WIDGET_TAIL_WIDTH = 100;
2363
- const WIDGET_SESSION_PAGE_SIZE = 5;
2373
+ const WIDGET_SESSION_PAGE_SIZE = 10;
2364
2374
 
2365
2375
  export interface WidgetSessionListState {
2366
- open: boolean;
2376
+ mode: "running" | "all";
2367
2377
  page: number;
2368
2378
  }
2369
2379
 
@@ -2372,6 +2382,8 @@ export interface WidgetSessionDisplay {
2372
2382
  header: string;
2373
2383
  tail: string[];
2374
2384
  expandable: boolean;
2385
+ active?: boolean;
2386
+ details?: string[];
2375
2387
  }
2376
2388
 
2377
2389
  interface WidgetMouseEvent {
@@ -2403,16 +2415,34 @@ export function createWidgetComponent(
2403
2415
  lines.push(truncateToWidth(text, Math.max(0, width), ""));
2404
2416
  targetByLine.push(target);
2405
2417
  };
2418
+ const pushWrapped = (text: string, target?: Target) => {
2419
+ for (const line of wrapTextWithAnsi(text, Math.max(1, width))) push(line, target);
2420
+ };
2406
2421
  const pushSession = (session: WidgetSessionDisplay) => {
2407
2422
  const expanded = session.expandable && expandedSessionIds.has(session.id);
2408
2423
  const tail = session.tail.slice(
2409
2424
  expanded ? -WIDGET_EXPANDED_TAIL_LINES : -WIDGET_COLLAPSED_TAIL_LINES,
2410
2425
  );
2411
2426
  const target = session.expandable ? { type: "session" as const, id: session.id } : undefined;
2412
- if (!expanded && tail.length === 1) {
2427
+ const details = session.details ?? [];
2428
+ const pushDetail = (detail: string) => {
2429
+ const text = detail.startsWith("task: ") ? detail.slice(6) : detail;
2430
+ if (width > 6) {
2431
+ for (const line of wrapTextWithAnsi(text, width - 6)) push(` ${line}`, target);
2432
+ } else {
2433
+ push(` ${text}`, target);
2434
+ }
2435
+ };
2436
+ if (!expanded && details[0]?.startsWith("task: ")) {
2437
+ pushWrapped(`${session.header} ${details[0].slice(6)}`, target);
2438
+ } else if (!expanded && details.length) {
2439
+ pushWrapped(`${session.header} ${details[0]}`, target);
2440
+ for (const detail of details.slice(1)) pushDetail(detail);
2441
+ } else if (!expanded && tail.length === 1) {
2413
2442
  push(`${session.header} ${tail[0]}`, target);
2414
2443
  } else {
2415
2444
  push(session.header, target);
2445
+ for (const detail of details) pushDetail(detail);
2416
2446
  for (const line of tail) push(` ${line}`, target);
2417
2447
  if (expanded && tail.length === 0) push(" (no output available)", target);
2418
2448
  }
@@ -2421,10 +2451,11 @@ export function createWidgetComponent(
2421
2451
  for (const line of summaryLines.slice(0, -1)) push(line);
2422
2452
  const summary = summaryLines.at(-1);
2423
2453
  if (listState && sessions.length) {
2424
- const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2454
+ const visible = listState.mode === "running" ? sessions.filter((session) => session.active) : sessions;
2455
+ const pageCount = Math.max(1, Math.ceil(visible.length / WIDGET_SESSION_PAGE_SIZE));
2425
2456
  listState.page = Math.min(Math.max(0, listState.page), pageCount - 1);
2426
- const label = ` SESSIONS(${sessions.length})`;
2427
- const showArrows = listState.open && pageCount > 1;
2457
+ const label = ` babysits(${visible.length} ${listState.mode})`;
2458
+ const showArrows = pageCount > 1;
2428
2459
  const suffix = `${label}${showArrows ? ` < ${listState.page + 1}/${pageCount} >` : ""}`;
2429
2460
  // Keep the count clickable on narrow terminals by moving it to its own row.
2430
2461
  const inline = summary !== undefined && visibleWidth(summary) + visibleWidth(suffix) <= width;
@@ -2439,11 +2470,9 @@ export function createWidgetComponent(
2439
2470
  prevX: showArrows ? prefixWidth + suffix.indexOf("<") : undefined,
2440
2471
  nextX: showArrows ? prefixWidth + suffix.indexOf(">") : undefined,
2441
2472
  });
2442
- if (listState.open) {
2443
- const start = listState.page * WIDGET_SESSION_PAGE_SIZE;
2444
- for (const session of sessions.slice(start, start + WIDGET_SESSION_PAGE_SIZE)) {
2445
- pushSession(session);
2446
- }
2473
+ const start = listState.page * WIDGET_SESSION_PAGE_SIZE;
2474
+ for (const session of visible.slice(start, start + WIDGET_SESSION_PAGE_SIZE)) {
2475
+ pushSession(session);
2447
2476
  }
2448
2477
  } else {
2449
2478
  if (summary !== undefined) push(summary);
@@ -2459,16 +2488,21 @@ export function createWidgetComponent(
2459
2488
  // Toggle on press so a terminal that omits release still works.
2460
2489
  if (event.type === "press") {
2461
2490
  if (target.type === "session") {
2462
- if (expandedSessionIds.has(target.id)) expandedSessionIds.delete(target.id);
2463
- else expandedSessionIds.add(target.id);
2491
+ if (expandedSessionIds.has(target.id)) expandedSessionIds.clear();
2492
+ else {
2493
+ expandedSessionIds.clear();
2494
+ expandedSessionIds.add(target.id);
2495
+ }
2464
2496
  } else if (listState) {
2465
- const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2497
+ const count = listState.mode === "running" ? sessions.filter((session) => session.active).length : sessions.length;
2498
+ const pageCount = Math.max(1, Math.ceil(count / WIDGET_SESSION_PAGE_SIZE));
2466
2499
  if (event.x === target.prevX && target.prevX !== undefined) {
2467
2500
  listState.page = (listState.page - 1 + pageCount) % pageCount;
2468
2501
  } else if (event.x === target.nextX && target.nextX !== undefined) {
2469
2502
  listState.page = (listState.page + 1) % pageCount;
2470
2503
  } else {
2471
- listState.open = !listState.open;
2504
+ listState.mode = listState.mode === "running" ? "all" : "running";
2505
+ listState.page = 0;
2472
2506
  }
2473
2507
  }
2474
2508
  return { handled: true, render: true };
@@ -3437,7 +3471,7 @@ export default function (pi: ExtensionAPI) {
3437
3471
  }
3438
3472
 
3439
3473
  const expandedWidgetSessions = new Set<string>();
3440
- const widgetListState: WidgetSessionListState = { open: true, page: 0 };
3474
+ const widgetListState: WidgetSessionListState = { mode: "running", page: 0 };
3441
3475
 
3442
3476
  const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
3443
3477
  if (!ctx.hasUI) return;
@@ -3475,7 +3509,14 @@ export default function (pi: ExtensionAPI) {
3475
3509
  const activeTails = new Map(active.map((session, index) => [session.id, tails[index]]));
3476
3510
  const displays = ordered.map((session): WidgetSessionDisplay => {
3477
3511
  const id = session.id;
3478
- const isSubagent = kindOf(id) === "subagent";
3512
+ const meta = readMeta(id);
3513
+ const isSubagent = meta?.kind === "subagent";
3514
+ const details = isSubagent
3515
+ ? [
3516
+ ...(meta?.task ? [`task: ${escapeCommandForDisplay(meta.task)}`] : []),
3517
+ ...(meta?.launchCommand ? [theme.bold(escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
3518
+ ]
3519
+ : meta?.command ? [theme.bold(escapeCommandForDisplay(meta.command))] : [];
3479
3520
  if (session.state === "running") {
3480
3521
  const state: WidgetSessionState = isSubagent && progressById.get(id)?.done ? "idle" : "running";
3481
3522
  return {
@@ -3483,6 +3524,8 @@ export default function (pi: ExtensionAPI) {
3483
3524
  header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme),
3484
3525
  tail: activeTails.get(id) ?? [],
3485
3526
  expandable: true,
3527
+ active: true,
3528
+ details,
3486
3529
  };
3487
3530
  }
3488
3531
  return {
@@ -3494,6 +3537,7 @@ export default function (pi: ExtensionAPI) {
3494
3537
  "",
3495
3538
  theme,
3496
3539
  ),
3540
+ details,
3497
3541
  // Only read/parse the five visible sessions on demand.
3498
3542
  get tail() {
3499
3543
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",