@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.
- package/README.md +5 -5
- package/index.ts +79 -69
- 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,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
finished.
|
|
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
|
|
2363
|
+
const WIDGET_SESSION_PAGE_SIZE = 5;
|
|
2362
2364
|
|
|
2363
|
-
export interface
|
|
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
|
-
|
|
2393
|
+
listState?: WidgetSessionListState,
|
|
2391
2394
|
) {
|
|
2392
|
-
type Target = { type: "session"; id: string } | { type: "
|
|
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
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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 (
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
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
|
|
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,
|
|
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
|
|
3456
|
-
|
|
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
|
|
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
|
|
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
|
-
? ` ${
|
|
4235
|
+
? ` ${escapeCommandForDisplay(payload)}`
|
|
4226
4236
|
: "";
|
|
4227
4237
|
return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
|
|
4228
4238
|
},
|