@tintinweb/pi-subagents 0.3.1 → 0.4.1

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
3
+ *
4
+ * Displays a scrollable, live-updating view of an agent's conversation.
5
+ * Subscribes to session events for real-time streaming updates.
6
+ */
7
+ import { type Component, type TUI } from "@mariozechner/pi-tui";
8
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
9
+ import type { Theme } from "./agent-widget.js";
10
+ import { type AgentActivity } from "./agent-widget.js";
11
+ import type { AgentRecord } from "../types.js";
12
+ export declare class ConversationViewer implements Component {
13
+ private tui;
14
+ private session;
15
+ private record;
16
+ private activity;
17
+ private theme;
18
+ private done;
19
+ private scrollOffset;
20
+ private autoScroll;
21
+ private unsubscribe;
22
+ private lastInnerW;
23
+ private closed;
24
+ constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void);
25
+ handleInput(data: string): void;
26
+ render(width: number): string[];
27
+ invalidate(): void;
28
+ dispose(): void;
29
+ private viewportHeight;
30
+ private buildContentLines;
31
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
3
+ *
4
+ * Displays a scrollable, live-updating view of an agent's conversation.
5
+ * Subscribes to session events for real-time streaming updates.
6
+ */
7
+ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
+ import { formatTokens, formatDuration, getDisplayName, getPromptModeLabel, describeActivity } from "./agent-widget.js";
9
+ import { extractText } from "../context.js";
10
+ /** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
11
+ const CHROME_LINES = 6;
12
+ const MIN_VIEWPORT = 3;
13
+ export class ConversationViewer {
14
+ tui;
15
+ session;
16
+ record;
17
+ activity;
18
+ theme;
19
+ done;
20
+ scrollOffset = 0;
21
+ autoScroll = true;
22
+ unsubscribe;
23
+ lastInnerW = 0;
24
+ closed = false;
25
+ constructor(tui, session, record, activity, theme, done) {
26
+ this.tui = tui;
27
+ this.session = session;
28
+ this.record = record;
29
+ this.activity = activity;
30
+ this.theme = theme;
31
+ this.done = done;
32
+ this.unsubscribe = session.subscribe(() => {
33
+ if (this.closed)
34
+ return;
35
+ this.tui.requestRender();
36
+ });
37
+ }
38
+ handleInput(data) {
39
+ if (matchesKey(data, "escape") || matchesKey(data, "q")) {
40
+ this.closed = true;
41
+ this.done(undefined);
42
+ return;
43
+ }
44
+ const totalLines = this.buildContentLines(this.lastInnerW).length;
45
+ const viewportHeight = this.viewportHeight();
46
+ const maxScroll = Math.max(0, totalLines - viewportHeight);
47
+ if (matchesKey(data, "up") || matchesKey(data, "k")) {
48
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
49
+ this.autoScroll = this.scrollOffset >= maxScroll;
50
+ }
51
+ else if (matchesKey(data, "down") || matchesKey(data, "j")) {
52
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
53
+ this.autoScroll = this.scrollOffset >= maxScroll;
54
+ }
55
+ else if (matchesKey(data, "pageUp")) {
56
+ this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
57
+ this.autoScroll = false;
58
+ }
59
+ else if (matchesKey(data, "pageDown")) {
60
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
61
+ this.autoScroll = this.scrollOffset >= maxScroll;
62
+ }
63
+ else if (matchesKey(data, "home")) {
64
+ this.scrollOffset = 0;
65
+ this.autoScroll = false;
66
+ }
67
+ else if (matchesKey(data, "end")) {
68
+ this.scrollOffset = maxScroll;
69
+ this.autoScroll = true;
70
+ }
71
+ }
72
+ render(width) {
73
+ if (width < 6)
74
+ return []; // too narrow for any meaningful rendering
75
+ const th = this.theme;
76
+ const innerW = width - 4; // border + padding
77
+ this.lastInnerW = innerW;
78
+ const lines = [];
79
+ const pad = (s, len) => {
80
+ const vis = visibleWidth(s);
81
+ return s + " ".repeat(Math.max(0, len - vis));
82
+ };
83
+ const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
84
+ const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
85
+ const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
86
+ const hrMid = row(th.fg("dim", "─".repeat(innerW)));
87
+ // Header
88
+ lines.push(hrTop);
89
+ const name = getDisplayName(this.record.type);
90
+ const modeLabel = getPromptModeLabel(this.record.type);
91
+ const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
92
+ const statusIcon = this.record.status === "running"
93
+ ? th.fg("accent", "●")
94
+ : this.record.status === "completed"
95
+ ? th.fg("success", "✓")
96
+ : this.record.status === "error"
97
+ ? th.fg("error", "✗")
98
+ : th.fg("dim", "○");
99
+ const duration = formatDuration(this.record.startedAt, this.record.completedAt);
100
+ const headerParts = [duration];
101
+ const toolUses = this.activity?.toolUses ?? this.record.toolUses;
102
+ if (toolUses > 0)
103
+ headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
104
+ if (this.activity?.session) {
105
+ try {
106
+ const tokens = this.activity.session.getSessionStats().tokens.total;
107
+ if (tokens > 0)
108
+ headerParts.push(formatTokens(tokens));
109
+ }
110
+ catch { /* */ }
111
+ }
112
+ lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`));
113
+ lines.push(hrMid);
114
+ // Content area — rebuild every render (live data, no cache needed)
115
+ const contentLines = this.buildContentLines(innerW);
116
+ const viewportHeight = this.viewportHeight();
117
+ const maxScroll = Math.max(0, contentLines.length - viewportHeight);
118
+ if (this.autoScroll) {
119
+ this.scrollOffset = maxScroll;
120
+ }
121
+ const visibleStart = Math.min(this.scrollOffset, maxScroll);
122
+ const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
123
+ for (let i = 0; i < viewportHeight; i++) {
124
+ lines.push(row(visible[i] ?? ""));
125
+ }
126
+ // Footer
127
+ lines.push(hrMid);
128
+ const scrollPct = contentLines.length <= viewportHeight
129
+ ? "100%"
130
+ : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
131
+ const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
132
+ const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
133
+ const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
134
+ lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
135
+ lines.push(hrBot);
136
+ return lines;
137
+ }
138
+ invalidate() { }
139
+ dispose() {
140
+ this.closed = true;
141
+ if (this.unsubscribe) {
142
+ this.unsubscribe();
143
+ this.unsubscribe = undefined;
144
+ }
145
+ }
146
+ // ---- Private ----
147
+ viewportHeight() {
148
+ return Math.max(MIN_VIEWPORT, this.tui.terminal.rows - CHROME_LINES);
149
+ }
150
+ buildContentLines(width) {
151
+ if (width <= 0)
152
+ return [];
153
+ const th = this.theme;
154
+ const messages = this.session.messages;
155
+ const lines = [];
156
+ if (messages.length === 0) {
157
+ lines.push(th.fg("dim", "(waiting for first message...)"));
158
+ return lines;
159
+ }
160
+ let needsSeparator = false;
161
+ for (const msg of messages) {
162
+ if (msg.role === "user") {
163
+ const text = typeof msg.content === "string"
164
+ ? msg.content
165
+ : extractText(msg.content);
166
+ if (!text.trim())
167
+ continue;
168
+ if (needsSeparator)
169
+ lines.push(th.fg("dim", "───"));
170
+ lines.push(th.fg("accent", "[User]"));
171
+ for (const line of wrapTextWithAnsi(text.trim(), width)) {
172
+ lines.push(line);
173
+ }
174
+ }
175
+ else if (msg.role === "assistant") {
176
+ const textParts = [];
177
+ const toolCalls = [];
178
+ for (const c of msg.content) {
179
+ if (c.type === "text" && c.text)
180
+ textParts.push(c.text);
181
+ else if (c.type === "toolCall") {
182
+ toolCalls.push(c.toolName ?? "unknown");
183
+ }
184
+ }
185
+ if (needsSeparator)
186
+ lines.push(th.fg("dim", "───"));
187
+ lines.push(th.bold("[Assistant]"));
188
+ if (textParts.length > 0) {
189
+ for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
190
+ lines.push(line);
191
+ }
192
+ }
193
+ for (const name of toolCalls) {
194
+ lines.push(truncateToWidth(th.fg("muted", ` [Tool: ${name}]`), width));
195
+ }
196
+ }
197
+ else if (msg.role === "toolResult") {
198
+ const text = extractText(msg.content);
199
+ const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
200
+ if (!truncated.trim())
201
+ continue;
202
+ if (needsSeparator)
203
+ lines.push(th.fg("dim", "───"));
204
+ lines.push(th.fg("dim", "[Result]"));
205
+ for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
206
+ lines.push(th.fg("dim", line));
207
+ }
208
+ }
209
+ else if (msg.role === "bashExecution") {
210
+ const bash = msg;
211
+ if (needsSeparator)
212
+ lines.push(th.fg("dim", "───"));
213
+ lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
214
+ if (bash.output?.trim()) {
215
+ const out = bash.output.length > 500
216
+ ? bash.output.slice(0, 500) + "... (truncated)"
217
+ : bash.output;
218
+ for (const line of wrapTextWithAnsi(out.trim(), width)) {
219
+ lines.push(th.fg("dim", line));
220
+ }
221
+ }
222
+ }
223
+ else {
224
+ continue;
225
+ }
226
+ needsSeparator = true;
227
+ }
228
+ // Streaming indicator for running agents
229
+ if (this.record.status === "running" && this.activity) {
230
+ const act = describeActivity(this.activity.activeTools, this.activity.responseText);
231
+ lines.push("");
232
+ lines.push(truncateToWidth(th.fg("accent", "▍ ") + th.fg("dim", act), width));
233
+ }
234
+ return lines;
235
+ }
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -33,8 +33,6 @@ interface SpawnOptions {
33
33
  isolated?: boolean;
34
34
  inheritContext?: boolean;
35
35
  thinkingLevel?: ThinkingLevel;
36
- systemPromptOverride?: string;
37
- systemPromptAppend?: string;
38
36
  isBackground?: boolean;
39
37
  /** Called on tool start/end with activity info (for streaming progress to UI). */
40
38
  onToolActivity?: (activity: ToolActivity) => void;
@@ -122,8 +120,6 @@ export class AgentManager {
122
120
  isolated: options.isolated,
123
121
  inheritContext: options.inheritContext,
124
122
  thinkingLevel: options.thinkingLevel,
125
- systemPromptOverride: options.systemPromptOverride,
126
- systemPromptAppend: options.systemPromptAppend,
127
123
  signal: record.abortController!.signal,
128
124
  onToolActivity: (activity) => {
129
125
  if (activity.type === "end") record.toolUses++;
@@ -275,6 +271,28 @@ export class AgentManager {
275
271
  }
276
272
  }
277
273
 
274
+ /** Whether any agents are still running or queued. */
275
+ hasRunning(): boolean {
276
+ return [...this.agents.values()].some(
277
+ r => r.status === "running" || r.status === "queued",
278
+ );
279
+ }
280
+
281
+ /** Wait for all running and queued agents to complete (including queued ones). */
282
+ async waitForAll(): Promise<void> {
283
+ // Loop because drainQueue respects the concurrency limit — as running
284
+ // agents finish they start queued ones, which need awaiting too.
285
+ while (true) {
286
+ this.drainQueue();
287
+ const pending = [...this.agents.values()]
288
+ .filter(r => r.status === "running" || r.status === "queued")
289
+ .map(r => r.promise)
290
+ .filter(Boolean);
291
+ if (pending.length === 0) break;
292
+ await Promise.allSettled(pending);
293
+ }
294
+ }
295
+
278
296
  dispose() {
279
297
  clearInterval(this.cleanupInterval);
280
298
  // Clear queue
@@ -84,10 +84,6 @@ export interface RunOptions {
84
84
  isolated?: boolean;
85
85
  inheritContext?: boolean;
86
86
  thinkingLevel?: ThinkingLevel;
87
- /** Override system prompt entirely (for custom agents with promptMode: "replace"). */
88
- systemPromptOverride?: string;
89
- /** Append to default system prompt (for custom agents with promptMode: "append"). */
90
- systemPromptAppend?: string;
91
87
  /** Called on tool start/end with activity info. */
92
88
  onToolActivity?: (activity: ToolActivity) => void;
93
89
  /** Called on streaming text deltas from the assistant response. */
@@ -142,57 +138,27 @@ export async function runAgent(
142
138
  const agentConfig = getAgentConfig(type);
143
139
  const env = await detectEnv(options.pi, ctx.cwd);
144
140
 
145
- // Build system prompt: custom override > custom append > config-driven
141
+ // Get parent system prompt for append-mode agents
142
+ const parentSystemPrompt = ctx.getSystemPrompt();
143
+
144
+ // Build system prompt from agent config
146
145
  let systemPrompt: string;
147
- if (options.systemPromptOverride) {
148
- systemPrompt = options.systemPromptOverride;
149
- } else if (options.systemPromptAppend) {
150
- // Build a default prompt and append to it
151
- const defaultConfig = agentConfig ?? {
152
- name: type,
153
- description: "",
154
- builtinToolNames: [],
155
- extensions: true,
156
- skills: true,
157
- systemPrompt: "",
158
- promptMode: "replace" as const,
159
- inheritContext: false,
160
- runInBackground: false,
161
- isolated: false,
162
- };
163
- systemPrompt = buildAgentPrompt(defaultConfig, ctx.cwd, env) + "\n\n" + options.systemPromptAppend;
164
- } else if (agentConfig) {
165
- systemPrompt = buildAgentPrompt(agentConfig, ctx.cwd, env);
146
+ if (agentConfig) {
147
+ systemPrompt = buildAgentPrompt(agentConfig, ctx.cwd, env, parentSystemPrompt);
166
148
  } else {
167
- // Unknown type use a minimal general-purpose prompt
149
+ // Unknown type fallback: general-purpose (defensive — unreachable in practice
150
+ // since index.ts resolves unknown types to "general-purpose" before calling runAgent)
168
151
  systemPrompt = buildAgentPrompt({
169
152
  name: type,
170
153
  description: "General-purpose agent",
171
- builtinToolNames: [],
154
+ systemPrompt: "",
155
+ promptMode: "append",
172
156
  extensions: true,
173
157
  skills: true,
174
- systemPrompt: `# Role
175
- You are a general-purpose coding agent for complex, multi-step tasks.
176
- You have full access to read, write, edit files, and execute commands.
177
- Do what has been asked; nothing more, nothing less.
178
-
179
- # Tool Usage
180
- - Use the read tool instead of cat/head/tail
181
- - Use the edit tool instead of sed/awk
182
- - Use the write tool instead of echo/heredoc
183
- - Use the find tool instead of bash find/ls for file search
184
- - Use the grep tool instead of bash grep/rg for content search
185
- - Make independent tool calls in parallel
186
-
187
- # Output
188
- - Use absolute file paths
189
- - Do not use emojis
190
- - Be concise but complete`,
191
- promptMode: "replace",
192
158
  inheritContext: false,
193
159
  runInBackground: false,
194
160
  isolated: false,
195
- }, ctx.cwd, env);
161
+ }, ctx.cwd, env, parentSystemPrompt);
196
162
  }
197
163
 
198
164
  const tools = getToolsForType(type, ctx.cwd);
@@ -125,6 +125,7 @@ export function getConfig(type: string): {
125
125
  builtinToolNames: string[];
126
126
  extensions: true | string[] | false;
127
127
  skills: true | string[] | false;
128
+ promptMode: "replace" | "append";
128
129
  } {
129
130
  const key = resolveKey(type);
130
131
  const config = key ? agents.get(key) : undefined;
@@ -135,6 +136,7 @@ export function getConfig(type: string): {
135
136
  builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
136
137
  extensions: config.extensions,
137
138
  skills: config.skills,
139
+ promptMode: config.promptMode,
138
140
  };
139
141
  }
140
142
 
@@ -147,6 +149,7 @@ export function getConfig(type: string): {
147
149
  builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
148
150
  extensions: gp.extensions,
149
151
  skills: gp.skills,
152
+ promptMode: gp.promptMode,
150
153
  };
151
154
  }
152
155
 
@@ -157,21 +160,7 @@ export function getConfig(type: string): {
157
160
  builtinToolNames: BUILTIN_TOOL_NAMES,
158
161
  extensions: true,
159
162
  skills: true,
163
+ promptMode: "append",
160
164
  };
161
165
  }
162
166
 
163
- // ---- Backwards-compatible aliases ----
164
-
165
- /** @deprecated Use registerAgents instead */
166
- export const registerCustomAgents = registerAgents;
167
-
168
- /** @deprecated Use getAgentConfig instead */
169
- export function getCustomAgentConfig(name: string): AgentConfig | undefined {
170
- const key = resolveKey(name);
171
- return key ? agents.get(key) : undefined;
172
- }
173
-
174
- /** @deprecated Use getUserAgentNames instead */
175
- export function getCustomAgentNames(): string[] {
176
- return getUserAgentNames();
177
- }
@@ -18,42 +18,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
19
  extensions: true,
20
20
  skills: true,
21
- systemPrompt: `# Role
22
- You are a general-purpose coding agent for complex, multi-step tasks.
23
- You have full access to read, write, edit files, and execute commands.
24
- Do what has been asked; nothing more, nothing less.
25
-
26
- # Tool Usage
27
- - Use the read tool instead of cat/head/tail
28
- - Use the edit tool instead of sed/awk
29
- - Use the write tool instead of echo/heredoc
30
- - Use the find tool instead of bash find/ls for file search
31
- - Use the grep tool instead of bash grep/rg for content search
32
- - Make independent tool calls in parallel
33
-
34
- # File Operations
35
- - NEVER create files unless absolutely necessary
36
- - Prefer editing existing files over creating new ones
37
- - NEVER create documentation files unless explicitly requested
38
-
39
- # Git Safety
40
- - NEVER update git config
41
- - NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) without explicit request
42
- - NEVER skip hooks (--no-verify, --no-gpg-sign) unless explicitly asked
43
- - NEVER force push to main/master — warn the user if they request it
44
- - Always create NEW commits, never amend existing ones. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit. Fix the issue, re-stage, and create a NEW commit
45
- - Stage specific files by name, not git add -A or git add .
46
- - NEVER commit changes unless the user explicitly asks
47
- - NEVER push unless the user explicitly asks
48
- - NEVER use git commands with the -i flag (like git rebase -i or git add -i) — they require interactive input
49
- - Do not use --no-edit with git rebase commands
50
- - Do not commit files that likely contain secrets (.env, credentials.json, etc); warn the user if they request it
51
-
52
- # Output
53
- - Use absolute file paths
54
- - Do not use emojis
55
- - Be concise but complete`,
56
- promptMode: "replace",
21
+ systemPrompt: "",
22
+ promptMode: "append",
57
23
  inheritContext: false,
58
24
  runInBackground: false,
59
25
  isolated: false,
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  formatMs,
31
31
  formatDuration,
32
32
  getDisplayName,
33
+ getPromptModeLabel,
33
34
  describeActivity,
34
35
  type AgentDetails,
35
36
  type AgentActivity,
@@ -120,20 +121,6 @@ function buildDetails(
120
121
  };
121
122
  }
122
123
 
123
- /** Resolve system prompt overrides from an agent config. */
124
- function resolveCustomPrompt(config: AgentConfig | undefined): {
125
- systemPromptOverride?: string;
126
- systemPromptAppend?: string;
127
- } {
128
- if (!config?.systemPrompt) return {};
129
- // Default agents use their systemPrompt via buildAgentPrompt in agent-runner,
130
- // not via override/append. Only non-default agents use this path.
131
- if (config.isDefault) return {};
132
- if (config.promptMode === "append") return { systemPromptAppend: config.systemPrompt };
133
- return { systemPromptOverride: config.systemPrompt };
134
- }
135
-
136
-
137
124
  export default function (pi: ExtensionAPI) {
138
125
  /** Reload agents from .pi/agents/*.md and merge with defaults (called on init and each Agent invocation). */
139
126
  const reloadCustomAgents = () => {
@@ -161,9 +148,11 @@ export default function (pi: ExtensionAPI) {
161
148
  agentActivity.delete(record.id);
162
149
  widget.markFinished(record.id);
163
150
 
151
+ const tokens = safeFormatTokens(record.session);
152
+ const toolStats = tokens ? `Tool uses: ${record.toolUses} | ${tokens}` : `Tool uses: ${record.toolUses}`;
164
153
  pi.sendUserMessage(
165
154
  `Background agent completed: ${displayName} (${record.description})\n` +
166
- `Agent ID: ${record.id} | Status: ${status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n\n` +
155
+ `Agent ID: ${record.id} | Status: ${status} | ${toolStats} | Duration: ${duration}\n\n` +
167
156
  resultPreview,
168
157
  { deliverAs: "followUp" },
169
158
  );
@@ -180,7 +169,9 @@ export default function (pi: ExtensionAPI) {
180
169
  ? record.result.slice(0, 300) + "\n...(truncated)"
181
170
  : record.result
182
171
  : "No output.";
183
- return `- ${displayName} (${record.description})\n ID: ${record.id} | Status: ${status} | Tools: ${record.toolUses} | Duration: ${duration}\n ${resultPreview}`;
172
+ const tokens = safeFormatTokens(record.session);
173
+ const toolStats = tokens ? `Tools: ${record.toolUses} | ${tokens}` : `Tools: ${record.toolUses}`;
174
+ return `- ${displayName} (${record.description})\n ID: ${record.id} | Status: ${status} | ${toolStats} | Duration: ${duration}\n ${resultPreview}`;
184
175
  }
185
176
 
186
177
  // ---- Group join manager ----
@@ -239,6 +230,21 @@ export default function (pi: ExtensionAPI) {
239
230
  widget.update();
240
231
  });
241
232
 
233
+ // Expose manager via Symbol.for() global registry for cross-package access.
234
+ // Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
235
+ const MANAGER_KEY = Symbol.for("pi-subagents:manager");
236
+ (globalThis as any)[MANAGER_KEY] = {
237
+ waitForAll: () => manager.waitForAll(),
238
+ hasRunning: () => manager.hasRunning(),
239
+ };
240
+
241
+ // Wait for all subagents on shutdown, then dispose the manager
242
+ pi.on("session_shutdown", async () => {
243
+ delete (globalThis as any)[MANAGER_KEY];
244
+ await manager.waitForAll();
245
+ manager.dispose();
246
+ });
247
+
242
248
  // Live widget: show running agents above editor
243
249
  const widget = new AgentWidget(manager, agentActivity);
244
250
 
@@ -539,8 +545,6 @@ Guidelines:
539
545
  const runInBackground = params.run_in_background ?? customConfig?.runInBackground ?? false;
540
546
  const isolated = params.isolated ?? customConfig?.isolated ?? false;
541
547
 
542
- const { systemPromptOverride, systemPromptAppend } = resolveCustomPrompt(customConfig);
543
-
544
548
  // Build display tags for non-default config
545
549
  const parentModelId = ctx.model?.id;
546
550
  const effectiveModelId = model?.id;
@@ -548,6 +552,8 @@ Guidelines:
548
552
  ? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
549
553
  : undefined;
550
554
  const agentTags: string[] = [];
555
+ const modeLabel = getPromptModeLabel(subagentType);
556
+ if (modeLabel) agentTags.push(modeLabel);
551
557
  if (thinking) agentTags.push(`thinking: ${thinking}`);
552
558
  if (isolated) agentTags.push("isolated");
553
559
  // Shared base fields for all AgentDetails in this call
@@ -589,8 +595,6 @@ Guidelines:
589
595
  isolated,
590
596
  inheritContext,
591
597
  thinkingLevel: thinking,
592
- systemPromptOverride,
593
- systemPromptAppend,
594
598
  isBackground: true,
595
599
  ...bgCallbacks,
596
600
  });
@@ -680,8 +684,6 @@ Guidelines:
680
684
  isolated,
681
685
  inheritContext,
682
686
  thinkingLevel: thinking,
683
- systemPromptOverride,
684
- systemPromptAppend,
685
687
  ...fgCallbacks,
686
688
  });
687
689
 
@@ -707,8 +709,10 @@ Guidelines:
707
709
  }
708
710
 
709
711
  const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
712
+ const statsParts = [`${record.toolUses} tool uses`];
713
+ if (tokenText) statsParts.push(tokenText);
710
714
  return textResult(
711
- `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${record.toolUses} tool uses)${getStatusNote(record.status)}.\n\n` +
715
+ `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
712
716
  (record.result ?? "No output."),
713
717
  details,
714
718
  );
@@ -750,10 +754,12 @@ Guidelines:
750
754
 
751
755
  const displayName = getDisplayName(record.type);
752
756
  const duration = formatDuration(record.startedAt, record.completedAt);
757
+ const tokens = safeFormatTokens(record.session);
758
+ const toolStats = tokens ? `Tool uses: ${record.toolUses} | ${tokens}` : `Tool uses: ${record.toolUses}`;
753
759
 
754
760
  let output =
755
761
  `Agent: ${record.id}\n` +
756
- `Type: ${displayName} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
762
+ `Type: ${displayName} | Status: ${record.status} | ${toolStats} | Duration: ${duration}\n` +
757
763
  `Description: ${record.description}\n\n`;
758
764
 
759
765
  if (record.status === "running") {