@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.
- package/README.md +8 -2
- package/index.ts +119 -44
- 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`.
|
|
84
|
-
|
|
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
|
|
2342
|
-
const icon = theme.fg(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(
|
|
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
|
|
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
|
-
|
|
2389
|
+
expandedSessionIds: Set<string>,
|
|
2390
|
+
finished?: { sessions: WidgetSessionDisplay[]; state: WidgetFinishedState },
|
|
2375
2391
|
) {
|
|
2376
|
-
|
|
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
|
-
|
|
2382
|
-
const push = (text: string,
|
|
2398
|
+
targetByLine = [];
|
|
2399
|
+
const push = (text: string, target?: Target) => {
|
|
2383
2400
|
lines.push(truncateToWidth(text, Math.max(0, width), ""));
|
|
2384
|
-
|
|
2401
|
+
targetByLine.push(target);
|
|
2385
2402
|
};
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
|
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]}`,
|
|
2410
|
+
push(`${session.header} ${tail[0]}`, target);
|
|
2396
2411
|
} else {
|
|
2397
|
-
push(session.header,
|
|
2398
|
-
for (const line of tail) push(` ${line}`,
|
|
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.
|
|
2405
|
-
const
|
|
2406
|
-
if (!
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2460
|
-
|
|
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
|
-
):
|
|
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.
|
|
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
|
|
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
|
|
3373
|
-
|
|
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
|
|
3387
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
3500
|
+
() => createWidgetComponent(summaryLines, displays, expandedWidgetSessions, {
|
|
3501
|
+
sessions: finishedDisplays,
|
|
3502
|
+
state: finishedWidgetState,
|
|
3503
|
+
}),
|
|
3429
3504
|
{ placement: "belowEditor" },
|
|
3430
3505
|
);
|
|
3431
3506
|
} else {
|