@yusukeshib/pi-babysit 0.6.5 → 0.6.6

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 +8 -2
  2. package/index.ts +119 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -80,8 +80,14 @@ backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
80
80
 
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
- `PROCESS RUNNING`, `AGENT RUNNING`, or `AGENT IDLE`. The displayed `age`
84
- is total session age, not time spent in the current state. Transcript details
83
+ `PROCESS RUNNING`, `AGENT RUNNING`, or `AGENT IDLE`. In fullscreen mode, click
84
+ any live row to toggle between its latest line and latest 20 available output
85
+ lines. Finished sessions are behind a clickable `FINISHED` row; browse all
86
+ retained sessions five at a time and click a row to expand it, including failed
87
+ processes and agents. 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
90
+ in the current state. Transcript details
85
91
  from tool results, snapshots, and completion notifications collapse or expand
86
92
  with Pi's tool-output toggle (`Ctrl+O` by default).
87
93
 
package/index.ts CHANGED
@@ -2287,14 +2287,23 @@ async function spawnSubagent(
2287
2287
  }
2288
2288
 
2289
2289
  // ---------------------------------------------------------------------------
2290
- // widget (live counts)
2290
+ // widget (live counts and browsable finished sessions)
2291
2291
  // ---------------------------------------------------------------------------
2292
2292
 
2293
+ export function widgetSessionGroups(sessions: BsSession[], startedAt: (id: string) => number) {
2294
+ const active = sessions.filter((session) => session.state === "running");
2295
+ // Default view stays compact; the finished section pages over every retained session.
2296
+ const finished = sessions.filter((session) => session.state !== "running");
2297
+ const started = new Map(finished.map((session) => [session.id, startedAt(session.id)]));
2298
+ finished.sort((a, b) => (started.get(b.id) ?? 0) - (started.get(a.id) ?? 0) || b.id.localeCompare(a.id));
2299
+ return { active, finished };
2300
+ }
2301
+
2293
2302
  // A subagent whose task is done stays alive as an idle RPC worker (for
2294
2303
  // follow-ups), so "running" in babysit does NOT mean "working" — count
2295
2304
  // processes / busy subagents / idle subagents separately.
2296
2305
  export type WidgetSessionKind = "process" | "agent";
2297
- export type WidgetSessionState = "running" | "idle";
2306
+ export type WidgetSessionState = "running" | "idle" | "finished" | "failed";
2298
2307
 
