@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.14.0
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 +115 -26
- package/package.json +2 -2
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-config.ts +11 -6
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +303 -32
- package/src/minimal-subagents-delivery-ledger.ts +14 -13
- package/src/minimal-subagents-extension.ts +62 -2
- package/src/minimal-subagents-registry-wire.ts +1 -0
- package/src/minimal-subagents-registry.ts +6 -1
- package/src/minimal-subagents-render-contract.ts +33 -2
- package/src/minimal-subagents-rendering.ts +321 -52
- package/src/minimal-subagents-sessions.ts +3 -1
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +20 -3
- package/src/minimal-subagents-tools.ts +109 -35
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +28 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
|
@@ -19,10 +19,13 @@ import {
|
|
|
19
19
|
visibleWidth,
|
|
20
20
|
type Component,
|
|
21
21
|
} from "@earendil-works/pi-tui";
|
|
22
|
+
import { Type } from "typebox";
|
|
23
|
+
import { Value } from "typebox/value";
|
|
22
24
|
import {
|
|
23
25
|
parseCoordinatorMessageDetails,
|
|
24
26
|
parseCoordinatorToolCall,
|
|
25
27
|
parseCoordinatorToolResult,
|
|
28
|
+
parseWaitProgressDetails,
|
|
26
29
|
type CancelRenderDetails,
|
|
27
30
|
type CoordinatorMessageRenderDetails,
|
|
28
31
|
type CoordinatorToolCallInput,
|
|
@@ -31,11 +34,13 @@ import {
|
|
|
31
34
|
type ManagementCallArguments,
|
|
32
35
|
type MessageCallArguments,
|
|
33
36
|
type MessageRenderDetails,
|
|
37
|
+
type RenderRecentActivity,
|
|
34
38
|
type RenderStatusAgent,
|
|
35
39
|
type SpawnCallArguments,
|
|
36
40
|
type SpawnRenderDetails,
|
|
37
41
|
type StatusRenderDetails,
|
|
38
42
|
type WaitCallArguments,
|
|
43
|
+
type WaitProgressRenderDetails,
|
|
39
44
|
type WaitRenderDetails,
|
|
40
45
|
} from "./minimal-subagents-render-contract.js";
|
|
41
46
|
import { stripCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
@@ -63,6 +68,7 @@ type SubagentPresentationStatus =
|
|
|
63
68
|
| "delivered"
|
|
64
69
|
| "delivered-via-wait"
|
|
65
70
|
| "queued"
|
|
71
|
+
| "started-turn"
|
|
66
72
|
| "message"
|
|
67
73
|
| "timed out";
|
|
68
74
|
|
|
@@ -80,6 +86,7 @@ const SUBAGENT_STATUS_PRESENTATION = {
|
|
|
80
86
|
delivered: { symbol: "→", color: "accent" },
|
|
81
87
|
"delivered-via-wait": { symbol: "→", color: "accent" },
|
|
82
88
|
queued: { symbol: "↗", color: "accent" },
|
|
89
|
+
"started-turn": { symbol: "◉", color: "accent" },
|
|
83
90
|
message: { symbol: "→", color: "accent" },
|
|
84
91
|
"timed out": { symbol: "!", color: "warning" },
|
|
85
92
|
} satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
|
|
@@ -106,7 +113,7 @@ export function orderActiveAgentSubtrees(agents: readonly AgentSummary[]): Agent
|
|
|
106
113
|
): { agent: AgentSummary; active: boolean }[] =>
|
|
107
114
|
siblings
|
|
108
115
|
.map((agent) => {
|
|
109
|
-
const children = orderSiblings(agent.children);
|
|
116
|
+
const children = orderSiblings(agent.children ?? []);
|
|
110
117
|
return {
|
|
111
118
|
agent: { ...agent, children: children.map((child) => child.agent) },
|
|
112
119
|
active: agent.state === "running" || children.some((child) => child.active),
|
|
@@ -215,6 +222,169 @@ function collapsedExpansionHint(theme: MinimalSubagentsRenderTheme): string {
|
|
|
215
222
|
return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
/** Collapsed previews match Pi's built-in `read` output height. */
|
|
226
|
+
const COLLAPSED_PREVIEW_LINES = 10;
|
|
227
|
+
|
|
228
|
+
function isBlankRenderedLine(line: string): boolean {
|
|
229
|
+
return visibleWidth(line.trim()) === 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function withoutTrailingBlankLines(lines: string[]): string[] {
|
|
233
|
+
const end = lines.findLastIndex((line) => !isBlankRenderedLine(line));
|
|
234
|
+
return lines.slice(0, end + 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Shows the first rendered lines of `content`, noting how many lines expanding would reveal. */
|
|
238
|
+
class CollapsedPreview implements Component {
|
|
239
|
+
constructor(
|
|
240
|
+
private readonly content: Component,
|
|
241
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
242
|
+
) {}
|
|
243
|
+
|
|
244
|
+
render(width: number): string[] {
|
|
245
|
+
const lines = withoutTrailingBlankLines(this.content.render(width));
|
|
246
|
+
if (lines.length <= COLLAPSED_PREVIEW_LINES) return lines;
|
|
247
|
+
// A cut at a paragraph break would leave a blank line above the hint.
|
|
248
|
+
const shown = withoutTrailingBlankLines(lines.slice(0, COLLAPSED_PREVIEW_LINES));
|
|
249
|
+
const hidden = lines.length - shown.length;
|
|
250
|
+
return [
|
|
251
|
+
...shown,
|
|
252
|
+
// The result's first line already carries the expansion hint.
|
|
253
|
+
this.theme.fg("muted", `... (${hidden} more lines)`),
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
invalidate(): void {
|
|
258
|
+
this.content.invalidate();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function collapsedMarkdownPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
|
|
263
|
+
return new CollapsedPreview(new Markdown(content, 0, 0, getMarkdownTheme()), theme);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collapsedTextPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
|
|
267
|
+
return new CollapsedPreview(new Text(content, 0, 0), theme);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ToolArgumentsSchema = Type.Record(Type.String(), Type.Unknown());
|
|
271
|
+
const ScalarArgumentSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean()]);
|
|
272
|
+
|
|
273
|
+
/** Summarize tool-call arguments by their values, e.g. a path or command rather than its JSON. */
|
|
274
|
+
function toolArgumentSummary(content: string): string {
|
|
275
|
+
let parsed: unknown;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(content);
|
|
278
|
+
} catch {
|
|
279
|
+
// Recent Activity keeps the tail of long arguments, which is no longer valid JSON.
|
|
280
|
+
return content;
|
|
281
|
+
}
|
|
282
|
+
if (!Value.Check(ToolArgumentsSchema, parsed)) return content;
|
|
283
|
+
return Object.values(parsed)
|
|
284
|
+
.filter((value) => value !== null && value !== undefined)
|
|
285
|
+
.map((value) =>
|
|
286
|
+
Value.Check(ScalarArgumentSchema, value) ? String(value) : JSON.stringify(value),
|
|
287
|
+
)
|
|
288
|
+
.join(" ");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function lastNonEmptyLine(content: string): string {
|
|
292
|
+
return (
|
|
293
|
+
content
|
|
294
|
+
.split("\n")
|
|
295
|
+
.map((line) => line.trim())
|
|
296
|
+
.findLast((line) => line.length > 0) ?? ""
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** One activity entry as a single width-bounded line, truncating plain text before styling it. */
|
|
301
|
+
function formatActivityLine(
|
|
302
|
+
activity: RenderRecentActivity,
|
|
303
|
+
theme: MinimalSubagentsRenderTheme,
|
|
304
|
+
width: number,
|
|
305
|
+
): string {
|
|
306
|
+
const fit = (prefix: string, text: string) =>
|
|
307
|
+
formatSubagentPreview(text, Math.max(1, width - visibleWidth(prefix)));
|
|
308
|
+
if (activity.label.startsWith("tool call ")) {
|
|
309
|
+
const name = activity.label.slice("tool call ".length);
|
|
310
|
+
const prefix = `\u2192 ${name} `;
|
|
311
|
+
return `${theme.fg("accent", "\u2192")} ${theme.fg("toolTitle", name)} ${theme.fg("dim", fit(prefix, toolArgumentSummary(activity.content)))}`;
|
|
312
|
+
}
|
|
313
|
+
if (activity.label.startsWith("tool result ")) {
|
|
314
|
+
const failed = activity.label.endsWith(" (error)");
|
|
315
|
+
const name = activity.label.slice("tool result ".length).replace(/ \(error\)$/, "");
|
|
316
|
+
const prefix = `${failed ? "\u2717" : "\u2190"} ${name}: `;
|
|
317
|
+
return theme.fg(failed ? "error" : "dim", `${prefix}${fit(prefix, activity.content)}`);
|
|
318
|
+
}
|
|
319
|
+
if (activity.label === "reasoning") {
|
|
320
|
+
const prefix = "thinking: ";
|
|
321
|
+
return theme.fg("dim", `${prefix}${fit(prefix, lastNonEmptyLine(activity.content))}`);
|
|
322
|
+
}
|
|
323
|
+
if (activity.label === "assistant message") {
|
|
324
|
+
return theme.fg("muted", fit("", lastNonEmptyLine(activity.content)));
|
|
325
|
+
}
|
|
326
|
+
const prefix = `${activity.label}: `;
|
|
327
|
+
return theme.fg("dim", `${prefix}${fit(prefix, activity.content)}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Columns the activity rail takes before each item's own lines. */
|
|
331
|
+
export const ACTIVITY_RAIL_WIDTH = 3;
|
|
332
|
+
|
|
333
|
+
/** Hang rendered items off a tree rail: `├─` opens each item, `│` continues it, `└─` opens the last. */
|
|
334
|
+
export function drawActivityRail(
|
|
335
|
+
items: readonly (readonly string[])[],
|
|
336
|
+
theme: MinimalSubagentsRenderTheme,
|
|
337
|
+
): string[] {
|
|
338
|
+
return items.flatMap((lines, index) => {
|
|
339
|
+
const last = index === items.length - 1;
|
|
340
|
+
const [first = "", ...rest] = lines;
|
|
341
|
+
return [
|
|
342
|
+
`${theme.fg("dim", last ? "└─ " : "├─ ")}${first}`,
|
|
343
|
+
...rest.map((line) => `${theme.fg("dim", last ? " " : "│ ")}${line}`),
|
|
344
|
+
];
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Width-aware rendering of recorded activity, one rail item per entry. */
|
|
349
|
+
class ActivityLines implements Component {
|
|
350
|
+
constructor(
|
|
351
|
+
private readonly activity: readonly RenderRecentActivity[],
|
|
352
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
353
|
+
) {}
|
|
354
|
+
|
|
355
|
+
render(width: number): string[] {
|
|
356
|
+
const itemWidth = Math.max(1, width - ACTIVITY_RAIL_WIDTH);
|
|
357
|
+
return drawActivityRail(
|
|
358
|
+
this.activity.map((entry) => [formatActivityLine(entry, this.theme, itemWidth)]),
|
|
359
|
+
this.theme,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
invalidate(): void {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const COLLAPSED_ACTIVITY_LINES = 3;
|
|
367
|
+
|
|
368
|
+
/** Latest progress-relevant activity: model output and tool calls, plus only failed tool results. */
|
|
369
|
+
function latestProgressActivity(activity: readonly RenderRecentActivity[]): RenderRecentActivity[] {
|
|
370
|
+
return activity
|
|
371
|
+
.filter(
|
|
372
|
+
(entry) =>
|
|
373
|
+
entry.label === "assistant message" ||
|
|
374
|
+
entry.label === "reasoning" ||
|
|
375
|
+
entry.label.startsWith("tool call ") ||
|
|
376
|
+
(entry.label.startsWith("tool result ") && entry.label.endsWith(" (error)")),
|
|
377
|
+
)
|
|
378
|
+
.slice(-COLLAPSED_ACTIVITY_LINES);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Format a turn's cost for summary rows; sub-cent costs keep enough precision to be non-zero. */
|
|
382
|
+
function formatSubagentCost(usage: Usage | undefined): string | undefined {
|
|
383
|
+
const total = usage?.cost.total;
|
|
384
|
+
if (total === undefined || !(total > 0)) return undefined;
|
|
385
|
+
return `$${total.toFixed(total < 0.01 ? 4 : 2)}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
218
388
|
/** Format milliseconds for compact subagent rows without losing sub-second durations. */
|
|
219
389
|
export function formatSubagentDuration(elapsedMs: number | undefined): string | undefined {
|
|
220
390
|
if (elapsedMs === undefined || !Number.isFinite(elapsedMs) || elapsedMs < 0) return undefined;
|
|
@@ -245,10 +415,6 @@ export function formatSubagentPreview(content: string | undefined, maxWidth = 72
|
|
|
245
415
|
return `${sliceByColumn(singleLine, 0, boundedWidth - 1, true).trimEnd()}…`;
|
|
246
416
|
}
|
|
247
417
|
|
|
248
|
-
function currentSubagentPreviewWidth(reservedWidth: number): number {
|
|
249
|
-
return Math.max(12, Math.min(72, (process.stdout.columns || 100) - reservedWidth));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
418
|
/** Format complete Pi usage metrics for expanded subagent output. */
|
|
253
419
|
export function formatSubagentUsage(usage: Usage | undefined): string | undefined {
|
|
254
420
|
if (usage === undefined) return undefined;
|
|
@@ -267,13 +433,41 @@ function coordinatorToolCallTitle(theme: MinimalSubagentsRenderTheme, label: str
|
|
|
267
433
|
return theme.fg("toolTitle", theme.bold(label));
|
|
268
434
|
}
|
|
269
435
|
|
|
270
|
-
|
|
436
|
+
/** Launch settings the caller chose explicitly; defaults are omitted to keep the header short. */
|
|
437
|
+
function spawnCallLaunchSummary(args: SpawnCallArguments): string[] {
|
|
438
|
+
const profile = args.model
|
|
439
|
+
? `${args.model}${args.thinking_level ? `:${args.thinking_level}` : ""}`
|
|
440
|
+
: args.thinking_level
|
|
441
|
+
? `thinking ${args.thinking_level}`
|
|
442
|
+
: undefined;
|
|
443
|
+
const tools =
|
|
444
|
+
args.tools === undefined
|
|
445
|
+
? undefined
|
|
446
|
+
: `tools ${Array.isArray(args.tools) ? args.tools.join(", ") || "none" : args.tools}`;
|
|
447
|
+
return [
|
|
448
|
+
args.role ? `role ${args.role}` : undefined,
|
|
449
|
+
profile,
|
|
450
|
+
tools,
|
|
451
|
+
args.session_context && args.session_context !== "omit"
|
|
452
|
+
? `context ${args.session_context}`
|
|
453
|
+
: undefined,
|
|
454
|
+
args.project_context === "omit" ? "no project context" : undefined,
|
|
455
|
+
args.delegation && args.delegation !== "none" ? args.delegation : undefined,
|
|
456
|
+
].filter((part): part is string => part !== undefined);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A call header followed by its long text argument, previewed when collapsed. */
|
|
460
|
+
function renderCallWithText(
|
|
461
|
+
header: string,
|
|
462
|
+
text: string | undefined,
|
|
271
463
|
theme: MinimalSubagentsRenderTheme,
|
|
272
|
-
|
|
273
|
-
):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
464
|
+
expanded: boolean,
|
|
465
|
+
): Component {
|
|
466
|
+
if (!text) return new Text(header, 0, 0);
|
|
467
|
+
const container = new Container();
|
|
468
|
+
container.addChild(new Text(header, 0, 0));
|
|
469
|
+
container.addChild(expanded ? new Text(text, 0, 0) : collapsedTextPreview(text, theme));
|
|
470
|
+
return container;
|
|
277
471
|
}
|
|
278
472
|
|
|
279
473
|
function renderManagementToolCall(
|
|
@@ -308,13 +502,13 @@ function renderSpawnResult(
|
|
|
308
502
|
const container = new Container();
|
|
309
503
|
container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
|
|
310
504
|
container.addChild(renderLabelValue(theme, "Turn", details.turn_id));
|
|
311
|
-
appendTextSection(container, theme, "Task", args.task ?? "(task unavailable)");
|
|
312
505
|
const resolvedModel = launchContract?.model ?? args.model;
|
|
313
506
|
const resolvedThinking = launchContract?.thinking_level ?? args.thinking_level;
|
|
314
507
|
const launch = [
|
|
315
508
|
`delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
|
|
316
|
-
`session context ${launchContract?.session_context ?? args.session_context ?? "
|
|
509
|
+
`session context ${launchContract?.session_context ?? args.session_context ?? "omit"}`,
|
|
317
510
|
`project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
|
|
511
|
+
launchContract?.role ? `role ${launchContract.role}` : undefined,
|
|
318
512
|
resolvedModel ? `model ${resolvedModel}` : undefined,
|
|
319
513
|
resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
|
|
320
514
|
].filter((value): value is string => value !== undefined);
|
|
@@ -350,13 +544,65 @@ function renderMessageResult(
|
|
|
350
544
|
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
351
545
|
const container = new Container();
|
|
352
546
|
container.addChild(new Text(summary, 0, 0));
|
|
353
|
-
appendTextSection(container, theme, "Message", args.message ?? "(message unavailable)");
|
|
354
547
|
appendTextSection(container, theme, "Recipient", agentId);
|
|
355
548
|
appendTextSection(container, theme, "Disposition", disposition);
|
|
549
|
+
if ("turn_id" in details && details.turn_id) {
|
|
550
|
+
appendTextSection(container, theme, "Started turn", details.turn_id);
|
|
551
|
+
}
|
|
356
552
|
if (details.error) appendTextSection(container, theme, "Error", details.error);
|
|
357
553
|
return container;
|
|
358
554
|
}
|
|
359
555
|
|
|
556
|
+
/** Renders a running child's turn for a waiting parent; returns nothing when the turn is not live. */
|
|
557
|
+
export type LiveTurnRenderer = (
|
|
558
|
+
agentId: string,
|
|
559
|
+
turnId: string | undefined,
|
|
560
|
+
expanded: boolean,
|
|
561
|
+
) => Component | undefined;
|
|
562
|
+
|
|
563
|
+
function renderWaitProgress(
|
|
564
|
+
details: WaitProgressRenderDetails,
|
|
565
|
+
options: ToolRenderResultOptions,
|
|
566
|
+
theme: MinimalSubagentsRenderTheme,
|
|
567
|
+
renderLiveTurn: LiveTurnRenderer | undefined,
|
|
568
|
+
): Component {
|
|
569
|
+
const toolCalls = details.tool_calls ?? 0;
|
|
570
|
+
const metrics = [
|
|
571
|
+
formatSubagentDuration(details.elapsed_ms),
|
|
572
|
+
toolCalls > 0 ? `${toolCalls} tool ${toolCalls === 1 ? "call" : "calls"}` : undefined,
|
|
573
|
+
].filter((metric): metric is string => metric !== undefined);
|
|
574
|
+
const summary = renderSubagentSummary(theme, "waiting", details.agent_id, metrics);
|
|
575
|
+
const liveTurn = renderLiveTurn?.(details.agent_id, details.turn_id, options.expanded);
|
|
576
|
+
if (!liveTurn) return new Text(summary, 0, 0);
|
|
577
|
+
const container = new Container();
|
|
578
|
+
container.addChild(
|
|
579
|
+
new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
|
|
580
|
+
);
|
|
581
|
+
container.addChild(liveTurn);
|
|
582
|
+
return container;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
586
|
+
function collapsedWaitBody(
|
|
587
|
+
details: WaitRenderDetails,
|
|
588
|
+
status: string,
|
|
589
|
+
theme: MinimalSubagentsRenderTheme,
|
|
590
|
+
): Component | undefined {
|
|
591
|
+
if (details.event === "message") return collapsedTextPreview(details.message, theme);
|
|
592
|
+
if (details.event === "timeout") {
|
|
593
|
+
const activity = latestProgressActivity(details.agent.recent_activity ?? []);
|
|
594
|
+
return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
|
|
595
|
+
}
|
|
596
|
+
const output = details.output ?? "";
|
|
597
|
+
if (status === "completed") {
|
|
598
|
+
return output ? collapsedMarkdownPreview(output, theme) : undefined;
|
|
599
|
+
}
|
|
600
|
+
return collapsedTextPreview(
|
|
601
|
+
theme.fg("error", details.error ?? (output || "(no error detail)")),
|
|
602
|
+
theme,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
360
606
|
function renderWaitResult(
|
|
361
607
|
details: WaitRenderDetails,
|
|
362
608
|
options: ToolRenderResultOptions,
|
|
@@ -374,9 +620,8 @@ function renderWaitResult(
|
|
|
374
620
|
const duration = formatSubagentDuration(
|
|
375
621
|
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
376
622
|
);
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
);
|
|
623
|
+
const usage = details.event === "timeout" ? undefined : details.usage;
|
|
624
|
+
const tokens = formatSubagentTokenCount(usage?.totalTokens);
|
|
380
625
|
const drainedMessageCount =
|
|
381
626
|
details.event === "message" || details.event === "timeout"
|
|
382
627
|
? 0
|
|
@@ -384,11 +629,19 @@ function renderWaitResult(
|
|
|
384
629
|
const metrics = [
|
|
385
630
|
duration,
|
|
386
631
|
tokens ? `${tokens} tokens` : undefined,
|
|
632
|
+
formatSubagentCost(usage),
|
|
387
633
|
drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
|
|
388
634
|
].filter((metric): metric is string => metric !== undefined);
|
|
389
635
|
const summary = renderSubagentSummary(theme, status, agentId, metrics);
|
|
390
|
-
if (options.isPartial
|
|
391
|
-
|
|
636
|
+
if (options.isPartial) return new Text(summary, 0, 0);
|
|
637
|
+
if (!options.expanded) {
|
|
638
|
+
const body = collapsedWaitBody(details, status, theme);
|
|
639
|
+
const heading = `${summary}${collapsedExpansionHint(theme)}`;
|
|
640
|
+
if (!body) return new Text(heading, 0, 0);
|
|
641
|
+
const container = new Container();
|
|
642
|
+
container.addChild(new Text(heading, 0, 0));
|
|
643
|
+
container.addChild(body);
|
|
644
|
+
return container;
|
|
392
645
|
}
|
|
393
646
|
const container = new Container();
|
|
394
647
|
container.addChild(new Text(summary, 0, 0));
|
|
@@ -460,9 +713,10 @@ function renderDirectStatusRows(
|
|
|
460
713
|
return agents.map((agent) => {
|
|
461
714
|
const duration = formatSubagentDuration(agent.elapsed_ms);
|
|
462
715
|
const childCount = agent.child_count ?? 0;
|
|
463
|
-
const metrics = [
|
|
464
|
-
|
|
465
|
-
|
|
716
|
+
const metrics = [
|
|
717
|
+
duration,
|
|
718
|
+
childCount > 0 ? `${childCount} ${childCount === 1 ? "child" : "children"}` : undefined,
|
|
719
|
+
].filter((metric): metric is string => metric !== undefined);
|
|
466
720
|
return renderSubagentSummary(
|
|
467
721
|
theme,
|
|
468
722
|
statusAgentPresentation(agent),
|
|
@@ -484,11 +738,19 @@ function renderDetailedStatusAgent(
|
|
|
484
738
|
const id = agent.agent_id ?? "agent";
|
|
485
739
|
const childCount = agent.child_count ?? 0;
|
|
486
740
|
const duration = formatSubagentDuration(agent.elapsed_ms);
|
|
487
|
-
const metrics = [duration, `${childCount} children`].filter(
|
|
741
|
+
const metrics = [duration, `${childCount} ${childCount === 1 ? "child" : "children"}`].filter(
|
|
488
742
|
(metric): metric is string => metric !== undefined,
|
|
489
743
|
);
|
|
490
744
|
const summary = renderSubagentSummary(theme, status, id, metrics);
|
|
491
|
-
if (!options.expanded)
|
|
745
|
+
if (!options.expanded) {
|
|
746
|
+
const activity = latestProgressActivity(agent.recent_activity ?? []);
|
|
747
|
+
const heading = `${summary}${collapsedExpansionHint(theme)}`;
|
|
748
|
+
if (activity.length === 0) return new Text(heading, 0, 0);
|
|
749
|
+
const collapsed = new Container();
|
|
750
|
+
collapsed.addChild(new Text(heading, 0, 0));
|
|
751
|
+
collapsed.addChild(new ActivityLines(activity, theme));
|
|
752
|
+
return collapsed;
|
|
753
|
+
}
|
|
492
754
|
const container = new Container();
|
|
493
755
|
container.addChild(new Text(summary, 0, 0));
|
|
494
756
|
const labels = [
|
|
@@ -510,6 +772,7 @@ function renderDetailedStatusAgent(
|
|
|
510
772
|
const launchValues = [
|
|
511
773
|
`session context ${launchContract.session_context ?? "inherit"}`,
|
|
512
774
|
`project context ${launchContract.project_context ?? "inherit"}`,
|
|
775
|
+
...(launchContract.role ? [`role ${launchContract.role}`] : []),
|
|
513
776
|
`model ${launchContract.model ?? agent.model ?? "unknown"}`,
|
|
514
777
|
`thinking ${launchContract.thinking_level ?? agent.thinking_level ?? "unknown"}`,
|
|
515
778
|
`delegation ${launchContract.delegation ?? "none"}`,
|
|
@@ -590,12 +853,14 @@ function renderStatusResult(
|
|
|
590
853
|
theme.fg("muted", `${counts.children} children`),
|
|
591
854
|
theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
|
|
592
855
|
].join(renderSubagentSeparator(theme));
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
0,
|
|
856
|
+
const rows =
|
|
857
|
+
renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)");
|
|
858
|
+
const container = new Container();
|
|
859
|
+
container.addChild(
|
|
860
|
+
new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
|
|
598
861
|
);
|
|
862
|
+
container.addChild(options.expanded ? new Text(rows, 0, 0) : collapsedTextPreview(rows, theme));
|
|
863
|
+
return container;
|
|
599
864
|
}
|
|
600
865
|
return renderDetailedStatusAgent(details.agent, options, theme);
|
|
601
866
|
}
|
|
@@ -609,7 +874,8 @@ function renderCancelResult(
|
|
|
609
874
|
const summary =
|
|
610
875
|
turns.length > 0
|
|
611
876
|
? renderSubagentSummary(theme, "cancelled", details.agent_id, [
|
|
612
|
-
`${turns.length} turns cancelled`,
|
|
877
|
+
`${turns.length} ${turns.length === 1 ? "turn" : "turns"} cancelled`,
|
|
878
|
+
details.affected_agent_ids.join(", "),
|
|
613
879
|
])
|
|
614
880
|
: renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
|
|
615
881
|
if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
|
|
@@ -635,9 +901,10 @@ function renderDeleteResult(
|
|
|
635
901
|
theme: MinimalSubagentsRenderTheme,
|
|
636
902
|
): Component {
|
|
637
903
|
const status = details.failures.length > 0 ? "failed" : "completed";
|
|
904
|
+
const deletedCount = details.deleted_agent_ids.length;
|
|
638
905
|
const metrics = [
|
|
639
|
-
`${
|
|
640
|
-
|
|
906
|
+
`${deletedCount} ${deletedCount === 1 ? "agent" : "agents"} deleted`,
|
|
907
|
+
deletedCount > 0 ? details.deleted_agent_ids.join(", ") : undefined,
|
|
641
908
|
details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
|
|
642
909
|
].filter((metric): metric is string => metric !== undefined);
|
|
643
910
|
const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
|
|
@@ -676,21 +943,24 @@ export function renderCoordinatorToolCall(
|
|
|
676
943
|
toolName: CoordinatorToolName,
|
|
677
944
|
args: CoordinatorToolCallInput,
|
|
678
945
|
theme: MinimalSubagentsRenderTheme,
|
|
946
|
+
expanded = false,
|
|
679
947
|
): Component {
|
|
680
948
|
const parsed = parseCoordinatorToolCall(toolName, args);
|
|
681
949
|
if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
|
|
682
950
|
switch (parsed.toolName) {
|
|
683
|
-
case "subagent":
|
|
684
|
-
|
|
685
|
-
`${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
951
|
+
case "subagent": {
|
|
952
|
+
const header = [
|
|
953
|
+
`${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}`,
|
|
954
|
+
...spawnCallLaunchSummary(parsed.args).map((part) => theme.fg("dim", part)),
|
|
955
|
+
].join(theme.fg("dim", " · "));
|
|
956
|
+
return renderCallWithText(header, parsed.args.task, theme, expanded);
|
|
957
|
+
}
|
|
689
958
|
case "agent_message":
|
|
690
|
-
return
|
|
691
|
-
`${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}
|
|
692
|
-
|
|
693
|
-
|
|
959
|
+
return renderCallWithText(
|
|
960
|
+
`${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}`,
|
|
961
|
+
parsed.args.message,
|
|
962
|
+
theme,
|
|
963
|
+
expanded,
|
|
694
964
|
);
|
|
695
965
|
case "subagent_wait":
|
|
696
966
|
return new Text(
|
|
@@ -719,7 +989,12 @@ export function renderCoordinatorToolResult(
|
|
|
719
989
|
theme: MinimalSubagentsRenderTheme,
|
|
720
990
|
args: CoordinatorToolCallInput,
|
|
721
991
|
isError = false,
|
|
992
|
+
renderLiveTurn?: LiveTurnRenderer,
|
|
722
993
|
): Component {
|
|
994
|
+
if (toolName === "subagent_wait" && options.isPartial) {
|
|
995
|
+
const progress = parseWaitProgressDetails(result.details);
|
|
996
|
+
if (progress) return renderWaitProgress(progress, options, theme, renderLiveTurn);
|
|
997
|
+
}
|
|
723
998
|
const parsedResult = parseCoordinatorToolResult(toolName, result.details);
|
|
724
999
|
if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
|
|
725
1000
|
const parsedCall = parseCoordinatorToolCall(toolName, args);
|
|
@@ -795,30 +1070,24 @@ function renderCoordinatorMessage(
|
|
|
795
1070
|
const destination = details?.destination_agent_id ?? "recipient";
|
|
796
1071
|
const sourceTurn = messageSourceTurn(details);
|
|
797
1072
|
const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
|
|
1073
|
+
const metrics = [formatSubagentDuration(details?.elapsed_ms), formatSubagentCost(details?.usage)];
|
|
798
1074
|
const heading = [
|
|
799
1075
|
`${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
|
|
800
1076
|
route,
|
|
801
|
-
sourceTurn ? theme.fg("dim", `turn ${sourceTurn}`) : undefined,
|
|
802
1077
|
details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
|
|
1078
|
+
...metrics.map((metric) => (metric ? theme.fg("muted", metric) : undefined)),
|
|
803
1079
|
]
|
|
804
1080
|
.filter((part): part is string => part !== undefined)
|
|
805
1081
|
.join(renderSubagentSeparator(theme));
|
|
806
1082
|
const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
807
1083
|
if (!options.expanded) {
|
|
808
|
-
box.addChild(
|
|
809
|
-
|
|
810
|
-
`${heading}\n${theme.fg("muted", formatSubagentPreview(content, currentSubagentPreviewWidth(24)))}`,
|
|
811
|
-
0,
|
|
812
|
-
0,
|
|
813
|
-
),
|
|
814
|
-
);
|
|
1084
|
+
box.addChild(new Text(`${heading}${collapsedExpansionHint(theme)}`, 0, 0));
|
|
1085
|
+
box.addChild(collapsedMarkdownPreview(content, theme));
|
|
815
1086
|
return box;
|
|
816
1087
|
}
|
|
817
1088
|
const container = new Container();
|
|
818
1089
|
container.addChild(new Text(heading, 0, 0));
|
|
819
1090
|
if (sourceTurn) container.addChild(renderLabelValue(theme, "Source turn", sourceTurn));
|
|
820
|
-
const duration = formatSubagentDuration(details?.elapsed_ms);
|
|
821
|
-
if (duration) container.addChild(renderLabelValue(theme, "Duration", duration));
|
|
822
1091
|
container.addChild(new Spacer(1));
|
|
823
1092
|
container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
|
|
824
1093
|
const usageText = formatSubagentUsage(details?.usage);
|
|
@@ -503,7 +503,9 @@ export function findDeliveryEvidence(
|
|
|
503
503
|
))
|
|
504
504
|
);
|
|
505
505
|
}
|
|
506
|
-
|
|
506
|
+
// A message or timeout wait result never carried the turn's terminal result.
|
|
507
|
+
if (waitToolResult && (details.event === "message" || details.event === "timeout"))
|
|
508
|
+
return false;
|
|
507
509
|
return details.source_agent_id === sourceAgentId && details.source_turn_id === sourceTurnId;
|
|
508
510
|
});
|
|
509
511
|
}
|