@harms-haus/pi-subagents 0.1.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/LICENSE +21 -0
- package/README.md +362 -0
- package/docs/architecture.md +554 -0
- package/docs/changelog.md +61 -0
- package/docs/profiles.md +546 -0
- package/docs/settings.md +52 -0
- package/docs/tools-reference.md +519 -0
- package/package.json +59 -0
- package/src/cache.ts +24 -0
- package/src/commands/profile.ts +176 -0
- package/src/format-tool-call.ts +597 -0
- package/src/format-transcript.ts +151 -0
- package/src/index.ts +117 -0
- package/src/profile-editor.ts +356 -0
- package/src/profile-formatting.ts +178 -0
- package/src/profile-types.ts +73 -0
- package/src/profiles.ts +577 -0
- package/src/schemas.ts +65 -0
- package/src/settings.ts +155 -0
- package/src/skill-discovery.ts +30 -0
- package/src/spawner.ts +523 -0
- package/src/tools/delegate-render.ts +285 -0
- package/src/tools/delegate.ts +867 -0
- package/src/tools/retrieval.ts +287 -0
- package/src/types.ts +232 -0
- package/src/utils.ts +168 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegate Rendering Functions
|
|
3
|
+
*
|
|
4
|
+
* Pure rendering functions for the delegate_to_subagents tool,
|
|
5
|
+
* extracted for testability and reuse.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
9
|
+
import { countWindowStatuses } from "../utils";
|
|
10
|
+
import type { SubAgentWindow, WindowedSubagentDetails, WindowLine } from "../types";
|
|
11
|
+
import type {
|
|
12
|
+
AgentToolResult,
|
|
13
|
+
Theme,
|
|
14
|
+
ToolRenderResultOptions,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
// ── Tool-line colorizing helpers ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Apply theme colors to specific patterns in tool call lines.
|
|
21
|
+
* Colorizes diff stats (+N/-M) and line counts (N lines) while keeping
|
|
22
|
+
* the rest of the line in muted color.
|
|
23
|
+
*/
|
|
24
|
+
export function colorizeToolLine(line: string, theme: Theme): string {
|
|
25
|
+
return (
|
|
26
|
+
colorizeDiffStats(line, theme) ??
|
|
27
|
+
colorizeWriteCount(line, theme) ??
|
|
28
|
+
colorizeReadLineCount(line, theme) ??
|
|
29
|
+
colorizeInlineResult(line, theme) ??
|
|
30
|
+
colorizeResultSummary(line, theme) ??
|
|
31
|
+
theme.fg("muted", line)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pattern 1: +N/-M diff stats (edit tool) */
|
|
36
|
+
function colorizeDiffStats(line: string, theme: Theme): string | null {
|
|
37
|
+
const m = line.match(/^(.*?)(\+\d+\/-\d+)(.*)$/);
|
|
38
|
+
if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined) return null;
|
|
39
|
+
const prefix = m[1];
|
|
40
|
+
const stats = m[2];
|
|
41
|
+
const suffix = m[3];
|
|
42
|
+
const plusIdx = stats.indexOf("+");
|
|
43
|
+
const slashIdx = stats.indexOf("/");
|
|
44
|
+
const added = stats.substring(plusIdx, slashIdx);
|
|
45
|
+
const removed = stats.substring(slashIdx + 1);
|
|
46
|
+
return (
|
|
47
|
+
theme.fg("muted", prefix) +
|
|
48
|
+
theme.fg("toolDiffAdded", added) +
|
|
49
|
+
theme.fg("muted", "/") +
|
|
50
|
+
theme.fg("toolDiffRemoved", removed) +
|
|
51
|
+
theme.fg("muted", suffix)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Pattern 2: +N at end (write tool, no removal count) */
|
|
56
|
+
function colorizeWriteCount(line: string, theme: Theme): string | null {
|
|
57
|
+
if (!line.includes("write")) return null;
|
|
58
|
+
const m = line.match(/^(.*?)(\+\d+)(\s*)$/);
|
|
59
|
+
if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined) return null;
|
|
60
|
+
return theme.fg("muted", m[1]) + theme.fg("toolDiffAdded", m[2]) + m[3];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Pattern 3: (N lines) line count (read tool) */
|
|
64
|
+
function colorizeReadLineCount(line: string, theme: Theme): string | null {
|
|
65
|
+
const m = line.match(/^(.*?)(\()(\d+)( lines?\))(.*)$/);
|
|
66
|
+
if (
|
|
67
|
+
!m ||
|
|
68
|
+
m[1] === undefined ||
|
|
69
|
+
m[2] === undefined ||
|
|
70
|
+
m[3] === undefined ||
|
|
71
|
+
m[4] === undefined ||
|
|
72
|
+
m[5] === undefined
|
|
73
|
+
)
|
|
74
|
+
return null;
|
|
75
|
+
return (
|
|
76
|
+
theme.fg("muted", m[1]) +
|
|
77
|
+
theme.fg("muted", m[2]) +
|
|
78
|
+
theme.fg("toolDiffAdded", m[3]) +
|
|
79
|
+
theme.fg("muted", m[4]) +
|
|
80
|
+
theme.fg("muted", m[5])
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Pattern 3.5: Inline ls/find result summaries (→ ls → <path> → <summary>) */
|
|
85
|
+
function colorizeInlineResult(line: string, theme: Theme): string | null {
|
|
86
|
+
const m = line.match(/^(→ (?:ls|find) → .*? → )(\d+)(\s.*)$/);
|
|
87
|
+
if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined) return null;
|
|
88
|
+
const count = m[2];
|
|
89
|
+
if (count === "0") return theme.fg("muted", line);
|
|
90
|
+
return theme.fg("muted", m[1]) + theme.fg("toolDiffAdded", count) + theme.fg("muted", m[3]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Pattern 4: ls/find result summaries (indented lines with entry counts) */
|
|
94
|
+
function colorizeResultSummary(line: string, theme: Theme): string | null {
|
|
95
|
+
const m = line.match(/^(\s{2})(\d+)(\s.*)$/);
|
|
96
|
+
if (!m || m[1] === undefined || m[2] === undefined || m[3] === undefined) return null;
|
|
97
|
+
return theme.fg("muted", m[1]) + theme.fg("toolDiffAdded", m[2]) + theme.fg("muted", m[3]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Delegate tool call rendering ────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** Type for the renderCall args parameter (subset of DelegateParams) */
|
|
103
|
+
export interface DelegateToolArgs {
|
|
104
|
+
tasks?: Array<{ name?: string; prompt?: string; profile?: string }>;
|
|
105
|
+
profile?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Render the delegate_to_subagents tool call display.
|
|
110
|
+
*/
|
|
111
|
+
export function renderDelegateCall(
|
|
112
|
+
args: DelegateToolArgs,
|
|
113
|
+
theme: Theme,
|
|
114
|
+
_context: unknown,
|
|
115
|
+
): InstanceType<typeof Text> {
|
|
116
|
+
const count = args.tasks?.length ?? 1;
|
|
117
|
+
const taskProfiles = (args.tasks ?? []).map((t) => t.profile).filter(Boolean) as string[];
|
|
118
|
+
const defaultProfile = args.profile;
|
|
119
|
+
|
|
120
|
+
let text =
|
|
121
|
+
theme.fg("toolTitle", theme.bold("delegate_to_subagents ")) +
|
|
122
|
+
theme.fg("accent", `${count} sub-agent${count > 1 ? "s" : ""}`);
|
|
123
|
+
|
|
124
|
+
if (defaultProfile) {
|
|
125
|
+
text += theme.fg("dim", ` (default profile: ${defaultProfile})`);
|
|
126
|
+
}
|
|
127
|
+
if (taskProfiles.length > 0) {
|
|
128
|
+
text += theme.fg("dim", ` profiles: [${taskProfiles.join(", ")}]`);
|
|
129
|
+
}
|
|
130
|
+
return new Text(text, 0, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Delegate tool result rendering ──────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** Build the global status header line showing running/done/error counts. */
|
|
136
|
+
function buildStatusHeader(running: number, done: number, errors: number, theme: Theme): string {
|
|
137
|
+
let header = theme.fg("toolTitle", theme.bold("Sub-agents: "));
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
if (running > 0) {
|
|
140
|
+
parts.push(theme.fg("warning", `${running} running`));
|
|
141
|
+
}
|
|
142
|
+
if (done > 0) {
|
|
143
|
+
parts.push(theme.fg("success", `${done} done`));
|
|
144
|
+
}
|
|
145
|
+
if (errors > 0) {
|
|
146
|
+
parts.push(theme.fg("error", `${errors} error${errors > 1 ? "s" : ""}`));
|
|
147
|
+
}
|
|
148
|
+
header += parts.join(theme.fg("dim", ", "));
|
|
149
|
+
return header;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Build the per-agent header line with profile, tool count, file count, todos, and elapsed time. */
|
|
153
|
+
function buildWindowHeader(win: SubAgentWindow, theme: Theme): string {
|
|
154
|
+
const icon = win.status === "running" ? "⏳" : win.status === "error" ? "✗" : "✓";
|
|
155
|
+
const color = win.status === "running" ? "warning" : win.status === "error" ? "error" : "success";
|
|
156
|
+
|
|
157
|
+
let headerLine = `${theme.fg(color, icon)} ${theme.fg("accent", theme.bold(win.name))}`;
|
|
158
|
+
|
|
159
|
+
const headerParts: string[] = [];
|
|
160
|
+
if (win.profileName) {
|
|
161
|
+
headerParts.push(buildProfileSegment(win));
|
|
162
|
+
}
|
|
163
|
+
headerParts.push(`${win.toolCount} tools`);
|
|
164
|
+
if (win.fileCount > 0) {
|
|
165
|
+
headerParts.push(`${win.fileCount} files`);
|
|
166
|
+
}
|
|
167
|
+
if (win.todoTotal !== undefined && win.todoTotal > 0 && win.todoCompleted !== win.todoTotal) {
|
|
168
|
+
headerParts.push(`[${win.todoCompleted ?? 0}/${win.todoTotal}]`);
|
|
169
|
+
}
|
|
170
|
+
const endTime = win.completedAt ?? Date.now();
|
|
171
|
+
const elapsed = Math.floor((endTime - win.startedAt) / 1000);
|
|
172
|
+
headerParts.push(`${elapsed}s/${win.timeout}s`);
|
|
173
|
+
|
|
174
|
+
headerLine += theme.fg("dim", ` • ${headerParts.join(" • ")}`);
|
|
175
|
+
return headerLine;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Build the profile info segment for a window header. */
|
|
179
|
+
function buildProfileSegment(win: SubAgentWindow): string {
|
|
180
|
+
let profilePart = win.profileName ?? "";
|
|
181
|
+
const provModel = [win.provider, win.model].filter(Boolean).join("/");
|
|
182
|
+
if (provModel) {
|
|
183
|
+
profilePart += ` (${provModel}`;
|
|
184
|
+
}
|
|
185
|
+
if (win.thinkingLevel) {
|
|
186
|
+
profilePart += provModel ? ` ${win.thinkingLevel}` : ` (${win.thinkingLevel})`;
|
|
187
|
+
}
|
|
188
|
+
if (provModel) {
|
|
189
|
+
profilePart += ")";
|
|
190
|
+
}
|
|
191
|
+
return profilePart;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Add window message lines (expanded or collapsed) to the container. */
|
|
195
|
+
function addWindowMessages(
|
|
196
|
+
container: Container,
|
|
197
|
+
win: SubAgentWindow,
|
|
198
|
+
expanded: boolean,
|
|
199
|
+
theme: Theme,
|
|
200
|
+
): void {
|
|
201
|
+
const renderLine = (entry: WindowLine) => {
|
|
202
|
+
if (entry.kind === "tool") {
|
|
203
|
+
container.addChild(new Text(` ${colorizeToolLine(entry.text, theme)}`, 0, 0));
|
|
204
|
+
} else {
|
|
205
|
+
for (const line of entry.text.split("\n")) {
|
|
206
|
+
container.addChild(new Text(` ${line}`, 0, 0));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (expanded) {
|
|
212
|
+
if (win.allMessages.length === 0) {
|
|
213
|
+
container.addChild(new Text(theme.fg("muted", " (no output)"), 0, 0));
|
|
214
|
+
} else {
|
|
215
|
+
for (const entry of win.allMessages) {
|
|
216
|
+
renderLine(entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if (win.lines.length === 0) {
|
|
221
|
+
container.addChild(new Text(theme.fg("muted", " (starting...)"), 0, 0));
|
|
222
|
+
} else {
|
|
223
|
+
for (const entry of win.lines) {
|
|
224
|
+
renderLine(entry);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Add the footer section (running indicator or session IDs) to the container. */
|
|
231
|
+
function addFooter(
|
|
232
|
+
container: Container,
|
|
233
|
+
running: number,
|
|
234
|
+
windows: SubAgentWindow[],
|
|
235
|
+
theme: Theme,
|
|
236
|
+
): void {
|
|
237
|
+
if (running > 0) {
|
|
238
|
+
container.addChild(new Text(theme.fg("muted", `${running} running...`), 0, 0));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const idLines = windows.map((w) => ` ${w.name}: ${theme.fg("accent", w.sessionId)}`);
|
|
242
|
+
container.addChild(
|
|
243
|
+
new Text(theme.fg("dim", "Session IDs (use with get_subagent_output):"), 0, 0),
|
|
244
|
+
);
|
|
245
|
+
for (const line of idLines) {
|
|
246
|
+
container.addChild(new Text(` ${line}`, 0, 0));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render the delegate_to_subagents tool result display (live rolling window).
|
|
252
|
+
*/
|
|
253
|
+
export function renderDelegateResult(
|
|
254
|
+
result: AgentToolResult<WindowedSubagentDetails | undefined>,
|
|
255
|
+
options: ToolRenderResultOptions,
|
|
256
|
+
theme: Theme,
|
|
257
|
+
_context: unknown,
|
|
258
|
+
): Container | Text {
|
|
259
|
+
const details = result.details;
|
|
260
|
+
if (!details) {
|
|
261
|
+
return new Text("(no sub-agent details)", 0, 0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const container = new Container();
|
|
265
|
+
const { running, completed: done, error: errors } = countWindowStatuses(details.windows);
|
|
266
|
+
|
|
267
|
+
// ── Global status header ──
|
|
268
|
+
container.addChild(new Text(buildStatusHeader(running, done, errors, theme), 0, 0));
|
|
269
|
+
container.addChild(new Spacer(1));
|
|
270
|
+
|
|
271
|
+
// ── Per-agent windows ──
|
|
272
|
+
for (const win of details.windows) {
|
|
273
|
+
container.addChild(new Text(buildWindowHeader(win, theme), 0, 0));
|
|
274
|
+
addWindowMessages(container, win, options.expanded, theme);
|
|
275
|
+
if (win.status === "error" && win.errorMessage) {
|
|
276
|
+
container.addChild(new Text(theme.fg("error", ` Error: ${win.errorMessage}`), 0, 0));
|
|
277
|
+
}
|
|
278
|
+
container.addChild(new Spacer(1));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Footer ──
|
|
282
|
+
addFooter(container, running, details.windows, theme);
|
|
283
|
+
|
|
284
|
+
return container;
|
|
285
|
+
}
|