@yusukeshib/pi-babysit 0.6.6 → 0.6.7

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 +5 -5
  2. package/index.ts +79 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -80,11 +80,11 @@ 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`. 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
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
88
  placeholder when expanded. Mouse clicks require fullscreen mode; `/babysit`
89
89
  works in regular mode. The displayed `age` is total session age, not time spent
90
90
  in the current state. Transcript details
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 } from "@earendil-works/pi-tui";
46
+ import { Box, Markdown, Text, truncateToWidth, visibleWidth } 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";
@@ -1229,16 +1229,18 @@ async function selectedProcessOutput(
1229
1229
  return tail ? `\n\nSelected tail (${lines} lines max):\n${clip(tail, maxBytes)}` : "";
1230
1230
  }
1231
1231
 
1232
+ function escapeCommandForDisplay(command: string): string {
1233
+ return command
1234
+ .replace(/\r/g, "\\r")
1235
+ .replace(/\n/g, "\\n")
1236
+ .replace(/\t/g, "\\t")
1237
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, (char) =>
1238
+ `\\x${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
1239
+ );
1240
+ }
1241
+
1232
1242
  export function summarizeNotificationCommand(command: string | undefined): string {
1233
- const preview =
1234
- (command ?? "?")
1235
- .trim()
1236
- .replace(/\r/g, "\\r")
1237
- .replace(/\n/g, "\\n")
1238
- .replace(/\t/g, "\\t")
1239
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, (char) =>
1240
- `\\x${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
1241
- ) || "?";
1243
+ const preview = escapeCommandForDisplay((command ?? "?").trim()) || "?";
1242
1244
  const bytes = Buffer.from(preview, "utf8");
1243
1245
  if (bytes.length <= NOTIFY_COMMAND_MAX_BYTES) return preview;
1244
1246
  if (NOTIFY_COMMAND_MAX_BYTES === 0) return "";
@@ -2287,16 +2289,16 @@ async function spawnSubagent(
2287
2289
  }
2288
2290
 
2289
2291
  // ---------------------------------------------------------------------------
2290
- // widget (live counts and browsable finished sessions)
2292
+ // widget (live counts and browsable sessions)
2291
2293
  // ---------------------------------------------------------------------------
2292
2294
 
2293
2295
  export function widgetSessionGroups(sessions: BsSession[], startedAt: (id: string) => number) {
2294
2296
  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 };
2297
+ const started = new Map(sessions.map((session) => [session.id, startedAt(session.id)]));
2298
+ const newestFirst = (a: BsSession, b: BsSession) =>
2299
+ (started.get(b.id) ?? 0) - (started.get(a.id) ?? 0) || b.id.localeCompare(a.id);
2300
+ const finished = sessions.filter((session) => session.state !== "running").sort(newestFirst);
2301
+ return { active, finished, all: [...sessions].sort(newestFirst) };
2300
2302
  }
2301
2303
 
2302
2304
  // A subagent whose task is done stays alive as an idle RPC worker (for
@@ -2358,9 +2360,9 @@ function renderWidgetSessionHeader(
2358
2360
  const WIDGET_COLLAPSED_TAIL_LINES = 1;
2359
2361
  const WIDGET_EXPANDED_TAIL_LINES = 20;
2360
2362
  const WIDGET_TAIL_WIDTH = 100;
2361
- const WIDGET_FINISHED_PAGE_SIZE = 5;
2363
+ const WIDGET_SESSION_PAGE_SIZE = 5;
2362
2364
 
2363
- export interface WidgetFinishedState {
2365
+ export interface WidgetSessionListState {
2364
2366
  open: boolean;
2365
2367
  page: number;
2366
2368
  }
@@ -2376,6 +2378,7 @@ interface WidgetMouseEvent {
2376
2378
  type: string;
2377
2379
  button: string;
2378
2380
  y: number;
2381
+ x?: number;
2379
2382
  }
2380
2383
 
2381
2384
  /**
@@ -2387,9 +2390,9 @@ export function createWidgetComponent(
2387
2390
  summaryLines: string[],
2388
2391
  sessions: WidgetSessionDisplay[],
2389
2392
  expandedSessionIds: Set<string>,
2390
- finished?: { sessions: WidgetSessionDisplay[]; state: WidgetFinishedState },
2393
+ listState?: WidgetSessionListState,
2391
2394
  ) {
2392
- type Target = { type: "session"; id: string } | { type: "finished" | "prev" | "next" };
2395
+ type Target = { type: "session"; id: string } | { type: "sessions"; endX: number; prevX?: number; nextX?: number };
2393
2396
  let targetByLine: Array<Target | undefined> = [];
2394
2397
 
2395
2398
  return {
@@ -2415,41 +2418,58 @@ export function createWidgetComponent(
2415
2418
  }
2416
2419
  };
2417
2420
 
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)) {
2421
+ for (const line of summaryLines.slice(0, -1)) push(line);
2422
+ const summary = summaryLines.at(-1);
2423
+ if (listState && sessions.length) {
2424
+ const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2425
+ listState.page = Math.min(Math.max(0, listState.page), pageCount - 1);
2426
+ const label = ` SESSIONS(${sessions.length})`;
2427
+ const showArrows = listState.open && pageCount > 1;
2428
+ const suffix = `${label}${showArrows ? ` < ${listState.page + 1}/${pageCount} >` : ""}`;
2429
+ // Keep the count clickable on narrow terminals by moving it to its own row.
2430
+ const inline = summary !== undefined && visibleWidth(summary) + visibleWidth(suffix) <= width;
2431
+ if (summary !== undefined && !inline) {
2432
+ push(summary, { type: "sessions", endX: Math.min(visibleWidth(summary), Math.max(0, width)) });
2433
+ }
2434
+ const prefix = inline ? summary ?? "" : "";
2435
+ const prefixWidth = visibleWidth(prefix);
2436
+ push(`${prefix}${suffix}`, {
2437
+ type: "sessions",
2438
+ endX: Math.min(prefixWidth + visibleWidth(suffix), Math.max(0, width)),
2439
+ prevX: showArrows ? prefixWidth + suffix.indexOf("<") : undefined,
2440
+ nextX: showArrows ? prefixWidth + suffix.indexOf(">") : undefined,
2441
+ });
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)) {
2433
2445
  pushSession(session);
2434
2446
  }
2435
2447
  }
2448
+ } else {
2449
+ if (summary !== undefined) push(summary);
2450
+ for (const session of sessions) pushSession(session);
2436
2451
  }
2437
- for (const session of sessions) pushSession(session);
2438
2452
  return lines;
2439
2453
  },
2440
2454
  handleMouse(event: WidgetMouseEvent) {
2441
2455
  if (event.button !== "left") return undefined;
2442
2456
  const target = targetByLine[event.y];
2443
2457
  if (!target) return undefined;
2458
+ if (target.type === "sessions" && (event.x === undefined || event.x < 0 || event.x >= target.endX)) return undefined;
2444
2459
  // Toggle on press so a terminal that omits release still works.
2445
2460
  if (event.type === "press") {
2446
2461
  if (target.type === "session") {
2447
2462
  if (expandedSessionIds.has(target.id)) expandedSessionIds.delete(target.id);
2448
2463
  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);
2464
+ } else if (listState) {
2465
+ const pageCount = Math.ceil(sessions.length / WIDGET_SESSION_PAGE_SIZE);
2466
+ if (event.x === target.prevX && target.prevX !== undefined) {
2467
+ listState.page = (listState.page - 1 + pageCount) % pageCount;
2468
+ } else if (event.x === target.nextX && target.nextX !== undefined) {
2469
+ listState.page = (listState.page + 1) % pageCount;
2470
+ } else {
2471
+ listState.open = !listState.open;
2472
+ }
2453
2473
  }
2454
2474
  return { handled: true, render: true };
2455
2475
  }
@@ -3417,12 +3437,12 @@ export default function (pi: ExtensionAPI) {
3417
3437
  }
3418
3438
 
3419
3439
  const expandedWidgetSessions = new Set<string>();
3420
- const finishedWidgetState: WidgetFinishedState = { open: false, page: 0 };
3440
+ const widgetListState: WidgetSessionListState = { open: true, page: 0 };
3421
3441
 
3422
3442
  const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
3423
3443
  if (!ctx.hasUI) return;
3424
3444
  const all = snapshot ?? (await listSessions()).sessions;
3425
- const { active, finished } = widgetSessionGroups(all, (id) => readMeta(id)?.startedAt ?? 0);
3445
+ const { active, all: ordered } = widgetSessionGroups(all, (id) => readMeta(id)?.startedAt ?? 0);
3426
3446
  const subs = active.filter((session) => kindOf(session.id) === "subagent");
3427
3447
  const procs = active.length - subs.length;
3428
3448
  const progressById = new Map<string, Progress>();
@@ -3452,27 +3472,19 @@ export default function (pi: ExtensionAPI) {
3452
3472
  );
3453
3473
  }),
3454
3474
  );
3455
- const displays = active.map((session, index): WidgetSessionDisplay => {
3456
- const isSubagent = kindOf(session.id) === "subagent";
3457
- const state: WidgetSessionState = isSubagent && progressById.get(session.id)?.done
3458
- ? "idle"
3459
- : "running";
3460
- return {
3461
- id: session.id,
3462
- header: renderWidgetSessionHeader(
3463
- session.id,
3464
- isSubagent ? "agent" : "process",
3465
- state,
3466
- elapsedOf(session.id),
3467
- theme,
3468
- ),
3469
- tail: tails[index],
3470
- expandable: true,
3471
- };
3472
- });
3473
- const finishedDisplays = finished.map((session): WidgetSessionDisplay => {
3474
- const isSubagent = kindOf(session.id) === "subagent";
3475
+ const activeTails = new Map(active.map((session, index) => [session.id, tails[index]]));
3476
+ const displays = ordered.map((session): WidgetSessionDisplay => {
3475
3477
  const id = session.id;
3478
+ const isSubagent = kindOf(id) === "subagent";
3479
+ if (session.state === "running") {
3480
+ const state: WidgetSessionState = isSubagent && progressById.get(id)?.done ? "idle" : "running";
3481
+ return {
3482
+ id,
3483
+ header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme),
3484
+ tail: activeTails.get(id) ?? [],
3485
+ expandable: true,
3486
+ };
3487
+ }
3476
3488
  return {
3477
3489
  id,
3478
3490
  header: renderWidgetSessionHeader(
@@ -3482,7 +3494,7 @@ export default function (pi: ExtensionAPI) {
3482
3494
  "",
3483
3495
  theme,
3484
3496
  ),
3485
- // Only read/parse the five visible finished sessions on demand.
3497
+ // Only read/parse the five visible sessions on demand.
3486
3498
  get tail() {
3487
3499
  try {
3488
3500
  return widgetTail(id, isSubagent, undefined, WIDGET_EXPANDED_TAIL_LINES);
@@ -3497,14 +3509,12 @@ export default function (pi: ExtensionAPI) {
3497
3509
  if (ctx.mode === "tui") {
3498
3510
  ctx.ui.setWidget(
3499
3511
  "pi-babysit",
3500
- () => createWidgetComponent(summaryLines, displays, expandedWidgetSessions, {
3501
- sessions: finishedDisplays,
3502
- state: finishedWidgetState,
3503
- }),
3512
+ () => createWidgetComponent(summaryLines, displays, expandedWidgetSessions, widgetListState),
3504
3513
  { placement: "belowEditor" },
3505
3514
  );
3506
3515
  } else {
3507
- const component = createWidgetComponent(summaryLines, displays, new Set());
3516
+ const activeIds = new Set(active.map((session) => session.id));
3517
+ const component = createWidgetComponent(summaryLines, displays.filter((session) => activeIds.has(session.id)), new Set());
3508
3518
  ctx.ui.setWidget("pi-babysit", component.render(10_000), {
3509
3519
  placement: "belowEditor",
3510
3520
  });
@@ -4222,7 +4232,7 @@ export default function (pi: ExtensionAPI) {
4222
4232
  const title = theme.bold(`babysit_run ${kind}`);
4223
4233
  const detail =
4224
4234
  typeof payload === "string" && payload.length > 0
4225
- ? ` ${summarizeNotificationCommand(payload)}`
4235
+ ? ` ${escapeCommandForDisplay(payload)}`
4226
4236
  : "";
4227
4237
  return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
4228
4238
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",