oira666_pi-subagent 0.3.0 → 0.3.2
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 +3 -6
- package/index.ts +1 -22
- package/package.json +1 -2
- package/tree.ts +1 -1
- package/footer.ts +0 -256
package/README.md
CHANGED
|
@@ -128,12 +128,9 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
|
|
|
128
128
|
Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
|
|
129
129
|
(call start, per-subagent run start, and each live log entry).
|
|
130
130
|
|
|
131
|
-
In the interactive TUI the extension
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(prefixed `parent`), with other extensions' statuses (e.g. SSH) on their own
|
|
135
|
-
line below. If the builtin footer component cannot be wrapped (e.g. pi
|
|
136
|
-
internals changed), the extension falls back to the plain single-line status.
|
|
131
|
+
In the interactive TUI the extension publishes the combined `total` usage line
|
|
132
|
+
(parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
|
|
133
|
+
status line. Pi renders all extension statuses on the same footer status line.
|
|
137
134
|
|
|
138
135
|
## Steering Running Subagents
|
|
139
136
|
|
package/index.ts
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
updateNameRecord,
|
|
32
32
|
SUBAGENT_NAMES_CUSTOM_TYPE,
|
|
33
33
|
} from "./names.js";
|
|
34
|
-
import { installSubagentFooter, type SubagentFooterController } from "./footer.js";
|
|
35
34
|
import {
|
|
36
35
|
recordToolCallStart,
|
|
37
36
|
renderCall,
|
|
@@ -820,7 +819,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
820
819
|
let currentNamesFile = "";
|
|
821
820
|
/** Stable ownership/fork key for this session's delegation tree position. */
|
|
822
821
|
let currentOwnerId = "ephemeral";
|
|
823
|
-
let footerController: SubagentFooterController | null = null;
|
|
824
822
|
let pendingResumePlans: ResumableSubagentCall[] = [];
|
|
825
823
|
let modelToRestoreAfterResume: any | undefined;
|
|
826
824
|
const approvedProjectAgentDirsForSession = new Set<string>();
|
|
@@ -1225,21 +1223,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1225
1223
|
targetCtx,
|
|
1226
1224
|
Array.from(activeSubagentUsageSummaries.values()),
|
|
1227
1225
|
);
|
|
1228
|
-
if (footerController) {
|
|
1229
|
-
// Custom footer renders the total directly below the builtin stats
|
|
1230
|
-
// line; keep the single-line status slot empty so other extensions'
|
|
1231
|
-
// statuses (e.g. SSH) get their own clean line below.
|
|
1232
|
-
footerController.updateContext(targetCtx);
|
|
1233
|
-
footerController.setTotalLine(line);
|
|
1234
|
-
if (typeof targetCtx.ui.setStatus === "function") {
|
|
1235
|
-
targetCtx.ui.setStatus("subagent-usage", undefined);
|
|
1236
|
-
}
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
1226
|
if (typeof targetCtx.ui.setStatus !== "function") return;
|
|
1240
1227
|
targetCtx.ui.setStatus(
|
|
1241
1228
|
"subagent-usage",
|
|
1242
|
-
line ? formatFooterStatusText(targetCtx,
|
|
1229
|
+
line ? formatFooterStatusText(targetCtx, line) : undefined,
|
|
1243
1230
|
);
|
|
1244
1231
|
} catch (err) {
|
|
1245
1232
|
console.error("[pi-subagent] Failed to update combined subagent status line:", err);
|
|
@@ -1249,14 +1236,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1249
1236
|
// Auto-discover agents on session start
|
|
1250
1237
|
pi.on("session_start", async (event, ctx) => {
|
|
1251
1238
|
latestSessionCtx = ctx;
|
|
1252
|
-
if (!footerController && ctx.hasUI && !isRpcMode(process.argv)) {
|
|
1253
|
-
try {
|
|
1254
|
-
footerController = await installSubagentFooter(ctx, "subagent-usage");
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
console.error("[pi-subagent] Failed to install custom footer (falling back to status line):", err);
|
|
1257
|
-
footerController = null;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
1239
|
updateCombinedUsageStatus(ctx);
|
|
1261
1240
|
try {
|
|
1262
1241
|
// Always repair sessions left on the synthetic resume model, even in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"runner.ts",
|
|
11
11
|
"resume.ts",
|
|
12
12
|
"names.ts",
|
|
13
|
-
"footer.ts",
|
|
14
13
|
"shared.ts",
|
|
15
14
|
"render.ts",
|
|
16
15
|
"tree.ts",
|
package/tree.ts
CHANGED
|
@@ -92,7 +92,7 @@ export function formatCombinedUsageStatusLine(
|
|
|
92
92
|
const cache = cacheRead || cacheWrite
|
|
93
93
|
? ` R${formatTokens(cacheRead)} W${formatTokens(cacheWrite)}`
|
|
94
94
|
: "";
|
|
95
|
-
return
|
|
95
|
+
return `with subagents (${subagentCount}) Σ $${cost.toFixed(4)} • ↑${formatTokens(input)} ↓${formatTokens(output)}${cache} T${formatTokens(total)} • ${turns} turn${turns === 1 ? "" : "s"}`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export function formatUsage(usage: Partial<UsageStats>, model?: string): string {
|
package/footer.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom footer that places the combined subagent "total" usage line directly
|
|
3
|
-
* below pi's normal stats line, with other extensions' statuses (e.g. SSH) on
|
|
4
|
-
* their own line below.
|
|
5
|
-
*
|
|
6
|
-
* Layout:
|
|
7
|
-
* <pwd (git branch) line> — from the builtin footer
|
|
8
|
-
* parent <stats line> — builtin stats, prefixed when a total exists
|
|
9
|
-
* total Σ ... — combined parent + subagent usage
|
|
10
|
-
* <blank>
|
|
11
|
-
* <other extension statuses> — joined on one separate line
|
|
12
|
-
*
|
|
13
|
-
* Implementation: pi joins all `ctx.ui.setStatus()` texts onto a single
|
|
14
|
-
* footer line, so multi-line layouts require `ctx.ui.setFooter()`. To avoid
|
|
15
|
-
* re-implementing (and drifting from) the builtin stats line, the builtin
|
|
16
|
-
* FooterComponent is loaded from the running pi installation and wrapped. If
|
|
17
|
-
* that ever fails (pi internals moved), installation reports failure and the
|
|
18
|
-
* caller falls back to the plain single-line setStatus behavior.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import * as fs from "node:fs";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
import { pathToFileURL } from "node:url";
|
|
24
|
-
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
25
|
-
import { getPiCliScriptPath } from "./runner.js";
|
|
26
|
-
|
|
27
|
-
export interface SubagentFooterController {
|
|
28
|
-
/** Update the rendered total line (undefined hides it). */
|
|
29
|
-
setTotalLine(text: string | undefined): void;
|
|
30
|
-
/** Track the most recent extension context (model/session may change). */
|
|
31
|
-
updateContext(ctx: any): void;
|
|
32
|
-
/** Restore the builtin footer. */
|
|
33
|
-
dispose(): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sanitizeStatusText(text: string): string {
|
|
37
|
-
return text
|
|
38
|
-
.replace(/[\r\n\t]/g, " ")
|
|
39
|
-
.replace(/ +/g, " ")
|
|
40
|
-
.trim();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function loadBuiltinFooterComponent(): Promise<any | null> {
|
|
44
|
-
try {
|
|
45
|
-
const cliScript = getPiCliScriptPath();
|
|
46
|
-
if (!cliScript) return null;
|
|
47
|
-
const distDir = path.dirname(fs.realpathSync(cliScript));
|
|
48
|
-
const footerPath = path.join(distDir, "modes", "interactive", "components", "footer.js");
|
|
49
|
-
if (!fs.existsSync(footerPath)) return null;
|
|
50
|
-
const mod = await import(pathToFileURL(footerPath).href);
|
|
51
|
-
return typeof mod?.FooterComponent === "function" ? mod.FooterComponent : null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getThinkingLevel(ctx: any): string {
|
|
58
|
-
try {
|
|
59
|
-
const entries = ctx?.sessionManager?.getEntries?.() ?? [];
|
|
60
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
61
|
-
const entry = entries[i];
|
|
62
|
-
if (entry?.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") {
|
|
63
|
-
return entry.thinkingLevel;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
/* fall through */
|
|
68
|
-
}
|
|
69
|
-
return "off";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Install the custom subagent footer. Returns a controller on success or null
|
|
74
|
-
* when the builtin footer component cannot be wrapped (caller should fall back
|
|
75
|
-
* to ctx.ui.setStatus).
|
|
76
|
-
*/
|
|
77
|
-
export async function installSubagentFooter(
|
|
78
|
-
initialCtx: any,
|
|
79
|
-
ownStatusKey: string,
|
|
80
|
-
): Promise<SubagentFooterController | null> {
|
|
81
|
-
const FooterComponent = await loadBuiltinFooterComponent();
|
|
82
|
-
if (!FooterComponent) return null;
|
|
83
|
-
if (typeof initialCtx?.ui?.setFooter !== "function") return null;
|
|
84
|
-
|
|
85
|
-
let ctx = initialCtx;
|
|
86
|
-
let totalLine: string | undefined;
|
|
87
|
-
let requestRender: (() => void) | undefined;
|
|
88
|
-
let installed = false;
|
|
89
|
-
|
|
90
|
-
const shimSession = {
|
|
91
|
-
get state() {
|
|
92
|
-
return {
|
|
93
|
-
model: ctx?.model,
|
|
94
|
-
thinkingLevel: getThinkingLevel(ctx),
|
|
95
|
-
};
|
|
96
|
-
},
|
|
97
|
-
get sessionManager() {
|
|
98
|
-
return ctx?.sessionManager;
|
|
99
|
-
},
|
|
100
|
-
get modelRegistry() {
|
|
101
|
-
return ctx?.modelRegistry ?? { isUsingOAuth: () => false };
|
|
102
|
-
},
|
|
103
|
-
getContextUsage() {
|
|
104
|
-
try {
|
|
105
|
-
return ctx?.getContextUsage?.();
|
|
106
|
-
} catch {
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Smoke-test the wrapped component once before replacing the real footer.
|
|
113
|
-
// If pi's internal constructor or render contract changed, fail installation
|
|
114
|
-
// so the caller falls back to the plain status line instead of risking a
|
|
115
|
-
// broken footer.
|
|
116
|
-
try {
|
|
117
|
-
const probe = new FooterComponent(shimSession, {
|
|
118
|
-
getGitBranch: () => undefined,
|
|
119
|
-
getExtensionStatuses: () => new Map(),
|
|
120
|
-
});
|
|
121
|
-
const probeLines = probe.render(80);
|
|
122
|
-
if (!Array.isArray(probeLines)) return null;
|
|
123
|
-
probe.dispose?.();
|
|
124
|
-
} catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
initialCtx.ui.setFooter((tui: any, theme: any, footerData: any) => {
|
|
130
|
-
let builtin: any = null;
|
|
131
|
-
try {
|
|
132
|
-
builtin = new FooterComponent(shimSession, footerData);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error("[pi-subagent] Failed to construct builtin footer wrapper:", err);
|
|
135
|
-
}
|
|
136
|
-
requestRender = () => {
|
|
137
|
-
try {
|
|
138
|
-
tui.requestRender();
|
|
139
|
-
} catch {
|
|
140
|
-
/* TUI gone */
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const unsubscribe = (() => {
|
|
144
|
-
try {
|
|
145
|
-
return typeof footerData?.onBranchChange === "function"
|
|
146
|
-
? footerData.onBranchChange(() => requestRender?.())
|
|
147
|
-
: undefined;
|
|
148
|
-
} catch {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
})();
|
|
152
|
-
|
|
153
|
-
const fallbackRender = (width: number): string[] => {
|
|
154
|
-
const lines: string[] = [];
|
|
155
|
-
if (totalLine) lines.push(truncateToWidth(theme.fg("dim", totalLine), width, theme.fg("dim", "...")));
|
|
156
|
-
try {
|
|
157
|
-
const extensionStatuses = footerData?.getExtensionStatuses?.();
|
|
158
|
-
if (extensionStatuses && extensionStatuses.size > 0) {
|
|
159
|
-
const statusLine = Array.from(extensionStatuses.entries() as Iterable<[string, string]>)
|
|
160
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
161
|
-
.filter(([key]) => key !== ownStatusKey)
|
|
162
|
-
.map(([, text]) => sanitizeStatusText(text))
|
|
163
|
-
.join(" ");
|
|
164
|
-
if (statusLine) lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
/* statuses unavailable */
|
|
168
|
-
}
|
|
169
|
-
return lines.length > 0 ? lines : [""];
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
dispose() {
|
|
174
|
-
try {
|
|
175
|
-
unsubscribe?.();
|
|
176
|
-
} catch {
|
|
177
|
-
/* ignore */
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
builtin?.dispose?.();
|
|
181
|
-
} catch {
|
|
182
|
-
/* ignore */
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
invalidate() {
|
|
186
|
-
try {
|
|
187
|
-
builtin?.invalidate?.();
|
|
188
|
-
} catch {
|
|
189
|
-
/* ignore */
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
render(width: number): string[] {
|
|
193
|
-
try {
|
|
194
|
-
if (!builtin) return fallbackRender(width);
|
|
195
|
-
const parentPrefix = totalLine ? theme.fg("dim", "parent ") : "";
|
|
196
|
-
const prefixWidth = totalLine ? "parent ".length : 0;
|
|
197
|
-
const builtinLines: string[] = builtin.render(Math.max(20, width - prefixWidth));
|
|
198
|
-
// Builtin layout: [pwdLine, statsLine, joinedExtensionStatuses?].
|
|
199
|
-
// We re-render the status section ourselves on a separate line.
|
|
200
|
-
const lines: string[] = [];
|
|
201
|
-
if (builtinLines.length >= 2) {
|
|
202
|
-
const pwdLine = prefixWidth > 0 ? builtin.render(width)[0] : builtinLines[0];
|
|
203
|
-
lines.push(pwdLine ?? builtinLines[0]);
|
|
204
|
-
lines.push(parentPrefix + builtinLines[1]);
|
|
205
|
-
} else {
|
|
206
|
-
lines.push(...builtinLines.slice(0, 2));
|
|
207
|
-
}
|
|
208
|
-
if (totalLine) lines.push(truncateToWidth(theme.fg("dim", totalLine), width, theme.fg("dim", "...")));
|
|
209
|
-
|
|
210
|
-
const statuses: string[] = [];
|
|
211
|
-
const extensionStatuses = footerData?.getExtensionStatuses?.();
|
|
212
|
-
if (extensionStatuses && extensionStatuses.size > 0) {
|
|
213
|
-
for (const [key, text] of Array.from(extensionStatuses.entries() as Iterable<[string, string]>).sort(
|
|
214
|
-
([a], [b]) => a.localeCompare(b),
|
|
215
|
-
)) {
|
|
216
|
-
if (key === ownStatusKey) continue;
|
|
217
|
-
statuses.push(sanitizeStatusText(text));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (statuses.length > 0) {
|
|
221
|
-
lines.push("");
|
|
222
|
-
lines.push(truncateToWidth(statuses.join(" "), width, theme.fg("dim", "...")));
|
|
223
|
-
}
|
|
224
|
-
return lines;
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error("[pi-subagent] Custom footer render failed:", err);
|
|
227
|
-
return fallbackRender(width);
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
installed = true;
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
if (!installed) return null;
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
setTotalLine(text: string | undefined) {
|
|
240
|
-
const next = text ? `total ${text}` : undefined;
|
|
241
|
-
if (next === totalLine) return;
|
|
242
|
-
totalLine = next;
|
|
243
|
-
requestRender?.();
|
|
244
|
-
},
|
|
245
|
-
updateContext(nextCtx: any) {
|
|
246
|
-
if (nextCtx) ctx = nextCtx;
|
|
247
|
-
},
|
|
248
|
-
dispose() {
|
|
249
|
-
try {
|
|
250
|
-
ctx?.ui?.setFooter?.(undefined);
|
|
251
|
-
} catch {
|
|
252
|
-
/* ignore */
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|