2299
2308
  export function widgetSummaryText(procs: number, busy: number, idle: number): string {
2300
2309
  const sections: string[] = [];
@@ -2312,7 +2321,7 @@ export function widgetSessionHeader(
2312
2321
  state: WidgetSessionState,
2313
2322
  elapsed?: string,
2314
2323
  ): string {
2315
- const icon = state === "running" ? "▶" : "○";
2324
+ const icon = state === "running" ? "▶" : state === "failed" ? "✗" : "○";
2316
2325
  return ` ${icon} ${id} ${kind.toUpperCase()} ${state.toUpperCase()}${elapsed ? ` age ${elapsed}` : ""}`;
2317
2326
  }
2318
2327
 
@@ -2338,17 +2347,23 @@ function renderWidgetSessionHeader(
2338
2347
  elapsed: string,
2339
2348
  theme: Theme,
2340
2349
  ): string {
2341
- const running = state === "running";
2342
- const icon = theme.fg(running ? "success" : "muted", running ? "▶" : "○");
2350
+ const color = state === "running" ? "success" : state === "failed" ? "error" : "muted";
2351
+ const icon = theme.fg(color, state === "running" ? "▶" : state === "failed" ? "✗" : "○");
2343
2352
  const kindLabel = theme.fg(kind === "process" ? "accent" : "warning", theme.bold(kind.toUpperCase()));
2344
- const stateLabel = theme.fg(running ? "success" : "muted", theme.bold(state.toUpperCase()));
2353
+ const stateLabel = theme.fg(color, theme.bold(state.toUpperCase()));
2345
2354
  return ` ${icon} ${id} ${kindLabel} ${stateLabel}${elapsed ? theme.fg("dim", ` age ${elapsed}`) : ""}`;
2346
2355
  }
2347
2356
 
2348
- // How many trailing output lines to show per running session in the widget.
2357
+ // How many trailing output lines to show per session in the widget.
2349
2358
  const WIDGET_COLLAPSED_TAIL_LINES = 1;
2350
2359
  const WIDGET_EXPANDED_TAIL_LINES = 20;
2351
2360
  const WIDGET_TAIL_WIDTH = 100;
2361
+ const WIDGET_FINISHED_PAGE_SIZE = 5;
2362
+
2363
+ export interface WidgetFinishedState {
2364
+ open: boolean;
2365
+ page: number;
2366
+ }
2352
2367
 
2353
2368
  export interface WidgetSessionDisplay {
2354
2369
  id: string;
@@ -2371,45 +2386,79 @@ interface WidgetMouseEvent {
2371
2386
  export function createWidgetComponent(
2372
2387
  summaryLines: string[],
2373
2388
  sessions: WidgetSessionDisplay[],
2374
- expandedProcessIds: Set<string>,
2389
+ expandedSessionIds: Set<string>,
2390
+ finished?: { sessions: WidgetSessionDisplay[]; state: WidgetFinishedState },
2375
2391
  ) {
2376
- let processIdByLine: Array<string | undefined> = [];
2392
+ type Target = { type: "session"; id: string } | { type: "finished" | "prev" | "next" };
2393
+ let targetByLine: Array<Target | undefined> = [];
2377
2394
 
2378
2395
  return {
2379
2396
  render(width: number): string[] {
2380
2397
  const lines: string[] = [];
2381
- processIdByLine = [];
2382
- const push = (text: string, processId?: string) => {
2398
+ targetByLine = [];
2399
+ const push = (text: string, target?: Target) => {
2383
2400
  lines.push(truncateToWidth(text, Math.max(0, width), ""));
2384
- processIdByLine.push(processId);
2401
+ targetByLine.push(target);
2385
2402
  };
2386
-
2387
- for (const line of summaryLines) push(line);
2388
- for (const session of sessions) {
2389
- const expanded = session.expandable && expandedProcessIds.has(session.id);
2403
+ const pushSession = (session: WidgetSessionDisplay) => {
2404
+ const expanded = session.expandable && expandedSessionIds.has(session.id);
2390
2405
  const tail = session.tail.slice(
2391
2406
  expanded ? -WIDGET_EXPANDED_TAIL_LINES : -WIDGET_COLLAPSED_TAIL_LINES,
2392
2407
  );
2393
- const processId = session.expandable ? session.id : undefined;
2408
+ const target = session.expandable ? { type: "session" as const, id: session.id } : undefined;
2394
2409
  if (!expanded && tail.length === 1) {
2395
- push(`${session.header} ${tail[0]}`, processId);
2410
+ push(`${session.header} ${tail[0]}`, target);
2396
2411
  } else {
2397
- push(session.header, processId);
2398
- for (const line of tail) push(` ${line}`, processId);
2412
+ push(session.header, target);
2413
+ for (const line of tail) push(` ${line}`, target);
2414
+ if (expanded && tail.length === 0) push(" (no output available)", target);
2415
+ }
2416
+ };
2417
+
2418
+ for (const line of summaryLines) push(line);
2419
+ if (finished?.sessions.length) {
2420
+ const pageCount = Math.ceil(finished.sessions.length / WIDGET_FINISHED_PAGE_SIZE);
2421
+ finished.state.page = Math.min(Math.max(0, finished.state.page), pageCount - 1);
2422
+ push(
2423
+ ` FINISHED ${finished.sessions.length} — ${finished.state.open ? "hide" : "browse"} (click)`,
2424
+ { type: "finished" },
2425
+ );
2426
+ if (finished.state.open) {
2427
+ if (pageCount > 1) {
2428
+ if (finished.state.page > 0) push(" ← Previous", { type: "prev" });
2429
+ push(` ${finished.state.page === pageCount - 1 ? "↻ First" : "Next →"} (${finished.state.page + 1}/${pageCount})`, { type: "next" });
2430
+ }
2431
+ const start = finished.state.page * WIDGET_FINISHED_PAGE_SIZE;
2432
+ for (const session of finished.sessions.slice(start, start + WIDGET_FINISHED_PAGE_SIZE)) {
2433
+ pushSession(session);
2434
+ }
2399
2435
  }
2400
2436
  }
2437
+ for (const session of sessions) pushSession(session);
2401
2438
  return lines;
2402
2439
  },
2403
2440
  handleMouse(event: WidgetMouseEvent) {
2404
- if (event.type !== "click" || event.button !== "left") return undefined;
2405
- const id = processIdByLine[event.y];
2406
- if (!id) return undefined;
2407
- if (expandedProcessIds.has(id)) expandedProcessIds.delete(id);
2408
- else expandedProcessIds.add(id);
2409
- return { handled: true, render: true };
2441
+ if (event.button !== "left") return undefined;
2442
+ const target = targetByLine[event.y];
2443
+ if (!target) return undefined;
2444
+ // Toggle on press so a terminal that omits release still works.
2445
+ if (event.type === "press") {
2446
+ if (target.type === "session") {
2447
+ if (expandedSessionIds.has(target.id)) expandedSessionIds.delete(target.id);
2448
+ else expandedSessionIds.add(target.id);
2449
+ } else if (finished) {
2450
+ if (target.type === "finished") finished.state.open = !finished.state.open;
2451
+ else if (target.type === "prev") finished.state.page--;
2452
+ else finished.state.page = (finished.state.page + 1) % Math.ceil(finished.sessions.length / WIDGET_FINISHED_PAGE_SIZE);
2453
+ }
2454
+ return { handled: true, render: true };
2455
+ }
2456
+ // Consume Pi's synthesized click without toggling a second time.
2457
+ if (event.type === "click") return { handled: true, render: false };
2458
+ return undefined;
2410
2459
  },
2411
2460
  invalidate() {
2412
- processIdByLine = [];
2461
+ targetByLine = [];
2413
2462
  },
2414
2463
  };
2415
2464
  }
@@ -2454,21 +2503,23 @@ function readTailLines(file: string, lines: number, maxBytes = 64_000): string[]
2454
2503
  }
2455
2504
  }
2456
2505
 
2457
- // Trailing lines to show for a running session (sanitized, unprefixed).
2506
+ // Trailing lines to show for a session (sanitized, unprefixed).
2458
2507
  // Process tails are read directly from the bounded end of output.log, avoiding
2459
- // one `babysit log` subprocess per active process on every poll.
2460
- async function widgetTail(
2508
+ // one `babysit log` subprocess per visible process.
2509
+ export function widgetTail(
2461
2510
  id: string,
2462
2511
  isSub: boolean,
2463
2512
  subagentProgress?: Progress,
2464
2513
  lines = WIDGET_COLLAPSED_TAIL_LINES,
2465
- ): Promise<string[]> {
2514
+ ): string[] {
2466
2515
  let raw: string[];
2467
2516
  if (!isSub) {
2468
2517
  raw = readTailLines(logPath(id), lines);
2469
2518
  } else {
2470
2519
  const progress = subagentProgress ?? taskProgressOf(id).progress;
2471
- if (progress.finalText.trim()) {
2520
+ if (progress.streamingText.trim()) {
2521
+ raw = progress.streamingText.trim().split("\n");
2522
+ } else if (progress.finalText.trim()) {
2472
2523
  raw = progress.finalText.trim().split("\n");
2473
2524
  } else if (progress.toolCalls.length > 0) {
2474
2525
  raw = progress.toolCalls.map((tool) => tool.summary);
@@ -3365,13 +3416,13 @@ export default function (pi: ExtensionAPI) {
3365
3416
  }
3366
3417
  }
3367
3418
 
3368
- const expandedWidgetProcesses = new Set<string>();
3419
+ const expandedWidgetSessions = new Set<string>();
3420
+ const finishedWidgetState: WidgetFinishedState = { open: false, page: 0 };
3369
3421
 
3370
3422
  const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
3371
3423
  if (!ctx.hasUI) return;
3372
- const active = (snapshot ?? (await listSessions()).sessions).filter(
3373
- (session) => session.state === "running",
3374
- );
3424
+ const all = snapshot ?? (await listSessions()).sessions;
3425
+ const { active, finished } = widgetSessionGroups(all, (id) => readMeta(id)?.startedAt ?? 0);
3375
3426
  const subs = active.filter((session) => kindOf(session.id) === "subagent");
3376
3427
  const procs = active.length - subs.length;
3377
3428
  const progressById = new Map<string, Progress>();
@@ -3383,11 +3434,9 @@ export default function (pi: ExtensionAPI) {
3383
3434
  }
3384
3435
  }
3385
3436
  const idle = subs.filter((session) => progressById.get(session.id)?.done).length;
3386
- const activeProcessIds = new Set(
3387
- active.filter((session) => kindOf(session.id) !== "subagent").map((session) => session.id),
3388
- );
3389
- for (const id of expandedWidgetProcesses) {
3390
- if (!activeProcessIds.has(id)) expandedWidgetProcesses.delete(id);
3437
+ const sessionIds = new Set(all.map((session) => session.id));
3438
+ for (const id of expandedWidgetSessions) {
3439
+ if (!sessionIds.has(id)) expandedWidgetSessions.delete(id);
3391
3440
  }
3392
3441
 
3393
3442
  const theme = ctx.ui.theme;
@@ -3399,7 +3448,7 @@ export default function (pi: ExtensionAPI) {
3399
3448
  session.id,
3400
3449
  isSubagent,
3401
3450
  isSubagent ? progressById.get(session.id) : undefined,
3402
- isSubagent ? WIDGET_COLLAPSED_TAIL_LINES : WIDGET_EXPANDED_TAIL_LINES,
3451
+ WIDGET_EXPANDED_TAIL_LINES,
3403
3452
  );
3404
3453
  }),
3405
3454
  );
@@ -3418,14 +3467,40 @@ export default function (pi: ExtensionAPI) {
3418
3467
  theme,
3419
3468
  ),
3420
3469
  tail: tails[index],
3421
- expandable: !isSubagent,
3470
+ expandable: true,
3471
+ };
3472
+ });
3473
+ const finishedDisplays = finished.map((session): WidgetSessionDisplay => {
3474
+ const isSubagent = kindOf(session.id) === "subagent";
3475
+ const id = session.id;
3476
+ return {
3477
+ id,
3478
+ header: renderWidgetSessionHeader(
3479
+ id,
3480
+ isSubagent ? "agent" : "process",
3481
+ session.state === "exited" && session.exit_code === 0 ? "finished" : "failed",
3482
+ "",
3483
+ theme,
3484
+ ),
3485
+ // Only read/parse the five visible finished sessions on demand.
3486
+ get tail() {
3487
+ try {
3488
+ return widgetTail(id, isSubagent, undefined, WIDGET_EXPANDED_TAIL_LINES);
3489
+ } catch {
3490
+ return [];
3491
+ }
3492
+ },
3493
+ expandable: true,
3422
3494
  };
3423
3495
  });
3424
3496
 
3425
3497
  if (ctx.mode === "tui") {
3426
3498
  ctx.ui.setWidget(
3427
3499
  "pi-babysit",
3428
- () => createWidgetComponent(summaryLines, displays, expandedWidgetProcesses),
3500
+ () => createWidgetComponent(summaryLines, displays, expandedWidgetSessions, {
3501
+ sessions: finishedDisplays,
3502
+ state: finishedWidgetState,
3503
+ }),
3429
3504
  { placement: "belowEditor" },
3430
3505
  );
3431
3506
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",