@yusukeshib/pi-babysit 0.6.5 → 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 +8 -2
- package/index.ts +151 -66
- 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, 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
|
+
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
|
@@ -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,14 +2289,23 @@ async function spawnSubagent(
|
|
|
2287
2289
|
}
|
|
2288
2290
|
|
|
2289
2291
|
// ---------------------------------------------------------------------------
|
|
2290
|
-
// widget (live counts)
|
|
2292
|
+
// widget (live counts and browsable sessions)
|
|
2291
2293
|
// ---------------------------------------------------------------------------
|
|
2292
2294
|
|
|
2295
|
+
export function widgetSessionGroups(sessions: BsSession[], startedAt: (id: string) => number) {
|
|
2296
|
+
const active = sessions.filter((session) => session.state === "running");
|
|
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) };
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2293
2304
|
// A subagent whose task is done stays alive as an idle RPC worker (for
|
|
2294
2305
|
// follow-ups), so "running" in babysit does NOT mean "working" — count
|
|
2295
2306
|
// processes / busy subagents / idle subagents separately.
|
|
2296
2307
|
export type WidgetSessionKind = "process" | "agent";
|
|
2297
|
-
export type WidgetSessionState = "running" | "idle";
|
|
2308
|
+
export type WidgetSessionState = "running" | "idle" | "finished" | "failed";
|
|
2298
2309
|
|
|
2299
2310
|
export function widgetSummaryText(procs: number, busy: number, idle: number): string {
|
|
2300
2311
|
const sections: string[] = [];
|
|
@@ -2312,7 +2323,7 @@ export function widgetSessionHeader(
|
|
|
2312
2323
|
state: WidgetSessionState,
|
|
2313
2324
|
elapsed?: string,
|
|
2314
2325
|
): string {
|
|
2315
|
-
const icon = state === "running" ? "▶" : "○";
|
|
2326
|
+
const icon = state === "running" ? "▶" : state === "failed" ? "✗" : "○";
|
|
2316
2327
|
return ` ${icon} ${id} ${kind.toUpperCase()} ${state.toUpperCase()}${elapsed ? ` age ${elapsed}` : ""}`;
|
|
2317
2328
|
}
|
|
2318
2329
|
|
|
@@ -2338,17 +2349,23 @@ function renderWidgetSessionHeader(
|
|
|
2338
2349
|
elapsed: string,
|
|
2339
2350
|
theme: Theme,
|
|
2340
2351
|
): string {
|
|
2341
|
-
const
|
|
2342
|
-
const icon = theme.fg(running ? "
|
|
2352
|
+
const color = state === "running" ? "success" : state === "failed" ? "error" : "muted";
|
|
2353
|
+
const icon = theme.fg(color, state === "running" ? "▶" : state === "failed" ? "✗" : "○");
|
|
2343
2354
|
const kindLabel = theme.fg(kind === "process" ? "accent" : "warning", theme.bold(kind.toUpperCase()));
|
|
2344
|
-
const stateLabel = theme.fg(
|
|
2355
|
+
const stateLabel = theme.fg(color, theme.bold(state.toUpperCase()));
|
|
2345
2356
|
return ` ${icon} ${id} ${kindLabel} ${stateLabel}${elapsed ? theme.fg("dim", ` age ${elapsed}`) : ""}`;
|
|
2346
2357
|
}
|
|
2347
2358
|
|
|
2348
|
-
// How many trailing output lines to show per
|
|
2359
|
+
// How many trailing output lines to show per session in the widget.
|
|
2349
2360
|
const WIDGET_COLLAPSED_TAIL_LINES = 1;
|
|
2350
2361
|
const WIDGET_EXPANDED_TAIL_LINES = 20;
|
|
2351
2362
|
const WIDGET_TAIL_WIDTH = 100;
|
|
2363
|
+
const WIDGET_SESSION_PAGE_SIZE = 5;
|
|
2364
|
+
|
|
2365
|
+
export interface WidgetSessionListState {
|
|
2366
|
+
open: boolean;
|
|
2367
|
+
page: number;
|
|
2368
|
+
}
|
|
2352
2369
|
|
|
2353
2370
|
export interface WidgetSessionDisplay {
|
|
2354
2371
|
id: string;
|
|
@@ -2361,6 +2378,7 @@ interface WidgetMouseEvent {
|
|
|
2361
2378
|
type: string;
|
|
2362
2379
|
button: string;
|
|
2363
2380
|
y: number;
|
|
2381
|
+
x?: number;
|
|
2364
2382
|
}
|
|
2365
2383
|
|
|
2366
2384
|
/**
|
|
@@ -2371,45 +2389,96 @@ interface WidgetMouseEvent {
|
|
|
2371
2389
|
export function createWidgetComponent(
|
|
2372
2390
|
summaryLines: string[],
|
|
2373
2391
|
sessions: WidgetSessionDisplay[],
|
|
2374
|
-
|
|
2392
|
+
expandedSessionIds: Set<string>,
|
|
2393
|
+
listState?: WidgetSessionListState,
|
|
2375
2394
|
) {
|
|
2376
|
-
|
|
2395
|
+
type Target = { type: "session"; id: string } | { type: "sessions"; endX: number; prevX?: number; nextX?: number };
|
|
2396
|
+
let targetByLine: Array<Target | undefined> = [];
|
|
2377
2397
|
|
|
2378
2398
|
return {
|
|
2379
2399
|
render(width: number): string[] {
|
|
2380
2400
|
const lines: string[] = [];
|
|
2381
|
-
|
|
2382
|
-
const push = (text: string,
|
|
2401
|
+
targetByLine = [];
|
|
2402
|
+
const push = (text: string, target?: Target) => {
|
|
2383
2403
|
lines.push(truncateToWidth(text, Math.max(0, width), ""));
|
|
2384
|
-
|
|
2404
|
+
targetByLine.push(target);
|
|
2385
2405
|
};
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
for (const session of sessions) {
|
|
2389
|
-
const expanded = session.expandable && expandedProcessIds.has(session.id);
|
|
2406
|
+
const pushSession = (session: WidgetSessionDisplay) => {
|
|
2407
|
+
const expanded = session.expandable && expandedSessionIds.has(session.id);
|
|
2390
2408
|
const tail = session.tail.slice(
|
|
2391
2409
|
expanded ? -WIDGET_EXPANDED_TAIL_LINES : -WIDGET_COLLAPSED_TAIL_LINES,
|
|
2392
2410
|
);
|
|
2393
|
-
const
|
|
2411
|
+
const target = session.expandable ? { type: "session" as const, id: session.id } : undefined;
|
|
2394
2412
|
if (!expanded && tail.length === 1) {
|
|
2395
|
-
push(`${session.header} ${tail[0]}`,
|
|
2413
|
+
push(`${session.header} ${tail[0]}`, target);
|
|
2396
2414
|
} else {
|
|
2397
|
-
push(session.header,
|
|
2398
|
-
for (const line of tail) push(` ${line}`,
|
|
2415
|
+
push(session.header, target);
|
|
2416
|
+
for (const line of tail) push(` ${line}`, target);
|
|
2417
|
+
if (expanded && tail.length === 0) push(" (no output available)", target);
|
|
2399
2418
|
}
|
|
2419
|
+
};
|
|
2420
|
+
|
|
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)) {
|
|
2445
|
+
pushSession(session);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
} else {
|
|
2449
|
+
if (summary !== undefined) push(summary);
|
|
2450
|
+
for (const session of sessions) pushSession(session);
|
|
2400
2451
|
}
|
|
2401
2452
|
return lines;
|
|
2402
2453
|
},
|
|
2403
2454
|
handleMouse(event: WidgetMouseEvent) {
|
|
2404
|
-
if (event.
|
|
2405
|
-
const
|
|
2406
|
-
if (!
|
|
2407
|
-
if (
|
|
2408
|
-
|
|
2409
|
-
|
|
2455
|
+
if (event.button !== "left") return undefined;
|
|
2456
|
+
const target = targetByLine[event.y];
|
|
2457
|
+
if (!target) return undefined;
|
|
2458
|
+
if (target.type === "sessions" && (event.x === undefined || event.x < 0 || event.x >= target.endX)) return undefined;
|
|
2459
|
+
// Toggle on press so a terminal that omits release still works.
|
|
2460
|
+
if (event.type === "press") {
|
|
2461
|
+
if (target.type === "session") {
|
|
2462
|
+
if (expandedSessionIds.has(target.id)) expandedSessionIds.delete(target.id);
|
|
2463
|
+
else expandedSessionIds.add(target.id);
|
|
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
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return { handled: true, render: true };
|
|
2475
|
+
}
|
|
2476
|
+
// Consume Pi's synthesized click without toggling a second time.
|
|
2477
|
+
if (event.type === "click") return { handled: true, render: false };
|
|
2478
|
+
return undefined;
|
|
2410
2479
|
},
|
|
2411
2480
|
invalidate() {
|
|
2412
|
-
|
|
2481
|
+
targetByLine = [];
|
|
2413
2482
|
},
|
|
2414
2483
|
};
|
|
2415
2484
|
}
|
|
@@ -2454,21 +2523,23 @@ function readTailLines(file: string, lines: number, maxBytes = 64_000): string[]
|
|
|
2454
2523
|
}
|
|
2455
2524
|
}
|
|
2456
2525
|
|
|
2457
|
-
// Trailing lines to show for a
|
|
2526
|
+
// Trailing lines to show for a session (sanitized, unprefixed).
|
|
2458
2527
|
// Process tails are read directly from the bounded end of output.log, avoiding
|
|
2459
|
-
// one `babysit log` subprocess per
|
|
2460
|
-
|
|
2528
|
+
// one `babysit log` subprocess per visible process.
|
|
2529
|
+
export function widgetTail(
|
|
2461
2530
|
id: string,
|
|
2462
2531
|
isSub: boolean,
|
|
2463
2532
|
subagentProgress?: Progress,
|
|
2464
2533
|
lines = WIDGET_COLLAPSED_TAIL_LINES,
|
|
2465
|
-
):
|
|
2534
|
+
): string[] {
|
|
2466
2535
|
let raw: string[];
|
|
2467
2536
|
if (!isSub) {
|
|
2468
2537
|
raw = readTailLines(logPath(id), lines);
|
|
2469
2538
|
} else {
|
|
2470
2539
|
const progress = subagentProgress ?? taskProgressOf(id).progress;
|
|
2471
|
-
if (progress.
|
|
2540
|
+
if (progress.streamingText.trim()) {
|
|
2541
|
+
raw = progress.streamingText.trim().split("\n");
|
|
2542
|
+
} else if (progress.finalText.trim()) {
|
|
2472
2543
|
raw = progress.finalText.trim().split("\n");
|
|
2473
2544
|
} else if (progress.toolCalls.length > 0) {
|
|
2474
2545
|
raw = progress.toolCalls.map((tool) => tool.summary);
|
|
@@ -3365,13 +3436,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3365
3436
|
}
|
|
3366
3437
|
}
|
|
3367
3438
|
|
|
3368
|
-
const
|
|
3439
|
+
const expandedWidgetSessions = new Set<string>();
|
|
3440
|
+
const widgetListState: WidgetSessionListState = { open: true, page: 0 };
|
|
3369
3441
|
|
|
3370
3442
|
const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
|
|
3371
3443
|
if (!ctx.hasUI) return;
|
|
3372
|
-
const
|
|
3373
|
-
|
|
3374
|
-
);
|
|
3444
|
+
const all = snapshot ?? (await listSessions()).sessions;
|
|
3445
|
+
const { active, all: ordered } = widgetSessionGroups(all, (id) => readMeta(id)?.startedAt ?? 0);
|
|
3375
3446
|
const subs = active.filter((session) => kindOf(session.id) === "subagent");
|
|
3376
3447
|
const procs = active.length - subs.length;
|
|
3377
3448
|
const progressById = new Map<string, Progress>();
|
|
@@ -3383,11 +3454,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3383
3454
|
}
|
|
3384
3455
|
}
|
|
3385
3456
|
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);
|
|
3457
|
+
const sessionIds = new Set(all.map((session) => session.id));
|
|
3458
|
+
for (const id of expandedWidgetSessions) {
|
|
3459
|
+
if (!sessionIds.has(id)) expandedWidgetSessions.delete(id);
|
|
3391
3460
|
}
|
|
3392
3461
|
|
|
3393
3462
|
const theme = ctx.ui.theme;
|
|
@@ -3399,37 +3468,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
3399
3468
|
session.id,
|
|
3400
3469
|
isSubagent,
|
|
3401
3470
|
isSubagent ? progressById.get(session.id) : undefined,
|
|
3402
|
-
|
|
3471
|
+
WIDGET_EXPANDED_TAIL_LINES,
|
|
3403
3472
|
);
|
|
3404
3473
|
}),
|
|
3405
3474
|
);
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3475
|
+
const activeTails = new Map(active.map((session, index) => [session.id, tails[index]]));
|
|
3476
|
+
const displays = ordered.map((session): WidgetSessionDisplay => {
|
|
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
|
+
}
|
|
3411
3488
|
return {
|
|
3412
|
-
id
|
|
3489
|
+
id,
|
|
3413
3490
|
header: renderWidgetSessionHeader(
|
|
3414
|
-
|
|
3491
|
+
id,
|
|
3415
3492
|
isSubagent ? "agent" : "process",
|
|
3416
|
-
state,
|
|
3417
|
-
|
|
3493
|
+
session.state === "exited" && session.exit_code === 0 ? "finished" : "failed",
|
|
3494
|
+
"",
|
|
3418
3495
|
theme,
|
|
3419
3496
|
),
|
|
3420
|
-
|
|
3421
|
-
|
|
3497
|
+
// Only read/parse the five visible sessions on demand.
|
|
3498
|
+
get tail() {
|
|
3499
|
+
try {
|
|
3500
|
+
return widgetTail(id, isSubagent, undefined, WIDGET_EXPANDED_TAIL_LINES);
|
|
3501
|
+
} catch {
|
|
3502
|
+
return [];
|
|
3503
|
+
}
|
|
3504
|
+
},
|
|
3505
|
+
expandable: true,
|
|
3422
3506
|
};
|
|
3423
3507
|
});
|
|
3424
3508
|
|
|
3425
3509
|
if (ctx.mode === "tui") {
|
|
3426
3510
|
ctx.ui.setWidget(
|
|
3427
3511
|
"pi-babysit",
|
|
3428
|
-
() => createWidgetComponent(summaryLines, displays,
|
|
3512
|
+
() => createWidgetComponent(summaryLines, displays, expandedWidgetSessions, widgetListState),
|
|
3429
3513
|
{ placement: "belowEditor" },
|
|
3430
3514
|
);
|
|
3431
3515
|
} else {
|
|
3432
|
-
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());
|
|
3433
3518
|
ctx.ui.setWidget("pi-babysit", component.render(10_000), {
|
|
3434
3519
|
placement: "belowEditor",
|
|
3435
3520
|
});
|
|
@@ -4147,7 +4232,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4147
4232
|
const title = theme.bold(`babysit_run ${kind}`);
|
|
4148
4233
|
const detail =
|
|
4149
4234
|
typeof payload === "string" && payload.length > 0
|
|
4150
|
-
? ` ${
|
|
4235
|
+
? ` ${escapeCommandForDisplay(payload)}`
|
|
4151
4236
|
: "";
|
|
4152
4237
|
return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
|
|
4153
4238
|
},
|