@johnnywu/pi-subagents 1.0.0

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,270 @@
1
+ import { homedir } from 'node:os';
2
+ import type { AgentProgress } from './subagent-executor.ts';
3
+
4
+ export interface SubagentCallArgs {
5
+ agent?: string;
6
+ task?: string;
7
+ }
8
+
9
+ export interface RenderTextOptions {
10
+ expanded: boolean;
11
+ suppressOutput?: boolean;
12
+ suppressUsage?: boolean;
13
+ expandHint?: string;
14
+ }
15
+
16
+ export type SubagentResultLineKind = 'status' | 'tool' | 'hint' | 'usage' | 'output' | 'blank';
17
+
18
+ export interface SubagentResultLine {
19
+ text: string;
20
+ kind: SubagentResultLineKind;
21
+ singleLine: boolean;
22
+ tool?: {
23
+ name: string;
24
+ args: Record<string, unknown>;
25
+ status: 'running' | 'done' | 'error';
26
+ };
27
+ }
28
+
29
+ const TOOL_LOG_LIMIT = 20;
30
+
31
+ export type ContextUsageSeverity = 'dim' | 'warning' | 'error';
32
+
33
+ export function contextUsageSeverity(usage: {
34
+ contextTokens?: number;
35
+ contextWindow?: number;
36
+ }): ContextUsageSeverity {
37
+ if (!usage.contextWindow || usage.contextWindow <= 0) return 'dim';
38
+ const percent = (usage.contextTokens ?? 0) / usage.contextWindow;
39
+ if (percent >= 0.9) return 'error';
40
+ if (percent >= 0.7) return 'warning';
41
+ return 'dim';
42
+ }
43
+
44
+ function preview(text: string, length: number): string {
45
+ return text.length > length ? `${text.slice(0, length)}...` : text;
46
+ }
47
+
48
+ function elapsedSeconds(ms: number): number {
49
+ return Math.round(ms / 1000);
50
+ }
51
+
52
+ function summaryText(markdown: string): string {
53
+ return markdown.split('\n').slice(0, 20).join('\n');
54
+ }
55
+
56
+ function formatTokens(value: number): string {
57
+ if (Math.abs(value) >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}m`;
58
+ if (Math.abs(value) >= 1_000) return `${Number((value / 1_000).toFixed(1))}k`;
59
+ return String(value);
60
+ }
61
+
62
+ export function formatUsage(progress: AgentProgress): string {
63
+ const usage = progress.usage;
64
+ const parts: string[] = [];
65
+
66
+ if (usage.contextWindow && usage.contextWindow > 0) {
67
+ const percent = ((usage.contextTokens / usage.contextWindow) * 100).toFixed(1);
68
+ parts.push(`${percent}%/${formatTokens(usage.contextWindow)}`);
69
+ }
70
+
71
+ parts.push(`↑${formatTokens(usage.input)}`);
72
+ parts.push(`↓${formatTokens(usage.output)}`);
73
+ parts.push(`R${formatTokens(usage.cacheRead)}`);
74
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
75
+ parts.push(`$${usage.cost.toFixed(3)}`);
76
+
77
+ return parts.join(' ');
78
+ }
79
+
80
+ function indent(text: string, spaces: number): string {
81
+ const prefix = ' '.repeat(spaces);
82
+ return text
83
+ .split('\n')
84
+ .map((line) => `${prefix}${line}`)
85
+ .join('\n');
86
+ }
87
+
88
+ function shortenPath(value: unknown): string | undefined {
89
+ if (typeof value !== 'string') return undefined;
90
+ const home = homedir();
91
+ return value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
92
+ }
93
+
94
+ function quote(value: string | undefined): string {
95
+ return value ? JSON.stringify(value) : '';
96
+ }
97
+
98
+ function stringArg(args: Record<string, unknown>, key: string): string | undefined {
99
+ const value = args[key];
100
+ return typeof value === 'string' ? value : undefined;
101
+ }
102
+
103
+ function numberArg(args: Record<string, unknown>, key: string): number | undefined {
104
+ const value = args[key];
105
+ return typeof value === 'number' ? value : undefined;
106
+ }
107
+
108
+ function pathArg(args: Record<string, unknown>, fallback?: string): string {
109
+ return shortenPath(args.path ?? args.file_path) ?? fallback ?? '...';
110
+ }
111
+
112
+ function lineRange(args: Record<string, unknown>): string {
113
+ if (args.offset === undefined && args.limit === undefined) return '';
114
+ const startLine = numberArg(args, 'offset') ?? 1;
115
+ const limit = numberArg(args, 'limit');
116
+ const endLine = limit !== undefined ? startLine + limit - 1 : undefined;
117
+ return `:${startLine}${endLine !== undefined ? `-${endLine}` : ''}`;
118
+ }
119
+
120
+ function limitSuffix(args: Record<string, unknown>): string {
121
+ const limit = numberArg(args, 'limit');
122
+ return limit !== undefined ? ` (limit ${limit})` : '';
123
+ }
124
+
125
+ function formatToolTitle(name: string, args: Record<string, unknown>): string {
126
+ switch (name) {
127
+ case 'read':
128
+ return `read ${pathArg(args)}${lineRange(args)}`;
129
+ case 'bash': {
130
+ const command = stringArg(args, 'command') ?? '';
131
+ const timeout = numberArg(args, 'timeout');
132
+ return `$ ${command}${timeout !== undefined ? ` (timeout ${timeout}s)` : ''}`;
133
+ }
134
+ case 'edit':
135
+ return `edit ${pathArg(args)}`;
136
+ case 'write':
137
+ return `write ${pathArg(args)}`;
138
+ case 'find': {
139
+ const pattern = stringArg(args, 'pattern') ?? '';
140
+ return `find ${pattern} in ${pathArg(args, '.')}${limitSuffix(args)}`;
141
+ }
142
+ case 'grep': {
143
+ const pattern = stringArg(args, 'pattern') ?? '';
144
+ const glob = stringArg(args, 'glob');
145
+ const globSuffix = glob ? ` (${glob})` : '';
146
+ const limit = numberArg(args, 'limit');
147
+ const limitText = limit !== undefined ? ` limit ${limit}` : '';
148
+ return `grep /${pattern}/ in ${pathArg(args, '.')}${globSuffix}${limitText}`;
149
+ }
150
+ case 'ls':
151
+ return `ls ${pathArg(args, '.')}${limitSuffix(args)}`;
152
+ case 'webfetch': {
153
+ const url = stringArg(args, 'url') ?? '';
154
+ const mode = stringArg(args, 'mode');
155
+ return `webfetch ${url}${mode ? ` (${mode})` : ''}`.trimEnd();
156
+ }
157
+ case 'subagent': {
158
+ const agent = stringArg(args, 'agent');
159
+ const task = stringArg(args, 'task');
160
+ return `${name} ${agent ?? ''}${task ? ` ${quote(preview(task, 60))}` : ''}`.trimEnd();
161
+ }
162
+ default: {
163
+ const values = Object.values(args).filter((value) => typeof value === 'string') as string[];
164
+ return `${name}${values.length > 0 ? ` ${quote(preview(values[0], 80))}` : ''}`;
165
+ }
166
+ }
167
+ }
168
+
169
+ function formatToolLineItems(
170
+ progress: AgentProgress,
171
+ options: RenderTextOptions,
172
+ ): SubagentResultLine[] {
173
+ const lines: SubagentResultLine[] = [];
174
+ const hiddenCount = options.expanded ? 0 : Math.max(0, progress.tools.length - TOOL_LOG_LIMIT);
175
+ const visibleTools = progress.tools.slice(hiddenCount);
176
+
177
+ if (hiddenCount > 0) {
178
+ const expandHint = options.expandHint ?? 'to expand';
179
+ lines.push({
180
+ text: ` ... (${hiddenCount} earlier tool calls, ${expandHint})`,
181
+ kind: 'hint',
182
+ singleLine: true,
183
+ });
184
+ }
185
+
186
+ for (const tool of visibleTools) {
187
+ lines.push({
188
+ text: `${tool.status === 'running' ? '▸' : ' '} ${formatToolTitle(tool.name, tool.args)}`,
189
+ kind: 'tool',
190
+ singleLine: true,
191
+ tool: { name: tool.name, args: tool.args, status: tool.status },
192
+ });
193
+ if ((options.expanded || progress.status === 'running') && tool.nested) {
194
+ for (const line of formatSubagentResultLines(tool.nested, {
195
+ expanded: false,
196
+ suppressOutput: true,
197
+ })) {
198
+ lines.push({ ...line, text: indent(line.text, 2) });
199
+ }
200
+ }
201
+ }
202
+ return lines;
203
+ }
204
+
205
+ export function formatSubagentCall(
206
+ args: SubagentCallArgs,
207
+ options: Partial<RenderTextOptions> = {},
208
+ ): string {
209
+ if (options.expanded) return `subagent ${args.agent ?? '...'}\n${args.task ?? '...'}`;
210
+ return `subagent ${args.agent ?? '...'} ${preview(args.task ?? '...', 60)}`;
211
+ }
212
+
213
+ export function formatSubagentResultLines(
214
+ progress: AgentProgress,
215
+ options: RenderTextOptions,
216
+ ): SubagentResultLine[] {
217
+ const icon = progress.status === 'error' ? '✗' : progress.status === 'done' ? '✓' : '▸';
218
+ const statusLine = `${icon} ${progress.agent}${progress.model ? ` (${progress.model})` : ''} — ${
219
+ progress.tools.length
220
+ } tools · ${elapsedSeconds(progress.elapsedMs)}s`;
221
+ const toolLines = formatToolLineItems(progress, options);
222
+ const usage = formatUsage(progress);
223
+ const lines: SubagentResultLine[] = [
224
+ { text: statusLine, kind: 'status', singleLine: false },
225
+ ...toolLines,
226
+ ];
227
+
228
+ if (progress.status === 'running' || options.suppressOutput) {
229
+ if (!options.suppressUsage) {
230
+ lines.push({ text: '', kind: 'blank', singleLine: false });
231
+ lines.push({ text: usage, kind: 'usage', singleLine: false });
232
+ }
233
+ return lines;
234
+ }
235
+
236
+ const output = options.expanded
237
+ ? progress.output || '(no output)'
238
+ : summaryText(progress.output) || '(no output)';
239
+
240
+ lines.push({ text: '', kind: 'blank', singleLine: false });
241
+ lines.push({ text: output, kind: 'output', singleLine: false });
242
+
243
+ if (!options.expanded) {
244
+ const totalLines = (progress.output || '').split('\n').length;
245
+ if (totalLines > 20) {
246
+ const hidden = totalLines - 20;
247
+ const expandHint = options.expandHint ?? 'to expand';
248
+ lines.push({
249
+ text: `... (${hidden} more lines, ${expandHint})`,
250
+ kind: 'hint',
251
+ singleLine: true,
252
+ });
253
+ }
254
+ }
255
+
256
+ if (!options.suppressUsage) {
257
+ lines.push({ text: '', kind: 'blank', singleLine: false });
258
+ lines.push({ text: usage, kind: 'usage', singleLine: false });
259
+ }
260
+ return lines;
261
+ }
262
+
263
+ export function formatSubagentResultText(
264
+ progress: AgentProgress,
265
+ options: RenderTextOptions,
266
+ ): string {
267
+ return formatSubagentResultLines(progress, options)
268
+ .map((line) => line.text)
269
+ .join('\n');
270
+ }
@@ -0,0 +1,329 @@
1
+ import { homedir } from 'node:os';
2
+ import {
3
+ getMarkdownTheme,
4
+ keyHint,
5
+ type AgentToolUpdateCallback,
6
+ type ExtensionAPI,
7
+ type ExtensionContext,
8
+ type ThemeColor,
9
+ } from '@earendil-works/pi-coding-agent';
10
+ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
11
+ import type { AgentConfig } from './agent-loader.ts';
12
+ import { type AgentProgress, type AgentResult, runSubagent } from './subagent-executor.ts';
13
+ import {
14
+ contextUsageSeverity,
15
+ formatSubagentCall,
16
+ formatSubagentResultLines,
17
+ formatUsage,
18
+ type SubagentResultLine,
19
+ } from './subagent-render.ts';
20
+
21
+ const SubagentParams = {
22
+ type: 'object',
23
+ properties: {
24
+ agent: { type: 'string', description: 'Name of the agent to invoke' },
25
+ task: { type: 'string', description: 'Task to delegate to the agent' },
26
+ cwd: { type: 'string', description: 'Working directory for the agent process' },
27
+ },
28
+ required: ['agent', 'task'],
29
+ additionalProperties: false,
30
+ } as any;
31
+
32
+ type SubagentParamsType = {
33
+ agent: string;
34
+ task: string;
35
+ cwd?: string;
36
+ };
37
+
38
+ type RegisterablePi = Pick<ExtensionAPI, 'registerTool'>;
39
+ type RecursionEnv = Partial<
40
+ Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
41
+ >;
42
+
43
+ export interface RegisterSubagentToolOptions {
44
+ agents: AgentConfig[];
45
+ run?: typeof runSubagent;
46
+ env?: RecursionEnv;
47
+ }
48
+
49
+ function availableAgentsText(agents: AgentConfig[]): string {
50
+ return (
51
+ agents
52
+ .map((agent) => agent.name)
53
+ .sort()
54
+ .join(', ') || 'none'
55
+ );
56
+ }
57
+
58
+ function toToolResult(result: AgentResult) {
59
+ return {
60
+ content: [{ type: 'text' as const, text: result.output || '(no output)' }],
61
+ details: result,
62
+ isError: result.isError,
63
+ };
64
+ }
65
+
66
+ function toProgressResult(progress: AgentProgress) {
67
+ return {
68
+ content: [{ type: 'text' as const, text: progress.output || '(running...)' }],
69
+ details: progress,
70
+ };
71
+ }
72
+
73
+ function parseEnvNumber(value: string | undefined): number | undefined {
74
+ if (value === undefined) return undefined;
75
+ const parsed = Number(value);
76
+ return Number.isFinite(parsed) ? parsed : undefined;
77
+ }
78
+
79
+ function allowedAgentNames(env: RecursionEnv): Set<string> | undefined {
80
+ const raw = env?.PI_SUBAGENT_ALLOWED;
81
+ if (!raw) return undefined;
82
+ return new Set(
83
+ raw
84
+ .split(',')
85
+ .map((name) => name.trim())
86
+ .filter(Boolean),
87
+ );
88
+ }
89
+
90
+ function isPastMaxDepth(env: RecursionEnv): boolean {
91
+ const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
92
+ const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
93
+ return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
94
+ }
95
+
96
+ type CollapsedTheme = {
97
+ fg: (name: ThemeColor, text: string) => string;
98
+ bold: (text: string) => string;
99
+ };
100
+
101
+ function preview(text: string, length: number): string {
102
+ return text.length > length ? `${text.slice(0, length)}...` : text;
103
+ }
104
+
105
+ function shortenPath(value: unknown): string | undefined {
106
+ if (typeof value !== 'string') return undefined;
107
+ const home = homedir();
108
+ return value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
109
+ }
110
+
111
+ function stringArg(args: Record<string, unknown>, key: string): string | undefined {
112
+ const value = args[key];
113
+ return typeof value === 'string' ? value : undefined;
114
+ }
115
+
116
+ function numberArg(args: Record<string, unknown>, key: string): number | undefined {
117
+ const value = args[key];
118
+ return typeof value === 'number' ? value : undefined;
119
+ }
120
+
121
+ function styledPathArg(
122
+ args: Record<string, unknown>,
123
+ theme: CollapsedTheme,
124
+ fallback?: string,
125
+ ): string {
126
+ const path = shortenPath(args.path ?? args.file_path) ?? fallback;
127
+ return path ? theme.fg('accent', path) : theme.fg('toolOutput', '...');
128
+ }
129
+
130
+ function styledLineRange(args: Record<string, unknown>, theme: CollapsedTheme): string {
131
+ if (args.offset === undefined && args.limit === undefined) return '';
132
+ const startLine = numberArg(args, 'offset') ?? 1;
133
+ const limit = numberArg(args, 'limit');
134
+ const endLine = limit !== undefined ? startLine + limit - 1 : undefined;
135
+ return theme.fg('warning', `:${startLine}${endLine !== undefined ? `-${endLine}` : ''}`);
136
+ }
137
+
138
+ function styledLimitSuffix(args: Record<string, unknown>, theme: CollapsedTheme): string {
139
+ const limit = numberArg(args, 'limit');
140
+ return limit !== undefined ? theme.fg('toolOutput', ` (limit ${limit})`) : '';
141
+ }
142
+
143
+ function styledToolTitle(
144
+ name: string,
145
+ args: Record<string, unknown>,
146
+ theme: CollapsedTheme,
147
+ ): string {
148
+ switch (name) {
149
+ case 'read':
150
+ return `${theme.fg('toolTitle', theme.bold('read'))} ${styledPathArg(args, theme)}${styledLineRange(args, theme)}`;
151
+ case 'bash': {
152
+ const command = stringArg(args, 'command') ?? '';
153
+ const timeout = numberArg(args, 'timeout');
154
+ const timeoutSuffix =
155
+ timeout !== undefined ? theme.fg('muted', ` (timeout ${timeout}s)`) : '';
156
+ return `${theme.fg('toolTitle', theme.bold(`$ ${command || '...'}`))}${timeoutSuffix}`;
157
+ }
158
+ case 'edit':
159
+ return `${theme.fg('toolTitle', theme.bold('edit'))} ${styledPathArg(args, theme)}`;
160
+ case 'write':
161
+ return `${theme.fg('toolTitle', theme.bold('write'))} ${styledPathArg(args, theme)}`;
162
+ case 'find': {
163
+ const pattern = stringArg(args, 'pattern') ?? '';
164
+ return `${theme.fg('toolTitle', theme.bold('find'))} ${theme.fg('accent', pattern)}${theme.fg('toolOutput', ` in ${shortenPath(args.path) ?? '.'}`)}${styledLimitSuffix(args, theme)}`;
165
+ }
166
+ case 'grep': {
167
+ const pattern = stringArg(args, 'pattern') ?? '';
168
+ const glob = stringArg(args, 'glob');
169
+ const limit = numberArg(args, 'limit');
170
+ let text = `${theme.fg('toolTitle', theme.bold('grep'))} ${theme.fg('syntaxKeyword', `/${pattern}/`)}${theme.fg('dim', ' in ')}${theme.fg('accent', shortenPath(args.path) ?? '.')}`;
171
+ if (glob) text += theme.fg('muted', ` (${glob})`);
172
+ if (limit !== undefined) text += theme.fg('toolOutput', ` limit ${limit}`);
173
+ return text;
174
+ }
175
+ case 'ls':
176
+ return `${theme.fg('toolTitle', theme.bold('ls'))} ${styledPathArg(args, theme, '.')}${styledLimitSuffix(args, theme)}`;
177
+ case 'webfetch': {
178
+ const url = stringArg(args, 'url') ?? '';
179
+ const mode = stringArg(args, 'mode');
180
+ return `${theme.fg('toolTitle', theme.bold('webfetch'))} ${theme.fg('accent', url)}${mode ? theme.fg('toolOutput', ` (${mode})`) : ''}`;
181
+ }
182
+ case 'subagent': {
183
+ const agent = stringArg(args, 'agent') ?? '';
184
+ const task = stringArg(args, 'task');
185
+ return `${theme.fg('toolTitle', theme.bold('subagent'))} ${theme.fg('accent', agent)}${task ? ` ${theme.fg('dim', JSON.stringify(preview(task, 60)))}` : ''}`.trimEnd();
186
+ }
187
+ default: {
188
+ const values = Object.values(args).filter((value) => typeof value === 'string') as string[];
189
+ return `${theme.fg('toolTitle', theme.bold(name))}${values.length > 0 ? ` ${theme.fg('dim', JSON.stringify(preview(values[0], 80)))}` : ''}`;
190
+ }
191
+ }
192
+ }
193
+
194
+ function styledCollapsedLine(
195
+ line: SubagentResultLine,
196
+ details: AgentResult | AgentProgress,
197
+ theme: CollapsedTheme,
198
+ ): string {
199
+ if (line.kind === 'status')
200
+ return theme.fg(details.status === 'error' ? 'error' : 'success', line.text);
201
+ if (line.kind === 'hint') return theme.fg('dim', line.text);
202
+ if (line.kind === 'tool' && line.tool) {
203
+ const prefix = line.tool.status === 'running' ? theme.fg('warning', '▸') : ' ';
204
+ return `${prefix} ${styledToolTitle(line.tool.name, line.tool.args, theme)}`;
205
+ }
206
+ if (line.kind === 'usage') return theme.fg(contextUsageSeverity(details.usage), line.text);
207
+ return line.text;
208
+ }
209
+
210
+ function styledSubagentCall(
211
+ args: SubagentParamsType,
212
+ theme: CollapsedTheme,
213
+ expanded: boolean,
214
+ ): string {
215
+ const title = `${theme.fg('toolTitle', theme.bold('subagent'))} ${theme.fg('accent', args.agent ?? '...')}`;
216
+ if (expanded) return `${title}\n${theme.fg('dim', args.task ?? '...')}`;
217
+ return `${title} ${theme.fg('dim', formatSubagentCall(args).replace(/^subagent \S+ /, ''))}`;
218
+ }
219
+
220
+ function renderResultLines(
221
+ details: AgentResult | AgentProgress,
222
+ theme: CollapsedTheme,
223
+ options: {
224
+ expanded: boolean;
225
+ suppressOutput?: boolean;
226
+ suppressUsage?: boolean;
227
+ expandHint?: string;
228
+ },
229
+ ): Container {
230
+ const container = new Container();
231
+ for (const line of formatSubagentResultLines(details, options)) {
232
+ if (line.kind === 'blank') {
233
+ container.addChild(new Spacer(1));
234
+ continue;
235
+ }
236
+
237
+ const text = styledCollapsedLine(line, details, theme);
238
+ container.addChild(new Text(text, 0, 0));
239
+ }
240
+ return container;
241
+ }
242
+
243
+ function renderCollapsedResult(
244
+ details: AgentResult | AgentProgress,
245
+ theme: CollapsedTheme,
246
+ ): Container {
247
+ return renderResultLines(details, theme, {
248
+ expanded: false,
249
+ expandHint: keyHint('app.tools.expand', 'to expand'),
250
+ });
251
+ }
252
+
253
+ export function registerSubagentTool(
254
+ pi: RegisterablePi,
255
+ options: RegisterSubagentToolOptions,
256
+ ): void {
257
+ const env: RecursionEnv = options.env ?? process.env;
258
+ if (isPastMaxDepth(env)) return;
259
+
260
+ const allowed = allowedAgentNames(env);
261
+ const agents = allowed
262
+ ? options.agents.filter((candidate) => allowed.has(candidate.name))
263
+ : options.agents;
264
+ const runner = options.run ?? runSubagent;
265
+
266
+ pi.registerTool({
267
+ name: 'subagent',
268
+ label: 'Subagent',
269
+ description: 'Delegate a task to a named sub-agent running in an isolated pi process.',
270
+ promptSnippet: 'Delegate isolated tasks with subagent({ agent, task, cwd? }).',
271
+ parameters: SubagentParams,
272
+
273
+ async execute(
274
+ _toolCallId: string,
275
+ params: SubagentParamsType,
276
+ signal: AbortSignal | undefined,
277
+ onUpdate: AgentToolUpdateCallback<AgentProgress> | undefined,
278
+ ctx: ExtensionContext,
279
+ ) {
280
+ const agent = agents.find((candidate) => candidate.name === params.agent);
281
+ if (!agent) {
282
+ throw new Error(
283
+ `Unknown agent: ${params.agent}. Available agents: ${availableAgentsText(agents)}.`,
284
+ );
285
+ }
286
+
287
+ const result = await runner({
288
+ agent,
289
+ task: params.task,
290
+ cwd: params.cwd ?? ctx.cwd,
291
+ signal,
292
+ depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
293
+ onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
294
+ });
295
+
296
+ return toToolResult(result);
297
+ },
298
+
299
+ renderCall(args, theme, context) {
300
+ const typedArgs = args as SubagentParamsType;
301
+ return new Text(styledSubagentCall(typedArgs, theme, context.expanded), 0, 0);
302
+ },
303
+
304
+ renderResult(result, options, theme) {
305
+ const details = result.details as AgentResult | AgentProgress | undefined;
306
+ if (!details) {
307
+ const first = result.content[0];
308
+ return new Text(first?.type === 'text' ? first.text : '(no output)', 0, 0);
309
+ }
310
+
311
+ if (options.expanded) {
312
+ const container = renderResultLines(details, theme, {
313
+ expanded: true,
314
+ suppressOutput: true,
315
+ suppressUsage: true,
316
+ });
317
+ container.addChild(new Spacer(1));
318
+ container.addChild(new Markdown(details.output || '(no output)', 0, 0, getMarkdownTheme()));
319
+ container.addChild(new Spacer(1));
320
+ container.addChild(
321
+ new Text(theme.fg(contextUsageSeverity(details.usage), formatUsage(details)), 0, 0),
322
+ );
323
+ return container;
324
+ }
325
+
326
+ return renderCollapsedResult(details, theme);
327
+ },
328
+ });
329
+ }
package/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "@johnnywu/pi-subagents",
3
+ "version": "1.0.0",
4
+ "description": "Sub-agents extension for pi coding agent.",
5
+ "homepage": "https://github.com/jwu/pi-subagents#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jwu/pi-subagents.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/jwu/pi-subagents/issues"
12
+ },
13
+ "type": "module",
14
+ "keywords": [
15
+ "pi-package",
16
+ "pi",
17
+ "pi-coding-agent",
18
+ "extension",
19
+ "subagents"
20
+ ],
21
+ "author": "jwu",
22
+ "license": "MIT",
23
+ "files": [
24
+ "extensions/",
25
+ "README.md",
26
+ "package.json",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "scripts": {
34
+ "test": "bun test",
35
+ "typecheck": "bun tsc --noEmit --ignoreDeprecations 6.0",
36
+ "lint": "bun run format:check && bun run typecheck",
37
+ "format": "prettier --write '**/*.ts'",
38
+ "format:check": "prettier --check '**/*.ts'",
39
+ "prepare": "[ -d .git ] && simple-git-hooks || true",
40
+ "release": "GH_TOKEN=$(gh auth token) semantic-release --no-ci",
41
+ "release:dry": "GH_TOKEN=$(gh auth token) semantic-release --no-ci --dry-run"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "release": {
47
+ "repositoryUrl": "https://github.com/jwu/pi-subagents.git",
48
+ "branches": [
49
+ "main"
50
+ ],
51
+ "plugins": [
52
+ "@semantic-release/commit-analyzer",
53
+ "@semantic-release/release-notes-generator",
54
+ [
55
+ "@semantic-release/changelog",
56
+ {
57
+ "changelogFile": "CHANGELOG.md"
58
+ }
59
+ ],
60
+ "@semantic-release/npm",
61
+ "@semantic-release/github",
62
+ [
63
+ "@semantic-release/git",
64
+ {
65
+ "assets": [
66
+ "package.json",
67
+ "CHANGELOG.md"
68
+ ],
69
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
70
+ }
71
+ ]
72
+ ]
73
+ },
74
+ "simple-git-hooks": {
75
+ "pre-commit": "bun prettier --check '**/*.ts' && bun test",
76
+ "commit-msg": "bun commitlint --edit \"$1\""
77
+ },
78
+ "pi": {
79
+ "extensions": [
80
+ "./extensions"
81
+ ]
82
+ },
83
+ "peerDependencies": {
84
+ "@earendil-works/pi-ai": "*",
85
+ "@earendil-works/pi-coding-agent": "*",
86
+ "@earendil-works/pi-tui": "*",
87
+ "typebox": "*"
88
+ },
89
+ "devDependencies": {
90
+ "@commitlint/cli": "^20.5.3",
91
+ "@commitlint/config-conventional": "^20.5.3",
92
+ "@semantic-release/changelog": "^6.0.3",
93
+ "@semantic-release/commit-analyzer": "^13.0.1",
94
+ "@semantic-release/git": "^10.0.1",
95
+ "@semantic-release/github": "^12.0.6",
96
+ "@semantic-release/npm": "^13.1.5",
97
+ "@semantic-release/release-notes-generator": "^14.1.0",
98
+ "@types/node": "^24.3.0",
99
+ "prettier": "^3.8.3",
100
+ "semantic-release": "^25.0.3",
101
+ "simple-git-hooks": "^2.13.1",
102
+ "typescript": "^6.0.3"
103
+ }
104
+ }