@fosterg4/pi-subagent 1.0.3 → 1.0.6
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/index.ts +40 -1
- package/package.json +1 -1
- package/ui.ts +174 -0
package/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "./agents.ts";
|
|
37
37
|
import { type ValidationResult, validateSchema } from "./validate.ts";
|
|
38
38
|
import { fmt, usageLine, sumUsage } from "./utils.ts";
|
|
39
|
+
import { AgentWidget, type WidgetEntry } from "./ui.ts";
|
|
39
40
|
|
|
40
41
|
const MAX_PARALLEL_TASKS = 8;
|
|
41
42
|
const MAX_CONCURRENCY = 4;
|
|
@@ -236,6 +237,7 @@ async function runSingleAgent(
|
|
|
236
237
|
signal: AbortSignal | undefined,
|
|
237
238
|
onUpdate: OnUpdateCallback | undefined,
|
|
238
239
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
240
|
+
onStats?: (stats: { turns: number; tokens: number }) => void,
|
|
239
241
|
): Promise<SingleResult> {
|
|
240
242
|
const agent = agents.find((a) => a.name === agentName);
|
|
241
243
|
|
|
@@ -373,6 +375,8 @@ async function runSingleAgent(
|
|
|
373
375
|
currentResult.usage.cost += usage.cost?.total || 0;
|
|
374
376
|
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
375
377
|
}
|
|
378
|
+
const total = currentResult.usage.input + currentResult.usage.output + currentResult.usage.cacheRead;
|
|
379
|
+
onStats?.({ turns: currentResult.usage.turns, tokens: total });
|
|
376
380
|
if (!currentResult.model && msg.model)
|
|
377
381
|
currentResult.model = msg.model;
|
|
378
382
|
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
@@ -640,8 +644,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
640
644
|
if (params.chain && params.chain.length > 0) {
|
|
641
645
|
const results: SingleResult[] = [];
|
|
642
646
|
let previousStructured: Record<string, unknown> | undefined;
|
|
647
|
+
let widgetHandle: ReturnType<typeof pi.ui.custom> | undefined;
|
|
648
|
+
|
|
649
|
+
const closeWidget = () => {
|
|
650
|
+
if (widgetTimer) { clearInterval(widgetTimer); widgetTimer = undefined; }
|
|
651
|
+
if (widgetHandle) {
|
|
652
|
+
widgetHandle.close();
|
|
653
|
+
widgetHandle = undefined;
|
|
654
|
+
}
|
|
655
|
+
widgetRef = undefined;
|
|
656
|
+
};
|
|
643
657
|
|
|
644
658
|
for (let i = 0; i < params.chain.length; i++) {
|
|
659
|
+
closeWidget();
|
|
660
|
+
|
|
645
661
|
const step = params.chain[i];
|
|
646
662
|
let taskWithContext = step.task;
|
|
647
663
|
|
|
@@ -651,13 +667,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
651
667
|
/\{previous\}/g,
|
|
652
668
|
JSON.stringify(previousStructured, null, 2),
|
|
653
669
|
);
|
|
654
|
-
} else {
|
|
670
|
+
} else if (i > 0) {
|
|
655
671
|
taskWithContext = taskWithContext.replace(
|
|
656
672
|
/\{previous\}/g,
|
|
657
673
|
getFinalOutput(results[i - 1]?.messages ?? ""),
|
|
658
674
|
);
|
|
659
675
|
}
|
|
660
676
|
|
|
677
|
+
// Spawn live widget for this step
|
|
678
|
+
let widgetRef: AgentWidget | undefined;
|
|
679
|
+
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
680
|
+
if (ctx.hasUI) {
|
|
681
|
+
widgetRef = new AgentWidget();
|
|
682
|
+
widgetRef.addAgent(step.agent, step.task.replace(/\{[^}]+\}/g, "").trim());
|
|
683
|
+
widgetHandle = ctx.ui.custom(
|
|
684
|
+
(_tui, _theme, _kb, done) => {
|
|
685
|
+
widgetTimer = setInterval(() => widgetRef?.invalidate(), 200);
|
|
686
|
+
return widgetRef!;
|
|
687
|
+
},
|
|
688
|
+
{ overlay: true },
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const stepStats = (stats: { turns: number; tokens: number }) => {
|
|
693
|
+
widgetRef?.invalidate();
|
|
694
|
+
};
|
|
695
|
+
|
|
661
696
|
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
662
697
|
? (partial) => {
|
|
663
698
|
const currentResult = partial.details?.results[0];
|
|
@@ -681,6 +716,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
716
|
signal,
|
|
682
717
|
chainUpdate,
|
|
683
718
|
makeDetails("chain"),
|
|
719
|
+
stepStats,
|
|
684
720
|
);
|
|
685
721
|
results.push(result);
|
|
686
722
|
|
|
@@ -697,6 +733,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
697
733
|
}
|
|
698
734
|
|
|
699
735
|
if (isFailedResult(result)) {
|
|
736
|
+
closeWidget();
|
|
700
737
|
const errorMsg = getResultOutput(result);
|
|
701
738
|
return {
|
|
702
739
|
content: [
|
|
@@ -711,6 +748,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
748
|
}
|
|
712
749
|
}
|
|
713
750
|
|
|
751
|
+
closeWidget();
|
|
752
|
+
|
|
714
753
|
const lastResult = results[results.length - 1];
|
|
715
754
|
return {
|
|
716
755
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fosterg4/pi-subagent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Delegate tasks to specialized subagents with isolated context windows, structured JSON handoff, contract schemas, and live TUI streaming",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/ui.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-style live progress widget for subagent execution.
|
|
3
|
+
*
|
|
4
|
+
* Renders a compact overlay showing spinner + agent name + stats.
|
|
5
|
+
* Designed to be used with pi's ctx.ui.custom(…) overlay system.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const widget = new AgentWidget();
|
|
9
|
+
* widget.addAgent("scout", "Find auth files");
|
|
10
|
+
* const handle = ctx.ui.custom(widget, { overlay: true });
|
|
11
|
+
* widget.updateAgent("scout", { turns: 3, tokens: 12400, elapsedMs: 4100 });
|
|
12
|
+
* handle.close();
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fmt } from "./utils.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type AgentStatus = "running" | "done" | "error";
|
|
22
|
+
|
|
23
|
+
export interface WidgetEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
task: string;
|
|
26
|
+
status: AgentStatus;
|
|
27
|
+
turns: number;
|
|
28
|
+
tokens: number;
|
|
29
|
+
contextUsagePct?: number;
|
|
30
|
+
elapsedMs: number;
|
|
31
|
+
model?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Spinner
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function formatTime(ms: number): string {
|
|
45
|
+
const s = ms / 1000;
|
|
46
|
+
if (s < 10) return `${s.toFixed(1)}s`;
|
|
47
|
+
if (s < 60) return `${Math.round(s)}s`;
|
|
48
|
+
const m = Math.floor(s / 60);
|
|
49
|
+
const sec = Math.round(s % 60);
|
|
50
|
+
return `${m}m${sec}s`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function statusIcon(status: AgentStatus, frame: number): string {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case "done":
|
|
56
|
+
return "✓";
|
|
57
|
+
case "error":
|
|
58
|
+
return "✗";
|
|
59
|
+
case "running":
|
|
60
|
+
default:
|
|
61
|
+
return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Widget
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export class AgentWidget {
|
|
70
|
+
private entries: WidgetEntry[] = [];
|
|
71
|
+
private frame = 0;
|
|
72
|
+
private startTimes: Map<string, number> = new Map();
|
|
73
|
+
|
|
74
|
+
addAgent(name: string, task: string): void {
|
|
75
|
+
const exists = this.entries.find((e) => e.name === name);
|
|
76
|
+
if (exists) return;
|
|
77
|
+
this.entries.push({
|
|
78
|
+
name,
|
|
79
|
+
task,
|
|
80
|
+
status: "running",
|
|
81
|
+
turns: 0,
|
|
82
|
+
tokens: 0,
|
|
83
|
+
elapsedMs: 0,
|
|
84
|
+
});
|
|
85
|
+
this.startTimes.set(name, Date.now());
|
|
86
|
+
this.invalidate();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
updateAgent(name: string, update: Partial<WidgetEntry>): void {
|
|
90
|
+
const entry = this.entries.find((e) => e.name === name);
|
|
91
|
+
if (!entry) return;
|
|
92
|
+
Object.assign(entry, update);
|
|
93
|
+
this.invalidate();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeAgent(name: string): void {
|
|
97
|
+
this.entries = this.entries.filter((e) => e.name !== name);
|
|
98
|
+
this.startTimes.delete(name);
|
|
99
|
+
this.invalidate();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getAgent(name: string): WidgetEntry | undefined {
|
|
103
|
+
return this.entries.find((e) => e.name === name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
markDone(name: string): void {
|
|
107
|
+
const entry = this.entries.find((e) => e.name === name);
|
|
108
|
+
if (entry) { entry.status = "done"; }
|
|
109
|
+
this.invalidate();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
invalidate(): void {
|
|
113
|
+
// No cache to invalidate — render() is always fresh
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private entryLine(entry: WidgetEntry, width: number): string[] {
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const elapsedMs = entry.elapsedMs || (now - (this.startTimes.get(entry.name) ?? now));
|
|
120
|
+
|
|
121
|
+
const icon = statusIcon(entry.status, this.frame);
|
|
122
|
+
const parts: string[] = [];
|
|
123
|
+
|
|
124
|
+
// Always show icon + name
|
|
125
|
+
let line = `\u00A0${icon} ${entry.name}`;
|
|
126
|
+
|
|
127
|
+
// Turns
|
|
128
|
+
parts.push(entry.turns > 0 ? `↻${entry.turns}` : "↻0");
|
|
129
|
+
|
|
130
|
+
// Tokens
|
|
131
|
+
if (entry.tokens > 0) {
|
|
132
|
+
const tokStr = fmt(entry.tokens);
|
|
133
|
+
parts.push(`${tokStr} token`);
|
|
134
|
+
if (entry.contextUsagePct !== undefined && entry.contextUsagePct > 0) {
|
|
135
|
+
parts.push(`(${entry.contextUsagePct}%)`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Time
|
|
140
|
+
if (elapsedMs > 99) parts.push(formatTime(elapsedMs));
|
|
141
|
+
else parts.push("0.0s");
|
|
142
|
+
|
|
143
|
+
// Model
|
|
144
|
+
if (entry.model) parts.push(entry.model);
|
|
145
|
+
|
|
146
|
+
line += ` · ${parts.join(" · ")}`;
|
|
147
|
+
if (line.length > width) line = line.slice(0, width - 3) + "…";
|
|
148
|
+
lines.push(line);
|
|
149
|
+
|
|
150
|
+
// Activity sub-line (running agents only)
|
|
151
|
+
if (entry.task && entry.status === "running") {
|
|
152
|
+
const preview = entry.task.length > 60 ? `${entry.task.slice(0, 57)}…` : entry.task;
|
|
153
|
+
lines.push(` ⎿ ${preview}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render(width: number): string[] {
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
|
|
162
|
+
if (this.entries.length === 0) return [];
|
|
163
|
+
|
|
164
|
+
// Header
|
|
165
|
+
lines.push("\u25CF Agents");
|
|
166
|
+
|
|
167
|
+
for (const entry of this.entries) {
|
|
168
|
+
lines.push(...this.entryLine(entry, width));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.frame++;
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
}
|