@esso0428/pi-subagents 0.17.31 → 0.17.32
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/CHANGELOG.md +11 -0
- package/ROADMAP.md +28 -72
- package/package.json +1 -1
- package/src/abortable.ts +14 -0
- package/src/agent-manager.ts +83 -5
- package/src/agent-recovery.ts +7 -0
- package/src/agent-runner.ts +69 -17
- package/src/agent-types.ts +88 -15
- package/src/child-context.ts +12 -0
- package/src/custom-agents.ts +12 -0
- package/src/index.ts +38 -7
- package/src/model-scope.ts +35 -0
- package/src/nested-tools.ts +361 -0
- package/src/settings.ts +18 -0
- package/src/types.ts +22 -0
- package/src/ui/agent-widget.ts +148 -63
- package/src/ui/conversation-viewer.ts +7 -3
package/src/ui/agent-widget.ts
CHANGED
|
@@ -91,6 +91,15 @@ export interface AgentActivity {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** Metadata attached to Agent tool results for custom rendering. */
|
|
94
|
+
type WidgetRow = {
|
|
95
|
+
record: AgentRecord;
|
|
96
|
+
depth: number;
|
|
97
|
+
ancestorContinues: boolean[];
|
|
98
|
+
isLast: boolean;
|
|
99
|
+
/** True when the row's parent is gone, so it keeps its original indent. */
|
|
100
|
+
orphaned: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
94
103
|
export interface AgentDetails {
|
|
95
104
|
displayName: string;
|
|
96
105
|
description: string;
|
|
@@ -245,12 +254,22 @@ export function describeActivity(activeTools: Map<string, string>, responseText?
|
|
|
245
254
|
* which is not simply `body.length - 1`: the bounded viewport pads the body
|
|
246
255
|
* with blank lines, and two-line rows carry a `│` continuation underneath.
|
|
247
256
|
*/
|
|
248
|
-
function markLastBranch(body: string[], bodyIndex: number): void {
|
|
257
|
+
function markLastBranch(body: string[], bodyIndex: number, depth = 0): void {
|
|
249
258
|
const header = body[bodyIndex];
|
|
250
259
|
if (header === undefined) return;
|
|
251
260
|
body[bodyIndex] = header.replace("├─", "└─");
|
|
252
261
|
const continuation = body[bodyIndex + 1];
|
|
253
|
-
if (continuation
|
|
262
|
+
if (!continuation) return;
|
|
263
|
+
let from = 0;
|
|
264
|
+
for (let level = 0; level <= depth; level++) {
|
|
265
|
+
const index = continuation.indexOf("│ ", from);
|
|
266
|
+
if (index < 0) return;
|
|
267
|
+
if (level === depth) {
|
|
268
|
+
body[bodyIndex + 1] = continuation.slice(0, index) + " " + continuation.slice(index + 3);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
from = index + 3;
|
|
272
|
+
}
|
|
254
273
|
}
|
|
255
274
|
|
|
256
275
|
// ---- Widget manager ----
|
|
@@ -306,7 +325,7 @@ export class AgentWidget {
|
|
|
306
325
|
const all = this.manager.listAgents();
|
|
307
326
|
switch (this.mode()) {
|
|
308
327
|
case "off": return [];
|
|
309
|
-
case "background": return all.filter(a => a.isBackground !== false);
|
|
328
|
+
case "background": return all.filter(a => a.parentAgentId !== undefined || a.isBackground !== false);
|
|
310
329
|
default: return all;
|
|
311
330
|
}
|
|
312
331
|
}
|
|
@@ -378,16 +397,65 @@ export class AgentWidget {
|
|
|
378
397
|
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
379
398
|
* panel exposes currently useful work before terminal history.
|
|
380
399
|
*/
|
|
381
|
-
private
|
|
400
|
+
private rosterRows(): WidgetRow[] {
|
|
382
401
|
const agents = this.widgetAgents();
|
|
383
|
-
const
|
|
384
|
-
record.status
|
|
385
|
-
|
|
386
|
-
&& this.options.canOpenHistory(record),
|
|
402
|
+
const visible = agents.filter(record =>
|
|
403
|
+
record.status === "running" || record.status === "queued"
|
|
404
|
+
|| (record.completedAt !== undefined && this.options.canOpenHistory(record)),
|
|
387
405
|
);
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
406
|
+
const rank = (record: AgentRecord): number =>
|
|
407
|
+
record.status === "running" ? 0 : record.status === "queued" ? 1 : 2;
|
|
408
|
+
const order = new Map(agents.map((record, index) => [record.id, index]));
|
|
409
|
+
const compare = (a: AgentRecord, b: AgentRecord) => rank(a) - rank(b)
|
|
410
|
+
|| (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0);
|
|
411
|
+
const visibleIds = new Set(visible.map(record => record.id));
|
|
412
|
+
const children = new Map<string, AgentRecord[]>();
|
|
413
|
+
const roots: AgentRecord[] = [];
|
|
414
|
+
const orphans: AgentRecord[] = [];
|
|
415
|
+
for (const record of visible) {
|
|
416
|
+
const parent = record.parentAgentId;
|
|
417
|
+
if (parent && visibleIds.has(parent)) {
|
|
418
|
+
const siblings = children.get(parent) ?? [];
|
|
419
|
+
siblings.push(record);
|
|
420
|
+
children.set(parent, siblings);
|
|
421
|
+
} else if (parent) {
|
|
422
|
+
// The parent record is gone. Keep the row at its original depth so the
|
|
423
|
+
// subtree does not silently flatten into a top-level agent, and mark it
|
|
424
|
+
// so the render can say why it sits there alone.
|
|
425
|
+
orphans.push(record);
|
|
426
|
+
} else {
|
|
427
|
+
roots.push(record);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
roots.sort(compare);
|
|
431
|
+
for (const siblings of children.values()) siblings.sort(compare);
|
|
432
|
+
|
|
433
|
+
const rows: WidgetRow[] = [];
|
|
434
|
+
const visit = (
|
|
435
|
+
record: AgentRecord,
|
|
436
|
+
depth: number,
|
|
437
|
+
ancestorContinues: boolean[],
|
|
438
|
+
isLast: boolean,
|
|
439
|
+
orphaned = false,
|
|
440
|
+
) => {
|
|
441
|
+
rows.push({ record, depth, ancestorContinues, isLast, orphaned });
|
|
442
|
+
const nested = children.get(record.id) ?? [];
|
|
443
|
+
nested.forEach((child, index) => {
|
|
444
|
+
visit(child, depth + 1, [...ancestorContinues, !isLast], index === nested.length - 1);
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
roots.forEach((record, index) => {
|
|
448
|
+
visit(record, 0, [], index === roots.length - 1);
|
|
449
|
+
});
|
|
450
|
+
orphans.sort(compare);
|
|
451
|
+
orphans.forEach((record, index) => {
|
|
452
|
+
visit(record, Math.max(1, record.depth ?? 1), new Array(Math.max(0, (record.depth ?? 1) - 1)).fill(true), index === orphans.length - 1, true);
|
|
453
|
+
});
|
|
454
|
+
return rows;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private roster(): AgentRecord[] {
|
|
458
|
+
return this.rosterRows().map(row => row.record);
|
|
391
459
|
}
|
|
392
460
|
|
|
393
461
|
/**
|
|
@@ -533,7 +601,7 @@ export class AgentWidget {
|
|
|
533
601
|
}
|
|
534
602
|
|
|
535
603
|
/** Render a finished agent line. */
|
|
536
|
-
private renderFinishedLine(a:
|
|
604
|
+
private renderFinishedLine(a: AgentRecord, theme: Theme): string {
|
|
537
605
|
const name = getDisplayName(a.type);
|
|
538
606
|
const modeLabel = getPromptModeLabel(a.type);
|
|
539
607
|
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
@@ -561,12 +629,19 @@ export class AgentWidget {
|
|
|
561
629
|
|
|
562
630
|
const parts: string[] = [];
|
|
563
631
|
const activity = this.agentActivity.get(a.id);
|
|
564
|
-
|
|
632
|
+
const live = activity ?? a.liveActivity;
|
|
633
|
+
if (live) parts.push(formatTurns(live.turnCount, live.maxTurns));
|
|
565
634
|
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
635
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
636
|
+
if (tokens > 0) {
|
|
637
|
+
parts.push(formatSessionTokens(tokens, getSessionContextPercent(activity?.session ?? a.liveActivity?.session ?? a.session), theme, a.compactionCount));
|
|
638
|
+
if (a.parentAgentId) parts.push(theme.fg("dim", "already counted in parent"));
|
|
639
|
+
}
|
|
566
640
|
parts.push(duration);
|
|
567
641
|
|
|
568
642
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
569
|
-
|
|
643
|
+
const issue = a.nestedIssue ? theme.fg("warning", ` · nested blocked: ${truncateLine(a.nestedIssue)}`) : "";
|
|
644
|
+
return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", truncateLine(a.description))} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}${issue}`;
|
|
570
645
|
}
|
|
571
646
|
|
|
572
647
|
/**
|
|
@@ -600,59 +675,69 @@ export class AgentWidget {
|
|
|
600
675
|
// Build sections separately for overflow-aware assembly.
|
|
601
676
|
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
602
677
|
|
|
603
|
-
const
|
|
604
|
-
|
|
678
|
+
const rosterRows = this.rosterRows();
|
|
679
|
+
const ancestorPrefix = (row: WidgetRow): string => row.ancestorContinues
|
|
680
|
+
.map(continues => continues ? "│ " : " ")
|
|
681
|
+
.join("");
|
|
682
|
+
const orphanNote = (row: WidgetRow): string =>
|
|
683
|
+
row.orphaned ? theme.fg("dim", ` · parent ${row.record.parentDescription ?? row.record.parentAgentId} is gone`) : "";
|
|
684
|
+
const rowPrefix = (row: WidgetRow): string => ancestorPrefix(row) + (row.isLast ? "└─ " : "├─ ");
|
|
685
|
+
const detailPrefix = (row: WidgetRow): string => ancestorPrefix(row) + "│ ";
|
|
686
|
+
const renderedRows: { record: AgentRecord; lines: string[]; depth: number }[] = [];
|
|
687
|
+
|
|
688
|
+
for (const row of rosterRows) {
|
|
689
|
+
const a = row.record;
|
|
690
|
+
const note = orphanNote(row);
|
|
605
691
|
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
692
|
+
const prefix = theme.fg("dim", rowPrefix(row));
|
|
693
|
+
if (a.status === "running") {
|
|
694
|
+
const name = getDisplayName(a.type);
|
|
695
|
+
const modeLabel = getPromptModeLabel(a.type);
|
|
696
|
+
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
697
|
+
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
698
|
+
const activityState = this.agentActivity.get(a.id);
|
|
699
|
+
const live = activityState ?? a.liveActivity;
|
|
700
|
+
const toolUses = activityState?.toolUses ?? a.toolUses;
|
|
701
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
702
|
+
const contextPercent = getSessionContextPercent(activityState?.session ?? a.liveActivity?.session ?? a.session);
|
|
703
|
+
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount)
|
|
704
|
+
+ (a.parentAgentId ? theme.fg("dim", " (in parent)") : "") : "";
|
|
705
|
+
const parts: string[] = [];
|
|
706
|
+
if (live) parts.push(formatTurns(live.turnCount, live.maxTurns));
|
|
707
|
+
if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
708
|
+
if (tokenText) parts.push(tokenText);
|
|
709
|
+
parts.push(elapsed);
|
|
710
|
+
const statsText = parts.join(" · ");
|
|
711
|
+
const activity = a.nestedIssue
|
|
712
|
+
? `nested blocked: ${a.nestedIssue}`
|
|
713
|
+
: live ? describeActivity(live.activeTools, live.responseText) : "thinking…";
|
|
714
|
+
renderedRows.push({
|
|
715
|
+
record: a,
|
|
716
|
+
depth: row.depth,
|
|
717
|
+
lines: [
|
|
718
|
+
truncate(prefix + `${marker} ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}${note}`),
|
|
719
|
+
truncate(theme.fg("dim", detailPrefix(row)) + theme.fg("dim", `⎿ ${activity}`)),
|
|
720
|
+
],
|
|
721
|
+
});
|
|
722
|
+
} else if (a.status === "queued") {
|
|
723
|
+
renderedRows.push({
|
|
724
|
+
record: a,
|
|
725
|
+
depth: row.depth,
|
|
726
|
+
lines: [truncate(prefix + `${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}${note}`)],
|
|
727
|
+
});
|
|
728
|
+
} else {
|
|
729
|
+
renderedRows.push({
|
|
730
|
+
record: a,
|
|
731
|
+
depth: row.depth,
|
|
732
|
+
lines: [truncate(prefix + `${marker} ` + this.renderFinishedLine(a, theme) + note)],
|
|
733
|
+
});
|
|
734
|
+
}
|
|
642
735
|
}
|
|
643
736
|
|
|
644
|
-
const queuedLines: { record: AgentRecord; lines: string[] }[] = queued.map(a => {
|
|
645
|
-
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
646
|
-
return {
|
|
647
|
-
record: a,
|
|
648
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
649
|
-
};
|
|
650
|
-
});
|
|
651
|
-
|
|
652
737
|
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
653
738
|
// reserved lines when content exceeds the available body budget).
|
|
654
739
|
const maxBody = maxLines - 1; // heading takes 1 line
|
|
655
|
-
const rows =
|
|
740
|
+
const rows = renderedRows;
|
|
656
741
|
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
657
742
|
|
|
658
743
|
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
@@ -668,7 +753,7 @@ export class AgentWidget {
|
|
|
668
753
|
for (const row of rows) lines.push(...row.lines);
|
|
669
754
|
if (rows.length > 0) {
|
|
670
755
|
const lastRow = rows[rows.length - 1];
|
|
671
|
-
markLastBranch(lines, lines.length - lastRow.lines.length);
|
|
756
|
+
markLastBranch(lines, lines.length - lastRow.lines.length, lastRow.depth);
|
|
672
757
|
}
|
|
673
758
|
} else {
|
|
674
759
|
// Reserve one line for a directional overflow summary. The viewport is
|
|
@@ -722,7 +807,7 @@ export class AgentWidget {
|
|
|
722
807
|
visibleBody.push(rows[start].lines[0]);
|
|
723
808
|
end = start + 1;
|
|
724
809
|
}
|
|
725
|
-
markLastBranch(visibleBody, lastRowLine);
|
|
810
|
+
markLastBranch(visibleBody, lastRowLine, rows[Math.max(start, end - 1)]?.depth ?? 0);
|
|
726
811
|
|
|
727
812
|
const hiddenBefore = start;
|
|
728
813
|
const hiddenAfter = Math.max(0, rows.length - end);
|
|
@@ -549,12 +549,16 @@ export class ConversationViewer implements Component {
|
|
|
549
549
|
const headerParts: string[] = [duration];
|
|
550
550
|
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
551
551
|
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
552
|
-
const tokens = getLifetimeTotal(this.
|
|
552
|
+
const tokens = getLifetimeTotal(this.record.lifetimeUsage);
|
|
553
553
|
if (tokens > 0) {
|
|
554
|
-
const percent = getSessionContextPercent(this.activity?.session);
|
|
554
|
+
const percent = getSessionContextPercent(this.activity?.session ?? this.record.session);
|
|
555
555
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
556
|
+
if (this.record.parentAgentId) headerParts.push(th.fg("dim", "already counted in parent"));
|
|
556
557
|
}
|
|
557
|
-
const
|
|
558
|
+
const owner = this.record.parentAgentId
|
|
559
|
+
? ` · child of ${this.record.parentDescription ?? this.record.parentAgentId}`
|
|
560
|
+
: "";
|
|
561
|
+
const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)}${th.fg("dim", owner)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
|
|
558
562
|
const previewLabel = this.hoveredPreview ? th.fg("text", th.bold("[preview]")) : th.fg("dim", "[preview]");
|
|
559
563
|
const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[Esc]")) : th.fg("dim", "[Esc]");
|
|
560
564
|
const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);
|