@yandy0725/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/README.md +155 -0
- package/README.zh.md +155 -0
- package/index.ts +1 -0
- package/package.json +49 -0
- package/src/config/agent-types.ts +127 -0
- package/src/config/custom-agents.ts +109 -0
- package/src/config/default-agents.ts +117 -0
- package/src/config/invocation-config.ts +30 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/handlers/lifecycle.ts +63 -0
- package/src/handlers/tool-start.ts +32 -0
- package/src/index.ts +186 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +88 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +240 -0
- package/src/lifecycle/parent-snapshot.ts +45 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/subagent-manager.ts +353 -0
- package/src/lifecycle/subagent-session.ts +232 -0
- package/src/lifecycle/subagent-state.ts +216 -0
- package/src/lifecycle/subagent.ts +498 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +65 -0
- package/src/lifecycle/workspace-bracket.ts +59 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification-state.ts +27 -0
- package/src/observation/notification.ts +186 -0
- package/src/observation/record-observer.ts +75 -0
- package/src/observation/renderer.ts +63 -0
- package/src/observation/subagent-events-observer.ts +94 -0
- package/src/runtime.ts +77 -0
- package/src/service/service-adapter.ts +131 -0
- package/src/service/service.ts +123 -0
- package/src/session/content-items.ts +51 -0
- package/src/session/context.ts +78 -0
- package/src/session/conversation.ts +44 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +121 -0
- package/src/session/prompts.ts +83 -0
- package/src/session/session-config.ts +172 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +227 -0
- package/src/tools/agent-tool.ts +220 -0
- package/src/tools/background-spawner.ts +66 -0
- package/src/tools/foreground-runner.ts +114 -0
- package/src/tools/get-result-tool.ts +120 -0
- package/src/tools/helpers.ts +105 -0
- package/src/tools/result-renderer.ts +109 -0
- package/src/tools/spawn-config.ts +150 -0
- package/src/tools/steer-tool.ts +90 -0
- package/src/types.ts +115 -0
- package/src/ui/agent-widget.ts +311 -0
- package/src/ui/display.ts +174 -0
- package/src/ui/session-navigation.ts +147 -0
- package/src/ui/session-navigator.ts +406 -0
- package/src/ui/subagents-settings.ts +77 -0
- package/src/ui/widget-renderer.ts +296 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* widget-renderer.ts — Pure rendering functions for the agent widget.
|
|
3
|
+
*
|
|
4
|
+
* All functions are stateless: they receive data and return formatted strings.
|
|
5
|
+
* No timers, no SDK types, no side effects. Consumed by AgentWidget.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import type { AgentConfigLookup } from "../config/agent-types";
|
|
10
|
+
import type { LifetimeUsage } from "../lifecycle/usage";
|
|
11
|
+
import { getLifetimeTotal } from "../lifecycle/usage";
|
|
12
|
+
import type { SubagentType } from "../types";
|
|
13
|
+
import {
|
|
14
|
+
describeActivity,
|
|
15
|
+
formatMs,
|
|
16
|
+
formatSessionTokens,
|
|
17
|
+
formatTurns,
|
|
18
|
+
getDisplayName,
|
|
19
|
+
getPromptModeLabel,
|
|
20
|
+
SPINNER,
|
|
21
|
+
type Theme,
|
|
22
|
+
} from "../ui/display";
|
|
23
|
+
|
|
24
|
+
// ── Data interfaces ──────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Minimal agent snapshot for rendering — no class methods, no mutation surface. */
|
|
27
|
+
export interface WidgetAgent {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly type: SubagentType;
|
|
30
|
+
readonly status: string;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly toolUses: number;
|
|
33
|
+
readonly startedAt: number;
|
|
34
|
+
readonly completedAt?: number;
|
|
35
|
+
readonly error?: string;
|
|
36
|
+
readonly lifetimeUsage?: Readonly<LifetimeUsage>;
|
|
37
|
+
readonly compactionCount: number;
|
|
38
|
+
// Live activity (folded from the former WidgetActivity — precomputed by AgentWidget)
|
|
39
|
+
readonly turnCount: number;
|
|
40
|
+
readonly maxTurns?: number;
|
|
41
|
+
readonly activeTools: ReadonlyMap<string, string>;
|
|
42
|
+
readonly responseText: string;
|
|
43
|
+
/** Context-window utilisation (0–100), or null when unavailable. */
|
|
44
|
+
readonly contextPercent: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Per-agent rendering ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Render a single finished agent line (no tree connector prefix). */
|
|
50
|
+
export function renderFinishedLine(agent: WidgetAgent, registry: AgentConfigLookup, theme: Theme): string {
|
|
51
|
+
const name = getDisplayName(agent.type, registry);
|
|
52
|
+
const modeLabel = getPromptModeLabel(agent.type, registry);
|
|
53
|
+
const duration = formatMs((agent.completedAt ?? Date.now()) - agent.startedAt);
|
|
54
|
+
|
|
55
|
+
let icon: string;
|
|
56
|
+
let statusText: string;
|
|
57
|
+
if (agent.status === "completed") {
|
|
58
|
+
icon = theme.fg("success", "✓");
|
|
59
|
+
statusText = "";
|
|
60
|
+
} else if (agent.status === "steered") {
|
|
61
|
+
icon = theme.fg("warning", "✓");
|
|
62
|
+
statusText = theme.fg("warning", " (turn limit)");
|
|
63
|
+
} else if (agent.status === "stopped") {
|
|
64
|
+
icon = theme.fg("dim", "■");
|
|
65
|
+
statusText = theme.fg("dim", " stopped");
|
|
66
|
+
} else if (agent.status === "error") {
|
|
67
|
+
icon = theme.fg("error", "✗");
|
|
68
|
+
const errMsg = agent.error ? `: ${agent.error.slice(0, 60)}` : "";
|
|
69
|
+
statusText = theme.fg("error", ` error${errMsg}`);
|
|
70
|
+
} else {
|
|
71
|
+
// aborted
|
|
72
|
+
icon = theme.fg("error", "✗");
|
|
73
|
+
statusText = theme.fg("warning", " aborted");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
78
|
+
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
79
|
+
parts.push(duration);
|
|
80
|
+
|
|
81
|
+
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
82
|
+
return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", agent.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Render a single running agent as header + activity line pair (no tree connector prefix). */
|
|
86
|
+
export function renderRunningLines(
|
|
87
|
+
agent: WidgetAgent,
|
|
88
|
+
registry: AgentConfigLookup,
|
|
89
|
+
spinnerFrame: number,
|
|
90
|
+
theme: Theme,
|
|
91
|
+
): [header: string, activity: string] {
|
|
92
|
+
const name = getDisplayName(agent.type, registry);
|
|
93
|
+
const modeLabel = getPromptModeLabel(agent.type, registry);
|
|
94
|
+
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
95
|
+
const elapsed = formatMs(Date.now() - agent.startedAt);
|
|
96
|
+
|
|
97
|
+
const tokens = getLifetimeTotal(agent.lifetimeUsage);
|
|
98
|
+
const tokenText = tokens > 0 ? formatSessionTokens(tokens, agent.contextPercent, theme, agent.compactionCount) : "";
|
|
99
|
+
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
parts.push(formatTurns(agent.turnCount, agent.maxTurns));
|
|
102
|
+
if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool use${agent.toolUses === 1 ? "" : "s"}`);
|
|
103
|
+
if (tokenText) parts.push(tokenText);
|
|
104
|
+
parts.push(elapsed);
|
|
105
|
+
const statsText = parts.join(" · ");
|
|
106
|
+
|
|
107
|
+
const frame = SPINNER[spinnerFrame % SPINNER.length];
|
|
108
|
+
const activityText = describeActivity(agent.activeTools, agent.responseText);
|
|
109
|
+
|
|
110
|
+
const header = `${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", agent.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`;
|
|
111
|
+
const activityLine = theme.fg("dim", ` \u23BF ${activityText}`);
|
|
112
|
+
|
|
113
|
+
return [header, activityLine];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Full widget rendering ────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
119
|
+
const MAX_WIDGET_LINES = 12;
|
|
120
|
+
|
|
121
|
+
interface AgentCategories {
|
|
122
|
+
running: WidgetAgent[];
|
|
123
|
+
queued: WidgetAgent[];
|
|
124
|
+
finished: WidgetAgent[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Partition agents into rendering buckets. */
|
|
128
|
+
function categorizeAgents(
|
|
129
|
+
agents: readonly WidgetAgent[],
|
|
130
|
+
shouldShowFinished: (agentId: string, status: string) => boolean,
|
|
131
|
+
): AgentCategories {
|
|
132
|
+
return {
|
|
133
|
+
running: agents.filter((a) => a.status === "running"),
|
|
134
|
+
queued: agents.filter((a) => a.status === "queued"),
|
|
135
|
+
finished: agents.filter(
|
|
136
|
+
(a) =>
|
|
137
|
+
a.status !== "running" && a.status !== "queued" && a.completedAt != null && shouldShowFinished(a.id, a.status),
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface WidgetSections {
|
|
143
|
+
finishedLines: string[];
|
|
144
|
+
runningLines: [string, string][];
|
|
145
|
+
queuedLine: string | undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
|
|
149
|
+
function buildSections(
|
|
150
|
+
categories: AgentCategories,
|
|
151
|
+
registry: AgentConfigLookup,
|
|
152
|
+
spinnerFrame: number,
|
|
153
|
+
theme: Theme,
|
|
154
|
+
truncate: (line: string) => string,
|
|
155
|
+
): WidgetSections {
|
|
156
|
+
const finishedLines: string[] = [];
|
|
157
|
+
for (const a of categories.finished) {
|
|
158
|
+
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, registry, theme)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const runningLines: [string, string][] = [];
|
|
162
|
+
for (const a of categories.running) {
|
|
163
|
+
const [header, act] = renderRunningLines(a, registry, spinnerFrame, theme);
|
|
164
|
+
runningLines.push([
|
|
165
|
+
truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
|
|
166
|
+
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const queuedLine =
|
|
171
|
+
categories.queued.length > 0
|
|
172
|
+
? truncate(
|
|
173
|
+
theme.fg("dim", "\u251C\u2500") +
|
|
174
|
+
` ${theme.fg("muted", "\u25E6")} ${theme.fg("dim", `${categories.queued.length} queued`)}`,
|
|
175
|
+
)
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
return { finishedLines, runningLines, queuedLine };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Assemble widget lines when total body fits within MAX_WIDGET_LINES.
|
|
183
|
+
* Fixes the last tree connector: ├─ → └─, and │ → space for the running-agent activity line.
|
|
184
|
+
*/
|
|
185
|
+
function assembleWithinBudget(heading: string, sections: WidgetSections): string[] {
|
|
186
|
+
const { finishedLines, runningLines, queuedLine } = sections;
|
|
187
|
+
const lines: string[] = [heading, ...finishedLines];
|
|
188
|
+
for (const pair of runningLines) lines.push(...pair);
|
|
189
|
+
if (queuedLine) lines.push(queuedLine);
|
|
190
|
+
|
|
191
|
+
// Fix last connector: swap \u251C\u2500 \u2192 \u2514\u2500.
|
|
192
|
+
if (lines.length > 1) {
|
|
193
|
+
const last = lines.length - 1;
|
|
194
|
+
lines[last] = lines[last].replace("\u251C\u2500", "\u2514\u2500");
|
|
195
|
+
if (runningLines.length > 0 && !queuedLine) {
|
|
196
|
+
if (last >= 2) {
|
|
197
|
+
lines[last - 1] = lines[last - 1].replace("\u251C\u2500", "\u2514\u2500");
|
|
198
|
+
lines[last] = lines[last].replace("\u2502 ", " ");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Assemble widget lines when total body exceeds MAX_WIDGET_LINES.
|
|
207
|
+
* Prioritizes running > queued > finished and appends an overflow indicator.
|
|
208
|
+
*/
|
|
209
|
+
function assembleOverflow(
|
|
210
|
+
heading: string,
|
|
211
|
+
sections: WidgetSections,
|
|
212
|
+
maxBody: number,
|
|
213
|
+
truncate: (line: string) => string,
|
|
214
|
+
theme: Theme,
|
|
215
|
+
): string[] {
|
|
216
|
+
const { finishedLines, runningLines, queuedLine } = sections;
|
|
217
|
+
const lines: string[] = [heading];
|
|
218
|
+
let budget = maxBody - 1;
|
|
219
|
+
let hiddenRunning = 0;
|
|
220
|
+
let hiddenFinished = 0;
|
|
221
|
+
|
|
222
|
+
for (const pair of runningLines) {
|
|
223
|
+
if (budget >= 2) {
|
|
224
|
+
lines.push(...pair);
|
|
225
|
+
budget -= 2;
|
|
226
|
+
} else {
|
|
227
|
+
hiddenRunning++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (queuedLine && budget >= 1) {
|
|
232
|
+
lines.push(queuedLine);
|
|
233
|
+
budget--;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const fl of finishedLines) {
|
|
237
|
+
if (budget >= 1) {
|
|
238
|
+
lines.push(fl);
|
|
239
|
+
budget--;
|
|
240
|
+
} else {
|
|
241
|
+
hiddenFinished++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const overflowParts: string[] = [];
|
|
246
|
+
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
|
|
247
|
+
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
|
|
248
|
+
const overflowText = overflowParts.join(", ");
|
|
249
|
+
lines.push(
|
|
250
|
+
truncate(
|
|
251
|
+
theme.fg("dim", "\u2514\u2500") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Pure rendering of the widget body. Returns lines to display. */
|
|
258
|
+
export function renderWidgetLines(params: {
|
|
259
|
+
agents: readonly WidgetAgent[];
|
|
260
|
+
registry: AgentConfigLookup;
|
|
261
|
+
spinnerFrame: number;
|
|
262
|
+
terminalWidth: number;
|
|
263
|
+
theme: Theme;
|
|
264
|
+
shouldShowFinished: (agentId: string, status: string) => boolean;
|
|
265
|
+
}): string[] {
|
|
266
|
+
const { agents, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
|
|
267
|
+
|
|
268
|
+
const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
|
|
269
|
+
|
|
270
|
+
const hasActive = running.length > 0 || queued.length > 0;
|
|
271
|
+
const hasFinished = finished.length > 0;
|
|
272
|
+
|
|
273
|
+
if (!hasActive && !hasFinished) return [];
|
|
274
|
+
|
|
275
|
+
const truncate = (line: string) => truncateToWidth(line, terminalWidth);
|
|
276
|
+
const headingColor = hasActive ? "accent" : "dim";
|
|
277
|
+
const headingIcon = hasActive ? "\u25CF" : "\u25CB";
|
|
278
|
+
|
|
279
|
+
const { finishedLines, runningLines, queuedLine } = buildSections(
|
|
280
|
+
{ running, queued, finished },
|
|
281
|
+
registry,
|
|
282
|
+
spinnerFrame,
|
|
283
|
+
theme,
|
|
284
|
+
truncate,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Assemble with overflow cap (heading takes 1 line).
|
|
288
|
+
const maxBody = MAX_WIDGET_LINES - 1;
|
|
289
|
+
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
290
|
+
const heading = truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"));
|
|
291
|
+
|
|
292
|
+
if (totalBody <= maxBody) {
|
|
293
|
+
return assembleWithinBudget(heading, { finishedLines, runningLines, queuedLine });
|
|
294
|
+
}
|
|
295
|
+
return assembleOverflow(heading, { finishedLines, runningLines, queuedLine }, maxBody, truncate, theme);
|
|
296
|
+
}
|