@yusukeshib/pi-babysit 0.6.13 → 0.6.15
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 +26 -16
- package/index.ts +139 -61
- package/log-viewer.ts +346 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
|
|
|
76
76
|
| Command | What it does |
|
|
77
77
|
| ------- | ------------ |
|
|
78
78
|
| `/babysit` | Arrow-key picker over all sessions. Renders an **inline snapshot** (no tmux): running **process** → current rendered screen + recent output + a copy-paste `babysit attach` take-over hint (detach `Ctrl-\ Ctrl-\`); running **subagent** → read-only progress (RPC stdin stays untouchable); finished → summary. Re-run `/babysit` to refresh |
|
|
79
|
+
| `/babysit view` | Keyboard picker to open a session in the widget log viewer (Pi TUI) |
|
|
79
80
|
| `/babysit gc [days]` | Preview and confirm deletion of old Pi-session roots (default 14 days). Active leases, live supervisor/child PIDs, unknown states, the current root, and recent roots are retained; old empty roots are eligible once their lease is gone. Deletion uses a GC lock and atomic rename |
|
|
80
81
|
|
|
81
82
|
A widget below the editor separates session **kind** from task **state** at a glance:
|
|
@@ -85,20 +86,24 @@ summary counts use `RUNNING` / `IDLE`, and every row is labeled
|
|
|
85
86
|
Click the header to toggle to `babysits(n all)`, which also shows finished and failed
|
|
86
87
|
sessions, and click again to return to running-only. Both modes show newest first,
|
|
87
88
|
ten per page; `< 1/4 >` shows the current/total page when needed. Click the
|
|
88
|
-
arrows to page.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
arrows to page. Rows occupy one line, previewing the command for processes and
|
|
90
|
+
the task for agents (with `…` when clipped). Click an AGENT or PROCESS row to
|
|
91
|
+
replace the list with a tall **log viewer in the same widget area**, not an
|
|
92
|
+
overlay or separate screen. `Esc` or the right-hand **Close** button restores
|
|
93
|
+
the list, preserving its mode and page. The viewer uses Pi's `ScrollView` for
|
|
94
|
+
scroll position and follow behavior, with a widget-local scrollbar styled using
|
|
95
|
+
Pi's scrollbar theme. Wheel, scrollbar drag/track clicks, and ↑/↓ navigate the
|
|
96
|
+
log without requiring any Pi core changes. Home/End and PageUp/PageDown remain
|
|
97
|
+
Pi's transcript shortcuts. At the bottom it follows new output; scrolling up
|
|
98
|
+
pauses following until you return to the bottom. The viewer resizes with the
|
|
99
|
+
terminal, leaving room for the editor/status/footer. Agent logs separate
|
|
100
|
+
assistant text, tool calls, and results; process logs show terminal output.
|
|
101
|
+
Only the selected log is loaded, bounded to the latest 1 MiB / 2,000 display
|
|
102
|
+
lines with an omission notice; the full log file is unchanged. Empty logs show
|
|
103
|
+
a placeholder. Works in the released Pi 1.0 widget host. Mouse interaction
|
|
104
|
+
requires fullscreen mode; `/babysit view` opens the viewer using a keyboard
|
|
105
|
+
picker, and `/babysit` still provides a snapshot. The displayed `age` is total
|
|
106
|
+
session age, not time spent in the current state. Transcript details
|
|
102
107
|
from tool results, snapshots, and completion notifications collapse or expand
|
|
103
108
|
with Pi's tool-output toggle (`Ctrl+O` by default).
|
|
104
109
|
|
|
@@ -210,8 +215,13 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
210
215
|
incrementally, which keeps follow-up tasks efficient.
|
|
211
216
|
|
|
212
217
|
Subagents load `self-reap.ts`, which exits an idle finished subagent after a
|
|
213
|
-
grace window
|
|
214
|
-
|
|
218
|
+
grace window using the same parked-turn rule, so a subagent waiting on a long
|
|
219
|
+
build is never false-killed. Callers can set `reapAfter: "5m"` on
|
|
220
|
+
`babysit_run { profile: "subagent", task, reapAfter: "5m" }`. Omission uses
|
|
221
|
+
`PI_BABYSIT_REAP_AFTER`, then `PI_SUBAGENT_REAP_AFTER`, then 120s.
|
|
222
|
+
`reapAfter: "none"` (also `"off"` or `"0"`) disables idle reaping; the absolute
|
|
223
|
+
`timeout` still applies. Follow-up tasks cancel the idle timer. This differs
|
|
224
|
+
from `idleTimeout`, which kills based on lack of output, even during work. Give bounded
|
|
215
225
|
recon/review tasks at least one cost, turn, tool-call, or token budget; omit
|
|
216
226
|
budgets only for intentionally open-ended work. Optional task budgets are
|
|
217
227
|
observed by the parent poller. `maxUsageTokens` counts cumulative input, output,
|
package/index.ts
CHANGED
|
@@ -43,7 +43,8 @@ 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,
|
|
46
|
+
import { Box, Markdown, Text, truncateToWidth, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
47
|
+
import { createLogViewer } from "./log-viewer";
|
|
47
48
|
import { Type, type TSchema } from "typebox";
|
|
48
49
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
49
50
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents";
|
|
@@ -464,6 +465,7 @@ interface Meta {
|
|
|
464
465
|
// subagent
|
|
465
466
|
task?: string;
|
|
466
467
|
launchCommand?: string;
|
|
468
|
+
reapAfter?: string;
|
|
467
469
|
promptOffset?: number;
|
|
468
470
|
model?: string;
|
|
469
471
|
tools?: string[];
|
|
@@ -1065,9 +1067,11 @@ const SUBAGENT_BUDGET_GRACE_MS =
|
|
|
1065
1067
|
parseDurMs(process.env.PI_BABYSIT_BUDGET_GRACE ?? "90s") ?? 90_000;
|
|
1066
1068
|
const SUBAGENT_REAP_AFTER =
|
|
1067
1069
|
process.env.PI_BABYSIT_REAP_AFTER ?? process.env.PI_SUBAGENT_REAP_AFTER ?? "120s";
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1070
|
+
export function subagentReuseHint(reapAfter = SUBAGENT_REAP_AFTER): string {
|
|
1071
|
+
return ["0", "off", "none"].includes(reapAfter)
|
|
1072
|
+
? "Session remains available until its absolute timeout."
|
|
1073
|
+
: `Session remains available for follow-ups during the ${reapAfter} idle grace.`;
|
|
1074
|
+
}
|
|
1071
1075
|
|
|
1072
1076
|
export function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
1073
1077
|
if (maxBytes <= 0) return "";
|
|
@@ -2101,6 +2105,7 @@ interface SubagentOpts {
|
|
|
2101
2105
|
depth: number;
|
|
2102
2106
|
maxDepth: number;
|
|
2103
2107
|
budget?: SubagentBudget;
|
|
2108
|
+
reapAfter?: string;
|
|
2104
2109
|
// Idle-timeout is OFF by default: an RPC-mode pi is silent while it works,
|
|
2105
2110
|
// so idle detection would false-kill a busy subagent. The absolute timeout
|
|
2106
2111
|
// is the safety valve instead.
|
|
@@ -2111,6 +2116,11 @@ interface SubagentOpts {
|
|
|
2111
2116
|
async function spawnSubagent(
|
|
2112
2117
|
opts: SubagentOpts,
|
|
2113
2118
|
): Promise<{ id: string; model?: string } | { error: string }> {
|
|
2119
|
+
const reapAfter = opts.reapAfter?.trim() ?? SUBAGENT_REAP_AFTER;
|
|
2120
|
+
if (opts.reapAfter !== undefined && !["0", "off", "none"].includes(reapAfter) &&
|
|
2121
|
+
(!/^\d+(ms|s|m|h)?$/.test(reapAfter) || !Number.isFinite(parseDurMs(reapAfter)))) {
|
|
2122
|
+
return { error: "Invalid reapAfter: use a duration such as 30s or 5m, or none/off/0 to disable." };
|
|
2123
|
+
}
|
|
2114
2124
|
// Long-lived RPC worker: the task is NOT passed as argv — it is injected
|
|
2115
2125
|
// below as an RPC `prompt` command, whose response we validate so spawn
|
|
2116
2126
|
// failures (bad model, missing API key) are loud instead of a silent exit=1.
|
|
@@ -2189,6 +2199,7 @@ async function spawnSubagent(
|
|
|
2189
2199
|
r = await bs(bsArgs, {
|
|
2190
2200
|
cwd: opts.cwd,
|
|
2191
2201
|
env: {
|
|
2202
|
+
PI_BABYSIT_REAP_AFTER: reapAfter,
|
|
2192
2203
|
[SUBAGENT_DEPTH_ENV]: String(opts.depth),
|
|
2193
2204
|
[SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
|
|
2194
2205
|
},
|
|
@@ -2221,6 +2232,7 @@ async function spawnSubagent(
|
|
|
2221
2232
|
depth: opts.depth,
|
|
2222
2233
|
maxDepth: opts.maxDepth,
|
|
2223
2234
|
budget: opts.budget,
|
|
2235
|
+
reapAfter,
|
|
2224
2236
|
});
|
|
2225
2237
|
|
|
2226
2238
|
// Wait for pi to boot (first JSON event in the log), then inject the task.
|
|
@@ -2314,6 +2326,7 @@ async function spawnSubagent(
|
|
|
2314
2326
|
depth: opts.depth,
|
|
2315
2327
|
maxDepth: opts.maxDepth,
|
|
2316
2328
|
budget: opts.budget,
|
|
2329
|
+
reapAfter,
|
|
2317
2330
|
startedAt: Date.now(),
|
|
2318
2331
|
});
|
|
2319
2332
|
return { id, model: resolvedModel };
|
|
@@ -2389,7 +2402,6 @@ function renderWidgetSessionHeader(
|
|
|
2389
2402
|
|
|
2390
2403
|
// How many trailing output lines to show per session in the widget.
|
|
2391
2404
|
const WIDGET_COLLAPSED_TAIL_LINES = 1;
|
|
2392
|
-
const WIDGET_EXPANDED_TAIL_LINES = 50;
|
|
2393
2405
|
const WIDGET_SESSION_PAGE_SIZE = 10;
|
|
2394
2406
|
|
|
2395
2407
|
export interface WidgetSessionListState {
|
|
@@ -2401,7 +2413,7 @@ export interface WidgetSessionDisplay {
|
|
|
2401
2413
|
id: string;
|
|
2402
2414
|
header: string;
|
|
2403
2415
|
tail: string[];
|
|
2404
|
-
|
|
2416
|
+
viewable: boolean;
|
|
2405
2417
|
active?: boolean;
|
|
2406
2418
|
details?: string[];
|
|
2407
2419
|
}
|
|
@@ -2414,14 +2426,12 @@ interface WidgetMouseEvent {
|
|
|
2414
2426
|
}
|
|
2415
2427
|
|
|
2416
2428
|
/**
|
|
2417
|
-
*
|
|
2418
|
-
* pi-tui mouse classes. Older hosts still render it normally; fullscreen hosts
|
|
2419
|
-
* dispatch component-local click coordinates to handleMouse.
|
|
2429
|
+
* Compact session list. Item presses open a viewer; rows never expand inline.
|
|
2420
2430
|
*/
|
|
2421
2431
|
export function createWidgetComponent(
|
|
2422
2432
|
summaryLines: string[],
|
|
2423
2433
|
sessions: WidgetSessionDisplay[],
|
|
2424
|
-
|
|
2434
|
+
onOpen: (id: string) => void,
|
|
2425
2435
|
listState?: WidgetSessionListState,
|
|
2426
2436
|
) {
|
|
2427
2437
|
type Target = { type: "session"; id: string } | { type: "sessions"; endX: number; prevX?: number; nextX?: number };
|
|
@@ -2436,31 +2446,10 @@ export function createWidgetComponent(
|
|
|
2436
2446
|
targetByLine.push(target);
|
|
2437
2447
|
};
|
|
2438
2448
|
const pushSession = (session: WidgetSessionDisplay) => {
|
|
2439
|
-
const
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
);
|
|
2443
|
-
const target = session.expandable ? { type: "session" as const, id: session.id } : undefined;
|
|
2444
|
-
const details = session.details ?? [];
|
|
2445
|
-
const pushWrapped = (text: string) => {
|
|
2446
|
-
const indent = width > 6 ? " " : "";
|
|
2447
|
-
for (const line of wrapTextWithAnsi(text, Math.max(1, width - indent.length))) {
|
|
2448
|
-
push(`${indent}${line}`, target);
|
|
2449
|
-
}
|
|
2450
|
-
};
|
|
2451
|
-
const pushDetail = (detail: string) =>
|
|
2452
|
-
pushWrapped(detail.startsWith("task: ") ? detail.slice(6) : detail);
|
|
2453
|
-
if (!expanded && details.length) {
|
|
2454
|
-
const preview = details[0].startsWith("task: ") ? details[0].slice(6) : details[0];
|
|
2455
|
-
push(`${session.header} ${preview}`, target, "…");
|
|
2456
|
-
} else if (!expanded && tail.length === 1) {
|
|
2457
|
-
push(`${session.header} ${tail[0]}`, target);
|
|
2458
|
-
} else {
|
|
2459
|
-
push(session.header, target);
|
|
2460
|
-
for (const detail of details) pushDetail(detail);
|
|
2461
|
-
for (const line of tail) pushWrapped(line);
|
|
2462
|
-
if (expanded && tail.length === 0) push(" (no output available)", target);
|
|
2463
|
-
}
|
|
2449
|
+
const target = session.viewable ? { type: "session" as const, id: session.id } : undefined;
|
|
2450
|
+
const detail = session.details?.[0];
|
|
2451
|
+
const preview = detail?.startsWith("task: ") ? detail.slice(6) : detail ?? session.tail.at(-1);
|
|
2452
|
+
push(`${session.header}${preview ? ` ${preview}` : ""}`, target, "…");
|
|
2464
2453
|
};
|
|
2465
2454
|
|
|
2466
2455
|
for (const line of summaryLines.slice(0, -1)) push(line);
|
|
@@ -2503,11 +2492,7 @@ export function createWidgetComponent(
|
|
|
2503
2492
|
// Toggle on press so a terminal that omits release still works.
|
|
2504
2493
|
if (event.type === "press") {
|
|
2505
2494
|
if (target.type === "session") {
|
|
2506
|
-
|
|
2507
|
-
else {
|
|
2508
|
-
expandedSessionIds.clear();
|
|
2509
|
-
expandedSessionIds.add(target.id);
|
|
2510
|
-
}
|
|
2495
|
+
onOpen(target.id);
|
|
2511
2496
|
} else if (listState) {
|
|
2512
2497
|
const count = listState.mode === "running" ? sessions.filter((session) => session.active).length : sessions.length;
|
|
2513
2498
|
const pageCount = Math.max(1, Math.ceil(count / WIDGET_SESSION_PAGE_SIZE));
|
|
@@ -2532,6 +2517,78 @@ export function createWidgetComponent(
|
|
|
2532
2517
|
};
|
|
2533
2518
|
}
|
|
2534
2519
|
|
|
2520
|
+
export function createSessionWidget(options: {
|
|
2521
|
+
getHeight: () => number;
|
|
2522
|
+
requestRender: () => void;
|
|
2523
|
+
getTheme: () => Theme;
|
|
2524
|
+
readLog: (id: string) => { text: string; isAgent: boolean; clipped?: boolean };
|
|
2525
|
+
}) {
|
|
2526
|
+
const listState: WidgetSessionListState = { mode: "running", page: 0 };
|
|
2527
|
+
let selectedId: string | undefined;
|
|
2528
|
+
let displays: WidgetSessionDisplay[] = [];
|
|
2529
|
+
let viewer: ReturnType<typeof createLogViewer> | undefined;
|
|
2530
|
+
let list = createWidgetComponent([], [], open, listState);
|
|
2531
|
+
function refreshViewer() {
|
|
2532
|
+
if (!selectedId || !viewer) return;
|
|
2533
|
+
const log = options.readLog(selectedId);
|
|
2534
|
+
const session = displays.find((session) => session.id === selectedId);
|
|
2535
|
+
const header = session?.header ?? selectedId;
|
|
2536
|
+
const detail = session?.details?.[0] ?? "";
|
|
2537
|
+
viewer.update(
|
|
2538
|
+
`${log.clipped ? "[older log omitted: 1 MiB limit] " : ""}${header}`,
|
|
2539
|
+
log.text,
|
|
2540
|
+
log.isAgent,
|
|
2541
|
+
detail.startsWith("task: ") ? detail.slice(6) : detail,
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
function close() {
|
|
2545
|
+
selectedId = undefined;
|
|
2546
|
+
viewer = undefined;
|
|
2547
|
+
options.requestRender();
|
|
2548
|
+
}
|
|
2549
|
+
function open(id: string) {
|
|
2550
|
+
selectedId = id;
|
|
2551
|
+
viewer = createLogViewer({ ...options, onClose: close });
|
|
2552
|
+
refreshViewer();
|
|
2553
|
+
options.requestRender();
|
|
2554
|
+
}
|
|
2555
|
+
return {
|
|
2556
|
+
open,
|
|
2557
|
+
update(summary: string[], sessions: WidgetSessionDisplay[]) {
|
|
2558
|
+
displays = sessions;
|
|
2559
|
+
list = createWidgetComponent(summary, sessions, open, listState);
|
|
2560
|
+
refreshViewer();
|
|
2561
|
+
options.requestRender();
|
|
2562
|
+
},
|
|
2563
|
+
render(width: number) { return (viewer ?? list).render(width); },
|
|
2564
|
+
handleMouse(event: TuiMouseEvent) {
|
|
2565
|
+
// Stock Pi widget hosts forward mouse input to this component, not
|
|
2566
|
+
// to a nested native layout tree. Keep the full wheel/drag event.
|
|
2567
|
+
return (viewer ?? list).handleMouse(event);
|
|
2568
|
+
},
|
|
2569
|
+
handleKey(data: string) { return viewer?.handleKey(data) ?? false; },
|
|
2570
|
+
invalidate() { list.invalidate(); viewer?.invalidate(); },
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/** Only the selected session is read; bounded snapshots never affect worker logs. */
|
|
2575
|
+
export function readViewerLog(file: string): { text: string; clipped: boolean } {
|
|
2576
|
+
try {
|
|
2577
|
+
const fd = fs.openSync(file, "r");
|
|
2578
|
+
try {
|
|
2579
|
+
const size = fs.fstatSync(fd).size;
|
|
2580
|
+
const start = Math.max(0, size - 1024 * 1024);
|
|
2581
|
+
const buffer = Buffer.alloc(size - start);
|
|
2582
|
+
const bytes = fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
2583
|
+
let text = buffer.subarray(0, bytes).toString("utf8");
|
|
2584
|
+
if (start > 0) text = text.slice(text.indexOf("\n") + 1);
|
|
2585
|
+
return { text, clipped: start > 0 };
|
|
2586
|
+
} finally { fs.closeSync(fd); }
|
|
2587
|
+
} catch {
|
|
2588
|
+
return { text: "", clipped: false };
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2535
2592
|
// Strip ANSI/control escapes and clamp width so raw PTY output can't wrap or
|
|
2536
2593
|
// corrupt the widget area.
|
|
2537
2594
|
function sanitizeTailLine(s: string): string {
|
|
@@ -2804,7 +2861,7 @@ async function waitForTask(
|
|
|
2804
2861
|
ok: completed.ok,
|
|
2805
2862
|
text:
|
|
2806
2863
|
`Subagent ${id} finished its task (${stats}).\n` +
|
|
2807
|
-
`${
|
|
2864
|
+
`${subagentReuseHint(readMeta(id)?.reapAfter)} Follow-up: babysit_send { id: "${id}" }, ` +
|
|
2808
2865
|
`or babysit_kill when done.\n\n${completed.body}`,
|
|
2809
2866
|
status: st,
|
|
2810
2867
|
progress: prog,
|
|
@@ -3486,8 +3543,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3486
3543
|
}
|
|
3487
3544
|
}
|
|
3488
3545
|
|
|
3489
|
-
|
|
3490
|
-
|
|
3546
|
+
let sessionWidget: ReturnType<typeof createSessionWidget> | undefined;
|
|
3547
|
+
let unsubscribeWidgetInput: (() => void) | undefined;
|
|
3491
3548
|
|
|
3492
3549
|
const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
|
|
3493
3550
|
if (!ctx.hasUI) return;
|
|
@@ -3504,10 +3561,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
3504
3561
|
}
|
|
3505
3562
|
}
|
|
3506
3563
|
const idle = subs.filter((session) => progressById.get(session.id)?.done).length;
|
|
3507
|
-
const sessionIds = new Set(all.map((session) => session.id));
|
|
3508
|
-
for (const id of expandedWidgetSessions) {
|
|
3509
|
-
if (!sessionIds.has(id)) expandedWidgetSessions.delete(id);
|
|
3510
|
-
}
|
|
3511
3564
|
|
|
3512
3565
|
const theme = ctx.ui.theme;
|
|
3513
3566
|
const summaryLines = renderWidgetLines(procs, subs.length - idle, idle, theme);
|
|
@@ -3518,7 +3571,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3518
3571
|
session.id,
|
|
3519
3572
|
isSubagent,
|
|
3520
3573
|
isSubagent ? progressById.get(session.id) : undefined,
|
|
3521
|
-
|
|
3574
|
+
WIDGET_COLLAPSED_TAIL_LINES,
|
|
3522
3575
|
);
|
|
3523
3576
|
}),
|
|
3524
3577
|
);
|
|
@@ -3539,7 +3592,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3539
3592
|
id,
|
|
3540
3593
|
header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme),
|
|
3541
3594
|
tail: activeTails.get(id) ?? [],
|
|
3542
|
-
|
|
3595
|
+
viewable: true,
|
|
3543
3596
|
active: true,
|
|
3544
3597
|
details,
|
|
3545
3598
|
};
|
|
@@ -3554,27 +3607,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
3554
3607
|
theme,
|
|
3555
3608
|
),
|
|
3556
3609
|
details,
|
|
3557
|
-
// Only read/parse
|
|
3610
|
+
// Only read/parse visible sessions on demand.
|
|
3558
3611
|
get tail() {
|
|
3559
3612
|
try {
|
|
3560
|
-
return widgetTail(id, isSubagent, undefined,
|
|
3613
|
+
return widgetTail(id, isSubagent, undefined, WIDGET_COLLAPSED_TAIL_LINES);
|
|
3561
3614
|
} catch {
|
|
3562
3615
|
return [];
|
|
3563
3616
|
}
|
|
3564
3617
|
},
|
|
3565
|
-
|
|
3618
|
+
viewable: true,
|
|
3566
3619
|
};
|
|
3567
3620
|
});
|
|
3568
3621
|
|
|
3569
3622
|
if (ctx.mode === "tui") {
|
|
3570
|
-
|
|
3571
|
-
"pi-babysit",
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3623
|
+
if (!sessionWidget) {
|
|
3624
|
+
ctx.ui.setWidget("pi-babysit", (tui) => {
|
|
3625
|
+
sessionWidget = createSessionWidget({
|
|
3626
|
+
// Keep the viewer bounded while leaving room for editor/status/footer.
|
|
3627
|
+
getHeight: () => Math.max(3, tui.terminal.rows - 8),
|
|
3628
|
+
requestRender: () => tui.requestRender(),
|
|
3629
|
+
getTheme: () => ctx.ui.theme,
|
|
3630
|
+
readLog: (id) => ({ ...readViewerLog(logPath(id)), isAgent: kindOf(id) === "subagent" }),
|
|
3631
|
+
});
|
|
3632
|
+
return sessionWidget;
|
|
3633
|
+
}, { placement: "belowEditor" });
|
|
3634
|
+
unsubscribeWidgetInput = ctx.ui.onTerminalInput((data) =>
|
|
3635
|
+
sessionWidget?.handleKey(data) ? { consume: true } : undefined);
|
|
3636
|
+
}
|
|
3637
|
+
sessionWidget?.update(summaryLines, displays);
|
|
3575
3638
|
} else {
|
|
3576
3639
|
const activeIds = new Set(active.map((session) => session.id));
|
|
3577
|
-
const component = createWidgetComponent(summaryLines, displays.filter((session) => activeIds.has(session.id)),
|
|
3640
|
+
const component = createWidgetComponent(summaryLines, displays.filter((session) => activeIds.has(session.id)), () => {});
|
|
3578
3641
|
ctx.ui.setWidget("pi-babysit", component.render(10_000), {
|
|
3579
3642
|
placement: "belowEditor",
|
|
3580
3643
|
});
|
|
@@ -3756,6 +3819,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3756
3819
|
});
|
|
3757
3820
|
|
|
3758
3821
|
pi.on("session_shutdown", async (event) => {
|
|
3822
|
+
unsubscribeWidgetInput?.();
|
|
3823
|
+
unsubscribeWidgetInput = undefined;
|
|
3824
|
+
sessionWidget = undefined;
|
|
3759
3825
|
if (pollTimer) clearInterval(pollTimer);
|
|
3760
3826
|
pollTimer = undefined;
|
|
3761
3827
|
// Pi also emits session_shutdown while hot-reloading and switching sessions.
|
|
@@ -3901,6 +3967,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3901
3967
|
"Absolute auto-kill after this long (e.g. 30m). Default: none for processes (dev servers may run forever), 15m for subagents. 'none' disables.",
|
|
3902
3968
|
}),
|
|
3903
3969
|
),
|
|
3970
|
+
reapAfter: Type.Optional(
|
|
3971
|
+
Type.String({
|
|
3972
|
+
description: "Subagent only: stop the worker after this long in task-complete IDLE (e.g. 30s, 5m). Defaults to PI_BABYSIT_REAP_AFTER, then PI_SUBAGENT_REAP_AFTER, then 120s. none/off/0 disables; absolute timeout still applies. Follow-up tasks cancel the idle timer.",
|
|
3973
|
+
}),
|
|
3974
|
+
),
|
|
3904
3975
|
idleTimeout: Type.Optional(
|
|
3905
3976
|
Type.String({
|
|
3906
3977
|
description:
|
|
@@ -4221,6 +4292,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4221
4292
|
maxUsageTokens: params.maxUsageTokens,
|
|
4222
4293
|
}
|
|
4223
4294
|
: undefined,
|
|
4295
|
+
reapAfter: params.reapAfter,
|
|
4224
4296
|
timeout: params.timeout ?? "15m",
|
|
4225
4297
|
idleTimeout: params.idleTimeout,
|
|
4226
4298
|
});
|
|
@@ -5032,9 +5104,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
5032
5104
|
// output + a copy-paste `babysit attach` take-over hint; running subagent →
|
|
5033
5105
|
// read-only progress; finished → summary. Re-run to refresh.
|
|
5034
5106
|
pi.registerCommand("babysit", {
|
|
5035
|
-
description: "Pick a session to inspect
|
|
5107
|
+
description: "Pick a session to inspect; /babysit view opens its log viewer; /babysit gc [days] removes old terminal roots",
|
|
5036
5108
|
handler: async (args, ctx) => {
|
|
5037
|
-
const
|
|
5109
|
+
const view = args.trim() === "view";
|
|
5110
|
+
const command = view ? "" : args.trim();
|
|
5038
5111
|
if (command === "gc" || command.startsWith("gc ")) {
|
|
5039
5112
|
const daysText = command.slice(2).trim();
|
|
5040
5113
|
const days = daysText ? Number(daysText) : 14;
|
|
@@ -5069,7 +5142,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5069
5142
|
return;
|
|
5070
5143
|
}
|
|
5071
5144
|
if (command) {
|
|
5072
|
-
ctx.ui.notify("Usage: /babysit or /babysit gc [days]", "error");
|
|
5145
|
+
ctx.ui.notify("Usage: /babysit, /babysit view, or /babysit gc [days]", "error");
|
|
5073
5146
|
return;
|
|
5074
5147
|
}
|
|
5075
5148
|
if (!(await babysitAvailable())) {
|
|
@@ -5106,6 +5179,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
5106
5179
|
if (!choice) return;
|
|
5107
5180
|
const picked = sessions[labels.indexOf(choice)];
|
|
5108
5181
|
if (!picked) return;
|
|
5182
|
+
if (view && ctx.mode === "tui") {
|
|
5183
|
+
await refreshWidget(ctx);
|
|
5184
|
+
sessionWidget?.open(picked.id);
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5109
5187
|
const kind = kindOf(picked.id);
|
|
5110
5188
|
const elapsed = picked.state === "running" ? elapsedOf(picked.id) : null;
|
|
5111
5189
|
const elapsedSuffix = elapsed ? ` ${elapsed}` : "";
|
package/log-viewer.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Key,
|
|
4
|
+
Markdown,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Text,
|
|
7
|
+
matchesKey,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
visibleWidth,
|
|
10
|
+
type Component,
|
|
11
|
+
type TuiMouseEvent,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
|
|
14
|
+
export interface LogViewerOptions {
|
|
15
|
+
getHeight: () => number;
|
|
16
|
+
requestRender: () => void;
|
|
17
|
+
getTheme: () => Theme;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Only numeric SGR escapes are allowed; never forward cursor, hyperlink or image controls. */
|
|
22
|
+
function sanitize(text: string, sgr = false): string {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g, "")
|
|
25
|
+
.replace(/\x1b[P_^X][\s\S]*?(?:\x1b\\|$)/g, "")
|
|
26
|
+
.replace(/\r\n/g, "\n")
|
|
27
|
+
.replace(/\r/g, "\n")
|
|
28
|
+
.replace(/\t/g, " ")
|
|
29
|
+
.replace(
|
|
30
|
+
/\x1b\[[0-?]*[ -/]*[@-~]|\x1b[^\[]?|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,
|
|
31
|
+
(s) => (sgr && /^\x1b\[[\d;]*m$/.test(s) ? s : ""),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
type Block = { kind: "assistant" | "tool" | "result" | "error"; text: string };
|
|
35
|
+
function contentText(content: unknown): string {
|
|
36
|
+
if (typeof content === "string") return content;
|
|
37
|
+
return Array.isArray(content)
|
|
38
|
+
? content
|
|
39
|
+
.filter((c) => c?.type === "text")
|
|
40
|
+
.map((c) => c.text ?? "")
|
|
41
|
+
.join("")
|
|
42
|
+
: "";
|
|
43
|
+
}
|
|
44
|
+
function agentBlocks(raw: string): Block[] {
|
|
45
|
+
const blocks: Block[] = [];
|
|
46
|
+
let current: Block | undefined;
|
|
47
|
+
const toolResults = new Map<string, Block>();
|
|
48
|
+
const assistant = () =>
|
|
49
|
+
(current ??=
|
|
50
|
+
(blocks.push({ kind: "assistant", text: "" }),
|
|
51
|
+
blocks[blocks.length - 1]));
|
|
52
|
+
for (const line of raw.split("\n")) {
|
|
53
|
+
let e: any;
|
|
54
|
+
try {
|
|
55
|
+
e = JSON.parse(line);
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!e || typeof e !== "object") continue;
|
|
60
|
+
if (e.type === "message_start" && e.message?.role === "assistant")
|
|
61
|
+
current = undefined;
|
|
62
|
+
if (["message_start", "message_update", "message_end"].includes(e.type)) {
|
|
63
|
+
if (e.message?.role === "assistant") {
|
|
64
|
+
const text = contentText(e.message.content);
|
|
65
|
+
const b = assistant();
|
|
66
|
+
if (text) b.text = text; // cumulative/final snapshots replace streamed deltas
|
|
67
|
+
if (e.message.errorMessage)
|
|
68
|
+
blocks.push({ kind: "error", text: String(e.message.errorMessage) });
|
|
69
|
+
if (e.type === "message_end") current = undefined;
|
|
70
|
+
} else if (
|
|
71
|
+
!e.message &&
|
|
72
|
+
e.assistantMessageEvent?.type === "text_delta" &&
|
|
73
|
+
typeof e.assistantMessageEvent.delta === "string"
|
|
74
|
+
) {
|
|
75
|
+
assistant().text += e.assistantMessageEvent.delta;
|
|
76
|
+
} else if (e.type === "message_end" && e.message?.role === "toolResult") {
|
|
77
|
+
const id = e.message.toolCallId;
|
|
78
|
+
const text = contentText(e.message.content);
|
|
79
|
+
if (id && toolResults.has(id)) {
|
|
80
|
+
const b = toolResults.get(id)!;
|
|
81
|
+
b.text = text;
|
|
82
|
+
b.kind = e.message.isError ? "error" : b.kind;
|
|
83
|
+
} else {
|
|
84
|
+
const b: Block = {
|
|
85
|
+
kind: e.message.isError ? "error" : "result",
|
|
86
|
+
text,
|
|
87
|
+
};
|
|
88
|
+
blocks.push(b);
|
|
89
|
+
if (id) toolResults.set(id, b);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (e.type === "tool_execution_start") {
|
|
93
|
+
blocks.push({
|
|
94
|
+
kind: "tool",
|
|
95
|
+
text: `${e.toolName ?? "tool"} ${JSON.stringify(e.args ?? {})}`,
|
|
96
|
+
});
|
|
97
|
+
} else if (e.type === "tool_execution_end") {
|
|
98
|
+
const b: Block = {
|
|
99
|
+
kind: e.isError ? "error" : "result",
|
|
100
|
+
text:
|
|
101
|
+
contentText(e.result?.content) ||
|
|
102
|
+
String(e.error?.message ?? e.error ?? ""),
|
|
103
|
+
};
|
|
104
|
+
if (b.text) {
|
|
105
|
+
blocks.push(b);
|
|
106
|
+
if (e.toolCallId) toolResults.set(e.toolCallId, b);
|
|
107
|
+
}
|
|
108
|
+
} else if (e.type === "error")
|
|
109
|
+
blocks.push({
|
|
110
|
+
kind: "error",
|
|
111
|
+
text: String(
|
|
112
|
+
e.message ?? e.error?.message ?? e.error ?? "Unknown error",
|
|
113
|
+
),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return blocks
|
|
117
|
+
.filter((b) => b.text)
|
|
118
|
+
.map((b) => ({ ...b, text: sanitize(b.text) }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createLogViewer(options: LogViewerOptions) {
|
|
122
|
+
let header = "",
|
|
123
|
+
raw = "",
|
|
124
|
+
description = "",
|
|
125
|
+
isAgent = false;
|
|
126
|
+
let cache: string[] | undefined,
|
|
127
|
+
cacheWidth = -1,
|
|
128
|
+
cacheTheme: Theme | undefined;
|
|
129
|
+
const body: Component = {
|
|
130
|
+
invalidate() {
|
|
131
|
+
cache = undefined;
|
|
132
|
+
},
|
|
133
|
+
render(width) {
|
|
134
|
+
const theme = options.getTheme();
|
|
135
|
+
if (cache && cacheWidth === width && cacheTheme === theme) return cache;
|
|
136
|
+
const blocks = isAgent
|
|
137
|
+
? agentBlocks(raw)
|
|
138
|
+
: [{ kind: "result" as const, text: sanitize(raw, true) }];
|
|
139
|
+
const lines: string[] = description
|
|
140
|
+
? [
|
|
141
|
+
theme.fg("muted", isAgent ? "[task]" : "[command]"),
|
|
142
|
+
...new Text(sanitize(description), 0, 0).render(width),
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
: [];
|
|
146
|
+
for (const b of blocks) {
|
|
147
|
+
if (isAgent)
|
|
148
|
+
lines.push(
|
|
149
|
+
theme.fg(
|
|
150
|
+
b.kind === "error"
|
|
151
|
+
? "error"
|
|
152
|
+
: b.kind === "assistant"
|
|
153
|
+
? "accent"
|
|
154
|
+
: "toolTitle",
|
|
155
|
+
`[${b.kind}]`,
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
lines.push(
|
|
159
|
+
...(b.kind === "assistant"
|
|
160
|
+
? new Markdown(b.text, 0, 0, getMarkdownTheme()).render(width)
|
|
161
|
+
: new Text(b.text, 0, 0).render(width)),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (!blocks.some((block) => block.text.trim()))
|
|
165
|
+
lines.push(theme.fg("muted", "(no output available)"));
|
|
166
|
+
cache =
|
|
167
|
+
lines.length > 2000
|
|
168
|
+
? [
|
|
169
|
+
theme.fg("warning", "[Earlier log lines clipped]"),
|
|
170
|
+
...lines.slice(-1999),
|
|
171
|
+
]
|
|
172
|
+
: lines;
|
|
173
|
+
cacheWidth = width;
|
|
174
|
+
cacheTheme = theme;
|
|
175
|
+
return cache;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const scrollView = new ScrollView(body, {
|
|
179
|
+
follow: "end",
|
|
180
|
+
overscroll: "contain",
|
|
181
|
+
scrollbar: "always",
|
|
182
|
+
scrollbarTrackStyle: (s) => options.getTheme().fg("scrollbarTrack", s),
|
|
183
|
+
scrollbarThumbStyle: (s) => options.getTheme().fg("scrollbarThumb", s),
|
|
184
|
+
});
|
|
185
|
+
const heading = {
|
|
186
|
+
invalidate() {},
|
|
187
|
+
render(width: number) {
|
|
188
|
+
const close = truncateToWidth("[Close]", width);
|
|
189
|
+
const room = Math.max(0, width - visibleWidth(close) - 1);
|
|
190
|
+
const title = truncateToWidth(sanitize(header).replace(/\n/g, " "), room);
|
|
191
|
+
return [
|
|
192
|
+
options.getTheme().fg("accent", title) +
|
|
193
|
+
" ".repeat(
|
|
194
|
+
Math.max(0, width - visibleWidth(title) - visibleWidth(close)),
|
|
195
|
+
) +
|
|
196
|
+
options.getTheme().fg("muted", close),
|
|
197
|
+
];
|
|
198
|
+
},
|
|
199
|
+
handleMouse(e: TuiMouseEvent) {
|
|
200
|
+
if (
|
|
201
|
+
e.button !== "left" ||
|
|
202
|
+
e.x < Math.max(0, e.width - 7) ||
|
|
203
|
+
e.x >= e.width
|
|
204
|
+
)
|
|
205
|
+
return;
|
|
206
|
+
if (e.type === "press") {
|
|
207
|
+
options.onClose();
|
|
208
|
+
return { handled: true };
|
|
209
|
+
}
|
|
210
|
+
if (e.type === "click" || e.type === "release") return { handled: true };
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
const footer: Component = {
|
|
214
|
+
invalidate() {},
|
|
215
|
+
render: (width) => [
|
|
216
|
+
options.getTheme().fg("dim", truncateToWidth("↑↓ · Esc close", width)),
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
let geometry = {
|
|
220
|
+
width: 0,
|
|
221
|
+
height: 0,
|
|
222
|
+
thumbTop: 0,
|
|
223
|
+
thumbHeight: 0,
|
|
224
|
+
maxTop: 0,
|
|
225
|
+
};
|
|
226
|
+
let dragOffset: number | undefined;
|
|
227
|
+
// Stock widget Containers are opaque to native layout. Draw and route this
|
|
228
|
+
// viewport locally, retaining the public ScrollView's follow/scroll state.
|
|
229
|
+
return {
|
|
230
|
+
scrollView,
|
|
231
|
+
render(width: number) {
|
|
232
|
+
width = Math.max(0, Math.floor(width));
|
|
233
|
+
const height = Math.max(0, Math.floor(options.getHeight()) - 2);
|
|
234
|
+
const contentWidth = Math.max(0, width - 1);
|
|
235
|
+
const lines = body.render(Math.max(1, contentWidth));
|
|
236
|
+
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
237
|
+
const visible = lines.slice(
|
|
238
|
+
scrollView.scrollTop,
|
|
239
|
+
scrollView.scrollTop + height,
|
|
240
|
+
);
|
|
241
|
+
const thumbHeight =
|
|
242
|
+
height === 0
|
|
243
|
+
? 0
|
|
244
|
+
: Math.max(
|
|
245
|
+
1,
|
|
246
|
+
Math.min(
|
|
247
|
+
height,
|
|
248
|
+
Math.round((height * height) / Math.max(1, lines.length)),
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
const maxTop = Math.max(0, lines.length - height);
|
|
252
|
+
const thumbTop =
|
|
253
|
+
maxTop === 0
|
|
254
|
+
? 0
|
|
255
|
+
: Math.round(
|
|
256
|
+
(scrollView.scrollTop / maxTop) * (height - thumbHeight),
|
|
257
|
+
);
|
|
258
|
+
geometry = { width, height, thumbTop, thumbHeight, maxTop };
|
|
259
|
+
return [
|
|
260
|
+
...heading.render(width),
|
|
261
|
+
...Array.from({ length: height }, (_, row) => {
|
|
262
|
+
if (width === 0) return "";
|
|
263
|
+
const text = truncateToWidth(visible[row] ?? "", contentWidth);
|
|
264
|
+
const thumb = row >= thumbTop && row < thumbTop + thumbHeight;
|
|
265
|
+
return (
|
|
266
|
+
text +
|
|
267
|
+
" ".repeat(Math.max(0, contentWidth - visibleWidth(text))) +
|
|
268
|
+
options
|
|
269
|
+
.getTheme()
|
|
270
|
+
.fg(
|
|
271
|
+
thumb ? "scrollbarThumb" : "scrollbarTrack",
|
|
272
|
+
thumb ? (dragOffset === undefined ? "┃" : "█") : "│",
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
}),
|
|
276
|
+
...footer.render(width),
|
|
277
|
+
];
|
|
278
|
+
},
|
|
279
|
+
handleMouse(e: TuiMouseEvent) {
|
|
280
|
+
if (dragOffset !== undefined) {
|
|
281
|
+
if (e.type === "release") {
|
|
282
|
+
dragOffset = undefined;
|
|
283
|
+
return { handled: true, render: true };
|
|
284
|
+
}
|
|
285
|
+
if (e.type === "drag" || e.type === "move") {
|
|
286
|
+
const travel = geometry.height - geometry.thumbHeight;
|
|
287
|
+
const offset = Math.max(0, Math.min(travel, e.y - 1 - dragOffset));
|
|
288
|
+
scrollView.scrollTo(
|
|
289
|
+
travel > 0
|
|
290
|
+
? Math.round((offset / travel) * geometry.maxTop)
|
|
291
|
+
: scrollView.scrollTop,
|
|
292
|
+
);
|
|
293
|
+
return { handled: true, render: true };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (e.y === 0) return heading.handleMouse(e);
|
|
297
|
+
if (e.y < 1 || e.y > geometry.height || e.x < 0 || e.x >= geometry.width)
|
|
298
|
+
return;
|
|
299
|
+
if (e.type === "wheel") {
|
|
300
|
+
scrollView.scrollBy(e.wheelDelta ?? 0);
|
|
301
|
+
return { handled: true, render: true };
|
|
302
|
+
}
|
|
303
|
+
if (e.x === geometry.width - 1 && e.button === "left") {
|
|
304
|
+
if (e.type === "press") {
|
|
305
|
+
const row = e.y - 1;
|
|
306
|
+
if (
|
|
307
|
+
row >= geometry.thumbTop &&
|
|
308
|
+
row < geometry.thumbTop + geometry.thumbHeight
|
|
309
|
+
) {
|
|
310
|
+
dragOffset = row - geometry.thumbTop;
|
|
311
|
+
return { handled: true, capture: true, render: true };
|
|
312
|
+
}
|
|
313
|
+
scrollView.scrollBy(
|
|
314
|
+
row < geometry.thumbTop ? -geometry.height : geometry.height,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (["press", "click", "release"].includes(e.type))
|
|
318
|
+
return { handled: true, render: true };
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
invalidate() {
|
|
322
|
+
body.invalidate();
|
|
323
|
+
},
|
|
324
|
+
update(nextHeader: string, logText: string, agent: boolean, context = "") {
|
|
325
|
+
header = nextHeader;
|
|
326
|
+
if (raw !== logText || isAgent !== agent || description !== context) {
|
|
327
|
+
raw = logText;
|
|
328
|
+
isAgent = agent;
|
|
329
|
+
description = context;
|
|
330
|
+
body.invalidate();
|
|
331
|
+
}
|
|
332
|
+
options.requestRender();
|
|
333
|
+
},
|
|
334
|
+
handleKey(data: string): boolean {
|
|
335
|
+
if (matchesKey(data, Key.escape)) {
|
|
336
|
+
options.onClose();
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
if (matchesKey(data, Key.up)) scrollView.scrollBy(-1);
|
|
340
|
+
else if (matchesKey(data, Key.down)) scrollView.scrollBy(1);
|
|
341
|
+
else return false;
|
|
342
|
+
options.requestRender();
|
|
343
|
+
return true;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
4
4
|
"description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"files": [
|
|
22
22
|
"index.ts",
|
|
23
|
+
"log-viewer.ts",
|
|
23
24
|
"agents.ts",
|
|
24
25
|
"self-reap.ts",
|
|
25
26
|
"format-stream.mjs",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"@earendil-works/pi-ai": "*",
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
43
|
-
"@earendil-works/pi-tui": "
|
|
43
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
44
|
+
"@earendil-works/pi-tui": ">=1.0.0",
|
|
44
45
|
"typebox": "*"
|
|
45
46
|
}
|
|
46
47
|
}
|