@ian-pascoe/pi-minimal-subagents 0.15.0 → 0.17.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 +9 -7
- package/package.json +6 -6
- package/src/minimal-subagents-config.ts +65 -53
- package/src/minimal-subagents-context.ts +1 -1
- package/src/minimal-subagents-coordinator.ts +1 -1
- package/src/minimal-subagents-extension.ts +58 -19
- package/src/minimal-subagents-render-contract.ts +4 -1
- package/src/minimal-subagents-rendering.ts +214 -193
- package/src/minimal-subagents-settings-writer.ts +22 -36
- package/src/minimal-subagents-status-panel.ts +93 -69
- package/src/minimal-subagents-tools.ts +46 -15
- package/src/minimal-subagents-transcript.ts +16 -11
- package/src/minimal-subagents-ui.ts +51 -154
- package/src/minimal-subagents-usage.ts +30 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
1
|
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { rewriteNamespaceDocument } from "@ian-pascoe/pi-utils/layered-settings";
|
|
3
3
|
import { updateFileLocked } from "@ian-pascoe/pi-utils/locked-file-update";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
@@ -12,31 +12,6 @@ export interface MinimalSubagentsSettingsWriteContext {
|
|
|
12
12
|
isProjectTrusted(): boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
type SettingsJsonObject = Record<string, JsonValue>;
|
|
16
|
-
|
|
17
|
-
function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
|
|
18
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
|
|
19
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function parseSettings(content: string | undefined, path: string): SettingsJsonObject {
|
|
23
|
-
if (content === undefined) return {};
|
|
24
|
-
// JSON.parse is the provenance for this JSON type; the object shape is checked below.
|
|
25
|
-
let parsed: JsonValue;
|
|
26
|
-
try {
|
|
27
|
-
parsed = JSON.parse(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
|
|
28
|
-
} catch (cause) {
|
|
29
|
-
throw new Error(`Minimal subagents settings JSON is malformed at ${path}`, { cause });
|
|
30
|
-
}
|
|
31
|
-
if (!isSettingsJsonObject(parsed)) {
|
|
32
|
-
throw new Error(`Minimal subagents settings at ${path} must have an object root`);
|
|
33
|
-
}
|
|
34
|
-
if (parsed.minimalSubagents !== undefined && !isSettingsJsonObject(parsed.minimalSubagents)) {
|
|
35
|
-
throw new Error(`Minimal subagents settings at ${path} must have an object minimalSubagents`);
|
|
36
|
-
}
|
|
37
|
-
return parsed;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
15
|
/**
|
|
41
16
|
* Set or remove `minimalSubagents.enabled` in global or trusted-project Pi settings,
|
|
42
17
|
* preserving every unrelated setting. Writes re-read the file under Pi's settings lock.
|
|
@@ -58,15 +33,26 @@ export async function writeMinimalSubagentsEnabled(
|
|
|
58
33
|
);
|
|
59
34
|
}
|
|
60
35
|
|
|
61
|
-
await updateFileLocked(path, (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
36
|
+
await updateFileLocked(path, (current) =>
|
|
37
|
+
rewriteNamespaceDocument({
|
|
38
|
+
current,
|
|
39
|
+
namespace: "minimalSubagents",
|
|
40
|
+
invalid: (reason, cause) => {
|
|
41
|
+
if (reason === "malformed")
|
|
42
|
+
return new Error(`Minimal subagents settings JSON is malformed at ${path}`, { cause });
|
|
43
|
+
return new Error(
|
|
44
|
+
reason === "root"
|
|
45
|
+
? `Minimal subagents settings at ${path} must have an object root`
|
|
46
|
+
: `Minimal subagents settings at ${path} must have an object minimalSubagents`,
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
update: (stored) => {
|
|
50
|
+
const minimalSubagents = { ...stored };
|
|
51
|
+
if (enabled === undefined) delete minimalSubagents.enabled;
|
|
52
|
+
else minimalSubagents.enabled = enabled;
|
|
53
|
+
return minimalSubagents;
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
71
57
|
return path;
|
|
72
58
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
2
|
import {
|
|
3
|
+
DynamicBorder,
|
|
3
4
|
type ExtensionContext,
|
|
4
5
|
type KeybindingsManager,
|
|
5
6
|
type Theme,
|
|
@@ -8,16 +9,20 @@ import {
|
|
|
8
9
|
Text,
|
|
9
10
|
matchesKey,
|
|
10
11
|
truncateToWidth,
|
|
11
|
-
|
|
12
|
+
wrapTextWithAnsi,
|
|
12
13
|
type Component,
|
|
13
14
|
type OverlayHandle,
|
|
14
15
|
type TUI,
|
|
15
16
|
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { hintLine, joinInline, noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
16
18
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
17
19
|
import {
|
|
18
20
|
formatSubagentDuration,
|
|
19
21
|
orderActiveAgentSubtrees,
|
|
22
|
+
renderSubagentStatusLabel,
|
|
23
|
+
renderSubagentStatusMark,
|
|
20
24
|
subagentStatusLadder,
|
|
25
|
+
treeRowPrefixes,
|
|
21
26
|
} from "./minimal-subagents-rendering.js";
|
|
22
27
|
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
23
28
|
import {
|
|
@@ -82,21 +87,11 @@ function statusAccessSourceLabel(source: SubagentAccessSnapshot["source"]): stri
|
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* backgrounds; without this the rest of the row (ellipsis, padding, border) loses the panel fill.
|
|
91
|
-
*/
|
|
92
|
-
function keepBackgroundThroughResets(text: string, backgroundOpen: string): string {
|
|
93
|
-
if (!backgroundOpen) return text;
|
|
94
|
-
let result = text;
|
|
95
|
-
for (const sequence of BACKGROUND_CLEARING_SEQUENCES) {
|
|
96
|
-
result = result.replaceAll(sequence, `${sequence}${backgroundOpen}`);
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
90
|
+
/** The panel's ellipsis, in Pi's three-dot form. */
|
|
91
|
+
const ELLIPSIS = "...";
|
|
92
|
+
|
|
93
|
+
/** The theme operations the panel draws with; Pi's `Theme` satisfies it. */
|
|
94
|
+
type StatusPanelTheme = Pick<Theme, "fg" | "bold">;
|
|
100
95
|
|
|
101
96
|
function transcriptText(line: string): string {
|
|
102
97
|
return stripVTControlCharacters(line).replace(/\s/g, "");
|
|
@@ -152,14 +147,15 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
152
147
|
private toolOutputExpanded = false;
|
|
153
148
|
private disposed = false;
|
|
154
149
|
private readonly stopRefresh: () => void;
|
|
150
|
+
private readonly border = new DynamicBorder((text) => this.theme.fg("border", text));
|
|
155
151
|
|
|
156
152
|
/** Bind one live status component to its coordinator, terminal, and explicit refresh owner. */
|
|
157
153
|
constructor(
|
|
158
154
|
private readonly coordinator: MinimalSubagentsCoordinator,
|
|
159
155
|
private readonly getAccess: () => MinimalSubagentsStatusAccess,
|
|
160
156
|
private readonly tui: TUI,
|
|
161
|
-
private readonly theme:
|
|
162
|
-
private readonly keybindings: KeybindingsManager,
|
|
157
|
+
private readonly theme: StatusPanelTheme,
|
|
158
|
+
private readonly keybindings: Pick<KeybindingsManager, "matches" | "getKeys">,
|
|
163
159
|
private readonly cwd: string,
|
|
164
160
|
private readonly onClose: () => void,
|
|
165
161
|
startRefresh: StartStatusPanelRefresh = startStatusPanelRefresh,
|
|
@@ -208,7 +204,7 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
208
204
|
this.tui.requestRender();
|
|
209
205
|
}
|
|
210
206
|
|
|
211
|
-
/** Render
|
|
207
|
+
/** Render the tree or Child Session Transcript in Pi's selector frame, within the terminal. */
|
|
212
208
|
render(width: number): string[] {
|
|
213
209
|
if (width <= 0) return [];
|
|
214
210
|
const height = Math.max(
|
|
@@ -219,24 +215,24 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
219
215
|
),
|
|
220
216
|
);
|
|
221
217
|
if (width < 6 || height < 5) {
|
|
222
|
-
|
|
218
|
+
const cancelKey = this.keybindings.getKeys("tui.select.cancel").join("/");
|
|
219
|
+
return new Text(this.theme.fg("muted", `${cancelKey} back · Enlarge terminal`), 0, 0)
|
|
220
|
+
.render(width)
|
|
221
|
+
.slice(0, height);
|
|
223
222
|
}
|
|
224
|
-
const innerWidth = width -
|
|
223
|
+
const innerWidth = width - 2;
|
|
225
224
|
const selected = this.flattened.find(({ agent }) => agent.agent_id === this.selectedAgentId);
|
|
226
225
|
const transcriptView = this.view === "transcript" && this.transcript;
|
|
227
226
|
const header = transcriptView
|
|
228
227
|
? [
|
|
229
|
-
this.theme.bold(`Transcript · ${this.selectedAgentId}`),
|
|
230
|
-
selected ? this.renderAgentRow(selected.agent,
|
|
228
|
+
this.theme.fg("accent", this.theme.bold(`Transcript · ${this.selectedAgentId}`)),
|
|
229
|
+
selected ? this.renderAgentRow(selected.agent, "", false) : "",
|
|
231
230
|
]
|
|
232
|
-
: this.renderHeader(
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const help = new Text(this.theme.fg("text", helpText), 0, 0)
|
|
238
|
-
.render(innerWidth)
|
|
239
|
-
.slice(0, Math.min(2, height - 4));
|
|
231
|
+
: this.renderHeader();
|
|
232
|
+
const help = [
|
|
233
|
+
"",
|
|
234
|
+
...wrapTextWithAnsi(this.renderHints(Boolean(transcriptView)), innerWidth),
|
|
235
|
+
].slice(0, Math.min(3, height - 4));
|
|
240
236
|
const visibleHeader = header.slice(0, Math.max(1, height - help.length - 3));
|
|
241
237
|
this.bodyHeight = Math.max(1, height - 2 - visibleHeader.length - help.length);
|
|
242
238
|
let body: string[];
|
|
@@ -262,10 +258,11 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
262
258
|
const maximum = Math.max(0, body.length - this.bodyHeight);
|
|
263
259
|
this.scrollOffset = this.following ? maximum : Math.min(this.scrollOffset, maximum);
|
|
264
260
|
} else {
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
const prefixes = treeRowPrefixes(this.flattened.map(({ depth }) => depth));
|
|
262
|
+
body = this.flattened.map(({ agent }, index) =>
|
|
263
|
+
this.renderAgentRow(agent, prefixes[index] ?? "", agent.agent_id === this.selectedAgentId),
|
|
267
264
|
);
|
|
268
|
-
if (body.length === 0) body.push("No Child Agents yet.");
|
|
265
|
+
if (body.length === 0) body.push(this.theme.fg("muted", "No Child Agents yet."));
|
|
269
266
|
const selectedLine = this.flattened.findIndex(
|
|
270
267
|
({ agent }) => agent.agent_id === this.selectedAgentId,
|
|
271
268
|
);
|
|
@@ -279,19 +276,12 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
279
276
|
}
|
|
280
277
|
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyHeight);
|
|
281
278
|
while (visibleBody.length < this.bodyHeight) visibleBody.push("");
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
289
|
-
return this.theme.bg(
|
|
290
|
-
"customMessageBg",
|
|
291
|
-
`${border("│")} ${content}${" ".repeat(innerWidth - visibleWidth(content))} ${border("│")}`,
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
return [border(`╭${"─".repeat(width - 2)}╮`), ...rows, border(`╰${"─".repeat(width - 2)}╯`)];
|
|
279
|
+
// Each row keeps Pi's one-column margin and fills the pane so the overlay covers what is under it.
|
|
280
|
+
const rows = [...visibleHeader, ...visibleBody, ...help].map((line) =>
|
|
281
|
+
truncateToWidth(` ${line}`, width, ELLIPSIS, true),
|
|
282
|
+
);
|
|
283
|
+
const [border = ""] = this.border.render(width);
|
|
284
|
+
return [border, ...rows, border];
|
|
295
285
|
}
|
|
296
286
|
|
|
297
287
|
/** Rebuild native transcript components when their theme changes. */
|
|
@@ -326,7 +316,34 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
326
316
|
}
|
|
327
317
|
}
|
|
328
318
|
|
|
329
|
-
private
|
|
319
|
+
private keyLabel(binding: Parameters<KeybindingsManager["getKeys"]>[0]): string {
|
|
320
|
+
return this.keybindings.getKeys(binding).join("/");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private renderHints(transcript: boolean): string {
|
|
324
|
+
const page = `${this.keyLabel("tui.select.pageUp")}/${this.keyLabel("tui.select.pageDown")}`;
|
|
325
|
+
const hints = transcript
|
|
326
|
+
? hintLine(this.theme, [
|
|
327
|
+
{ key: this.keyLabel("tui.select.cancel"), description: "tree" },
|
|
328
|
+
{ key: "end", description: "live" },
|
|
329
|
+
{ key: this.keyLabel("app.tools.expand"), description: "tools" },
|
|
330
|
+
{ key: "\u2191\u2193", description: "scroll" },
|
|
331
|
+
{ key: page, description: "page" },
|
|
332
|
+
])
|
|
333
|
+
: hintLine(this.theme, [
|
|
334
|
+
{ key: "\u2191\u2193", description: "select" },
|
|
335
|
+
{ key: this.keyLabel("tui.select.confirm"), description: "transcript" },
|
|
336
|
+
{ key: page, description: "page" },
|
|
337
|
+
{ key: this.keyLabel("tui.select.cancel"), description: "close" },
|
|
338
|
+
]);
|
|
339
|
+
if (!transcript) return hints;
|
|
340
|
+
const following = this.following
|
|
341
|
+
? this.theme.fg("muted", "following")
|
|
342
|
+
: this.theme.fg("warning", "paused");
|
|
343
|
+
return `${hints} ${following}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private renderHeader(): string[] {
|
|
330
347
|
const direct = "agents" in this.status ? this.status.agents : [this.status.agent];
|
|
331
348
|
const running = direct.filter((agent) => agent.state === "running").length;
|
|
332
349
|
const idle = direct.length - running;
|
|
@@ -340,34 +357,41 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
340
357
|
const projectValue = this.access.projectTrusted
|
|
341
358
|
? authoredAccessValue(this.access.projectEnabled)
|
|
342
359
|
: "unavailable (untrusted)";
|
|
360
|
+
const field = (label: string, ...values: string[]) =>
|
|
361
|
+
`${this.theme.fg("muted", `${label}:`)} ${joinInline(this.theme, values)}`;
|
|
343
362
|
return [
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
`Defaults: branch ${this.access.branchOverride} · project ${projectValue} · global ${authoredAccessValue(this.access.globalEnabled)}`,
|
|
352
|
-
width,
|
|
353
|
-
"…",
|
|
363
|
+
this.theme.fg("accent", this.theme.bold("Subagents status")),
|
|
364
|
+
field("Access", accessState, statusAccessSourceLabel(this.access.source)),
|
|
365
|
+
field(
|
|
366
|
+
"Defaults",
|
|
367
|
+
`branch ${this.access.branchOverride}`,
|
|
368
|
+
`project ${projectValue}`,
|
|
369
|
+
`global ${authoredAccessValue(this.access.globalEnabled)}`,
|
|
354
370
|
),
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
this.theme.fg("warning", this.notice),
|
|
371
|
+
field("Coordinator Tools", toolState),
|
|
372
|
+
field("Direct Children", `${running} running`, `${idle} idle`),
|
|
373
|
+
...(this.notice ? [this.theme.fg("warning", this.notice)] : []),
|
|
358
374
|
];
|
|
359
375
|
}
|
|
360
376
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const disclosure = "▸";
|
|
377
|
+
/** One row of the hierarchy: selection marker, tree prefix, Status Mark, id, status, profile, task. */
|
|
378
|
+
private renderAgentRow(agent: AgentSummary, prefix: string, selected: boolean): string {
|
|
364
379
|
const status = subagentStatusLadder(agent);
|
|
365
380
|
const elapsed = formatSubagentDuration(agent.elapsed_ms);
|
|
366
381
|
const task = agent.task?.replace(/\s+/g, " ").trim();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
382
|
+
const identity = `${renderSubagentStatusMark(this.theme, status)} ${
|
|
383
|
+
selected ? this.theme.fg("accent", agent.agent_id) : agent.agent_id
|
|
384
|
+
}`;
|
|
385
|
+
const details = joinInline(this.theme, [
|
|
386
|
+
identity,
|
|
387
|
+
`${renderSubagentStatusLabel(this.theme, status)}${
|
|
388
|
+
elapsed ? ` ${this.theme.fg("muted", elapsed)}` : ""
|
|
389
|
+
}`,
|
|
390
|
+
this.theme.fg("muted", `${agent.model}:${agent.thinking_level}`),
|
|
391
|
+
task ? this.theme.fg("muted", task) : undefined,
|
|
392
|
+
]);
|
|
393
|
+
const marker = selected ? this.theme.fg("accent", "→ ") : " ";
|
|
394
|
+
return `${marker}${prefix ? this.theme.fg("dim", prefix) : ""}${details}`;
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
private scroll(delta: number): void {
|
|
@@ -491,7 +515,7 @@ export class MinimalSubagentsStatusPanelController {
|
|
|
491
515
|
},
|
|
492
516
|
)
|
|
493
517
|
.catch(() => {
|
|
494
|
-
this.context.ui.notify("Subagents
|
|
518
|
+
this.context.ui.notify(noticeText("Subagents", "Status view failed."), "error");
|
|
495
519
|
})
|
|
496
520
|
.finally(() => {
|
|
497
521
|
this.activePanel?.dispose();
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
type ToolDefinition,
|
|
10
10
|
type ToolRenderResultOptions,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import type
|
|
12
|
+
import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
13
|
+
import { appendDurationFooter, callDurationFooter } from "@ian-pascoe/pi-utils/ui";
|
|
13
14
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
14
15
|
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
15
16
|
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
17
|
+
import { roundMinimalSubagentsUsageCosts } from "./minimal-subagents-usage.js";
|
|
16
18
|
import {
|
|
17
19
|
renderCoordinatorToolCall,
|
|
18
20
|
renderCoordinatorToolResult,
|
|
@@ -42,11 +44,8 @@ import type {
|
|
|
42
44
|
WaitResult,
|
|
43
45
|
} from "./minimal-subagents-types.js";
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"subagent_wait",
|
|
48
|
-
"subagent_status",
|
|
49
|
-
]);
|
|
47
|
+
/** A child that cannot spawn can never have children, so only adjacent messaging is useful. */
|
|
48
|
+
const NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES = new Set(["agent_message"]);
|
|
50
49
|
|
|
51
50
|
/** Coordinator operations consumed by the six public coordinator tool definitions. */
|
|
52
51
|
export type CoordinatorToolOperations = Pick<
|
|
@@ -136,6 +135,13 @@ interface CoordinatorToolRowState {
|
|
|
136
135
|
liveTurnCache?: TranscriptRenderCache;
|
|
137
136
|
}
|
|
138
137
|
|
|
138
|
+
function withFooter(body: Component, footer: Component): Component {
|
|
139
|
+
const container = new Container();
|
|
140
|
+
container.addChild(body);
|
|
141
|
+
container.addChild(footer);
|
|
142
|
+
return container;
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
function createCoordinatorToolRendering(
|
|
140
146
|
options: CoordinatorToolDefinitionOptions,
|
|
141
147
|
toolName: CoordinatorToolName,
|
|
@@ -144,8 +150,13 @@ function createCoordinatorToolRendering(
|
|
|
144
150
|
renderCall: (
|
|
145
151
|
args: CoordinatorToolCallInput,
|
|
146
152
|
theme: Theme,
|
|
147
|
-
context:
|
|
148
|
-
) =>
|
|
153
|
+
context: CoordinatorToolRenderContext | undefined,
|
|
154
|
+
) => {
|
|
155
|
+
const call = renderCoordinatorToolCall(toolName, args, theme, context?.expanded ?? false);
|
|
156
|
+
// A wait streams partial results, whose footer already shows its running time.
|
|
157
|
+
if (!context || toolName === "subagent_wait") return call;
|
|
158
|
+
return withFooter(call, callDurationFooter(theme, context));
|
|
159
|
+
},
|
|
149
160
|
renderResult: (
|
|
150
161
|
result: AgentToolResult<CoordinatorToolResultDetails>,
|
|
151
162
|
renderOptions: ToolRenderResultOptions,
|
|
@@ -170,7 +181,7 @@ function createCoordinatorToolRendering(
|
|
|
170
181
|
theme,
|
|
171
182
|
);
|
|
172
183
|
};
|
|
173
|
-
|
|
184
|
+
const rendered = renderCoordinatorToolResult(
|
|
174
185
|
toolName,
|
|
175
186
|
result,
|
|
176
187
|
renderOptions,
|
|
@@ -179,14 +190,36 @@ function createCoordinatorToolRendering(
|
|
|
179
190
|
context.isError,
|
|
180
191
|
toolName === "subagent_wait" ? renderLiveTurn : undefined,
|
|
181
192
|
);
|
|
193
|
+
const container = new Container();
|
|
194
|
+
container.addChild(rendered);
|
|
195
|
+
appendDurationFooter(container, theme, context, { isPartial: renderOptions.isPartial });
|
|
196
|
+
return container;
|
|
182
197
|
},
|
|
183
198
|
};
|
|
184
199
|
}
|
|
185
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Serialize a tool result with every nested `usage` cost rounded for presentation. Exact values
|
|
203
|
+
* stay in `details`, the Registry, and session data, which never pass through here.
|
|
204
|
+
*/
|
|
205
|
+
function stringifyWithRoundedUsageCosts(
|
|
206
|
+
result: CoordinatorToolResultDetails,
|
|
207
|
+
space?: number,
|
|
208
|
+
): string {
|
|
209
|
+
// `JSON.stringify` types replacer values as `any`; a usage is recognized by key and `cost` object.
|
|
210
|
+
return JSON.stringify(
|
|
211
|
+
result,
|
|
212
|
+
(key, value) =>
|
|
213
|
+
key === "usage" && value?.cost ? roundMinimalSubagentsUsageCosts(value) : value,
|
|
214
|
+
space,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
187
219
|
result: TDetails,
|
|
188
220
|
): AgentToolResult<TDetails> {
|
|
189
|
-
|
|
221
|
+
// Presentation only: `details` keeps the exact values the session persists.
|
|
222
|
+
const json = stringifyWithRoundedUsageCosts(result, 2);
|
|
190
223
|
const truncated = truncateHead(json, {
|
|
191
224
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
192
225
|
maxLines: DEFAULT_MAX_LINES,
|
|
@@ -380,7 +413,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
380
413
|
? alreadyDeliveredContent(result)
|
|
381
414
|
: structuredToolResult(result).content,
|
|
382
415
|
details,
|
|
383
|
-
structuredContent: JSON.parse(
|
|
416
|
+
structuredContent: JSON.parse(stringifyWithRoundedUsageCosts(details)),
|
|
384
417
|
};
|
|
385
418
|
});
|
|
386
419
|
} finally {
|
|
@@ -459,9 +492,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
459
492
|
parameters.recursive ?? true,
|
|
460
493
|
);
|
|
461
494
|
if (result.failures.length > 0) {
|
|
462
|
-
options.onAttention?.(
|
|
463
|
-
`Minimal subagents deletion partially failed for ${parameters.agent_id}`,
|
|
464
|
-
);
|
|
495
|
+
options.onAttention?.(`Deletion partially failed for ${parameters.agent_id}`);
|
|
465
496
|
return failedStructuredToolResult("Minimal subagents deletion partially failed", result, {
|
|
466
497
|
troubleshootingHint: true,
|
|
467
498
|
});
|
|
@@ -483,5 +514,5 @@ export function createCoordinatorToolDefinitions(
|
|
|
483
514
|
const allowFanoutTools = options.allowFanoutTools ?? options.callerId === "root";
|
|
484
515
|
return allowFanoutTools
|
|
485
516
|
? coordinatorTools
|
|
486
|
-
: coordinatorTools.filter((tool) =>
|
|
517
|
+
: coordinatorTools.filter((tool) => NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES.has(tool.name));
|
|
487
518
|
}
|
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
UserMessageComponent,
|
|
12
12
|
type TruncationResult,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Container,
|
|
16
|
+
Text,
|
|
17
|
+
wrapTextWithAnsi,
|
|
18
|
+
type Component,
|
|
19
|
+
type TUI,
|
|
20
|
+
} from "@earendil-works/pi-tui";
|
|
21
|
+
import { COLLAPSED_LINES, expandHint } from "@ian-pascoe/pi-utils/ui";
|
|
15
22
|
import {
|
|
16
23
|
ACTIVITY_RAIL_WIDTH,
|
|
17
24
|
drawActivityRail,
|
|
@@ -224,7 +231,7 @@ export function createTranscriptRenderCache(): TranscriptRenderCache {
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
/** Collapsed live views keep at most this many lines of the child's recent work. */
|
|
227
|
-
const COLLAPSED_TRANSCRIPT_RAIL_LINES =
|
|
234
|
+
const COLLAPSED_TRANSCRIPT_RAIL_LINES = COLLAPSED_LINES.fallback;
|
|
228
235
|
|
|
229
236
|
function isSpacerLine(line: string): boolean {
|
|
230
237
|
// Includes a tool box's coloured padding rows, so each rail connector meets visible content.
|
|
@@ -277,16 +284,14 @@ export class TranscriptRail implements Component {
|
|
|
277
284
|
const kept = items
|
|
278
285
|
.slice(items.length - shown)
|
|
279
286
|
.map((lines) => lines.slice(0, COLLAPSED_TRANSCRIPT_RAIL_LINES));
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
]
|
|
287
|
-
: kept,
|
|
288
|
-
this.theme,
|
|
287
|
+
const hidden =
|
|
288
|
+
items.reduce((total, lines) => total + lines.length, 0) -
|
|
289
|
+
kept.reduce((total, lines) => total + lines.length, 0);
|
|
290
|
+
const hint = wrapTextWithAnsi(
|
|
291
|
+
expandHint(this.theme, hidden, "earlier"),
|
|
292
|
+
Math.max(1, width - ACTIVITY_RAIL_WIDTH),
|
|
289
293
|
);
|
|
294
|
+
return drawActivityRail(hidden > 0 ? [hint, ...kept] : kept, this.theme);
|
|
290
295
|
}
|
|
291
296
|
|
|
292
297
|
invalidate(): void {
|