pi-famulus 0.1.7 → 0.1.8
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 +14 -7
- package/package.json +8 -8
- package/src/bash-override.ts +34 -26
- package/src/bounded-output.ts +167 -0
- package/src/comms/reply-command.ts +1 -1
- package/src/format.ts +33 -7
- package/src/index.ts +3 -2
- package/src/manager-client.ts +18 -1
- package/src/monitor-batching.ts +84 -36
- package/src/monitor-output.ts +159 -0
- package/src/monitor.ts +148 -37
- package/src/notify.ts +100 -17
- package/src/prompts.generated.ts +14 -11
- package/src/shell-exec.ts +28 -2
- package/src/shell-start.ts +25 -0
- package/src/subagent/child-bash.ts +2 -1
- package/src/subagent/conversation.ts +57 -12
- package/src/subagent/fleet-widget.ts +8 -8
- package/src/subagent/runner.ts +2 -2
- package/src/subagent/tool.ts +2 -1
- package/src/task-tools.ts +32 -13
- package/src/tui/conversation-view.ts +402 -0
- package/src/tui/message-renderers.ts +16 -11
- package/src/tui/scroll-detail-view.ts +218 -99
- package/src/tui/task-detail-view.ts +417 -0
- package/src/tui/tasks-command.ts +117 -84
- package/src/tui/tool-component.ts +6 -2
- package/src/wake.ts +27 -1
package/src/tui/tasks-command.ts
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
* Type to filter, ↑↓ selects, Enter opens, Ctrl+X asks before stopping, Tab
|
|
5
5
|
* switches between active/recent and the manager's complete task history.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { realClock, type Clock } from "../clock";
|
|
9
9
|
import type { ManagerClient, TaskRecord } from "../manager-client";
|
|
10
10
|
import { taskOutputPath } from "../config";
|
|
11
|
-
import { formatConversation } from "../subagent/conversation";
|
|
12
11
|
import type { SubagentRegistry } from "../subagent/registry";
|
|
13
12
|
import { formatAge, nextAgeBoundary, type WorkIndex, type WorkItem } from "../work-index";
|
|
14
|
-
import { notifyPlainFallback, showScrollDetail } from "./scroll-detail-view";
|
|
13
|
+
import { fitKeys, keyHints, notifyPlainFallback, showScrollDetail, spread, type PaneRenderer, type PaneTheme } from "./scroll-detail-view";
|
|
14
|
+
import { plainTheme, renderConversation } from "./conversation-view";
|
|
15
|
+
import { formatDuration } from "../format";
|
|
16
|
+
import { showTaskDetail } from "./task-detail-view";
|
|
15
17
|
import { readTaskFileTailCached, stderrPathFor } from "./task-output-paths";
|
|
16
|
-
import { fitLines, loadPiTui, truncateToWidth } from "./pi-tui-load";
|
|
18
|
+
import { fitLines, loadPiTui, truncateToWidth, visibleWidth } from "./pi-tui-load";
|
|
17
19
|
import { statusGlyph } from "./tool-component";
|
|
18
20
|
|
|
19
21
|
|
|
@@ -150,20 +152,6 @@ export function formatWorkRows(
|
|
|
150
152
|
});
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function selectorHint(theme: { fg(color: string, text: string): string }): string {
|
|
154
|
-
try {
|
|
155
|
-
return [
|
|
156
|
-
keyHint("tui.select.up", ""),
|
|
157
|
-
keyHint("tui.select.down", ""),
|
|
158
|
-
keyHint("tui.select.confirm", "view"),
|
|
159
|
-
rawKeyHint("ctrl+x", "stop"),
|
|
160
|
-
keyHint("tui.select.cancel", "close"),
|
|
161
|
-
].join(" ");
|
|
162
|
-
} catch {
|
|
163
|
-
return theme.fg("dim", "↑↓ select · Enter view · Ctrl+X stop · Esc close · type to filter");
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
155
|
export interface TaskListChoice {
|
|
168
156
|
action: "close" | "view" | "stop" | "already-finished";
|
|
169
157
|
id?: string;
|
|
@@ -286,7 +274,6 @@ async function showTaskList(
|
|
|
286
274
|
tui.requestRender();
|
|
287
275
|
scheduleAgeTick();
|
|
288
276
|
});
|
|
289
|
-
const border = new DynamicBorder((text) => theme.fg("border", text));
|
|
290
277
|
const pageSize = () => Math.max(1, (tui.terminal?.rows ?? 24) - 8);
|
|
291
278
|
const matches = (data: string, id: "tui.select.up" | "tui.select.down" | "tui.select.confirm" | "tui.select.cancel") => {
|
|
292
279
|
try { return kb.matches(data, id); } catch { return false; }
|
|
@@ -336,6 +323,8 @@ async function showTaskList(
|
|
|
336
323
|
};
|
|
337
324
|
return {
|
|
338
325
|
render(width: number) {
|
|
326
|
+
const t = theme as unknown as PaneTheme;
|
|
327
|
+
const dim = (text: string) => t.fg("dim", text);
|
|
339
328
|
const items = visibleItems();
|
|
340
329
|
if (!items.some((item) => item.id === selectedId)) selectedId = items[0]?.id;
|
|
341
330
|
const rows = groupTaskRows(items);
|
|
@@ -346,33 +335,54 @@ async function showTaskList(
|
|
|
346
335
|
const window = rows.slice(start, start + height);
|
|
347
336
|
const inner = Math.max(1, width);
|
|
348
337
|
const active = items.filter(isLive).length;
|
|
349
|
-
const scopeLabel = scope === "all" ? "
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
: ""
|
|
338
|
+
const scopeLabel = scope === "all" ? "all history" : "active + recent";
|
|
339
|
+
const rule = t.fg("borderMuted", "─".repeat(inner));
|
|
340
|
+
const counts = `${active > 0 ? t.fg("success", `${active} active`) : dim("0 active")}${dim(` · ${items.length} shown · ${scopeLabel}`)}`;
|
|
341
|
+
const title = spread(`${t.fg("accent", "◆")} ${t.bold("Tasks")}`, counts, inner);
|
|
342
|
+
// A long filter keeps its end (what is being typed) in view.
|
|
343
|
+
const shownFilter = visibleWidth(filter) > inner - 4 ? `…${filter.slice(-(inner - 5))}` : filter;
|
|
344
|
+
const prompt = truncateToWidth(
|
|
345
|
+
filter ? `${t.fg("accent", "›")} ${shownFilter}${t.fg("accent", "▏")}` : `${t.fg("accent", "›")} ${dim("type to filter")}`,
|
|
346
|
+
inner,
|
|
347
|
+
"",
|
|
348
|
+
);
|
|
349
|
+
const notice = confirmId
|
|
350
|
+
? ` ${t.bold(t.fg("error", `■ stop ${confirmId}?`))} ${keyHints(t, [["y ⏎", "confirm"], ["n esc", "cancel"]])}`
|
|
351
|
+
: hint
|
|
352
|
+
? ` ${t.fg(hint.startsWith("Already") || hint.startsWith("Stop requested") ? "dim" : "warning", hint)}`
|
|
353
|
+
: "";
|
|
354
|
+
const kindWidth = width >= 60 ? 8 : 0;
|
|
357
355
|
const body = window.map((row) => {
|
|
358
|
-
if (row.type === "run")
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
356
|
+
if (row.type === "run") {
|
|
357
|
+
return truncateToWidth(` ${dim("▾")} ${t.fg("muted", row.runId)} ${dim(`· ${row.count} child${row.count === 1 ? "" : "ren"}`)}`, inner, "…");
|
|
358
|
+
}
|
|
359
|
+
const item = row.item;
|
|
360
|
+
const selected = item.id === selectedId;
|
|
361
|
+
const { color, glyph } = statusGlyph(item.status);
|
|
362
|
+
const age = formatAge(item.startedAt, item.endedAt, clock.now());
|
|
363
|
+
const terminal = item.exitCode !== undefined && item.exitCode !== null
|
|
364
|
+
? `exit=${item.exitCode}`
|
|
365
|
+
: item.signal ?? item.endReason ?? "";
|
|
366
|
+
const name = item.error ? `${item.title} ${t.fg("error", `· ${item.error}`)}` : item.title;
|
|
367
|
+
const lead = (selected ? t.fg("accent", "▌") : " ") + (row.indent ? " " : " ");
|
|
368
|
+
const kind = kindWidth ? t.fg("muted", item.kind.padEnd(kindWidth)) : "";
|
|
369
|
+
// Same rule as top: a finished row keeps a quiet status word.
|
|
370
|
+
const status = t.fg(isLive(item) || color !== "success" ? color : "dim", item.status.padEnd(10));
|
|
371
|
+
const left = `${lead}${t.fg(color, glyph)} ${kind}${status}${selected ? t.bold(name) : name}`;
|
|
372
|
+
const right = dim(`${terminal ? `${terminal} ` : ""}${age}`);
|
|
373
|
+
return spread(left, right, inner);
|
|
374
|
+
});
|
|
375
|
+
if (body.length === 0) body.push(` ${dim(filter ? "No task matches the filter." : "No background tasks.")}`);
|
|
376
|
+
const place = items.length ? `${selectedTask + 1}${dim(`/${items.length}`)}` : "";
|
|
377
|
+
const keys = fitKeys(
|
|
378
|
+
t,
|
|
379
|
+
"",
|
|
380
|
+
[["↑↓", "select", 2], ["⏎", "open", 1], ["ctrl+x", "stop", 3], ["tab", "scope", 4], ["esc", filter ? "clear" : "close", 0]],
|
|
381
|
+
inner - 2 - visibleWidth(place) - 2,
|
|
382
|
+
);
|
|
383
|
+
return [rule, title, prompt, ...(notice ? [truncateToWidth(notice, inner, "…")] : []), "", ...body, "", spread(` ${keys}`, place, inner), rule];
|
|
374
384
|
},
|
|
375
|
-
invalidate() {
|
|
385
|
+
invalidate() {},
|
|
376
386
|
handleInput(data: string) {
|
|
377
387
|
const items = visibleItems();
|
|
378
388
|
const current = items[selectedIndex(items, selectedId)];
|
|
@@ -383,7 +393,8 @@ async function showTaskList(
|
|
|
383
393
|
return;
|
|
384
394
|
}
|
|
385
395
|
if (confirmId) {
|
|
386
|
-
if (matchesKey(data, "tui.select.confirm") || data === "\r" || data === "\n") { void requestStop(); return; }
|
|
396
|
+
if (matchesKey(data, "tui.select.confirm") || data === "\r" || data === "\n" || data === "y" || data === "Y") { void requestStop(); return; }
|
|
397
|
+
if (data === "n" || data === "N") { confirmId = undefined; hint = "Stop cancelled."; tui.requestRender(); return; }
|
|
387
398
|
return;
|
|
388
399
|
}
|
|
389
400
|
if (matches(data, "tui.select.up")) { choose(moveSelection(items, selectedId, -1) ?? selectedId); return; }
|
|
@@ -445,7 +456,40 @@ export function taskDetailHeader(item: WorkItem, now: number): string {
|
|
|
445
456
|
const outcome = item.exitCode !== undefined && item.exitCode !== null
|
|
446
457
|
? `exit ${item.exitCode}`
|
|
447
458
|
: item.signal ?? item.endReason ?? item.status;
|
|
448
|
-
|
|
459
|
+
const head = `${item.id} · ${item.kind} · ${outcome} · ${formatAge(item.startedAt, item.endedAt, now)}`;
|
|
460
|
+
// Agents have no cwd or command line of their own.
|
|
461
|
+
if (item.kind === "agent") return head;
|
|
462
|
+
return `${head} · ${item.cwd ?? "cwd unavailable"}\n$ ${item.command ?? item.title}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"];
|
|
466
|
+
|
|
467
|
+
/** `12.4k`, `1.3M`: same as top's compact numbers. */
|
|
468
|
+
export function compactNum(n: number): string {
|
|
469
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
470
|
+
if (n >= 10_000) return `${Math.floor(n / 1000)}k`;
|
|
471
|
+
if (n >= 1_000) return `${(n / 1000).toFixed(1)}k`;
|
|
472
|
+
return String(n);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `⠋ working 12.4k in · 1.8k out · 24.0 tok/s · llm 1m02s · tool 31s`, as in top's agent footer. */
|
|
476
|
+
export function agentStatusLine(
|
|
477
|
+
handle: { tokenUsage(): { input: number; output: number }; wallUsage(): { llmMs: number; toolMs: number } } | undefined,
|
|
478
|
+
live: boolean,
|
|
479
|
+
now: number,
|
|
480
|
+
theme: { fg(color: string, text: string): string; italic(text: string): string },
|
|
481
|
+
): string {
|
|
482
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
483
|
+
const parts: string[] = [];
|
|
484
|
+
if (handle) {
|
|
485
|
+
const tokens = handle.tokenUsage();
|
|
486
|
+
const wall = handle.wallUsage();
|
|
487
|
+
parts.push(`${compactNum(tokens.input)}${dim(" in")}`, `${compactNum(tokens.output)}${dim(" out")}`);
|
|
488
|
+
if (wall.llmMs > 0) parts.push(`${((tokens.output * 1000) / wall.llmMs).toFixed(1)}${dim(" tok/s")}`);
|
|
489
|
+
parts.push(`${formatDuration(wall.llmMs)}${dim(" llm")}`, `${formatDuration(wall.toolMs)}${dim(" tool")}`);
|
|
490
|
+
}
|
|
491
|
+
const spin = live ? `${theme.fg("accent", SPIN[Math.floor(now / 80) % SPIN.length])} ${theme.italic(theme.fg("accent", "working"))} ` : "";
|
|
492
|
+
return spin + parts.join(dim(" · "));
|
|
449
493
|
}
|
|
450
494
|
|
|
451
495
|
export function resolveTaskOutputPath(item: WorkItem, home: string | undefined, sessionId: string | undefined): string {
|
|
@@ -467,75 +511,64 @@ export function taskDetailInfo(item: WorkItem, now: number): string {
|
|
|
467
511
|
return lines.join("\n");
|
|
468
512
|
}
|
|
469
513
|
|
|
470
|
-
function isLiveStatus(status: string): boolean {
|
|
471
|
-
return status === "running" || status === "pending";
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Tab bodies for a shell or monitor. `getItem` is read on every poll so the
|
|
476
|
-
* header follows the task (a view opened while it ran must not keep saying
|
|
477
|
-
* "running"), and an empty file says why it is empty.
|
|
478
|
-
*/
|
|
479
|
-
export function taskDetailTabs(
|
|
480
|
-
getItem: () => WorkItem,
|
|
481
|
-
paths: { outputPath: string; stderrPath: string },
|
|
482
|
-
now: () => number,
|
|
483
|
-
): { output: () => string; stderr: () => string; info: () => string } {
|
|
484
|
-
const body = (path: string, emptyLive: string, emptyDone: string) => {
|
|
485
|
-
const text = readTaskFileTailCached(path);
|
|
486
|
-
if (text !== "(empty)") return text;
|
|
487
|
-
return isLiveStatus(getItem().status) ? emptyLive : emptyDone;
|
|
488
|
-
};
|
|
489
|
-
return {
|
|
490
|
-
output: () => `${taskDetailHeader(getItem(), now())}\n\n${body(paths.outputPath, "(no output yet)", "(no output; the command printed nothing)")}`,
|
|
491
|
-
stderr: () => `${taskDetailHeader(getItem(), now())}\n\n${body(paths.stderrPath, "(nothing on stderr yet)", "(nothing on stderr)")}`,
|
|
492
|
-
info: () => taskDetailInfo({ ...getItem(), ...paths }, now()),
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
514
|
async function viewItem(ctx: ExtensionContext, item: WorkItem, deps: TasksCommandDeps): Promise<void> {
|
|
497
515
|
if (!ctx.hasUI) return;
|
|
498
516
|
const clock = deps.clock ?? realClock;
|
|
499
517
|
if (item.kind === "agent") {
|
|
500
518
|
const handle = () => deps.getRegistry()?.handle(item.id);
|
|
501
519
|
const conversation = () => handle()?.conversation() ?? [];
|
|
502
|
-
const
|
|
520
|
+
const name = item.name ?? item.title;
|
|
521
|
+
const turnsOrPrompt = () => {
|
|
503
522
|
const turns = conversation();
|
|
504
|
-
return turns.length > 0 ?
|
|
523
|
+
return turns.length > 0 ? turns : item.prompt ? [{ role: "user", text: item.prompt }] : [];
|
|
505
524
|
};
|
|
525
|
+
const thread: PaneRenderer = {
|
|
526
|
+
render(width, theme, state) {
|
|
527
|
+
return renderConversation(turnsOrPrompt(), width, theme, { ...state, agent: name });
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
const current = () => deps.getIndex()?.get(item.id) ?? item;
|
|
531
|
+
const live = () => ["running", "pending"].includes(current().status);
|
|
506
532
|
const resultText = () => {
|
|
507
533
|
const latest = [...conversation()].reverse().find((turn) => turn.role === "assistant")?.text;
|
|
508
534
|
return latest ?? item.text ?? "(no result yet)";
|
|
509
535
|
};
|
|
510
|
-
const infoText = () => taskDetailInfo(
|
|
536
|
+
const infoText = () => taskDetailInfo(current(), clock.now());
|
|
511
537
|
try {
|
|
512
538
|
await showScrollDetail(ctx.ui, {
|
|
513
|
-
title:
|
|
539
|
+
title: `${item.id} · ${name}`,
|
|
514
540
|
tabs: {
|
|
515
|
-
conversation:
|
|
541
|
+
conversation: thread,
|
|
516
542
|
result: resultText,
|
|
517
543
|
info: infoText,
|
|
518
544
|
},
|
|
545
|
+
live,
|
|
546
|
+
status: (theme) => agentStatusLine(handle(), live(), clock.now(), theme),
|
|
519
547
|
pollMs: 500,
|
|
520
548
|
clock: deps.clock,
|
|
521
549
|
});
|
|
522
550
|
} catch {
|
|
523
|
-
|
|
551
|
+
const plain = renderConversation(turnsOrPrompt(), 100, plainTheme, { expanded: true, agent: name }).join("\n");
|
|
552
|
+
notifyPlainFallback(ctx.ui.notify.bind(ctx.ui), item.title, `${infoText()}\n\n${plain}`);
|
|
524
553
|
}
|
|
525
554
|
return;
|
|
526
555
|
}
|
|
527
556
|
const outputPath = resolveTaskOutputPath(item, deps.home, deps.sessionId?.());
|
|
528
557
|
const stderrPath = item.stderrPath || stderrPathFor(outputPath);
|
|
529
|
-
const
|
|
530
|
-
const infoText = tabs.info;
|
|
558
|
+
const getItem = () => deps.getIndex()?.get(item.id) ?? item;
|
|
531
559
|
try {
|
|
532
|
-
await
|
|
533
|
-
title: `${item.
|
|
534
|
-
|
|
560
|
+
await showTaskDetail(ctx.ui, {
|
|
561
|
+
title: `${item.id} · ${item.title}`,
|
|
562
|
+
getItem,
|
|
563
|
+
paths: { outputPath, stderrPath },
|
|
535
564
|
pollMs: 500,
|
|
536
565
|
clock: deps.clock,
|
|
537
566
|
});
|
|
538
567
|
} catch {
|
|
539
|
-
notifyPlainFallback(
|
|
568
|
+
notifyPlainFallback(
|
|
569
|
+
ctx.ui.notify.bind(ctx.ui),
|
|
570
|
+
item.title,
|
|
571
|
+
`${taskDetailInfo({ ...getItem(), outputPath, stderrPath }, clock.now())}\n\n${readTaskFileTailCached(outputPath)}`,
|
|
572
|
+
);
|
|
540
573
|
}
|
|
541
574
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fitLines } from "./pi-tui-load";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function renderToolLines(lines: string[], width: number): string[] {
|
|
4
4
|
const limit = Math.max(1, width);
|
|
5
5
|
const out: string[] = [];
|
|
6
6
|
for (const line of lines) out.push(...fitLines(line, limit));
|
|
@@ -16,11 +16,15 @@ export function toolComponent(lines: string[]): { render(width: number): string[
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The one status mapping, shared with `pi-famulus top`: running ● green,
|
|
21
|
+
* pending ○ dim, done ✓ green, failed ✗ red, stopped / timeout ■ amber.
|
|
22
|
+
*/
|
|
19
23
|
export function statusGlyph(status: string | undefined, isError = false): { color: string; glyph: string } {
|
|
20
24
|
if (isError || status === "failed" || status === "killed" || status === "orphaned" || status === "interrupted") {
|
|
21
25
|
return { color: "error", glyph: "✗" };
|
|
22
26
|
}
|
|
23
|
-
if (status === "running") return { color: "
|
|
27
|
+
if (status === "running") return { color: "success", glyph: "●" };
|
|
24
28
|
if (status === "pending") return { color: "dim", glyph: "○" };
|
|
25
29
|
if (status === "partial" || status === "timeout" || status === "stopped") {
|
|
26
30
|
return { color: "warning", glyph: "■" };
|
package/src/wake.ts
CHANGED
|
@@ -94,6 +94,17 @@ export interface WakeTiming {
|
|
|
94
94
|
ageMs?: number;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** Original-output coordinates and observation times; independent of wake delivery time. */
|
|
98
|
+
export interface MonitorOutputInfo {
|
|
99
|
+
firstObservedAt?: number;
|
|
100
|
+
lastObservedAt?: number;
|
|
101
|
+
cursorStart?: number;
|
|
102
|
+
cursorEnd?: number;
|
|
103
|
+
truncated?: boolean;
|
|
104
|
+
suppressedCount?: number;
|
|
105
|
+
omittedChars?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
export type FamulusWake = FamulusWakeBody & WakeTiming;
|
|
98
109
|
|
|
99
110
|
type FamulusWakeBody =
|
|
@@ -106,6 +117,7 @@ type FamulusWakeBody =
|
|
|
106
117
|
event: string;
|
|
107
118
|
eventCount?: number;
|
|
108
119
|
droppedLines?: number;
|
|
120
|
+
output?: MonitorOutputInfo;
|
|
109
121
|
}
|
|
110
122
|
| {
|
|
111
123
|
kind: "subagent-handover";
|
|
@@ -316,7 +328,21 @@ function renderMonitor(details: Extract<FamulusWake, { kind: "monitor" }>): stri
|
|
|
316
328
|
if (details.droppedLines !== undefined && details.droppedLines > 0) {
|
|
317
329
|
attrs.push(`dropped-lines="${details.droppedLines}"`);
|
|
318
330
|
}
|
|
319
|
-
|
|
331
|
+
const output = details.output;
|
|
332
|
+
const parts = [`<pi-famulus-wake ${attrs.join(" ")}>`, ` <event>${escapeXml(details.event)}</event>`];
|
|
333
|
+
if (output?.cursorStart !== undefined) {
|
|
334
|
+
const fields: string[] = [];
|
|
335
|
+
if (output.firstObservedAt !== undefined) fields.push(`first-observed-at="${wakeTimestamp(output.firstObservedAt)}"`);
|
|
336
|
+
if (output.lastObservedAt !== undefined) fields.push(`last-observed-at="${wakeTimestamp(output.lastObservedAt)}"`);
|
|
337
|
+
fields.push(`cursor-start="${output.cursorStart}"`);
|
|
338
|
+
if (output.cursorEnd !== undefined) fields.push(`cursor-end="${output.cursorEnd}"`);
|
|
339
|
+
if (output.truncated) fields.push('truncated="true"');
|
|
340
|
+
if (output.suppressedCount) fields.push(`duplicates-suppressed="${output.suppressedCount}"`);
|
|
341
|
+
if (output.omittedChars) fields.push(`omitted-chars="${output.omittedChars}"`);
|
|
342
|
+
parts.push(` <output ${fields.join(" ")}>${escapeXml(PROMPTS["wakes.monitor.output-hint"])}</output>`);
|
|
343
|
+
}
|
|
344
|
+
parts.push("</pi-famulus-wake>");
|
|
345
|
+
return parts.join("\n");
|
|
320
346
|
}
|
|
321
347
|
|
|
322
348
|
function renderHandover(details: Extract<FamulusWake, { kind: "subagent-handover" }>): string {
|