@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
|
@@ -10,13 +10,15 @@ import {
|
|
|
10
10
|
type Component,
|
|
11
11
|
type TUI,
|
|
12
12
|
} from "@earendil-works/pi-tui";
|
|
13
|
+
import { joinInline, widgetLines } from "@ian-pascoe/pi-utils/ui";
|
|
13
14
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
14
15
|
import {
|
|
15
16
|
formatSubagentDuration,
|
|
16
17
|
orderActiveAgentSubtrees,
|
|
17
18
|
renderSubagentStatusLabel,
|
|
18
|
-
|
|
19
|
+
renderSubagentStatusMark,
|
|
19
20
|
subagentStatusLadder,
|
|
21
|
+
treeRowPrefixes,
|
|
20
22
|
} from "./minimal-subagents-rendering.js";
|
|
21
23
|
import type {
|
|
22
24
|
AgentSummary,
|
|
@@ -25,7 +27,7 @@ import type {
|
|
|
25
27
|
TurnStatus,
|
|
26
28
|
} from "./minimal-subagents-types.js";
|
|
27
29
|
|
|
28
|
-
const MINIMAL_SUBAGENTS_UI_KEY = "minimal-subagents";
|
|
30
|
+
const MINIMAL_SUBAGENTS_UI_KEY = "pi-minimal-subagents";
|
|
29
31
|
const MINIMAL_SUBAGENTS_RECENT_LIMIT = 3;
|
|
30
32
|
const MINIMAL_SUBAGENTS_WIDGET_ROW_LIMIT = 8;
|
|
31
33
|
const MINIMAL_SUBAGENTS_REFRESH_MS = 1_000;
|
|
@@ -154,24 +156,7 @@ export function buildMinimalSubagentsWidgetView(
|
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
const
|
|
158
|
-
const MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS = "…";
|
|
159
|
-
|
|
160
|
-
interface MinimalSubagentsWidgetRowParts {
|
|
161
|
-
identity: string;
|
|
162
|
-
status: string;
|
|
163
|
-
profile: string;
|
|
164
|
-
task?: string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function joinMinimalSubagentsWidgetRow(
|
|
168
|
-
parts: MinimalSubagentsWidgetRowParts,
|
|
169
|
-
separator: string,
|
|
170
|
-
): string {
|
|
171
|
-
return [parts.identity, parts.status, parts.profile, parts.task]
|
|
172
|
-
.filter((part): part is string => part !== undefined)
|
|
173
|
-
.join(separator);
|
|
174
|
-
}
|
|
159
|
+
const MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS = "...";
|
|
175
160
|
|
|
176
161
|
function formatMinimalSubagentsRuntimeProfile(
|
|
177
162
|
profile: RuntimeProfile,
|
|
@@ -193,143 +178,72 @@ function formatMinimalSubagentsRuntimeProfile(
|
|
|
193
178
|
return `${prefix}${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
|
|
194
179
|
}
|
|
195
180
|
|
|
196
|
-
|
|
181
|
+
/** One widget row: tree prefix, Status Mark, id, status word and duration, model, then task. */
|
|
182
|
+
function renderMinimalSubagentsWidgetRowText(
|
|
197
183
|
row: MinimalSubagentsWidgetRow,
|
|
198
|
-
|
|
199
|
-
duration: string
|
|
200
|
-
profile: string,
|
|
184
|
+
prefix: string,
|
|
185
|
+
parts: { task?: string; duration?: string; profile: string },
|
|
201
186
|
theme: MinimalSubagentsWidgetTheme,
|
|
202
|
-
):
|
|
203
|
-
const branch = row.depth > 0 ? `${" ".repeat(row.depth)}╰─ ` : " ";
|
|
204
|
-
const styledBranch = theme.fg("borderMuted", branch);
|
|
187
|
+
): string {
|
|
205
188
|
const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
|
|
206
|
-
const identity = `${
|
|
189
|
+
const identity = `${prefix ? theme.fg("dim", prefix) : ""}${renderSubagentStatusMark(theme, row.status)} ${agentId}`;
|
|
207
190
|
const status = `${renderSubagentStatusLabel(theme, row.status)}${
|
|
208
|
-
duration ? ` ${theme.fg("muted", duration)}` : ""
|
|
191
|
+
parts.duration ? ` ${theme.fg("muted", parts.duration)}` : ""
|
|
209
192
|
}`;
|
|
210
|
-
return
|
|
193
|
+
return joinInline(theme, [
|
|
211
194
|
identity,
|
|
212
195
|
status,
|
|
213
|
-
|
|
214
|
-
task
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function minimalSubagentsWidgetRowFits(
|
|
219
|
-
parts: MinimalSubagentsWidgetRowParts,
|
|
220
|
-
separator: string,
|
|
221
|
-
width: number,
|
|
222
|
-
): boolean {
|
|
223
|
-
return visibleWidth(joinMinimalSubagentsWidgetRow(parts, separator)) <= width;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function minimalSubagentsWidgetProfileBudget(
|
|
227
|
-
row: MinimalSubagentsWidgetRow,
|
|
228
|
-
task: string | undefined,
|
|
229
|
-
duration: string | undefined,
|
|
230
|
-
separator: string,
|
|
231
|
-
theme: MinimalSubagentsWidgetTheme,
|
|
232
|
-
width: number,
|
|
233
|
-
): number {
|
|
234
|
-
const fixedParts = renderMinimalSubagentsWidgetRowParts(row, task, duration, "", theme);
|
|
235
|
-
return width - visibleWidth(joinMinimalSubagentsWidgetRow(fixedParts, separator));
|
|
196
|
+
theme.fg("muted", parts.profile),
|
|
197
|
+
parts.task ? theme.fg("muted", parts.task) : undefined,
|
|
198
|
+
]);
|
|
236
199
|
}
|
|
237
200
|
|
|
238
201
|
function renderMinimalSubagentsWidgetRow(
|
|
239
202
|
row: MinimalSubagentsWidgetRow,
|
|
203
|
+
prefix: string,
|
|
240
204
|
width: number,
|
|
241
205
|
theme: MinimalSubagentsWidgetTheme,
|
|
242
206
|
): string {
|
|
243
|
-
const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
|
|
244
207
|
const task = row.task?.replace(/\s+/g, " ").trim() || undefined;
|
|
245
208
|
const duration = row.status === "unavailable" ? undefined : formatSubagentDuration(row.elapsedMs);
|
|
246
209
|
const fullProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile);
|
|
247
210
|
if (!fullProfile) return "";
|
|
211
|
+
const build = (parts: { task?: string; duration?: string; profile: string }) =>
|
|
212
|
+
renderMinimalSubagentsWidgetRowText(row, prefix, parts, theme);
|
|
213
|
+
const fits = (text: string) => visibleWidth(text) <= width;
|
|
214
|
+
// Width left for the profile once everything else is placed; a one-column stand-in keeps its separator.
|
|
215
|
+
const profileBudget = (parts: { duration?: string }) =>
|
|
216
|
+
width - visibleWidth(build({ ...parts, profile: "x" })) + 1;
|
|
248
217
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
task,
|
|
252
|
-
duration,
|
|
253
|
-
fullProfile,
|
|
254
|
-
theme,
|
|
255
|
-
);
|
|
256
|
-
if (minimalSubagentsWidgetRowFits(completeParts, separator, width)) {
|
|
257
|
-
return joinMinimalSubagentsWidgetRow(completeParts, separator);
|
|
258
|
-
}
|
|
218
|
+
const complete = build({ task, duration, profile: fullProfile });
|
|
219
|
+
if (fits(complete)) return complete;
|
|
259
220
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
undefined,
|
|
263
|
-
duration,
|
|
264
|
-
fullProfile,
|
|
265
|
-
theme,
|
|
266
|
-
);
|
|
267
|
-
if (minimalSubagentsWidgetRowFits(partsWithoutTask, separator, width)) {
|
|
268
|
-
return joinMinimalSubagentsWidgetRow(partsWithoutTask, separator);
|
|
269
|
-
}
|
|
221
|
+
const withoutTask = build({ duration, profile: fullProfile });
|
|
222
|
+
if (fits(withoutTask)) return withoutTask;
|
|
270
223
|
|
|
271
|
-
const
|
|
272
|
-
row,
|
|
273
|
-
|
|
274
|
-
duration,
|
|
275
|
-
separator,
|
|
276
|
-
theme,
|
|
277
|
-
width,
|
|
224
|
+
const shortenedProfile = formatMinimalSubagentsRuntimeProfile(
|
|
225
|
+
row.runtimeProfile,
|
|
226
|
+
profileBudget({ duration }),
|
|
278
227
|
);
|
|
279
|
-
const shortenedProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile, profileBudget);
|
|
280
228
|
if (shortenedProfile) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
undefined,
|
|
284
|
-
duration,
|
|
285
|
-
shortenedProfile,
|
|
286
|
-
theme,
|
|
287
|
-
);
|
|
288
|
-
if (minimalSubagentsWidgetRowFits(shortenedParts, separator, width)) {
|
|
289
|
-
return joinMinimalSubagentsWidgetRow(shortenedParts, separator);
|
|
290
|
-
}
|
|
229
|
+
const shortened = build({ duration, profile: shortenedProfile });
|
|
230
|
+
if (fits(shortened)) return shortened;
|
|
291
231
|
}
|
|
292
232
|
|
|
293
233
|
if (duration) {
|
|
294
|
-
const noDurationBudget = minimalSubagentsWidgetProfileBudget(
|
|
295
|
-
row,
|
|
296
|
-
undefined,
|
|
297
|
-
undefined,
|
|
298
|
-
separator,
|
|
299
|
-
theme,
|
|
300
|
-
width,
|
|
301
|
-
);
|
|
302
234
|
const noDurationProfile = formatMinimalSubagentsRuntimeProfile(
|
|
303
235
|
row.runtimeProfile,
|
|
304
|
-
|
|
236
|
+
profileBudget({}),
|
|
305
237
|
);
|
|
306
238
|
if (noDurationProfile) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
undefined,
|
|
310
|
-
undefined,
|
|
311
|
-
noDurationProfile,
|
|
312
|
-
theme,
|
|
313
|
-
);
|
|
314
|
-
if (minimalSubagentsWidgetRowFits(noDurationParts, separator, width)) {
|
|
315
|
-
return joinMinimalSubagentsWidgetRow(noDurationParts, separator);
|
|
316
|
-
}
|
|
239
|
+
const noDuration = build({ profile: noDurationProfile });
|
|
240
|
+
if (fits(noDuration)) return noDuration;
|
|
317
241
|
}
|
|
318
242
|
}
|
|
319
243
|
|
|
320
|
-
const shortestProfile =
|
|
321
|
-
row.runtimeProfile,
|
|
322
|
-
visibleWidth(`${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}:${row.runtimeProfile.thinking_level}`),
|
|
323
|
-
)!;
|
|
324
|
-
const lastResortParts = renderMinimalSubagentsWidgetRowParts(
|
|
325
|
-
row,
|
|
326
|
-
undefined,
|
|
327
|
-
undefined,
|
|
328
|
-
shortestProfile,
|
|
329
|
-
theme,
|
|
330
|
-
);
|
|
244
|
+
const shortestProfile = `${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}:${row.runtimeProfile.thinking_level}`;
|
|
331
245
|
return truncateToWidth(
|
|
332
|
-
|
|
246
|
+
build({ profile: shortestProfile }),
|
|
333
247
|
width,
|
|
334
248
|
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
335
249
|
);
|
|
@@ -342,37 +256,20 @@ export function renderMinimalSubagentsWidgetLines(
|
|
|
342
256
|
theme: MinimalSubagentsWidgetTheme,
|
|
343
257
|
): string[] {
|
|
344
258
|
if (width <= 0) return [];
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
view.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.filter((part): part is string => Boolean(part))
|
|
360
|
-
.join(separator),
|
|
361
|
-
width,
|
|
362
|
-
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
363
|
-
),
|
|
364
|
-
];
|
|
365
|
-
for (const row of view.rows) lines.push(renderMinimalSubagentsWidgetRow(row, width, theme));
|
|
366
|
-
if (view.overflowCount > 0) {
|
|
367
|
-
lines.push(
|
|
368
|
-
truncateToWidth(
|
|
369
|
-
theme.fg("dim", ` … +${view.overflowCount} more`),
|
|
370
|
-
width,
|
|
371
|
-
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
372
|
-
),
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return lines;
|
|
259
|
+
const counts = [
|
|
260
|
+
view.runningCount > 0 ? `${view.runningCount} running` : "idle",
|
|
261
|
+
width >= 44 && view.recentCount > 0 ? `${view.recentCount} recent` : undefined,
|
|
262
|
+
]
|
|
263
|
+
.filter((part): part is string => part !== undefined)
|
|
264
|
+
.join(" · ");
|
|
265
|
+
const prefixes = treeRowPrefixes(view.rows.map((row) => row.depth));
|
|
266
|
+
const rows = view.rows.map((row, index) =>
|
|
267
|
+
renderMinimalSubagentsWidgetRow(row, prefixes[index] ?? "", width, theme),
|
|
268
|
+
);
|
|
269
|
+
if (view.overflowCount > 0) rows.push(theme.fg("muted", `... ${view.overflowCount} more`));
|
|
270
|
+
return widgetLines(theme, { title: "Subagents", counts, rows }).map((line) =>
|
|
271
|
+
truncateToWidth(line, width, MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS),
|
|
272
|
+
);
|
|
376
273
|
}
|
|
377
274
|
|
|
378
275
|
class MinimalSubagentsWidgetComponent implements Component {
|
|
@@ -33,3 +33,33 @@ export function addMinimalSubagentsUsage(
|
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* USD decimal places kept in model-visible and structured usage costs. Six places is one
|
|
39
|
+
* millionth of a dollar: finer than any single turn's cost is worth reading, yet coarse enough to
|
|
40
|
+
* drop the binary-float tails that summing many per-message costs produces.
|
|
41
|
+
*/
|
|
42
|
+
export const MINIMAL_SUBAGENTS_COST_DECIMALS = 6;
|
|
43
|
+
|
|
44
|
+
const COST_SCALE = 10 ** MINIMAL_SUBAGENTS_COST_DECIMALS;
|
|
45
|
+
|
|
46
|
+
function roundCost(value: number): number {
|
|
47
|
+
return Number.isFinite(value) ? Math.round(value * COST_SCALE) / COST_SCALE : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return a clone whose cost fields are rounded for presentation. Each field rounds independently
|
|
52
|
+
* from its exact value, so `total` stays within half a unit per summed field of the rounded sum.
|
|
53
|
+
*/
|
|
54
|
+
export function roundMinimalSubagentsUsageCosts(usage: Usage): Usage {
|
|
55
|
+
return {
|
|
56
|
+
...usage,
|
|
57
|
+
cost: {
|
|
58
|
+
input: roundCost(usage.cost.input),
|
|
59
|
+
output: roundCost(usage.cost.output),
|
|
60
|
+
cacheRead: roundCost(usage.cost.cacheRead),
|
|
61
|
+
cacheWrite: roundCost(usage.cost.cacheWrite),
|
|
62
|
+
total: roundCost(usage.cost.total),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|