@jerryan/pi-subagent-tools 0.1.0 → 0.3.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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -21
- package/README.md +49 -17
- package/agents.ts +878 -0
- package/index.ts +9 -418
- package/package.json +22 -12
- package/prompts/delegate.md +6 -0
- package/prompts/explore.md +1 -1
- package/prompts/review.md +1 -1
- package/render.ts +87 -0
- package/sandbox-bash.ts +148 -0
- package/tui.ts +16 -9
- package/ui-bridge.ts +198 -0
- package/spawn.ts +0 -262
package/index.ts
CHANGED
|
@@ -1,428 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-subagent-tools — Specialized subagent delegation for pi
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* review
|
|
6
|
-
* explore
|
|
7
|
-
* delegate
|
|
4
|
+
* Registers four tools for the "agent as project manager" workflow:
|
|
5
|
+
* review — Read-only code review in the current project
|
|
6
|
+
* explore — Read-only exploration of external projects (requires cwd)
|
|
7
|
+
* delegate — General-purpose worker with full tool access
|
|
8
|
+
* follow_up — Continue an existing subagent's session with full context
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Delegate child: review/explore active, delegate registered but rejects at runtime
|
|
12
|
-
* - Review/explore child: no subagent tools registered
|
|
10
|
+
* Subagents run in-process as SDK AgentSessions (see agents.ts). Interactive
|
|
11
|
+
* prompts route to the parent TUI via the UI bridge (see ui-bridge.ts).
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
|
-
import * as fs from "node:fs";
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import * as path from "node:path";
|
|
19
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import {
|
|
21
|
-
import { Container, Spacer, Text, Markdown } from "@earendil-works/pi-tui";
|
|
22
|
-
import { Type } from "@sinclair/typebox";
|
|
23
|
-
import {
|
|
24
|
-
runSubagent,
|
|
25
|
-
READONLY_TOOLS,
|
|
26
|
-
} from "./spawn.ts";
|
|
27
|
-
import { shortenPath, formatUsage, createSpinner } from "./tui.ts";
|
|
28
|
-
|
|
29
|
-
const subagentSpinners = new Map<string, () => string>();
|
|
30
|
-
|
|
31
|
-
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Compact preview component — delegates to Text.render(width) for wrapping
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
export class CompactPreview {
|
|
38
|
-
private textComp: Text;
|
|
39
|
-
private maxLines: number;
|
|
40
|
-
|
|
41
|
-
constructor(text: string, maxLines: number = 5) {
|
|
42
|
-
this.maxLines = maxLines;
|
|
43
|
-
this.textComp = new Text(text, 0, 0);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
render(width: number): string[] {
|
|
47
|
-
const lines = this.textComp.render(width);
|
|
48
|
-
if (lines.length === 0) return [];
|
|
49
|
-
|
|
50
|
-
if (lines.length <= this.maxLines) return lines;
|
|
51
|
-
|
|
52
|
-
return ["...", ...lines.slice(-this.maxLines)];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
invalidate() {
|
|
56
|
-
this.textComp.invalidate();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const PROMPTS_DIR = path.resolve(EXTENSION_DIR, "prompts");
|
|
61
|
-
const REVIEW_PROMPT_FILE = path.join(PROMPTS_DIR, "review.md");
|
|
62
|
-
const EXPLORE_PROMPT_FILE = path.join(PROMPTS_DIR, "explore.md");
|
|
63
|
-
const EXTENSION_FILE = path.join(EXTENSION_DIR, "index.ts");
|
|
64
|
-
|
|
65
|
-
// Role guard
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
export const ROLE_ENV = "PI_SUBAGENT_TOOLS_ROLE";
|
|
69
|
-
|
|
70
|
-
export type SubagentRole = "delegate" | "review" | "explore";
|
|
71
|
-
|
|
72
|
-
export function getCurrentRole(): SubagentRole | undefined {
|
|
73
|
-
const value = process.env[ROLE_ENV];
|
|
74
|
-
if (value === "delegate" || value === "review" || value === "explore") {
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Shared parameter types
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
export const TaskParam = Type.String({
|
|
85
|
-
description: "Task to delegate to the subagent",
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
export const SkillsParam = Type.Optional(
|
|
89
|
-
Type.Array(Type.String(), {
|
|
90
|
-
description: "Optional startup skills to load via --skill",
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
export const CwdParam = Type.Optional(
|
|
95
|
-
Type.String({
|
|
96
|
-
description:
|
|
97
|
-
"Working directory for the subagent. Defaults to the parent's cwd.",
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
export const ContextParam = Type.Optional(
|
|
102
|
-
Type.String({
|
|
103
|
-
enum: ["fresh", "inherit"],
|
|
104
|
-
description:
|
|
105
|
-
'Context mode. "fresh" (default): isolated process with no parent history. "inherit": forks parent session so child sees conversation history.',
|
|
106
|
-
}),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Tool-specific parameter schemas
|
|
110
|
-
export const ReviewParams = Type.Object({
|
|
111
|
-
task: TaskParam,
|
|
112
|
-
skills: SkillsParam,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
export const ExploreParams = Type.Object({
|
|
116
|
-
task: TaskParam,
|
|
117
|
-
cwd: Type.String({
|
|
118
|
-
description:
|
|
119
|
-
"Working directory for the explorer. Required — the explorer runs in the target project to pick up its settings, skills, and context files.",
|
|
120
|
-
}),
|
|
121
|
-
skills: SkillsParam,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
export const DelegateParams = Type.Object({
|
|
125
|
-
task: TaskParam,
|
|
126
|
-
cwd: CwdParam,
|
|
127
|
-
skills: SkillsParam,
|
|
128
|
-
context: ContextParam,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Shared execute helper
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
async function subagentExecute(
|
|
136
|
-
cwd: string,
|
|
137
|
-
task: string,
|
|
138
|
-
skills: string[] | undefined,
|
|
139
|
-
allowedTools: string[] | undefined,
|
|
140
|
-
extensionFile: string | undefined,
|
|
141
|
-
systemPromptFile: string | undefined,
|
|
142
|
-
childRole: "review" | "explore" | "delegate" | undefined,
|
|
143
|
-
signal: AbortSignal | undefined,
|
|
144
|
-
onUpdate: ((result: any) => void) | undefined,
|
|
145
|
-
) {
|
|
146
|
-
// Validate cwd exists before spawning
|
|
147
|
-
if (!cwd) {
|
|
148
|
-
return {
|
|
149
|
-
content: [{ type: "text" as const, text: "Cannot spawn subagent: cwd is required." }],
|
|
150
|
-
isError: true,
|
|
151
|
-
details: { final: true },
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
const stat = fs.statSync(cwd);
|
|
156
|
-
if (!stat.isDirectory()) {
|
|
157
|
-
return {
|
|
158
|
-
content: [{
|
|
159
|
-
type: "text" as const,
|
|
160
|
-
text: `Cannot spawn subagent: "${cwd}" is not a directory.`,
|
|
161
|
-
}],
|
|
162
|
-
isError: true,
|
|
163
|
-
details: { final: true },
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
} catch (err: any) {
|
|
167
|
-
return {
|
|
168
|
-
content: [{
|
|
169
|
-
type: "text" as const,
|
|
170
|
-
text: `Cannot spawn subagent: cwd "${cwd}" does not exist or is not accessible (${err.message}).`,
|
|
171
|
-
}],
|
|
172
|
-
isError: true,
|
|
173
|
-
details: { final: true },
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
const result = await runSubagent({
|
|
179
|
-
cwd,
|
|
180
|
-
task,
|
|
181
|
-
skills,
|
|
182
|
-
allowedTools,
|
|
183
|
-
extensionFile,
|
|
184
|
-
systemPromptFile,
|
|
185
|
-
childRole,
|
|
186
|
-
signal,
|
|
187
|
-
onUpdate,
|
|
188
|
-
});
|
|
189
|
-
return {
|
|
190
|
-
content: [{ type: "text" as const, text: result.output }],
|
|
191
|
-
isError: result.isError,
|
|
192
|
-
details: {
|
|
193
|
-
usage: {
|
|
194
|
-
turns: result.turns,
|
|
195
|
-
total: result.tokens?.total,
|
|
196
|
-
input: result.tokens?.input,
|
|
197
|
-
output: result.tokens?.output,
|
|
198
|
-
durationMs: result.durationMs,
|
|
199
|
-
},
|
|
200
|
-
final: true,
|
|
201
|
-
stderr: result.stderr,
|
|
202
|
-
},
|
|
203
|
-
toolCalls: result.toolCalls,
|
|
204
|
-
};
|
|
205
|
-
} catch (err: any) {
|
|
206
|
-
return {
|
|
207
|
-
content: [{
|
|
208
|
-
type: "text" as const,
|
|
209
|
-
text: `Subagent failed: ${err.message || String(err)}`,
|
|
210
|
-
}],
|
|
211
|
-
isError: true,
|
|
212
|
-
details: { final: true },
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
// Shared render helpers
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
|
|
221
|
-
function renderSubagentCall(
|
|
222
|
-
label: string,
|
|
223
|
-
args: { task: string; cwd?: string },
|
|
224
|
-
theme: any,
|
|
225
|
-
) {
|
|
226
|
-
const firstLine = args.task.split("\n")[0];
|
|
227
|
-
const combined = args.cwd ? `${shortenPath(args.cwd)} | ${firstLine}` : firstLine;
|
|
228
|
-
const preview = combined.length > 80 ? combined.slice(0, 77) + "..." : combined;
|
|
229
|
-
return new Text(
|
|
230
|
-
`${theme.fg("toolTitle", theme.bold(label))}${theme.fg("dim", preview)}`,
|
|
231
|
-
0, 0,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function renderSubagentResult(
|
|
236
|
-
result: { content: Array<{ type: string; text?: string }>; isError?: boolean; details?: any },
|
|
237
|
-
options: { expanded: boolean },
|
|
238
|
-
theme: any,
|
|
239
|
-
context: { toolCallId: string },
|
|
240
|
-
) {
|
|
241
|
-
if (!subagentSpinners.has(context.toolCallId)) {
|
|
242
|
-
subagentSpinners.set(context.toolCallId, createSpinner());
|
|
243
|
-
}
|
|
244
|
-
const spin = subagentSpinners.get(context.toolCallId)!;
|
|
245
|
-
const raw = result.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("");
|
|
246
|
-
const output = raw.split("\n").map(l => l.startsWith("▸") ? theme.fg("muted", l) : l).join("\n");
|
|
247
|
-
const container = new Container();
|
|
248
|
-
|
|
249
|
-
if (result.details?.final) subagentSpinners.delete(context.toolCallId);
|
|
250
|
-
|
|
251
|
-
if (!options.expanded) {
|
|
252
|
-
const text = output || theme.fg("dim", "(no output)");
|
|
253
|
-
const usage = result.details?.usage;
|
|
254
|
-
const hint = ` · ${theme.fg("dim", "Ctrl+O to expand")}`;
|
|
255
|
-
const status = usage ? formatUsage(usage, spin) + hint : "";
|
|
256
|
-
container.addChild(new CompactPreview(text));
|
|
257
|
-
if (status) container.addChild(new Text(theme.fg("dim", status), 0, 0));
|
|
258
|
-
return container;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const isFinal = !!result.details?.final;
|
|
262
|
-
if (isFinal) {
|
|
263
|
-
const mdTheme = getMarkdownTheme();
|
|
264
|
-
container.addChild(new Markdown(raw, 0, 0, mdTheme));
|
|
265
|
-
} else {
|
|
266
|
-
container.addChild(new CompactPreview(output, 15));
|
|
267
|
-
}
|
|
268
|
-
if (result.details?.usage) {
|
|
269
|
-
container.addChild(new Spacer(1));
|
|
270
|
-
container.addChild(new Text(theme.fg("dim", formatUsage(result.details.usage, spin)), 0, 0));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return container;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
// Extension entry point
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
15
|
+
import { AgentManager, registerAgentTools } from "./agents.ts";
|
|
279
16
|
|
|
280
17
|
export default function (pi: ExtensionAPI) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// Review/explore children: register git tool only, then return
|
|
284
|
-
if (role === "review" || role === "explore") {
|
|
285
|
-
pi.registerTool({
|
|
286
|
-
name: "git",
|
|
287
|
-
label: "Git",
|
|
288
|
-
description: "Run read-only git commands (log, diff, status, show, branch, blame).",
|
|
289
|
-
promptSnippet: "Run a read-only git command",
|
|
290
|
-
parameters: Type.Object({
|
|
291
|
-
command: Type.String({
|
|
292
|
-
description: "Git command with arguments (e.g. 'log --oneline -10', 'diff HEAD~1', 'status').",
|
|
293
|
-
}),
|
|
294
|
-
}),
|
|
295
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
296
|
-
try {
|
|
297
|
-
const args = params.command.trim().split(/\s+/);
|
|
298
|
-
if (args.length === 0) {
|
|
299
|
-
return { content: [{ type: "text" as const, text: "No git command provided." }], isError: true };
|
|
300
|
-
}
|
|
301
|
-
const subcommand = args[0];
|
|
302
|
-
const READONLY_GIT = new Set(["log","diff","show","status","branch","blame",
|
|
303
|
-
"rev-parse","rev-list","ls-tree","ls-files","grep","describe","shortlog","whatchanged"]);
|
|
304
|
-
if (!READONLY_GIT.has(subcommand)) {
|
|
305
|
-
return {
|
|
306
|
-
content: [{ type: "text" as const, text: `git ${subcommand} is not allowed (read-only commands only).` }],
|
|
307
|
-
isError: true,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
const output = execFileSync("git", args, {
|
|
311
|
-
cwd: ctx.cwd,
|
|
312
|
-
encoding: "utf-8",
|
|
313
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
314
|
-
timeout: 30_000,
|
|
315
|
-
});
|
|
316
|
-
return { content: [{ type: "text" as const, text: output }] };
|
|
317
|
-
} catch (err: any) {
|
|
318
|
-
return {
|
|
319
|
-
content: [{ type: "text" as const, text: `git ${params.command}\n\n${err.stderr || err.message}` }],
|
|
320
|
-
isError: true,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// Delegate tool: always registered (cache compatibility), rejects in delegate children
|
|
330
|
-
pi.registerTool({
|
|
331
|
-
name: "delegate",
|
|
332
|
-
label: "Delegate",
|
|
333
|
-
description:
|
|
334
|
-
"Delegate a task to an isolated subagent process. " +
|
|
335
|
-
"For general-purpose work that doesn't fit the review or explore tools.",
|
|
336
|
-
promptSnippet: "Delegate a task to a worker subagent",
|
|
337
|
-
promptGuidelines: [
|
|
338
|
-
"Use the delegate tool for non-trivial, self-contained implementation tasks — it has full tool access unlike the read-only review and explore tools.",
|
|
339
|
-
],
|
|
340
|
-
parameters: DelegateParams,
|
|
341
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
342
|
-
if (role === "delegate") {
|
|
343
|
-
return {
|
|
344
|
-
content: [
|
|
345
|
-
{
|
|
346
|
-
type: "text" as const,
|
|
347
|
-
text: "Delegation not available in delegate subagents. Use review or explore instead.",
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
isError: true,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
return subagentExecute(
|
|
354
|
-
params.cwd ?? ctx.cwd,
|
|
355
|
-
params.task,
|
|
356
|
-
params.skills,
|
|
357
|
-
undefined, // no tool filtering — cache-compatible
|
|
358
|
-
EXTENSION_FILE, // same extensions → same system prompt → cache hit
|
|
359
|
-
undefined, // no system prompt — cache-compatible
|
|
360
|
-
"delegate", // delegate children can spawn review/explore but not delegate further
|
|
361
|
-
signal,
|
|
362
|
-
onUpdate,
|
|
363
|
-
);
|
|
364
|
-
},
|
|
365
|
-
renderCall: (args, theme) => renderSubagentCall("delegate ", args, theme),
|
|
366
|
-
renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Review tool
|
|
370
|
-
pi.registerTool({
|
|
371
|
-
name: "review",
|
|
372
|
-
label: "Review",
|
|
373
|
-
description:
|
|
374
|
-
"Review code changes or files in the current project. " +
|
|
375
|
-
"The reviewer is always read-only and runs in the parent's working directory. " +
|
|
376
|
-
"Use for code review, diff inspection, issue analysis, and quality checks.",
|
|
377
|
-
promptSnippet: "Review code or files with a read-only subagent",
|
|
378
|
-
promptGuidelines: [
|
|
379
|
-
"Use the review tool for code review, diff inspection, and quality checks — it is read-only and cannot modify files.",
|
|
380
|
-
],
|
|
381
|
-
parameters: ReviewParams,
|
|
382
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
383
|
-
return subagentExecute(
|
|
384
|
-
ctx.cwd,
|
|
385
|
-
params.task,
|
|
386
|
-
params.skills,
|
|
387
|
-
READONLY_TOOLS,
|
|
388
|
-
EXTENSION_FILE,
|
|
389
|
-
REVIEW_PROMPT_FILE,
|
|
390
|
-
"review",
|
|
391
|
-
signal,
|
|
392
|
-
onUpdate,
|
|
393
|
-
);
|
|
394
|
-
},
|
|
395
|
-
renderCall: (args, theme) => renderSubagentCall("review ", args, theme),
|
|
396
|
-
renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// Explore tool
|
|
400
|
-
pi.registerTool({
|
|
401
|
-
name: "explore",
|
|
402
|
-
label: "Explore",
|
|
403
|
-
description:
|
|
404
|
-
"Explore a project directory to understand its structure, patterns, and key files. " +
|
|
405
|
-
"The explorer is always read-only and runs in the specified working directory. " +
|
|
406
|
-
"Use for mapping codebases, scouting dependencies, or understanding unfamiliar projects.",
|
|
407
|
-
promptSnippet: "Explore a project directory with a read-only subagent",
|
|
408
|
-
promptGuidelines: [
|
|
409
|
-
"Use the explore tool to map unfamiliar codebases or scout dependencies — it runs read-only in the specified directory.",
|
|
410
|
-
],
|
|
411
|
-
parameters: ExploreParams,
|
|
412
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
413
|
-
return subagentExecute(
|
|
414
|
-
params.cwd,
|
|
415
|
-
params.task,
|
|
416
|
-
params.skills,
|
|
417
|
-
READONLY_TOOLS,
|
|
418
|
-
EXTENSION_FILE,
|
|
419
|
-
EXPLORE_PROMPT_FILE,
|
|
420
|
-
"explore",
|
|
421
|
-
signal,
|
|
422
|
-
onUpdate,
|
|
423
|
-
);
|
|
424
|
-
},
|
|
425
|
-
renderCall: (args, theme) => renderSubagentCall("explore ", args, theme),
|
|
426
|
-
renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
|
|
427
|
-
});
|
|
18
|
+
registerAgentTools(pi, new AgentManager());
|
|
428
19
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jerryan/pi-subagent-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Minimal pi extension: delegate, review, and explore via
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Minimal pi extension: delegate, review, and explore via in-process subagent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.ts",
|
|
9
|
-
"
|
|
9
|
+
"agents.ts",
|
|
10
|
+
"render.ts",
|
|
11
|
+
"sandbox-bash.ts",
|
|
12
|
+
"ui-bridge.ts",
|
|
10
13
|
"tui.ts",
|
|
11
14
|
"prompts/*.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
12
16
|
"README.md",
|
|
13
17
|
"LICENSE"
|
|
14
18
|
],
|
|
@@ -31,23 +35,29 @@
|
|
|
31
35
|
"registry": "https://registry.npmjs.org/"
|
|
32
36
|
},
|
|
33
37
|
"scripts": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
38
|
+
"test": "node --experimental-strip-types --import ./test/register-mocks.mjs --test test/*.test.ts",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
36
40
|
},
|
|
37
41
|
"pi": {
|
|
38
|
-
"extensions": [
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./index.ts"
|
|
44
|
+
]
|
|
39
45
|
},
|
|
40
46
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "
|
|
42
|
-
"@earendil-works/pi-coding-agent": "
|
|
43
|
-
"@earendil-works/pi-tui": "
|
|
47
|
+
"@earendil-works/pi-ai": ">=0.84.0",
|
|
48
|
+
"@earendil-works/pi-coding-agent": ">=0.84.0",
|
|
49
|
+
"@earendil-works/pi-tui": ">=0.84.0"
|
|
44
50
|
},
|
|
45
51
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-ai": "^0.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
48
|
-
"@earendil-works/pi-tui": "^0.
|
|
52
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
49
55
|
"@sinclair/typebox": "^0.34.0",
|
|
50
56
|
"@types/node": "^20.0.0",
|
|
51
57
|
"typescript": "^5.4.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"just-bash": "^3.4.2",
|
|
61
|
+
"just-git": "^1.8.0"
|
|
52
62
|
}
|
|
53
63
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
You are a task worker. Your job is to execute the assigned task directly — do the work, don't discuss it.
|
|
2
|
+
You have full file and bash access. Make changes, run commands, and complete the task.
|
|
3
|
+
You do not need to present plans for approval. If requirements are clear, implement immediately.
|
|
4
|
+
Minor uncertainties: proceed with a reasonable assumption, state it clearly in your result.
|
|
5
|
+
Major uncertainties or obstacles: cannot be resolved — report back, suggest the caller investigate and re-delegate with clarification.
|
|
6
|
+
Be concise. Deliver results, not deliberation.
|
package/prompts/explore.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
You are a codebase explorer. Map the structure, identify key files, and report patterns.
|
|
2
|
-
You are in read-only mode —
|
|
2
|
+
You are in read-only mode — project files cannot be modified (writes to them fail by design). Use bash for searching, file inspection, and git history.
|
|
3
3
|
Focus on entry points, architecture, dependencies, and areas of interest.
|
|
4
4
|
End with a clear summary of the codebase structure and notable findings.
|
package/prompts/review.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
You are a code reviewer. Inspect code, find issues, and report findings clearly.
|
|
2
|
-
You are in read-only mode —
|
|
2
|
+
You are in read-only mode — project files cannot be modified (writes to them fail by design). Use bash for searching, file inspection, and git history.
|
|
3
3
|
Focus on correctness, edge cases, readability, and adherence to the task requirements.
|
|
4
4
|
End with a clear summary of findings organized by severity.
|
package/render.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering for subagent tool calls and results.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Container, Spacer, Text, Markdown } from "@earendil-works/pi-tui";
|
|
7
|
+
import { formatUsage, spinnerFrame, truncate, TOOL_LINE_PREFIX } from "./tui.ts";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Compact preview component — delegates to Text.render(width) for wrapping
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export class CompactPreview {
|
|
14
|
+
private textComp: Text;
|
|
15
|
+
private maxLines: number;
|
|
16
|
+
|
|
17
|
+
constructor(text: string, maxLines: number = 5) {
|
|
18
|
+
this.maxLines = maxLines;
|
|
19
|
+
this.textComp = new Text(text, 0, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
render(width: number): string[] {
|
|
23
|
+
const lines = this.textComp.render(width);
|
|
24
|
+
if (lines.length === 0) return [];
|
|
25
|
+
|
|
26
|
+
if (lines.length <= this.maxLines) return lines;
|
|
27
|
+
|
|
28
|
+
return ["...", ...lines.slice(-this.maxLines)];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
invalidate() {
|
|
32
|
+
this.textComp.invalidate();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Call / result renderers shared by all subagent tools
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export function renderSubagentCall(
|
|
41
|
+
label: string,
|
|
42
|
+
args: { task: string; hint?: string },
|
|
43
|
+
theme: any,
|
|
44
|
+
) {
|
|
45
|
+
const firstLine = args.task.split("\n")[0];
|
|
46
|
+
const combined = args.hint ? `${args.hint} | ${firstLine}` : firstLine;
|
|
47
|
+
const preview = truncate(combined, 80);
|
|
48
|
+
return new Text(
|
|
49
|
+
`${theme.fg("toolTitle", theme.bold(label))}${theme.fg("dim", preview)}`,
|
|
50
|
+
0, 0,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function renderSubagentResult(
|
|
55
|
+
result: { content: Array<{ type: string; text?: string }>; isError?: boolean; details?: any },
|
|
56
|
+
options: { expanded: boolean },
|
|
57
|
+
theme: any,
|
|
58
|
+
_context: { toolCallId: string },
|
|
59
|
+
) {
|
|
60
|
+
const raw = result.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("");
|
|
61
|
+
const output = raw.split("\n").map(l => l.startsWith(TOOL_LINE_PREFIX) ? theme.fg("muted", l) : l).join("\n");
|
|
62
|
+
const container = new Container();
|
|
63
|
+
|
|
64
|
+
if (!options.expanded) {
|
|
65
|
+
const text = output || theme.fg("dim", "(no output)");
|
|
66
|
+
const usage = result.details?.usage;
|
|
67
|
+
const hint = ` · ${theme.fg("dim", "Ctrl+O to expand")}`;
|
|
68
|
+
const status = usage ? formatUsage(usage, spinnerFrame) + hint : "";
|
|
69
|
+
container.addChild(new CompactPreview(text));
|
|
70
|
+
if (status) container.addChild(new Text(theme.fg("dim", status), 0, 0));
|
|
71
|
+
return container;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isFinal = !!result.details?.final;
|
|
75
|
+
if (isFinal) {
|
|
76
|
+
const mdTheme = getMarkdownTheme();
|
|
77
|
+
container.addChild(new Markdown(raw, 0, 0, mdTheme));
|
|
78
|
+
} else {
|
|
79
|
+
container.addChild(new CompactPreview(output, 15));
|
|
80
|
+
}
|
|
81
|
+
if (result.details?.usage) {
|
|
82
|
+
container.addChild(new Spacer(1));
|
|
83
|
+
container.addChild(new Text(theme.fg("dim", formatUsage(result.details.usage, spinnerFrame)), 0, 0));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return container;
|
|
87
|
+
}
|