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 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 also replaces the footer with a wrapped
132
- version of Pi's builtin footer so that the combined `total` usage line (parent
133
- + all subagents, recursively) renders directly below the normal stats line
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, `total ${line}`) : undefined,
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.0",
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 ↑${formatTokens(input)} ↓${formatTokens(output)}${cache} T${formatTokens(total)} • ${turns} turn${turns === 1 ? "" : "s"} • $${cost.toFixed(4)} • ${subagentCount} subagent${subagentCount === 1 ? "" : "s"}`;
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
- }