@fosterg4/pi-subagent 1.0.3 → 1.0.4
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 +32 -1
- package/package.json +1 -1
- package/ui.ts +176 -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,18 @@ 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 (widgetHandle) {
|
|
651
|
+
widgetHandle.close();
|
|
652
|
+
widgetHandle = undefined;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
643
655
|
|
|
644
656
|
for (let i = 0; i < params.chain.length; i++) {
|
|
657
|
+
closeWidget();
|
|
658
|
+
|
|
645
659
|
const step = params.chain[i];
|
|
646
660
|
let taskWithContext = step.task;
|
|
647
661
|
|
|
@@ -651,13 +665,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
651
665
|
/\{previous\}/g,
|
|
652
666
|
JSON.stringify(previousStructured, null, 2),
|
|
653
667
|
);
|
|
654
|
-
} else {
|
|
668
|
+
} else if (i > 0) {
|
|
655
669
|
taskWithContext = taskWithContext.replace(
|
|
656
670
|
/\{previous\}/g,
|
|
657
671
|
getFinalOutput(results[i - 1]?.messages ?? ""),
|
|
658
672
|
);
|
|
659
673
|
}
|
|
660
674
|
|
|
675
|
+
// Spawn live widget for this step
|
|
676
|
+
if (ctx.hasUI) {
|
|
677
|
+
const widget = new AgentWidget();
|
|
678
|
+
widget.addAgent(step.agent, step.task.replace(/\{[^}]+\}/g, "").trim());
|
|
679
|
+
widgetHandle = ctx.ui.custom(widget, { overlay: true });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const stepStats = (stats: { turns: number; tokens: number }) => {
|
|
683
|
+
if (widgetHandle) {
|
|
684
|
+
widgetHandle.requestRender();
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
661
688
|
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
662
689
|
? (partial) => {
|
|
663
690
|
const currentResult = partial.details?.results[0];
|
|
@@ -681,6 +708,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
708
|
signal,
|
|
682
709
|
chainUpdate,
|
|
683
710
|
makeDetails("chain"),
|
|
711
|
+
stepStats,
|
|
684
712
|
);
|
|
685
713
|
results.push(result);
|
|
686
714
|
|
|
@@ -697,6 +725,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
697
725
|
}
|
|
698
726
|
|
|
699
727
|
if (isFailedResult(result)) {
|
|
728
|
+
closeWidget();
|
|
700
729
|
const errorMsg = getResultOutput(result);
|
|
701
730
|
return {
|
|
702
731
|
content: [
|
|
@@ -711,6 +740,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
740
|
}
|
|
712
741
|
}
|
|
713
742
|
|
|
743
|
+
closeWidget();
|
|
744
|
+
|
|
714
745
|
const lastResult = results[results.length - 1];
|
|
715
746
|
return {
|
|
716
747
|
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.4",
|
|
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,176 @@
|
|
|
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 { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { fmt } from "./utils.ts";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export type AgentStatus = "running" | "done" | "error";
|
|
23
|
+
|
|
24
|
+
export interface WidgetEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
task: string;
|
|
27
|
+
status: AgentStatus;
|
|
28
|
+
turns: number;
|
|
29
|
+
tokens: number;
|
|
30
|
+
contextUsagePct?: number;
|
|
31
|
+
elapsedMs: number;
|
|
32
|
+
model?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Spinner
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function formatTime(ms: number): string {
|
|
46
|
+
const s = ms / 1000;
|
|
47
|
+
if (s < 10) return `${s.toFixed(1)}s`;
|
|
48
|
+
if (s < 60) return `${Math.round(s)}s`;
|
|
49
|
+
const m = Math.floor(s / 60);
|
|
50
|
+
const sec = Math.round(s % 60);
|
|
51
|
+
return `${m}m${sec}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function statusIcon(status: AgentStatus, frame: number): string {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case "done":
|
|
57
|
+
return "✓";
|
|
58
|
+
case "error":
|
|
59
|
+
return "✗";
|
|
60
|
+
case "running":
|
|
61
|
+
default:
|
|
62
|
+
return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Widget
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export class AgentWidget {
|
|
71
|
+
private entries: WidgetEntry[] = [];
|
|
72
|
+
private frame = 0;
|
|
73
|
+
private startTimes: Map<string, number> = new Map();
|
|
74
|
+
private cachedWidth?: number;
|
|
75
|
+
private cachedLines?: string[];
|
|
76
|
+
|
|
77
|
+
addAgent(name: string, task: string): void {
|
|
78
|
+
const exists = this.entries.find((e) => e.name === name);
|
|
79
|
+
if (exists) return;
|
|
80
|
+
this.entries.push({
|
|
81
|
+
name,
|
|
82
|
+
task,
|
|
83
|
+
status: "running",
|
|
84
|
+
turns: 0,
|
|
85
|
+
tokens: 0,
|
|
86
|
+
elapsedMs: 0,
|
|
87
|
+
});
|
|
88
|
+
this.startTimes.set(name, Date.now());
|
|
89
|
+
this.invalidate();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateAgent(name: string, update: Partial<WidgetEntry>): void {
|
|
93
|
+
const entry = this.entries.find((e) => e.name === name);
|
|
94
|
+
if (!entry) return;
|
|
95
|
+
Object.assign(entry, update);
|
|
96
|
+
entry.elapsedMs = Date.now() - (this.startTimes.get(name) ?? Date.now());
|
|
97
|
+
this.invalidate();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeAgent(name: string): void {
|
|
101
|
+
this.entries = this.entries.filter((e) => e.name !== name);
|
|
102
|
+
this.startTimes.delete(name);
|
|
103
|
+
this.invalidate();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getAgent(name: string): WidgetEntry | undefined {
|
|
107
|
+
return this.entries.find((e) => e.name === name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
invalidate(): void {
|
|
111
|
+
this.cachedWidth = undefined;
|
|
112
|
+
this.cachedLines = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(width: number): string[] {
|
|
116
|
+
if (this.cachedLines && this.cachedWidth === width) {
|
|
117
|
+
return this.cachedLines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
|
|
122
|
+
if (this.entries.length === 0) {
|
|
123
|
+
this.cachedWidth = width;
|
|
124
|
+
this.cachedLines = [];
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Header
|
|
129
|
+
lines.push("● Agents");
|
|
130
|
+
|
|
131
|
+
for (const entry of this.entries) {
|
|
132
|
+
const icon = statusIcon(entry.status, this.frame);
|
|
133
|
+
const parts: string[] = [];
|
|
134
|
+
|
|
135
|
+
// Turns
|
|
136
|
+
if (entry.turns > 0) parts.push(`↻${entry.turns}`);
|
|
137
|
+
|
|
138
|
+
// Tokens
|
|
139
|
+
if (entry.tokens > 0) {
|
|
140
|
+
const tokStr = fmt(entry.tokens);
|
|
141
|
+
parts.push(`${tokStr} token`);
|
|
142
|
+
if (entry.contextUsagePct !== undefined && entry.contextUsagePct > 0) {
|
|
143
|
+
parts.push(`(${entry.contextUsagePct}%)`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Time
|
|
148
|
+
if (entry.elapsedMs > 0) parts.push(formatTime(entry.elapsedMs));
|
|
149
|
+
|
|
150
|
+
// Model
|
|
151
|
+
if (entry.model) parts.push(entry.model);
|
|
152
|
+
|
|
153
|
+
const stats = parts.length > 0 ? ` · ${parts.join(" · ")}` : "";
|
|
154
|
+
|
|
155
|
+
let line = ` ${icon} ${entry.name}${stats}`;
|
|
156
|
+
if (line.length > width) {
|
|
157
|
+
line = line.slice(0, width - 1) + "…";
|
|
158
|
+
}
|
|
159
|
+
lines.push(line);
|
|
160
|
+
|
|
161
|
+
// Activity sub-line
|
|
162
|
+
if (entry.task && entry.status === "running") {
|
|
163
|
+
const preview =
|
|
164
|
+
entry.task.length > 60
|
|
165
|
+
? `${entry.task.slice(0, 57)}…`
|
|
166
|
+
: entry.task;
|
|
167
|
+
lines.push(` ⎿ ${preview}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.frame++;
|
|
172
|
+
this.cachedWidth = width;
|
|
173
|
+
this.cachedLines = lines;
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
}
|