@yusukeshib/pi-babysit 0.6.7 → 0.6.8

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 +12 -6
  2. package/index.ts +61 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,12 +81,18 @@ 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 occupy one line: processes show the command,
89
+ while agents show a task preview. Click an agent row for the full wrapped task,
90
+ its RPC worker launch argv (bold, readable text rather than a copy-paste shell
91
+ command), and up to 20 output lines. Opening another row closes the previous
92
+ one. A session with no available output shows an explicit placeholder when
93
+ expanded. Mouse clicks
94
+ require fullscreen mode; `/babysit` works in regular mode. The displayed `age`
95
+ is total session age, not time spent
90
96
  in the current state. Transcript details
91
97
  from tool results, snapshots, and completion notifications collapse or expand
92
98
  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 {
@@ -2399,8 +2411,8 @@ export function createWidgetComponent(
2399
2411
  render(width: number): string[] {
2400
2412
  const lines: string[] = [];
2401
2413
  targetByLine = [];
2402
- const push = (text: string, target?: Target) => {
2403
- lines.push(truncateToWidth(text, Math.max(0, width), ""));
2414
+ const push = (text: string, target?: Target, ellipsis = "") => {
2415
+ lines.push(truncateToWidth(text, Math.max(0, width), ellipsis));
2404
2416
  targetByLine.push(target);
2405
2417
  };
2406
2418
  const pushSession = (session: WidgetSessionDisplay) => {
@@ -2409,10 +2421,25 @@ export function createWidgetComponent(
2409
2421
  expanded ? -WIDGET_EXPANDED_TAIL_LINES : -WIDGET_COLLAPSED_TAIL_LINES,
2410
2422
  );
2411
2423
  const target = session.expandable ? { type: "session" as const, id: session.id } : undefined;
2412
- if (!expanded && tail.length === 1) {
2424
+ const details = session.details ?? [];
2425
+ const pushDetail = (detail: string) => {
2426
+ const text = detail.startsWith("task: ") ? detail.slice(6) : detail;
2427
+ if (width > 6) {
2428
+ for (const line of wrapTextWithAnsi(text, width - 6)) push(` ${line}`, target);
2429
+ } else {
2430
+ push(` ${text}`, target);
2431
+ }
2432
+ };
2433
+ if (!expanded && details[0]?.startsWith("task: ")) {
2434
+ push(`${session.header} ${details[0].slice(6)}`, target, "…");
2435
+ } else if (!expanded && details.length) {
2436
+ push(`${session.header} ${details[0]}`, target);
2437
+ for (const detail of details.slice(1)) pushDetail(detail);
2438
+ } else if (!expanded && tail.length === 1) {
2413
2439
  push(`${session.header} ${tail[0]}`, target);
2414
2440
  } else {
2415
2441
  push(session.header, target);
2442
+ for (const detail of details) pushDetail(detail);
2416
2443
  for (const line of tail) push(` ${line}`, target);
2417
2444
  if (expanded && tail.length === 0) push(" (no output available)", target);
2418
2445
  }
@@ -2421,10 +2448,11 @@ export function createWidgetComponent(
2421
2448
  for (const line of summaryLines.slice(0, -1)) push(line);
2422
2449
  const summary = summaryLines.at(-1);
2423
2450
  if (listState && sessions.length) {
2424
- const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2451
+ const visible = listState.mode === "running" ? sessions.filter((session) => session.active) : sessions;
2452
+ const pageCount = Math.max(1, Math.ceil(visible.length / WIDGET_SESSION_PAGE_SIZE));
2425
2453
  listState.page = Math.min(Math.max(0, listState.page), pageCount - 1);
2426
- const label = ` SESSIONS(${sessions.length})`;
2427
- const showArrows = listState.open && pageCount > 1;
2454
+ const label = ` babysits(${visible.length} ${listState.mode})`;
2455
+ const showArrows = pageCount > 1;
2428
2456
  const suffix = `${label}${showArrows ? ` < ${listState.page + 1}/${pageCount} >` : ""}`;
2429
2457
  // Keep the count clickable on narrow terminals by moving it to its own row.
2430
2458
  const inline = summary !== undefined && visibleWidth(summary) + visibleWidth(suffix) <= width;
@@ -2439,11 +2467,9 @@ export function createWidgetComponent(
2439
2467
  prevX: showArrows ? prefixWidth + suffix.indexOf("<") : undefined,
2440
2468
  nextX: showArrows ? prefixWidth + suffix.indexOf(">") : undefined,
2441
2469
  });
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
- }
2470
+ const start = listState.page * WIDGET_SESSION_PAGE_SIZE;
2471
+ for (const session of visible.slice(start, start + WIDGET_SESSION_PAGE_SIZE)) {
2472
+ pushSession(session);
2447
2473
  }
2448
2474
  } else {
2449
2475
  if (summary !== undefined) push(summary);
@@ -2459,16 +2485,21 @@ export function createWidgetComponent(
2459
2485
  // Toggle on press so a terminal that omits release still works.
2460
2486
  if (event.type === "press") {
2461
2487
  if (target.type === "session") {
2462
- if (expandedSessionIds.has(target.id)) expandedSessionIds.delete(target.id);
2463
- else expandedSessionIds.add(target.id);
2488
+ if (expandedSessionIds.has(target.id)) expandedSessionIds.clear();
2489
+ else {
2490
+ expandedSessionIds.clear();
2491
+ expandedSessionIds.add(target.id);
2492
+ }
2464
2493
  } else if (listState) {
2465
- const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2494
+ const count = listState.mode === "running" ? sessions.filter((session) => session.active).length : sessions.length;
2495
+ const pageCount = Math.max(1, Math.ceil(count / WIDGET_SESSION_PAGE_SIZE));
2466
2496
  if (event.x === target.prevX && target.prevX !== undefined) {
2467
2497
  listState.page = (listState.page - 1 + pageCount) % pageCount;
2468
2498
  } else if (event.x === target.nextX && target.nextX !== undefined) {
2469
2499
  listState.page = (listState.page + 1) % pageCount;
2470
2500
  } else {
2471
- listState.open = !listState.open;
2501
+ listState.mode = listState.mode === "running" ? "all" : "running";
2502
+ listState.page = 0;
2472
2503
  }
2473
2504
  }
2474
2505
  return { handled: true, render: true };
@@ -3437,7 +3468,7 @@ export default function (pi: ExtensionAPI) {
3437
3468
  }
3438
3469
 
3439
3470
  const expandedWidgetSessions = new Set<string>();
3440
- const widgetListState: WidgetSessionListState = { open: true, page: 0 };
3471
+ const widgetListState: WidgetSessionListState = { mode: "running", page: 0 };
3441
3472
 
3442
3473
  const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
3443
3474
  if (!ctx.hasUI) return;
@@ -3475,7 +3506,14 @@ export default function (pi: ExtensionAPI) {
3475
3506
  const activeTails = new Map(active.map((session, index) => [session.id, tails[index]]));
3476
3507
  const displays = ordered.map((session): WidgetSessionDisplay => {
3477
3508
  const id = session.id;
3478
- const isSubagent = kindOf(id) === "subagent";
3509
+ const meta = readMeta(id);
3510
+ const isSubagent = meta?.kind === "subagent";
3511
+ const details = isSubagent
3512
+ ? [
3513
+ ...(meta?.task ? [`task: ${escapeCommandForDisplay(meta.task)}`] : []),
3514
+ ...(meta?.launchCommand ? [theme.bold(escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
3515
+ ]
3516
+ : meta?.command ? [theme.bold(summarizeNotificationCommand(meta.command))] : [];
3479
3517
  if (session.state === "running") {
3480
3518
  const state: WidgetSessionState = isSubagent && progressById.get(id)?.done ? "idle" : "running";
3481
3519
  return {
@@ -3483,6 +3521,8 @@ export default function (pi: ExtensionAPI) {
3483
3521
  header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme),
3484
3522
  tail: activeTails.get(id) ?? [],
3485
3523
  expandable: true,
3524
+ active: true,
3525
+ details,
3486
3526
  };
3487
3527
  }
3488
3528
  return {
@@ -3494,6 +3534,7 @@ export default function (pi: ExtensionAPI) {
3494
3534
  "",
3495
3535
  theme,
3496
3536
  ),
3537
+ details,
3497
3538
  // Only read/parse the five visible sessions on demand.
3498
3539
  get tail() {
3499
3540
  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.8",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",