pi-subagents 0.18.0 → 0.19.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 +24 -0
- package/README.md +4 -4
- package/agent-management.ts +11 -2
- package/agent-manager-chain-detail.ts +50 -6
- package/agent-manager-detail.ts +15 -2
- package/agent-manager.ts +76 -23
- package/async-execution.ts +47 -20
- package/chain-execution.ts +12 -2
- package/execution.ts +8 -10
- package/index.ts +104 -11
- package/intercom-bridge.ts +3 -3
- package/notify.ts +20 -10
- package/package.json +5 -1
- package/pi-args.ts +1 -0
- package/prompts/parallel-review.md +8 -0
- package/render.ts +371 -57
- package/schemas.ts +52 -14
- package/settings.ts +5 -0
- package/skills.ts +61 -0
- package/slash-commands.ts +82 -39
- package/slash-live-state.ts +3 -5
- package/subagent-executor.ts +110 -24
- package/subagent-runner.ts +8 -0
- package/subagents-status.ts +216 -2
- package/worktree.ts +19 -10
package/render.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
WIDGET_KEY,
|
|
14
14
|
} from "./types.ts";
|
|
15
15
|
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.ts";
|
|
16
|
-
import { getDisplayItems, getLastActivity,
|
|
16
|
+
import { getDisplayItems, getLastActivity, getSingleResultOutput } from "./utils.ts";
|
|
17
17
|
|
|
18
18
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
19
19
|
|
|
@@ -79,12 +79,49 @@ function truncLine(text: string, maxWidth: number): string {
|
|
|
79
79
|
return result + activeStyles.join("") + "…";
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
83
|
+
const WIDGET_ANIMATION_MS = 80;
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
86
|
+
let latestWidgetCtx: ExtensionContext | undefined;
|
|
87
|
+
let latestWidgetJobs: AsyncJobState[] = [];
|
|
88
|
+
|
|
89
|
+
const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationContext["state"]>();
|
|
90
|
+
const outputActivityCache = new Map<string, { checkedAt: number; text: string }>();
|
|
91
|
+
|
|
92
|
+
export interface ResultAnimationContext {
|
|
93
|
+
state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
|
|
94
|
+
invalidate: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function spinnerFrame(): string {
|
|
98
|
+
return SPINNER[Math.floor(Date.now() / WIDGET_ANIMATION_MS) % SPINNER.length]!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resultIsRunning(result: AgentToolResult<Details>): boolean {
|
|
102
|
+
return result.details?.progress?.some((entry) => entry.status === "running")
|
|
103
|
+
|| result.details?.results.some((entry) => entry.progress?.status === "running")
|
|
104
|
+
|| false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stopResultAnimation(context: ResultAnimationContext): void {
|
|
108
|
+
const timer = context.state.subagentResultAnimationTimer;
|
|
109
|
+
if (!timer) return;
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
resultAnimationTimers.delete(timer);
|
|
112
|
+
context.state.subagentResultAnimationTimer = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
|
|
116
|
+
if (!resultIsRunning(result)) {
|
|
117
|
+
stopResultAnimation(context);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (context.state.subagentResultAnimationTimer) return;
|
|
121
|
+
const timer = setInterval(() => context.invalidate(), WIDGET_ANIMATION_MS);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
context.state.subagentResultAnimationTimer = timer;
|
|
124
|
+
resultAnimationTimers.set(timer, context.state);
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
function extractOutputTarget(task: string): string | undefined {
|
|
@@ -146,67 +183,340 @@ function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "la
|
|
|
146
183
|
return formatActivityLabel(progress.lastActivityAt, progress.activityState === "needs_attention");
|
|
147
184
|
}
|
|
148
185
|
|
|
186
|
+
function themeBold(theme: Theme, text: string): string {
|
|
187
|
+
return ((theme as { bold?: (value: string) => string }).bold?.(text)) ?? text;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function statJoin(theme: Theme, parts: string[]): string {
|
|
191
|
+
return parts.filter(Boolean).map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatTokenStat(tokens: number): string {
|
|
195
|
+
return `${formatTokens(tokens)} token`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatToolUseStat(count: number): string {
|
|
199
|
+
return `${count} tool use${count === 1 ? "" : "s"}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
|
|
203
|
+
if (!progress) return "";
|
|
204
|
+
const parts: string[] = [];
|
|
205
|
+
if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
|
|
206
|
+
if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
|
|
207
|
+
if (includeDuration && progress.durationMs > 0) parts.push(formatDuration(progress.durationMs));
|
|
208
|
+
return statJoin(theme, parts);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function firstOutputLine(text: string): string {
|
|
212
|
+
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
216
|
+
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
217
|
+
if (result.interrupted) return "Paused";
|
|
218
|
+
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
219
|
+
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
220
|
+
return "Done";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running"): string {
|
|
224
|
+
if (running) return theme.fg("accent", spinnerFrame());
|
|
225
|
+
if (result.detached) return theme.fg("warning", "■");
|
|
226
|
+
if (result.interrupted) return theme.fg("warning", "■");
|
|
227
|
+
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
228
|
+
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
229
|
+
return theme.fg("success", "✓");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function compactCurrentActivity(progress: AgentProgress): string {
|
|
233
|
+
return formatCurrentToolLine(progress, getTermWidth() - 4, false) ?? buildLiveStatusLine(progress) ?? "thinking…";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
|
|
237
|
+
return jobs.some((job) => job.status === "running");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function widgetJobName(job: AsyncJobState): string {
|
|
241
|
+
if (job.agents?.length) return job.agents.join(" → ");
|
|
242
|
+
return job.mode ?? "subagent";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getCachedLastActivity(outputFile: string | undefined): string {
|
|
246
|
+
if (!outputFile) return "";
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const cached = outputActivityCache.get(outputFile);
|
|
249
|
+
if (cached && now - cached.checkedAt < 1000) return cached.text;
|
|
250
|
+
const text = getLastActivity(outputFile);
|
|
251
|
+
outputActivityCache.set(outputFile, { checkedAt: now, text });
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function widgetActivity(job: AsyncJobState): string {
|
|
256
|
+
if (job.currentTool && job.currentToolStartedAt !== undefined) {
|
|
257
|
+
return `${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`;
|
|
258
|
+
}
|
|
259
|
+
const activity = formatActivityLabel(job.lastActivityAt, job.activityState === "needs_attention")
|
|
260
|
+
?? (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
|
|
261
|
+
if (activity) return activity;
|
|
262
|
+
if (job.status === "queued") return "queued…";
|
|
263
|
+
if (job.status === "paused") return "Paused";
|
|
264
|
+
if (job.status === "failed") return "Failed";
|
|
265
|
+
return "Done";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
269
|
+
if (job.status === "running") return theme.fg("accent", spinnerFrame());
|
|
270
|
+
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
271
|
+
if (job.status === "complete") return theme.fg("success", "✓");
|
|
272
|
+
if (job.status === "paused") return theme.fg("warning", "■");
|
|
273
|
+
return theme.fg("error", "✗");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
277
|
+
const parts: string[] = [];
|
|
278
|
+
const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
279
|
+
if (job.currentStep !== undefined) parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
|
|
280
|
+
else if (stepsTotal > 1) parts.push(`steps ${stepsTotal}`);
|
|
281
|
+
if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
|
|
282
|
+
const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
|
|
283
|
+
if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
|
|
284
|
+
return statJoin(theme, parts);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth()): string[] {
|
|
288
|
+
if (jobs.length === 0) return [];
|
|
289
|
+
const running = jobs.filter((job) => job.status === "running");
|
|
290
|
+
const queued = jobs.filter((job) => job.status === "queued");
|
|
291
|
+
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
292
|
+
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
const hasActive = running.length > 0 || queued.length > 0;
|
|
295
|
+
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Agents")} ${theme.fg("dim", "· /subagents-status")}`, width));
|
|
296
|
+
|
|
297
|
+
const items: string[][] = [];
|
|
298
|
+
let hiddenRunning = 0;
|
|
299
|
+
let hiddenFinished = 0;
|
|
300
|
+
let queuedSummaryShown = false;
|
|
301
|
+
let slots = MAX_WIDGET_JOBS;
|
|
302
|
+
|
|
303
|
+
for (const job of running) {
|
|
304
|
+
if (slots <= 0) { hiddenRunning++; continue; }
|
|
305
|
+
const stats = widgetStats(job, theme);
|
|
306
|
+
items.push([
|
|
307
|
+
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
308
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
309
|
+
]);
|
|
310
|
+
slots--;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (queued.length > 0 && slots > 0) {
|
|
314
|
+
items.push([`${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`]);
|
|
315
|
+
queuedSummaryShown = true;
|
|
316
|
+
slots--;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const job of finished) {
|
|
320
|
+
if (slots <= 0) { hiddenFinished++; continue; }
|
|
321
|
+
const stats = widgetStats(job, theme);
|
|
322
|
+
items.push([
|
|
323
|
+
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
324
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
325
|
+
]);
|
|
326
|
+
slots--;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const hiddenQueued = queued.length > 0 && !queuedSummaryShown ? queued.length : 0;
|
|
330
|
+
const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
|
|
331
|
+
if (hiddenTotal > 0) {
|
|
332
|
+
const parts: string[] = [];
|
|
333
|
+
if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
|
|
334
|
+
if (hiddenQueued > 0) parts.push(`${hiddenQueued} queued`);
|
|
335
|
+
if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
|
|
336
|
+
items.push([theme.fg("dim", `+${hiddenTotal} more (${parts.join(", ")})`)]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let i = 0; i < items.length; i++) {
|
|
340
|
+
const item = items[i]!;
|
|
341
|
+
const last = i === items.length - 1;
|
|
342
|
+
const branch = last ? "└─" : "├─";
|
|
343
|
+
const continuation = last ? " " : "│ ";
|
|
344
|
+
lines.push(truncLine(`${theme.fg("dim", branch)} ${item[0]}`, width));
|
|
345
|
+
for (const detail of item.slice(1)) {
|
|
346
|
+
lines.push(truncLine(`${theme.fg("dim", continuation)} ${detail}`, width));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return lines;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function refreshAnimatedWidget(): void {
|
|
354
|
+
if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
|
|
355
|
+
latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetLines(latestWidgetJobs, latestWidgetCtx.ui.theme));
|
|
356
|
+
latestWidgetCtx.ui.requestRender?.();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ensureWidgetAnimation(): void {
|
|
360
|
+
if (widgetTimer) return;
|
|
361
|
+
widgetTimer = setInterval(() => {
|
|
362
|
+
if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
|
|
363
|
+
stopWidgetAnimation();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
refreshAnimatedWidget();
|
|
367
|
+
}, WIDGET_ANIMATION_MS);
|
|
368
|
+
widgetTimer.unref?.();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function stopWidgetAnimation(): void {
|
|
372
|
+
if (widgetTimer) {
|
|
373
|
+
clearInterval(widgetTimer);
|
|
374
|
+
widgetTimer = undefined;
|
|
375
|
+
}
|
|
376
|
+
latestWidgetCtx = undefined;
|
|
377
|
+
latestWidgetJobs = [];
|
|
378
|
+
outputActivityCache.clear();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function stopResultAnimations(): void {
|
|
382
|
+
for (const [timer, state] of resultAnimationTimers) {
|
|
383
|
+
clearInterval(timer);
|
|
384
|
+
state.subagentResultAnimationTimer = undefined;
|
|
385
|
+
}
|
|
386
|
+
resultAnimationTimers.clear();
|
|
387
|
+
}
|
|
388
|
+
|
|
149
389
|
/**
|
|
150
390
|
* Render the async jobs widget
|
|
151
391
|
*/
|
|
152
392
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
153
|
-
if (!ctx.hasUI) return;
|
|
154
393
|
if (jobs.length === 0) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
158
|
-
}
|
|
394
|
+
stopWidgetAnimation();
|
|
395
|
+
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
159
396
|
return;
|
|
160
397
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const hasRunningJobs = displayedJobs.some(job => job.status === "running");
|
|
164
|
-
const newHash = computeWidgetHash(jobs);
|
|
165
|
-
if (!hasRunningJobs && newHash === lastWidgetHash) {
|
|
398
|
+
if (!ctx.hasUI) {
|
|
399
|
+
stopWidgetAnimation();
|
|
166
400
|
return;
|
|
167
401
|
}
|
|
168
|
-
|
|
402
|
+
latestWidgetCtx = ctx;
|
|
403
|
+
latestWidgetJobs = [...jobs];
|
|
169
404
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
405
|
+
ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(jobs, ctx.ui.theme));
|
|
406
|
+
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
407
|
+
else stopWidgetAnimation();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
|
|
411
|
+
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
412
|
+
const progress = r.progress || r.progressSummary;
|
|
413
|
+
const isRunning = r.progress?.status === "running";
|
|
414
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
415
|
+
const stats = statJoin(theme, [
|
|
416
|
+
r.usage?.turns ? `⟳${r.usage.turns}` : "",
|
|
417
|
+
formatProgressStats(theme, progress),
|
|
418
|
+
]);
|
|
419
|
+
const c = new Container();
|
|
420
|
+
const width = getTermWidth() - 4;
|
|
421
|
+
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
422
|
+
|
|
423
|
+
if (isRunning && r.progress) {
|
|
424
|
+
const activity = compactCurrentActivity(r.progress);
|
|
425
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
426
|
+
const liveStatus = buildLiveStatusLine(r.progress);
|
|
427
|
+
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
428
|
+
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
429
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
430
|
+
return c;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
434
|
+
const preview = firstOutputLine(output);
|
|
435
|
+
if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
|
|
436
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
|
|
437
|
+
}
|
|
438
|
+
if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
|
|
439
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
440
|
+
if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
|
|
441
|
+
return c;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
445
|
+
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
446
|
+
|| d.results.some((r) => r.progress?.status === "running");
|
|
447
|
+
const ok = d.results.filter((r) =>
|
|
448
|
+
!r.interrupted
|
|
449
|
+
&& !r.detached
|
|
450
|
+
&& (r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running" && r.progress?.status !== "pending"))
|
|
451
|
+
).length;
|
|
452
|
+
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
|
|
453
|
+
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
|
|
454
|
+
let totalSummary = d.progressSummary;
|
|
455
|
+
if (!totalSummary) {
|
|
456
|
+
let sawProgress = false;
|
|
457
|
+
const summary = { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
458
|
+
for (const r of d.results) {
|
|
459
|
+
const prog = r.progress || r.progressSummary;
|
|
460
|
+
if (!prog) continue;
|
|
461
|
+
sawProgress = true;
|
|
462
|
+
summary.toolCount += prog.toolCount;
|
|
463
|
+
summary.tokens += prog.tokens;
|
|
464
|
+
summary.durationMs = d.mode === "chain" ? summary.durationMs + prog.durationMs : Math.max(summary.durationMs, prog.durationMs);
|
|
206
465
|
}
|
|
466
|
+
if (sawProgress) totalSummary = summary;
|
|
207
467
|
}
|
|
468
|
+
const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
|
|
469
|
+
const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
|
|
470
|
+
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : Math.min(totalCount, ok + (hasRunning ? 1 : 0));
|
|
471
|
+
const itemLabel = d.mode === "parallel" ? "agent" : "step";
|
|
472
|
+
const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
|
|
473
|
+
const stepInfo = hasRunning ? `${itemLabel} ${currentStep}/${totalCount}` : `${itemLabel} ${ok}/${totalCount}`;
|
|
474
|
+
const stats = statJoin(theme, [stepInfo, formatProgressStats(theme, totalSummary)]);
|
|
475
|
+
const glyph = hasRunning
|
|
476
|
+
? theme.fg("accent", spinnerFrame())
|
|
477
|
+
: failed
|
|
478
|
+
? theme.fg("error", "✗")
|
|
479
|
+
: paused
|
|
480
|
+
? theme.fg("warning", "■")
|
|
481
|
+
: theme.fg("success", "✓");
|
|
482
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
483
|
+
const c = new Container();
|
|
484
|
+
const width = getTermWidth() - 4;
|
|
485
|
+
c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
208
486
|
|
|
209
|
-
|
|
487
|
+
const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
|
|
488
|
+
const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
|
|
489
|
+
for (let i = 0; i < stepsToShow; i++) {
|
|
490
|
+
const r = d.results[i];
|
|
491
|
+
const agentName = useResultsDirectly ? (r?.agent || `${itemLabel}-${i + 1}`) : (d.chainAgents![i] || r?.agent || `${itemLabel}-${i + 1}`);
|
|
492
|
+
if (!r) {
|
|
493
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${itemTitle} ${i + 1}: ${agentName} · pending`), width), 0, 0));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const output = getSingleResultOutput(r);
|
|
497
|
+
const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
|
|
498
|
+
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
499
|
+
const rRunning = rProg && "status" in rProg && rProg.status === "running";
|
|
500
|
+
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
501
|
+
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
502
|
+
const stepStats = formatProgressStats(theme, rProg);
|
|
503
|
+
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning);
|
|
504
|
+
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
505
|
+
const line = `${glyph} ${itemTitle} ${stepNumber}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
506
|
+
c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
|
|
507
|
+
if (rRunning && rProg && "status" in rProg) {
|
|
508
|
+
const activity = compactCurrentActivity(rProg);
|
|
509
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
510
|
+
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
511
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
512
|
+
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
513
|
+
}
|
|
514
|
+
const outputTarget = extractOutputTarget(r.task);
|
|
515
|
+
if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
|
|
516
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
517
|
+
}
|
|
518
|
+
if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
|
|
519
|
+
return c;
|
|
210
520
|
}
|
|
211
521
|
|
|
212
522
|
/**
|
|
@@ -230,6 +540,7 @@ export function renderSubagentResult(
|
|
|
230
540
|
|
|
231
541
|
if (d.mode === "single" && d.results.length === 1) {
|
|
232
542
|
const r = d.results[0];
|
|
543
|
+
if (!expanded) return renderSingleCompact(d, r, theme);
|
|
233
544
|
const isRunning = r.progress?.status === "running";
|
|
234
545
|
const icon = isRunning
|
|
235
546
|
? theme.fg("warning", "running")
|
|
@@ -322,6 +633,8 @@ export function renderSubagentResult(
|
|
|
322
633
|
return c;
|
|
323
634
|
}
|
|
324
635
|
|
|
636
|
+
if (!expanded) return renderMultiCompact(d, theme);
|
|
637
|
+
|
|
325
638
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
326
639
|
|| d.results.some((r) => r.progress?.status === "running");
|
|
327
640
|
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
@@ -367,6 +680,7 @@ export function renderSubagentResult(
|
|
|
367
680
|
const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
|
|
368
681
|
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
|
|
369
682
|
const stepInfo = hasRunning ? ` ${currentStep}/${totalCount}` : ` ${ok}/${totalCount}`;
|
|
683
|
+
const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
|
|
370
684
|
|
|
371
685
|
const chainVis = d.chainAgents?.length && !hasParallelInChain
|
|
372
686
|
? d.chainAgents
|
|
@@ -418,7 +732,7 @@ export function renderSubagentResult(
|
|
|
418
732
|
: (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
|
|
419
733
|
|
|
420
734
|
if (!r) {
|
|
421
|
-
c.addChild(new Text(fit(theme.fg("dim", `
|
|
735
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${i + 1}: ${agentName}`)), 0, 0));
|
|
422
736
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
423
737
|
c.addChild(new Spacer(1));
|
|
424
738
|
continue;
|
|
@@ -441,8 +755,8 @@ export function renderSubagentResult(
|
|
|
441
755
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
442
756
|
const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
|
|
443
757
|
const stepHeader = rRunning
|
|
444
|
-
? `${statusIcon}
|
|
445
|
-
: `${statusIcon}
|
|
758
|
+
? `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
759
|
+
: `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
|
|
446
760
|
const toolCallLines = getToolCallLines(r, expanded);
|
|
447
761
|
c.addChild(new Text(fit(stepHeader), 0, 0));
|
|
448
762
|
|
package/schemas.ts
CHANGED
|
@@ -4,14 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const SkillOverride = Type.Unsafe({
|
|
8
|
+
type: ["string", "array", "boolean"],
|
|
9
|
+
items: { type: "string" },
|
|
10
|
+
description: "Skill name(s) to inject (comma-separated), array of strings, or boolean (false disables, true uses default)",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const OutputOverride = Type.Unsafe({
|
|
14
|
+
type: ["string", "boolean"],
|
|
15
|
+
description: "Output filename/path (string), or false to disable file output",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const ReadsOverride = Type.Unsafe({
|
|
19
|
+
type: ["array", "boolean"],
|
|
20
|
+
items: { type: "string" },
|
|
21
|
+
description: "Files to read before running (array of filenames), or false to disable",
|
|
22
|
+
});
|
|
9
23
|
|
|
10
24
|
export const TaskItem = Type.Object({
|
|
11
25
|
agent: Type.String(),
|
|
12
26
|
task: Type.String(),
|
|
13
27
|
cwd: Type.Optional(Type.String()),
|
|
14
28
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
29
|
+
output: Type.Optional(OutputOverride),
|
|
30
|
+
reads: Type.Optional(ReadsOverride),
|
|
31
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
15
32
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
16
33
|
skill: Type.Optional(SkillOverride),
|
|
17
34
|
});
|
|
@@ -23,8 +40,8 @@ export const SequentialStepSchema = Type.Object({
|
|
|
23
40
|
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
24
41
|
})),
|
|
25
42
|
cwd: Type.Optional(Type.String()),
|
|
26
|
-
output: Type.Optional(
|
|
27
|
-
reads: Type.Optional(
|
|
43
|
+
output: Type.Optional(OutputOverride),
|
|
44
|
+
reads: Type.Optional(ReadsOverride),
|
|
28
45
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
29
46
|
skill: Type.Optional(SkillOverride),
|
|
30
47
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
@@ -36,8 +53,8 @@ export const ParallelTaskSchema = Type.Object({
|
|
|
36
53
|
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
37
54
|
cwd: Type.Optional(Type.String()),
|
|
38
55
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
39
|
-
output: Type.Optional(
|
|
40
|
-
reads: Type.Optional(
|
|
56
|
+
output: Type.Optional(OutputOverride),
|
|
57
|
+
reads: Type.Optional(ReadsOverride),
|
|
41
58
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
42
59
|
skill: Type.Optional(SkillOverride),
|
|
43
60
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
@@ -53,9 +70,25 @@ export const ParallelStepSchema = Type.Object({
|
|
|
53
70
|
})),
|
|
54
71
|
});
|
|
55
72
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
// Flattened so providers that reject anyOf/oneOf can still accept either sequential or parallel steps.
|
|
74
|
+
export const ChainItem = Type.Object({
|
|
75
|
+
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
76
|
+
task: Type.Optional(Type.String({
|
|
77
|
+
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
78
|
+
})),
|
|
79
|
+
cwd: Type.Optional(Type.String()),
|
|
80
|
+
output: Type.Optional(OutputOverride),
|
|
81
|
+
reads: Type.Optional(ReadsOverride),
|
|
82
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
83
|
+
skill: Type.Optional(SkillOverride),
|
|
84
|
+
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
85
|
+
parallel: Type.Optional(Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" })),
|
|
86
|
+
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
87
|
+
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
88
|
+
worktree: Type.Optional(Type.Boolean({
|
|
89
|
+
description: "Create isolated git worktrees for each parallel task."
|
|
90
|
+
})),
|
|
91
|
+
}, { description: "Chain step: use {agent, task?, ...} for sequential or {parallel: [...]} for concurrent execution" });
|
|
59
92
|
|
|
60
93
|
export const ControlOverrides = Type.Object({
|
|
61
94
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
@@ -70,7 +103,7 @@ export const ControlOverrides = Type.Object({
|
|
|
70
103
|
|
|
71
104
|
export const SubagentParams = Type.Object({
|
|
72
105
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
73
|
-
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
106
|
+
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
74
107
|
// Management action (when present, tool operates in management mode)
|
|
75
108
|
action: Type.Optional(Type.String({
|
|
76
109
|
description: "Action: management ('list','get','create','update','delete') or control ('status','interrupt'). Omit for execution mode."
|
|
@@ -89,10 +122,12 @@ export const SubagentParams = Type.Object({
|
|
|
89
122
|
description: "Chain name for get/update/delete management actions"
|
|
90
123
|
})),
|
|
91
124
|
// Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
|
|
92
|
-
config: Type.Optional(Type.
|
|
93
|
-
|
|
125
|
+
config: Type.Optional(Type.Unsafe({
|
|
126
|
+
type: ["object", "string"],
|
|
127
|
+
additionalProperties: true,
|
|
128
|
+
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
94
129
|
})),
|
|
95
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
|
|
130
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, reads?, progress?}, ...]" })),
|
|
96
131
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
97
132
|
worktree: Type.Optional(Type.Boolean({
|
|
98
133
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
@@ -118,7 +153,10 @@ export const SubagentParams = Type.Object({
|
|
|
118
153
|
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
|
|
119
154
|
control: Type.Optional(ControlOverrides),
|
|
120
155
|
// Solo agent overrides
|
|
121
|
-
output: Type.Optional(Type.
|
|
156
|
+
output: Type.Optional(Type.Unsafe({
|
|
157
|
+
type: ["string", "boolean"],
|
|
158
|
+
description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
|
|
159
|
+
})),
|
|
122
160
|
skill: Type.Optional(SkillOverride),
|
|
123
161
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
124
162
|
});
|
package/settings.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentConfig } from "./agents.ts";
|
|
|
8
8
|
import { normalizeSkillInput } from "./skills.ts";
|
|
9
9
|
import { CHAIN_RUNS_DIR } from "./types.ts";
|
|
10
10
|
const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
+
const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
|
|
11
12
|
|
|
12
13
|
// =============================================================================
|
|
13
14
|
// Behavior Resolution Types
|
|
@@ -224,6 +225,10 @@ function resolveChainPath(filePath: string, chainDir: string): string {
|
|
|
224
225
|
* Build chain instructions from resolved behavior.
|
|
225
226
|
* These are appended to the task to tell the agent what to read/write.
|
|
226
227
|
*/
|
|
228
|
+
export function writeInitialProgressFile(progressDir: string): void {
|
|
229
|
+
fs.writeFileSync(path.join(progressDir, "progress.md"), INITIAL_PROGRESS_CONTENT);
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
export function buildChainInstructions(
|
|
228
233
|
behavior: ResolvedStepBehavior,
|
|
229
234
|
chainDir: string,
|