pi-subagents 0.3.0 → 0.3.1

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/index.ts CHANGED
@@ -12,904 +12,41 @@
12
12
  * { "asyncByDefault": true }
13
13
  */
14
14
 
15
- import { spawn } from "node:child_process";
16
15
  import { randomUUID } from "node:crypto";
17
16
  import * as fs from "node:fs";
18
- import { createRequire } from "node:module";
19
17
  import * as os from "node:os";
20
18
  import * as path from "node:path";
21
- import { fileURLToPath } from "node:url";
22
- import type { AgentToolResult } from "@mariozechner/pi-agent-core";
23
- import type { Message } from "@mariozechner/pi-ai";
24
- import { type ExtensionAPI, type ExtensionContext, type ToolDefinition, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
25
- import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
26
- import { Type } from "@sinclair/typebox";
19
+ import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
20
+ import { Text } from "@mariozechner/pi-tui";
27
21
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
28
- import {
29
- loadSubagentSettings,
30
- resolveChainTemplatesV2,
31
- createChainDir,
32
- removeChainDir,
33
- cleanupOldChainDirs,
34
- resolveStepBehavior,
35
- resolveParallelBehaviors,
36
- buildChainInstructions,
37
- createParallelDirs,
38
- aggregateParallelOutputs,
39
- isParallelStep,
40
- isSequentialStep,
41
- getStepAgents,
42
- type StepOverrides,
43
- type ChainStep,
44
- type SequentialStep,
45
- type ParallelStep,
46
- type ParallelTaskResult,
47
- type ResolvedTemplates,
48
- } from "./settings.js";
49
- import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.js";
50
- import {
51
- appendJsonl,
52
- cleanupOldArtifacts,
53
- ensureArtifactsDir,
54
- getArtifactPaths,
55
- getArtifactsDir,
56
- writeArtifact,
57
- writeMetadata,
58
- } from "./artifacts.js";
22
+ import { cleanupOldChainDirs, getStepAgents, isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
23
+ import { cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
59
24
  import {
60
25
  type AgentProgress,
61
26
  type ArtifactConfig,
62
27
  type ArtifactPaths,
28
+ type AsyncJobState,
29
+ type Details,
30
+ type ExtensionConfig,
31
+ type SingleResult,
32
+ ASYNC_DIR,
63
33
  DEFAULT_ARTIFACT_CONFIG,
64
34
  DEFAULT_MAX_OUTPUT,
65
- type MaxOutputConfig,
66
- type ProgressSummary,
67
- type TruncationResult,
68
- truncateOutput,
35
+ MAX_CONCURRENCY,
36
+ MAX_PARALLEL,
37
+ POLL_INTERVAL_MS,
38
+ RESULTS_DIR,
39
+ WIDGET_KEY,
69
40
  } from "./types.js";
41
+ import { formatDuration } from "./formatters.js";
42
+ import { readStatus, findByPrefix, getFinalOutput, mapConcurrent } from "./utils.js";
43
+ import { runSync } from "./execution.js";
44
+ import { renderWidget, renderSubagentResult } from "./render.js";
45
+ import { SubagentParams, StatusParams } from "./schemas.js";
46
+ import { executeChain } from "./chain-execution.js";
47
+ import { isAsyncAvailable, executeAsyncChain, executeAsyncSingle } from "./async-execution.js";
70
48
 
71
- const MAX_PARALLEL = 8;
72
- const MAX_CONCURRENCY = 4;
73
- const COLLAPSED_ITEMS = 8;
74
- const RESULTS_DIR = "/tmp/pi-async-subagent-results";
75
- const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
76
- const WIDGET_KEY = "subagent-async";
77
- const POLL_INTERVAL_MS = 1000;
78
- const MAX_WIDGET_JOBS = 4;
79
-
80
- const require = createRequire(import.meta.url);
81
- const jitiCliPath: string | undefined = (() => {
82
- try {
83
- return path.join(path.dirname(require.resolve("jiti/package.json")), "lib/jiti-cli.mjs");
84
- } catch {
85
- return undefined;
86
- }
87
- })();
88
-
89
- interface Usage {
90
- input: number;
91
- output: number;
92
- cacheRead: number;
93
- cacheWrite: number;
94
- cost: number;
95
- turns: number;
96
- }
97
-
98
- interface SingleResult {
99
- agent: string;
100
- task: string;
101
- exitCode: number;
102
- messages: Message[];
103
- usage: Usage;
104
- model?: string;
105
- error?: string;
106
- sessionFile?: string;
107
- // Sharing disabled - module resolution issues
108
- // shareUrl?: string;
109
- // gistUrl?: string;
110
- // shareError?: string;
111
- progress?: AgentProgress;
112
- progressSummary?: ProgressSummary;
113
- artifactPaths?: ArtifactPaths;
114
- truncation?: TruncationResult;
115
- }
116
-
117
- interface Details {
118
- mode: "single" | "parallel" | "chain";
119
- results: SingleResult[];
120
- asyncId?: string;
121
- asyncDir?: string;
122
- progress?: AgentProgress[];
123
- progressSummary?: ProgressSummary;
124
- artifacts?: {
125
- dir: string;
126
- files: ArtifactPaths[];
127
- };
128
- truncation?: {
129
- truncated: boolean;
130
- originalBytes?: number;
131
- originalLines?: number;
132
- artifactPath?: string;
133
- };
134
- // Chain metadata for observability
135
- chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
136
- totalSteps?: number; // Total steps in chain
137
- currentStepIndex?: number; // 0-indexed current step (for running chains)
138
- }
139
-
140
- type DisplayItem = { type: "text"; text: string } | { type: "tool"; name: string; args: Record<string, unknown> };
141
-
142
- interface TokenUsage {
143
- input: number;
144
- output: number;
145
- total: number;
146
- }
147
-
148
- interface AsyncStatus {
149
- runId: string;
150
- mode: "single" | "chain";
151
- state: "queued" | "running" | "complete" | "failed";
152
- startedAt: number;
153
- endedAt?: number;
154
- lastUpdate?: number;
155
- currentStep?: number;
156
- steps?: Array<{ agent: string; status: string; durationMs?: number; tokens?: TokenUsage }>;
157
- sessionDir?: string;
158
- outputFile?: string;
159
- totalTokens?: TokenUsage;
160
- sessionFile?: string;
161
- // Sharing disabled
162
- // shareUrl?: string;
163
- // shareError?: string;
164
- }
165
-
166
- interface AsyncJobState {
167
- asyncId: string;
168
- asyncDir: string;
169
- status: "queued" | "running" | "complete" | "failed";
170
- mode?: "single" | "chain";
171
- agents?: string[];
172
- currentStep?: number;
173
- stepsTotal?: number;
174
- startedAt?: number;
175
- updatedAt?: number;
176
- sessionDir?: string;
177
- outputFile?: string;
178
- totalTokens?: TokenUsage;
179
- sessionFile?: string;
180
- // shareUrl?: string;
181
- }
182
-
183
- function formatTokens(n: number): string {
184
- return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
185
- }
186
-
187
- function formatUsage(u: Usage, model?: string): string {
188
- const parts: string[] = [];
189
- if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
190
- if (u.input) parts.push(`in:${formatTokens(u.input)}`);
191
- if (u.output) parts.push(`out:${formatTokens(u.output)}`);
192
- if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
193
- if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
194
- if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
195
- if (model) parts.push(model);
196
- return parts.join(" ");
197
- }
198
-
199
- function formatDuration(ms: number): string {
200
- if (ms < 1000) return `${ms}ms`;
201
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
202
- return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
203
- }
204
-
205
- function buildChainSummary(
206
- steps: ChainStep[],
207
- results: SingleResult[],
208
- chainDir: string,
209
- status: "completed" | "failed",
210
- failedStep?: { index: number; error: string },
211
- ): string {
212
- // Build step names for display
213
- const stepNames = steps
214
- .map((s) => (isParallelStep(s) ? `parallel[${s.parallel.length}]` : (s as SequentialStep).agent))
215
- .join(" → ");
216
-
217
- // Calculate total duration from results
218
- const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
219
- const durationStr = formatDuration(totalDuration);
220
-
221
- // Check for progress.md
222
- const progressPath = path.join(chainDir, "progress.md");
223
- const hasProgress = fs.existsSync(progressPath);
224
-
225
- if (status === "completed") {
226
- const stepWord = results.length === 1 ? "step" : "steps";
227
- return `✅ Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})
228
-
229
- 📋 Progress: ${hasProgress ? progressPath : "(none)"}
230
- 📁 Artifacts: ${chainDir}`;
231
- } else {
232
- const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
233
- const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
234
- return `❌ Chain failed${stepInfo}${errorInfo}
235
-
236
- 📋 Progress: ${hasProgress ? progressPath : "(none)"}
237
- 📁 Artifacts: ${chainDir}`;
238
- }
239
- }
240
-
241
- function readStatus(asyncDir: string): AsyncStatus | null {
242
- const statusPath = path.join(asyncDir, "status.json");
243
- if (!fs.existsSync(statusPath)) return null;
244
- try {
245
- const content = fs.readFileSync(statusPath, "utf-8");
246
- return JSON.parse(content) as AsyncStatus;
247
- } catch {
248
- return null;
249
- }
250
- }
251
-
252
- function getOutputTail(outputFile: string | undefined, maxLines: number = 3): string[] {
253
- if (!outputFile || !fs.existsSync(outputFile)) return [];
254
- let fd: number | null = null;
255
- try {
256
- const stat = fs.statSync(outputFile);
257
- if (stat.size === 0) return [];
258
- const tailBytes = 4096;
259
- const start = Math.max(0, stat.size - tailBytes);
260
- fd = fs.openSync(outputFile, "r");
261
- const buffer = Buffer.alloc(Math.min(tailBytes, stat.size));
262
- fs.readSync(fd, buffer, 0, buffer.length, start);
263
- const content = buffer.toString("utf-8");
264
- const lines = content.split("\n").filter((l) => l.trim());
265
- return lines.slice(-maxLines).map((l) => l.slice(0, 80) + (l.length > 80 ? "..." : ""));
266
- } catch {
267
- return [];
268
- } finally {
269
- if (fd !== null) {
270
- try {
271
- fs.closeSync(fd);
272
- } catch {}
273
- }
274
- }
275
- }
276
-
277
- function getLastActivity(outputFile: string | undefined): string {
278
- if (!outputFile || !fs.existsSync(outputFile)) return "";
279
- try {
280
- const stat = fs.statSync(outputFile);
281
- const ago = Date.now() - stat.mtimeMs;
282
- if (ago < 1000) return "active now";
283
- if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
284
- return `active ${Math.floor(ago / 60000)}m ago`;
285
- } catch {
286
- return "";
287
- }
288
- }
289
-
290
- function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
291
- if (!ctx.hasUI) return;
292
- if (jobs.length === 0) {
293
- ctx.ui.setWidget(WIDGET_KEY, undefined);
294
- return;
295
- }
296
-
297
- const theme = ctx.ui.theme;
298
- const lines: string[] = [];
299
- lines.push(theme.fg("accent", "Async subagents"));
300
-
301
- for (const job of jobs.slice(0, MAX_WIDGET_JOBS)) {
302
- const id = job.asyncId.slice(0, 6);
303
- const status =
304
- job.status === "complete"
305
- ? theme.fg("success", "complete")
306
- : job.status === "failed"
307
- ? theme.fg("error", "failed")
308
- : theme.fg("warning", "running");
309
-
310
- const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
311
- const stepIndex = job.currentStep !== undefined ? job.currentStep + 1 : undefined;
312
- const stepText = stepIndex !== undefined ? `step ${stepIndex}/${stepsTotal}` : `steps ${stepsTotal}`;
313
- const endTime = (job.status === "complete" || job.status === "failed") ? (job.updatedAt ?? Date.now()) : Date.now();
314
- const elapsed = job.startedAt ? formatDuration(endTime - job.startedAt) : "";
315
- const agentLabel = job.agents ? job.agents.join(" -> ") : (job.mode ?? "single");
316
-
317
- const tokenText = job.totalTokens ? ` | ${formatTokens(job.totalTokens.total)} tok` : "";
318
- const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
319
- const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
320
-
321
- lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
322
-
323
- if (job.status === "running" && job.outputFile) {
324
- const tail = getOutputTail(job.outputFile, 3);
325
- for (const line of tail) {
326
- lines.push(theme.fg("dim", ` > ${line}`));
327
- }
328
- }
329
- }
330
-
331
- ctx.ui.setWidget(WIDGET_KEY, lines);
332
- }
333
-
334
- function findByPrefix(dir: string, prefix: string, suffix?: string): string | null {
335
- if (!fs.existsSync(dir)) return null;
336
- const entries = fs.readdirSync(dir).filter((entry) => entry.startsWith(prefix));
337
- if (suffix) {
338
- const withSuffix = entries.filter((entry) => entry.endsWith(suffix));
339
- if (withSuffix.length > 0) return path.join(dir, withSuffix.sort()[0]);
340
- }
341
- if (entries.length === 0) return null;
342
- return path.join(dir, entries.sort()[0]);
343
- }
344
-
345
- function getFinalOutput(messages: Message[]): string {
346
- for (let i = messages.length - 1; i >= 0; i--) {
347
- const msg = messages[i];
348
- if (msg.role === "assistant") {
349
- for (const part of msg.content) {
350
- if (part.type === "text") return part.text;
351
- }
352
- }
353
- }
354
- return "";
355
- }
356
-
357
- interface ErrorInfo {
358
- hasError: boolean;
359
- exitCode?: number;
360
- errorType?: string;
361
- details?: string;
362
- }
363
-
364
- function detectSubagentError(messages: Message[]): ErrorInfo {
365
- for (const msg of messages) {
366
- if (msg.role === "toolResult" && (msg as any).isError) {
367
- const text = msg.content.find((c) => c.type === "text");
368
- const details = text && "text" in text ? text.text : undefined;
369
- const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
370
- return {
371
- hasError: true,
372
- exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
373
- errorType: (msg as any).toolName || "tool",
374
- details: details?.slice(0, 200),
375
- };
376
- }
377
- }
378
-
379
- for (const msg of messages) {
380
- if (msg.role !== "toolResult") continue;
381
- const toolName = (msg as any).toolName;
382
- if (toolName !== "bash") continue;
383
-
384
- const text = msg.content.find((c) => c.type === "text");
385
- if (!text || !("text" in text)) continue;
386
- const output = text.text;
387
-
388
- const exitMatch = output.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
389
- if (exitMatch) {
390
- const code = parseInt(exitMatch[1], 10);
391
- if (code !== 0) {
392
- return { hasError: true, exitCode: code, errorType: "bash", details: output.slice(0, 200) };
393
- }
394
- }
395
-
396
- const errorPatterns = [
397
- /command not found/i,
398
- /permission denied/i,
399
- /no such file or directory/i,
400
- /segmentation fault/i,
401
- /killed|terminated/i,
402
- /out of memory/i,
403
- /connection refused/i,
404
- /timeout/i,
405
- ];
406
- for (const pattern of errorPatterns) {
407
- if (pattern.test(output)) {
408
- return { hasError: true, exitCode: 1, errorType: "bash", details: output.slice(0, 200) };
409
- }
410
- }
411
- }
412
-
413
- return { hasError: false };
414
- }
415
-
416
- function getDisplayItems(messages: Message[]): DisplayItem[] {
417
- const items: DisplayItem[] = [];
418
- for (const msg of messages) {
419
- if (msg.role === "assistant") {
420
- for (const part of msg.content) {
421
- if (part.type === "text") items.push({ type: "text", text: part.text });
422
- else if (part.type === "toolCall") items.push({ type: "tool", name: part.name, args: part.arguments });
423
- }
424
- }
425
- }
426
- return items;
427
- }
428
-
429
- function shortenPath(p: string): string {
430
- const home = os.homedir();
431
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
432
- }
433
-
434
- function formatToolCall(name: string, args: Record<string, unknown>): string {
435
- switch (name) {
436
- case "bash":
437
- return `$ ${((args.command as string) || "").slice(0, 60)}${(args.command as string)?.length > 60 ? "..." : ""}`;
438
- case "read":
439
- return `read ${shortenPath((args.path || args.file_path || "") as string)}`;
440
- case "write":
441
- return `write ${shortenPath((args.path || args.file_path || "") as string)}`;
442
- case "edit":
443
- return `edit ${shortenPath((args.path || args.file_path || "") as string)}`;
444
- default: {
445
- const s = JSON.stringify(args);
446
- return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
447
- }
448
- }
449
- }
450
-
451
- function extractToolArgsPreview(args: Record<string, unknown>): string {
452
- const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task"];
453
- for (const key of previewKeys) {
454
- if (args[key] && typeof args[key] === "string") {
455
- const value = args[key] as string;
456
- return value.length > 60 ? `${value.slice(0, 57)}...` : value;
457
- }
458
- }
459
- return "";
460
- }
461
-
462
- function extractTextFromContent(content: unknown): string {
463
- if (!Array.isArray(content)) return "";
464
- for (const part of content) {
465
- if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
466
- return String(part.text);
467
- }
468
- }
469
- return "";
470
- }
471
-
472
- function writePrompt(agent: string, prompt: string): { dir: string; path: string } {
473
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
474
- const p = path.join(dir, `${agent.replace(/[^\w.-]/g, "_")}.md`);
475
- fs.writeFileSync(p, prompt, { mode: 0o600 });
476
- return { dir, path: p };
477
- }
478
-
479
- function findLatestSessionFile(sessionDir: string): string | null {
480
- try {
481
- const files = fs
482
- .readdirSync(sessionDir)
483
- .filter((f) => f.endsWith(".jsonl"))
484
- .map((f) => path.join(sessionDir, f));
485
- if (files.length === 0) return null;
486
- files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
487
- return files[0] ?? null;
488
- } catch {
489
- return null;
490
- }
491
- }
492
-
493
- // HTML export and sharing removed - module resolution issues with global pi installation
494
- // The session files are still available at the paths shown in the output
495
-
496
- interface RunSyncOptions {
497
- cwd?: string;
498
- signal?: AbortSignal;
499
- onUpdate?: (r: AgentToolResult<Details>) => void;
500
- maxOutput?: MaxOutputConfig;
501
- artifactsDir?: string;
502
- artifactConfig?: ArtifactConfig;
503
- runId: string;
504
- index?: number;
505
- sessionDir?: string;
506
- share?: boolean;
507
- }
508
-
509
- async function runSync(
510
- runtimeCwd: string,
511
- agents: AgentConfig[],
512
- agentName: string,
513
- task: string,
514
- options: RunSyncOptions,
515
- ): Promise<SingleResult> {
516
- const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index } = options;
517
- const agent = agents.find((a) => a.name === agentName);
518
- if (!agent) {
519
- return {
520
- agent: agentName,
521
- task,
522
- exitCode: 1,
523
- messages: [],
524
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
525
- error: `Unknown agent: ${agentName}`,
526
- };
527
- }
528
-
529
- const args = ["--mode", "json", "-p"];
530
- const shareEnabled = options.share === true;
531
- const sessionEnabled = Boolean(options.sessionDir) || shareEnabled;
532
- if (!sessionEnabled) {
533
- args.push("--no-session");
534
- }
535
- if (options.sessionDir) {
536
- try {
537
- fs.mkdirSync(options.sessionDir, { recursive: true });
538
- } catch {}
539
- args.push("--session-dir", options.sessionDir);
540
- }
541
- if (agent.model) args.push("--model", agent.model);
542
- if (agent.tools?.length) {
543
- const builtinTools: string[] = [];
544
- const extensionPaths: string[] = [];
545
- for (const tool of agent.tools) {
546
- if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
547
- extensionPaths.push(tool);
548
- } else {
549
- builtinTools.push(tool);
550
- }
551
- }
552
- if (builtinTools.length > 0) {
553
- args.push("--tools", builtinTools.join(","));
554
- }
555
- for (const extPath of extensionPaths) {
556
- args.push("--extension", extPath);
557
- }
558
- }
559
-
560
- let tmpDir: string | null = null;
561
- if (agent.systemPrompt?.trim()) {
562
- const tmp = writePrompt(agent.name, agent.systemPrompt);
563
- tmpDir = tmp.dir;
564
- args.push("--append-system-prompt", tmp.path);
565
- }
566
- args.push(`Task: ${task}`);
567
-
568
- const result: SingleResult = {
569
- agent: agentName,
570
- task,
571
- exitCode: 0,
572
- messages: [],
573
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
574
- };
575
-
576
- const progress: AgentProgress = {
577
- index: index ?? 0,
578
- agent: agentName,
579
- status: "running",
580
- task,
581
- recentTools: [],
582
- recentOutput: [],
583
- toolCount: 0,
584
- tokens: 0,
585
- durationMs: 0,
586
- };
587
- result.progress = progress;
588
-
589
- const startTime = Date.now();
590
- const jsonlLines: string[] = [];
591
-
592
- let artifactPathsResult: ArtifactPaths | undefined;
593
- if (artifactsDir && artifactConfig?.enabled !== false) {
594
- artifactPathsResult = getArtifactPaths(artifactsDir, runId, agentName, index);
595
- ensureArtifactsDir(artifactsDir);
596
- if (artifactConfig?.includeInput !== false) {
597
- writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
598
- }
599
- }
600
-
601
- const exitCode = await new Promise<number>((resolve) => {
602
- const proc = spawn("pi", args, { cwd: cwd ?? runtimeCwd, stdio: ["ignore", "pipe", "pipe"] });
603
- let buf = "";
604
-
605
- const processLine = (line: string) => {
606
- if (!line.trim()) return;
607
- jsonlLines.push(line);
608
- try {
609
- const evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
610
- const now = Date.now();
611
- progress.durationMs = now - startTime;
612
-
613
- if (evt.type === "tool_execution_start") {
614
- progress.toolCount++;
615
- progress.currentTool = evt.toolName;
616
- progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
617
- if (onUpdate)
618
- onUpdate({
619
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
620
- details: { mode: "single", results: [result], progress: [progress] },
621
- });
622
- }
623
-
624
- if (evt.type === "tool_execution_end") {
625
- if (progress.currentTool) {
626
- progress.recentTools.unshift({
627
- tool: progress.currentTool,
628
- args: progress.currentToolArgs || "",
629
- endMs: now,
630
- });
631
- if (progress.recentTools.length > 5) {
632
- progress.recentTools.pop();
633
- }
634
- }
635
- progress.currentTool = undefined;
636
- progress.currentToolArgs = undefined;
637
- if (onUpdate)
638
- onUpdate({
639
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
640
- details: { mode: "single", results: [result], progress: [progress] },
641
- });
642
- }
643
-
644
- if (evt.type === "message_end" && evt.message) {
645
- result.messages.push(evt.message);
646
- if (evt.message.role === "assistant") {
647
- result.usage.turns++;
648
- const u = evt.message.usage;
649
- if (u) {
650
- result.usage.input += u.input || 0;
651
- result.usage.output += u.output || 0;
652
- result.usage.cacheRead += u.cacheRead || 0;
653
- result.usage.cacheWrite += u.cacheWrite || 0;
654
- result.usage.cost += u.cost?.total || 0;
655
- progress.tokens = result.usage.input + result.usage.output;
656
- }
657
- if (!result.model && evt.message.model) result.model = evt.message.model;
658
- if (evt.message.errorMessage) result.error = evt.message.errorMessage;
659
-
660
- const text = extractTextFromContent(evt.message.content);
661
- if (text) {
662
- const lines = text
663
- .split("\n")
664
- .filter((l) => l.trim())
665
- .slice(-8);
666
- progress.recentOutput = lines;
667
- }
668
- }
669
- if (onUpdate)
670
- onUpdate({
671
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
672
- details: { mode: "single", results: [result], progress: [progress] },
673
- });
674
- }
675
- if (evt.type === "tool_result_end" && evt.message) {
676
- result.messages.push(evt.message);
677
- if (onUpdate)
678
- onUpdate({
679
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
680
- details: { mode: "single", results: [result], progress: [progress] },
681
- });
682
- }
683
- } catch {}
684
- };
685
-
686
- let stderrBuf = "";
687
- let lastUpdateTime = 0;
688
- const UPDATE_THROTTLE_MS = 150;
689
-
690
- proc.stdout.on("data", (d) => {
691
- buf += d.toString();
692
- const lines = buf.split("\n");
693
- buf = lines.pop() || "";
694
- lines.forEach(processLine);
695
-
696
- // Throttled periodic update for smoother progress display
697
- const now = Date.now();
698
- if (onUpdate && now - lastUpdateTime > UPDATE_THROTTLE_MS) {
699
- lastUpdateTime = now;
700
- progress.durationMs = now - startTime;
701
- onUpdate({
702
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
703
- details: { mode: "single", results: [result], progress: [progress] },
704
- });
705
- }
706
- });
707
- proc.stderr.on("data", (d) => {
708
- stderrBuf += d.toString();
709
- });
710
- proc.on("close", (code) => {
711
- if (buf.trim()) processLine(buf);
712
- if (code !== 0 && stderrBuf.trim() && !result.error) {
713
- result.error = stderrBuf.trim();
714
- }
715
- resolve(code ?? 0);
716
- });
717
- proc.on("error", () => resolve(1));
718
-
719
- if (signal) {
720
- const kill = () => {
721
- proc.kill("SIGTERM");
722
- setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
723
- };
724
- if (signal.aborted) kill();
725
- else signal.addEventListener("abort", kill, { once: true });
726
- }
727
- });
728
-
729
- if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
730
- result.exitCode = exitCode;
731
-
732
- if (exitCode === 0 && !result.error) {
733
- const errInfo = detectSubagentError(result.messages);
734
- if (errInfo.hasError) {
735
- result.exitCode = errInfo.exitCode ?? 1;
736
- result.error = errInfo.details
737
- ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
738
- : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
739
- }
740
- }
741
-
742
- progress.status = result.exitCode === 0 ? "completed" : "failed";
743
- progress.durationMs = Date.now() - startTime;
744
- if (result.error) {
745
- progress.error = result.error;
746
- if (progress.currentTool) {
747
- progress.failedTool = progress.currentTool;
748
- }
749
- }
750
-
751
- result.progress = progress;
752
- result.progressSummary = {
753
- toolCount: progress.toolCount,
754
- tokens: progress.tokens,
755
- durationMs: progress.durationMs,
756
- };
757
-
758
- if (artifactPathsResult && artifactConfig?.enabled !== false) {
759
- result.artifactPaths = artifactPathsResult;
760
- const fullOutput = getFinalOutput(result.messages);
761
-
762
- if (artifactConfig?.includeOutput !== false) {
763
- writeArtifact(artifactPathsResult.outputPath, fullOutput);
764
- }
765
- if (artifactConfig?.includeJsonl !== false) {
766
- for (const line of jsonlLines) {
767
- appendJsonl(artifactPathsResult.jsonlPath, line);
768
- }
769
- }
770
- if (artifactConfig?.includeMetadata !== false) {
771
- writeMetadata(artifactPathsResult.metadataPath, {
772
- runId,
773
- agent: agentName,
774
- task,
775
- exitCode: result.exitCode,
776
- usage: result.usage,
777
- model: result.model,
778
- durationMs: progress.durationMs,
779
- toolCount: progress.toolCount,
780
- error: result.error,
781
- timestamp: Date.now(),
782
- });
783
- }
784
-
785
- if (maxOutput) {
786
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
787
- const truncationResult = truncateOutput(fullOutput, config, artifactPathsResult.outputPath);
788
- if (truncationResult.truncated) {
789
- result.truncation = truncationResult;
790
- }
791
- }
792
- } else if (maxOutput) {
793
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
794
- const fullOutput = getFinalOutput(result.messages);
795
- const truncationResult = truncateOutput(fullOutput, config);
796
- if (truncationResult.truncated) {
797
- result.truncation = truncationResult;
798
- }
799
- }
800
-
801
- if (shareEnabled && options.sessionDir) {
802
- const sessionFile = findLatestSessionFile(options.sessionDir);
803
- if (sessionFile) {
804
- result.sessionFile = sessionFile;
805
- // HTML export disabled - module resolution issues with global pi installation
806
- // Users can still access the session file directly
807
- }
808
- }
809
-
810
- return result;
811
- }
812
-
813
- async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
814
- const results: R[] = new Array(items.length);
815
- let next = 0;
816
- await Promise.all(
817
- Array(Math.min(limit, items.length))
818
- .fill(0)
819
- .map(async () => {
820
- while (next < items.length) {
821
- const i = next++;
822
- results[i] = await fn(items[i], i);
823
- }
824
- }),
825
- );
826
- return results;
827
- }
828
-
829
- const TaskItem = Type.Object({ agent: Type.String(), task: Type.String(), cwd: Type.Optional(Type.String()) });
830
-
831
- // Sequential chain step (single agent)
832
- const SequentialStepSchema = Type.Object({
833
- agent: Type.String(),
834
- task: Type.Optional(Type.String({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
835
- cwd: Type.Optional(Type.String()),
836
- // Chain behavior overrides
837
- output: Type.Optional(Type.Union([
838
- Type.String(),
839
- Type.Boolean(),
840
- ], { description: "Override output filename (string), or false for text-only" })),
841
- reads: Type.Optional(Type.Union([
842
- Type.Array(Type.String()),
843
- Type.Boolean(),
844
- ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
845
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
846
- });
847
-
848
- // Parallel task item (within a parallel step)
849
- const ParallelTaskSchema = Type.Object({
850
- agent: Type.String(),
851
- task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
852
- cwd: Type.Optional(Type.String()),
853
- output: Type.Optional(Type.Union([
854
- Type.String(),
855
- Type.Boolean(),
856
- ], { description: "Override output filename (string), or false for text-only" })),
857
- reads: Type.Optional(Type.Union([
858
- Type.Array(Type.String()),
859
- Type.Boolean(),
860
- ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
861
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
862
- });
863
-
864
- // Parallel chain step (multiple agents running concurrently)
865
- const ParallelStepSchema = Type.Object({
866
- parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
867
- concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
868
- failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
869
- });
870
-
871
- // Chain item can be either sequential or parallel
872
- const ChainItem = Type.Union([SequentialStepSchema, ParallelStepSchema]);
873
-
874
- const MaxOutputSchema = Type.Optional(
875
- Type.Object({
876
- bytes: Type.Optional(Type.Number({ description: "Max bytes (default: 204800)" })),
877
- lines: Type.Optional(Type.Number({ description: "Max lines (default: 5000)" })),
878
- }),
879
- );
880
-
881
- const Params = Type.Object({
882
- agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
883
- task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
884
- tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
885
- chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
886
- async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
887
- agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
888
- cwd: Type.Optional(Type.String()),
889
- maxOutput: MaxOutputSchema,
890
- artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
891
- includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
892
- share: Type.Optional(Type.Boolean({ description: "Create shareable session log (default: true)", default: true })),
893
- sessionDir: Type.Optional(
894
- Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
895
- ),
896
- // Chain clarification TUI
897
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
898
- // Solo agent output override
899
- output: Type.Optional(Type.Union([
900
- Type.String(),
901
- Type.Boolean(),
902
- ], { description: "Override output file for single agent (string), or false to disable (uses agent default if omitted)" })),
903
- });
904
-
905
- const StatusParams = Type.Object({
906
- id: Type.Optional(Type.String({ description: "Async run id or prefix" })),
907
- dir: Type.Optional(Type.String({ description: "Async run directory (overrides id search)" })),
908
- });
909
-
910
- interface ExtensionConfig {
911
- asyncByDefault?: boolean;
912
- }
49
+ // ExtensionConfig is now imported from ./types.js
913
50
 
914
51
  function loadConfig(): ExtensionConfig {
915
52
  const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
@@ -997,7 +134,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
997
134
  .filter((f) => f.endsWith(".json"))
998
135
  .forEach(handleResult);
999
136
 
1000
- const tool: ToolDefinition<typeof Params, Details> = {
137
+ const tool: ToolDefinition<typeof SubagentParams, Details> = {
1001
138
  name: "subagent",
1002
139
  label: "Subagent",
1003
140
  description: `Delegate to subagents. Use exactly ONE mode:
@@ -1005,7 +142,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
1005
142
  • CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential, {previous} passes output
1006
143
  • PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
1007
144
  For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
1008
- parameters: Params,
145
+ parameters: SubagentParams,
1009
146
 
1010
147
  async execute(_id, params, onUpdate, ctx, signal) {
1011
148
  const scope: AgentScope = params.agentScope ?? "user";
@@ -1071,9 +208,11 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
1071
208
  // First step must have a task
1072
209
  const firstStep = params.chain[0] as ChainStep;
1073
210
  if (isParallelStep(firstStep)) {
1074
- if (!firstStep.parallel[0]?.task) {
211
+ // All tasks in the first parallel step must have tasks (no {previous} to reference)
212
+ const missingTaskIndex = firstStep.parallel.findIndex((t) => !t.task);
213
+ if (missingTaskIndex !== -1) {
1075
214
  return {
1076
- content: [{ type: "text", text: "First parallel task must have a task" }],
215
+ content: [{ type: "text", text: `First parallel step: task ${missingTaskIndex + 1} must have a task (no previous output to reference)` }],
1077
216
  isError: true,
1078
217
  details: { mode: "chain" as const, results: [] },
1079
218
  };
@@ -1110,169 +249,51 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
1110
249
  }
1111
250
 
1112
251
  if (effectiveAsync) {
1113
- if (!jitiCliPath)
252
+ if (!isAsyncAvailable()) {
1114
253
  return {
1115
254
  content: [{ type: "text", text: "jiti not found" }],
1116
255
  isError: true,
1117
256
  details: { mode: "single" as const, results: [] },
1118
257
  };
258
+ }
1119
259
  const id = randomUUID();
1120
- const asyncDir = path.join(ASYNC_DIR, id);
1121
- try {
1122
- fs.mkdirSync(asyncDir, { recursive: true });
1123
- } catch {}
1124
- const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
1125
-
1126
- const spawnRunner = (cfg: object, suffix: string): number | undefined => {
1127
- const cfgPath = path.join(os.tmpdir(), `pi-async-cfg-${suffix}.json`);
1128
- fs.writeFileSync(cfgPath, JSON.stringify(cfg));
1129
- const proc = spawn("node", [jitiCliPath!, runner, cfgPath], {
1130
- cwd: (cfg as any).cwd ?? ctx.cwd,
1131
- detached: true,
1132
- stdio: "ignore",
1133
- });
1134
- proc.unref();
1135
- return proc.pid;
1136
- };
260
+ const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
1137
261
 
1138
262
  if (hasChain && params.chain) {
1139
- // Async mode doesn't support parallel steps (v1 limitation)
1140
- const chainStepsForAsync = params.chain as ChainStep[];
1141
- const hasParallelInChain = chainStepsForAsync.some(isParallelStep);
1142
- if (hasParallelInChain) {
1143
- return {
1144
- content: [{ type: "text", text: "Async mode doesn't support chains with parallel steps. Use clarify: true (sync mode) for parallel-in-chain." }],
1145
- isError: true,
1146
- details: { mode: "chain" as const, results: [] },
1147
- };
1148
- }
1149
-
1150
- // At this point, all steps are sequential
1151
- const seqSteps = chainStepsForAsync as SequentialStep[];
1152
-
1153
- // Validate all agents exist before building steps
1154
- for (const s of seqSteps) {
1155
- if (!agents.find((x) => x.name === s.agent)) {
1156
- return {
1157
- content: [{ type: "text", text: `Unknown agent: ${s.agent}` }],
1158
- isError: true,
1159
- details: { mode: "chain" as const, results: [] },
1160
- };
1161
- }
1162
- }
1163
- const steps = seqSteps.map((s, i) => {
1164
- const a = agents.find((x) => x.name === s.agent)!;
1165
- return {
1166
- agent: s.agent,
1167
- // For async, use simple defaults: first step uses inline task, others use {previous}
1168
- task: s.task ?? (i === 0 ? "{task}" : "{previous}"),
1169
- cwd: s.cwd,
1170
- model: a.model,
1171
- tools: a.tools,
1172
- systemPrompt: a.systemPrompt?.trim() || null,
1173
- };
263
+ return executeAsyncChain(id, {
264
+ chain: params.chain as ChainStep[],
265
+ agents,
266
+ ctx: asyncCtx,
267
+ cwd: params.cwd,
268
+ maxOutput: params.maxOutput,
269
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
270
+ artifactConfig,
271
+ shareEnabled,
272
+ sessionRoot,
1174
273
  });
1175
- const pid = spawnRunner(
1176
- {
1177
- id,
1178
- steps,
1179
- resultPath: path.join(RESULTS_DIR, `${id}.json`),
1180
- cwd: params.cwd ?? ctx.cwd,
1181
- placeholder: "{previous}",
1182
- maxOutput: params.maxOutput,
1183
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1184
- artifactConfig,
1185
- share: shareEnabled,
1186
- sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
1187
- asyncDir,
1188
- sessionId: currentSessionId,
1189
- },
1190
- id,
1191
- );
1192
- if (pid) {
1193
- pi.events.emit("subagent_enhanced:started", {
1194
- id,
1195
- pid,
1196
- agent: params.chain[0].agent,
1197
- task: params.chain[0].task?.slice(0, 50),
1198
- chain: params.chain.map((s) => s.agent),
1199
- cwd: params.cwd ?? ctx.cwd,
1200
- asyncDir,
1201
- });
1202
- pi.events.emit("subagent:started", {
1203
- id,
1204
- pid,
1205
- agent: params.chain[0].agent,
1206
- task: params.chain[0].task?.slice(0, 50),
1207
- chain: params.chain.map((s) => s.agent),
1208
- cwd: params.cwd ?? ctx.cwd,
1209
- asyncDir,
1210
- });
1211
- }
1212
- return {
1213
- content: [
1214
- { type: "text", text: `Async chain: ${params.chain.map((s) => s.agent).join(" -> ")} [${id}]` },
1215
- ],
1216
- details: { mode: "chain", results: [], asyncId: id, asyncDir },
1217
- };
1218
274
  }
1219
275
 
1220
276
  if (hasSingle) {
1221
277
  const a = agents.find((x) => x.name === params.agent);
1222
- if (!a)
278
+ if (!a) {
1223
279
  return {
1224
280
  content: [{ type: "text", text: `Unknown: ${params.agent}` }],
1225
281
  isError: true,
1226
282
  details: { mode: "single" as const, results: [] },
1227
283
  };
1228
- const pid = spawnRunner(
1229
- {
1230
- id,
1231
- steps: [
1232
- {
1233
- agent: params.agent,
1234
- task: params.task,
1235
- cwd: params.cwd,
1236
- model: a.model,
1237
- tools: a.tools,
1238
- systemPrompt: a.systemPrompt?.trim() || null,
1239
- },
1240
- ],
1241
- resultPath: path.join(RESULTS_DIR, `${id}.json`),
1242
- cwd: params.cwd ?? ctx.cwd,
1243
- placeholder: "{previous}",
1244
- maxOutput: params.maxOutput,
1245
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1246
- artifactConfig,
1247
- share: shareEnabled,
1248
- sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
1249
- asyncDir,
1250
- sessionId: currentSessionId,
1251
- },
1252
- id,
1253
- );
1254
- if (pid) {
1255
- pi.events.emit("subagent_enhanced:started", {
1256
- id,
1257
- pid,
1258
- agent: params.agent,
1259
- task: params.task?.slice(0, 50),
1260
- cwd: params.cwd ?? ctx.cwd,
1261
- asyncDir,
1262
- });
1263
- pi.events.emit("subagent:started", {
1264
- id,
1265
- pid,
1266
- agent: params.agent,
1267
- task: params.task?.slice(0, 50),
1268
- cwd: params.cwd ?? ctx.cwd,
1269
- asyncDir,
1270
- });
1271
284
  }
1272
- return {
1273
- content: [{ type: "text", text: `Async: ${params.agent} [${id}]` }],
1274
- details: { mode: "single", results: [], asyncId: id, asyncDir },
1275
- };
285
+ return executeAsyncSingle(id, {
286
+ agent: params.agent!,
287
+ task: params.task!,
288
+ agentConfig: a,
289
+ ctx: asyncCtx,
290
+ cwd: params.cwd,
291
+ maxOutput: params.maxOutput,
292
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
293
+ artifactConfig,
294
+ shareEnabled,
295
+ sessionRoot,
296
+ });
1276
297
  }
1277
298
  }
1278
299
 
@@ -1280,363 +301,22 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
1280
301
  const allArtifactPaths: ArtifactPaths[] = [];
1281
302
 
1282
303
  if (hasChain && params.chain) {
1283
- // Cast chain to typed steps
1284
- const chainSteps = params.chain as ChainStep[];
1285
-
1286
- // Compute chain metadata for observability
1287
- const chainAgents: string[] = chainSteps.map((step) =>
1288
- isParallelStep(step)
1289
- ? `[${step.parallel.map((t) => t.agent).join("+")}]`
1290
- : (step as SequentialStep).agent,
1291
- );
1292
- const totalSteps = chainSteps.length;
1293
-
1294
- // Get original task from first step
1295
- const firstStep = chainSteps[0]!;
1296
- const originalTask = isParallelStep(firstStep)
1297
- ? firstStep.parallel[0]!.task!
1298
- : (firstStep as SequentialStep).task!;
1299
-
1300
- // Create chain directory
1301
- const chainDir = createChainDir(runId);
1302
-
1303
- // Check if chain has any parallel steps
1304
- const hasParallelSteps = chainSteps.some(isParallelStep);
1305
-
1306
- // Resolve templates using V2 (parallel-aware)
1307
- const settings = loadSubagentSettings();
1308
- let templates: ResolvedTemplates = resolveChainTemplatesV2(chainSteps, settings);
1309
-
1310
- // For TUI: only show if no parallel steps (TUI v1 doesn't support parallel display)
1311
- // TODO: Update TUI to support parallel steps
1312
- const shouldClarify = params.clarify !== false && ctx.hasUI && !hasParallelSteps;
1313
-
1314
- // Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
1315
- let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
1316
-
1317
- if (shouldClarify) {
1318
- // Sequential-only chain: use existing TUI
1319
- const seqSteps = chainSteps as SequentialStep[];
1320
-
1321
- // Load agent configs for sequential steps
1322
- const agentConfigs: AgentConfig[] = [];
1323
- for (const step of seqSteps) {
1324
- const config = agents.find((a) => a.name === step.agent);
1325
- if (!config) {
1326
- removeChainDir(chainDir);
1327
- return {
1328
- content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
1329
- isError: true,
1330
- details: { mode: "chain" as const, results: [] },
1331
- };
1332
- }
1333
- agentConfigs.push(config);
1334
- }
1335
-
1336
- // Build step overrides
1337
- const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
1338
- output: step.output,
1339
- reads: step.reads,
1340
- progress: step.progress,
1341
- }));
1342
-
1343
- // Pre-resolve behaviors for TUI display
1344
- const resolvedBehaviors = agentConfigs.map((config, i) =>
1345
- resolveStepBehavior(config, stepOverrides[i]!),
1346
- );
1347
-
1348
- // Flatten templates for TUI (all strings for sequential)
1349
- const flatTemplates = templates as string[];
1350
-
1351
- const result = await ctx.ui.custom<ChainClarifyResult>(
1352
- (tui, theme, _kb, done) =>
1353
- new ChainClarifyComponent(
1354
- tui,
1355
- theme,
1356
- agentConfigs,
1357
- flatTemplates,
1358
- originalTask,
1359
- chainDir,
1360
- resolvedBehaviors,
1361
- done,
1362
- ),
1363
- {
1364
- overlay: true,
1365
- overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" },
1366
- },
1367
- );
1368
-
1369
- if (!result || !result.confirmed) {
1370
- removeChainDir(chainDir);
1371
- return {
1372
- content: [{ type: "text", text: "Chain cancelled" }],
1373
- details: { mode: "chain", results: [] },
1374
- };
1375
- }
1376
- // Update templates from TUI result
1377
- templates = result.templates;
1378
- // Store behavior overrides from TUI (used below in sequential step execution)
1379
- tuiBehaviorOverrides = result.behaviorOverrides;
1380
- }
1381
-
1382
- // Execute chain (handles both sequential and parallel steps)
1383
- const results: SingleResult[] = [];
1384
- let prev = "";
1385
- let globalTaskIndex = 0; // For unique artifact naming
1386
- let progressCreated = false; // Track if progress.md has been created
1387
-
1388
- for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
1389
- const step = chainSteps[stepIndex]!;
1390
- const stepTemplates = templates[stepIndex]!;
1391
-
1392
- if (isParallelStep(step)) {
1393
- // === PARALLEL STEP EXECUTION ===
1394
- const parallelTemplates = stepTemplates as string[];
1395
- const concurrency = step.concurrency ?? MAX_CONCURRENCY;
1396
- const failFast = step.failFast ?? false;
1397
-
1398
- // Create subdirectories for parallel outputs
1399
- const agentNames = step.parallel.map((t) => t.agent);
1400
- createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
1401
-
1402
- // Resolve behaviors for parallel tasks
1403
- const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex);
1404
-
1405
- // If any parallel task has progress enabled and progress.md hasn't been created,
1406
- // create it now to avoid race conditions
1407
- const anyNeedsProgress = parallelBehaviors.some((b) => b.progress);
1408
- if (anyNeedsProgress && !progressCreated) {
1409
- const progressPath = path.join(chainDir, "progress.md");
1410
- fs.writeFileSync(progressPath, "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n");
1411
- progressCreated = true;
1412
- }
1413
-
1414
- // Track if we should abort remaining tasks (for fail-fast)
1415
- let aborted = false;
1416
-
1417
- // Execute parallel tasks
1418
- const parallelResults = await mapConcurrent(
1419
- step.parallel,
1420
- concurrency,
1421
- async (task, taskIndex) => {
1422
- if (aborted && failFast) {
1423
- // Return a placeholder for skipped tasks
1424
- return {
1425
- agent: task.agent,
1426
- task: "(skipped)",
1427
- exitCode: -1,
1428
- messages: [],
1429
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
1430
- error: "Skipped due to fail-fast",
1431
- } as SingleResult;
1432
- }
1433
-
1434
- // Build task string
1435
- const taskTemplate = parallelTemplates[taskIndex] ?? "{previous}";
1436
- const templateHasPrevious = taskTemplate.includes("{previous}");
1437
- let taskStr = taskTemplate;
1438
- taskStr = taskStr.replace(/\{task\}/g, originalTask);
1439
- taskStr = taskStr.replace(/\{previous\}/g, prev);
1440
- taskStr = taskStr.replace(/\{chain_dir\}/g, chainDir);
1441
-
1442
- // Add chain instructions (include previous summary only if not already in template)
1443
- const behavior = parallelBehaviors[taskIndex]!;
1444
- // For parallel, no single "first progress" - each manages independently
1445
- taskStr += buildChainInstructions(behavior, chainDir, false, templateHasPrevious ? undefined : prev);
1446
-
1447
- const r = await runSync(ctx.cwd, agents, task.agent, taskStr, {
1448
- cwd: task.cwd ?? params.cwd,
1449
- signal,
1450
- runId,
1451
- index: globalTaskIndex + taskIndex,
1452
- sessionDir: sessionDirForIndex(globalTaskIndex + taskIndex),
1453
- share: shareEnabled,
1454
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1455
- artifactConfig,
1456
- onUpdate: onUpdate
1457
- ? (p) =>
1458
- onUpdate({
1459
- ...p,
1460
- details: {
1461
- mode: "chain",
1462
- results: [...results, ...(p.details?.results || [])],
1463
- progress: [...allProgress, ...(p.details?.progress || [])],
1464
- chainAgents,
1465
- totalSteps,
1466
- currentStepIndex: stepIndex,
1467
- },
1468
- })
1469
- : undefined,
1470
- });
1471
-
1472
- if (r.exitCode !== 0 && failFast) {
1473
- aborted = true;
1474
- }
1475
-
1476
- return r;
1477
- },
1478
- );
1479
-
1480
- // Update global task index
1481
- globalTaskIndex += step.parallel.length;
1482
-
1483
- // Collect results and progress
1484
- for (const r of parallelResults) {
1485
- results.push(r);
1486
- if (r.progress) allProgress.push(r.progress);
1487
- if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
1488
- }
1489
-
1490
- // Check for failures (track original task index for better error messages)
1491
- const failures = parallelResults
1492
- .map((r, originalIndex) => ({ ...r, originalIndex }))
1493
- .filter((r) => r.exitCode !== 0 && r.exitCode !== -1);
1494
- if (failures.length > 0) {
1495
- const failureSummary = failures
1496
- .map((f) => `- Task ${f.originalIndex + 1} (${f.agent}): ${f.error || "failed"}`)
1497
- .join("\n");
1498
- const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
1499
- const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1500
- index: stepIndex,
1501
- error: errorMsg,
1502
- });
1503
- return {
1504
- content: [{ type: "text", text: summary }],
1505
- details: {
1506
- mode: "chain",
1507
- results,
1508
- progress: params.includeProgress ? allProgress : undefined,
1509
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1510
- chainAgents,
1511
- totalSteps,
1512
- currentStepIndex: stepIndex,
1513
- },
1514
- isError: true,
1515
- };
1516
- }
1517
-
1518
- // Aggregate outputs for {previous}
1519
- const taskResults: ParallelTaskResult[] = parallelResults.map((r, i) => ({
1520
- agent: r.agent,
1521
- taskIndex: i,
1522
- output: getFinalOutput(r.messages),
1523
- exitCode: r.exitCode,
1524
- error: r.error,
1525
- }));
1526
- prev = aggregateParallelOutputs(taskResults);
1527
- } else {
1528
- // === SEQUENTIAL STEP EXECUTION ===
1529
- const seqStep = step as SequentialStep;
1530
- const stepTemplate = stepTemplates as string;
1531
-
1532
- // Get agent config
1533
- const agentConfig = agents.find((a) => a.name === seqStep.agent);
1534
- if (!agentConfig) {
1535
- removeChainDir(chainDir);
1536
- return {
1537
- content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
1538
- isError: true,
1539
- details: { mode: "chain" as const, results: [] },
1540
- };
1541
- }
1542
-
1543
- // Build task string (check if template has {previous} before replacement)
1544
- const templateHasPrevious = stepTemplate.includes("{previous}");
1545
- let stepTask = stepTemplate;
1546
- stepTask = stepTask.replace(/\{task\}/g, originalTask);
1547
- stepTask = stepTask.replace(/\{previous\}/g, prev);
1548
- stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
1549
-
1550
- // Resolve behavior (TUI overrides take precedence over step config)
1551
- const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
1552
- const stepOverride: StepOverrides = {
1553
- output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
1554
- reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
1555
- progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
1556
- };
1557
- const behavior = resolveStepBehavior(agentConfig, stepOverride);
1558
-
1559
- // Determine if this is the first agent to create progress.md
1560
- const isFirstProgress = behavior.progress && !progressCreated;
1561
- if (isFirstProgress) {
1562
- progressCreated = true;
1563
- }
1564
-
1565
- // Add chain instructions (include previous summary only if not already in template)
1566
- stepTask += buildChainInstructions(behavior, chainDir, isFirstProgress, templateHasPrevious ? undefined : prev);
1567
-
1568
- // Run step
1569
- const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1570
- cwd: seqStep.cwd ?? params.cwd,
1571
- signal,
1572
- runId,
1573
- index: globalTaskIndex,
1574
- sessionDir: sessionDirForIndex(globalTaskIndex),
1575
- share: shareEnabled,
1576
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1577
- artifactConfig,
1578
- onUpdate: onUpdate
1579
- ? (p) =>
1580
- onUpdate({
1581
- ...p,
1582
- details: {
1583
- mode: "chain",
1584
- results: [...results, ...(p.details?.results || [])],
1585
- progress: [...allProgress, ...(p.details?.progress || [])],
1586
- chainAgents,
1587
- totalSteps,
1588
- currentStepIndex: stepIndex,
1589
- },
1590
- })
1591
- : undefined,
1592
- });
1593
-
1594
- globalTaskIndex++;
1595
- results.push(r);
1596
- if (r.progress) allProgress.push(r.progress);
1597
- if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
1598
-
1599
- // On failure, leave chain_dir for debugging
1600
- if (r.exitCode !== 0) {
1601
- const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
1602
- index: stepIndex,
1603
- error: r.error || "Chain failed",
1604
- });
1605
- return {
1606
- content: [{ type: "text", text: summary }],
1607
- details: {
1608
- mode: "chain",
1609
- results,
1610
- progress: params.includeProgress ? allProgress : undefined,
1611
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1612
- chainAgents,
1613
- totalSteps,
1614
- currentStepIndex: stepIndex,
1615
- },
1616
- isError: true,
1617
- };
1618
- }
1619
-
1620
- prev = getFinalOutput(r.messages);
1621
- }
1622
- }
1623
-
1624
- // Chain complete - return summary with paths
1625
- // Chain dir left for inspection (cleaned up after 24h)
1626
- const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
1627
-
1628
- return {
1629
- content: [{ type: "text", text: summary }],
1630
- details: {
1631
- mode: "chain",
1632
- results,
1633
- progress: params.includeProgress ? allProgress : undefined,
1634
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1635
- chainAgents,
1636
- totalSteps,
1637
- // currentStepIndex omitted for completed chains
1638
- },
1639
- };
304
+ // Use extracted chain execution module
305
+ return executeChain({
306
+ chain: params.chain as ChainStep[],
307
+ agents,
308
+ ctx,
309
+ signal,
310
+ runId,
311
+ cwd: params.cwd,
312
+ shareEnabled,
313
+ sessionDirForIndex,
314
+ artifactsDir,
315
+ artifactConfig,
316
+ includeProgress: params.includeProgress,
317
+ clarify: params.clarify,
318
+ onUpdate,
319
+ });
1640
320
  }
1641
321
 
1642
322
  if (hasTasks && params.tasks) {
@@ -1775,230 +455,8 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
1775
455
  );
1776
456
  },
1777
457
 
1778
- renderResult(result, { expanded }, theme) {
1779
- const d = result.details;
1780
- if (!d || !d.results.length) {
1781
- const t = result.content[0];
1782
- return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
1783
- }
1784
-
1785
- const mdTheme = getMarkdownTheme();
1786
-
1787
- if (d.mode === "single" && d.results.length === 1) {
1788
- const r = d.results[0];
1789
- const isRunning = r.progress?.status === "running";
1790
- const icon = isRunning
1791
- ? theme.fg("warning", "...")
1792
- : r.exitCode === 0
1793
- ? theme.fg("success", "ok")
1794
- : theme.fg("error", "X");
1795
- const output = r.truncation?.text || getFinalOutput(r.messages);
1796
-
1797
- const progressInfo = isRunning && r.progress
1798
- ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
1799
- : r.progressSummary
1800
- ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tokens, ${formatDuration(r.progressSummary.durationMs)}`
1801
- : "";
1802
-
1803
- if (expanded) {
1804
- const c = new Container();
1805
- c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
1806
- c.addChild(new Spacer(1));
1807
- c.addChild(
1808
- new Text(theme.fg("dim", `Task: ${r.task.slice(0, 100)}${r.task.length > 100 ? "..." : ""}`), 0, 0),
1809
- );
1810
- c.addChild(new Spacer(1));
1811
-
1812
- const items = getDisplayItems(r.messages);
1813
- for (const item of items) {
1814
- if (item.type === "tool")
1815
- c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
1816
- }
1817
- if (items.length) c.addChild(new Spacer(1));
1818
-
1819
- if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
1820
- c.addChild(new Spacer(1));
1821
- c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
1822
- if (r.sessionFile) {
1823
- c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
1824
- }
1825
- // Sharing disabled - session file path shown above
1826
-
1827
- if (r.artifactPaths) {
1828
- c.addChild(new Spacer(1));
1829
- c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
1830
- }
1831
- return c;
1832
- }
1833
-
1834
- const lines = [`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`];
1835
-
1836
- if (isRunning && r.progress) {
1837
- if (r.progress.currentTool) {
1838
- const toolLine = r.progress.currentToolArgs
1839
- ? `${r.progress.currentTool}: ${r.progress.currentToolArgs.slice(0, 60)}${r.progress.currentToolArgs.length > 60 ? "..." : ""}`
1840
- : r.progress.currentTool;
1841
- lines.push(theme.fg("warning", `> ${toolLine}`));
1842
- }
1843
- for (const line of r.progress.recentOutput.slice(-3)) {
1844
- lines.push(theme.fg("dim", ` ${line.slice(0, 80)}${line.length > 80 ? "..." : ""}`));
1845
- }
1846
- lines.push(theme.fg("dim", "(ctrl+o to expand)"));
1847
- } else {
1848
- const items = getDisplayItems(r.messages).slice(-COLLAPSED_ITEMS);
1849
- for (const item of items) {
1850
- if (item.type === "tool") lines.push(theme.fg("muted", formatToolCall(item.name, item.args)));
1851
- else lines.push(item.text.slice(0, 80) + (item.text.length > 80 ? "..." : ""));
1852
- }
1853
- lines.push(theme.fg("dim", formatUsage(r.usage, r.model)));
1854
- }
1855
- return new Text(lines.join("\n"), 0, 0);
1856
- }
1857
-
1858
- const hasRunning = d.progress?.some((p) => p.status === "running")
1859
- || d.results.some((r) => r.progress?.status === "running");
1860
- const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
1861
- const icon = hasRunning
1862
- ? theme.fg("warning", "...")
1863
- : ok === d.results.length
1864
- ? theme.fg("success", "ok")
1865
- : theme.fg("error", "X");
1866
-
1867
- const totalSummary =
1868
- d.progressSummary ||
1869
- d.results.reduce(
1870
- (acc, r) => {
1871
- const prog = r.progress || r.progressSummary;
1872
- if (prog) {
1873
- acc.toolCount += prog.toolCount;
1874
- acc.tokens += prog.tokens;
1875
- acc.durationMs =
1876
- d.mode === "chain"
1877
- ? acc.durationMs + prog.durationMs
1878
- : Math.max(acc.durationMs, prog.durationMs);
1879
- }
1880
- return acc;
1881
- },
1882
- { toolCount: 0, tokens: 0, durationMs: 0 },
1883
- );
1884
-
1885
- const summaryStr =
1886
- totalSummary.toolCount || totalSummary.tokens
1887
- ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
1888
- : "";
1889
-
1890
- const modeLabel = d.mode === "parallel" ? "parallel (no live progress)" : d.mode;
1891
- // Use chain metadata for accurate step counts
1892
- const totalStepsCount = d.totalSteps ?? d.results.length;
1893
- const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
1894
- const stepInfo = hasRunning ? ` ${currentStep}/${totalStepsCount}` : ` ${ok}/${totalStepsCount}`;
1895
-
1896
- // Build chain visualization: "scout → planner" with status icons
1897
- // Note: Only works correctly for sequential chains. Chains with parallel steps
1898
- // (indicated by "[agent1+agent2]" format) have multiple results per step,
1899
- // breaking the 1:1 mapping between chainAgents and results.
1900
- const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
1901
- const chainVis = d.chainAgents?.length && !hasParallelInChain
1902
- ? d.chainAgents
1903
- .map((agent, i) => {
1904
- const result = d.results[i];
1905
- const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
1906
- const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
1907
- const isCurrent = i === (d.currentStepIndex ?? d.results.length);
1908
- const icon = isFailed
1909
- ? theme.fg("error", "✗")
1910
- : isComplete
1911
- ? theme.fg("success", "✓")
1912
- : isCurrent && hasRunning
1913
- ? theme.fg("warning", "●")
1914
- : theme.fg("dim", "○");
1915
- return `${icon}${agent}`;
1916
- })
1917
- .join(theme.fg("dim", " → "))
1918
- : null;
1919
-
1920
- if (expanded) {
1921
- const c = new Container();
1922
- c.addChild(
1923
- new Text(
1924
- `${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
1925
- 0,
1926
- 0,
1927
- ),
1928
- );
1929
- // Show chain visualization in expanded view
1930
- if (chainVis) {
1931
- c.addChild(new Text(` ${chainVis}`, 0, 0));
1932
- }
1933
- for (let i = 0; i < d.results.length; i++) {
1934
- const r = d.results[i];
1935
- c.addChild(new Spacer(1));
1936
- // Check both r.progress and d.progress array for running status
1937
- const progressFromArray = d.progress?.find((p) => p.index === i);
1938
- const rProg = r.progress || progressFromArray || r.progressSummary;
1939
- const rRunning = rProg?.status === "running";
1940
- const rIcon = rRunning
1941
- ? theme.fg("warning", "...")
1942
- : r.exitCode === 0
1943
- ? theme.fg("success", "ok")
1944
- : theme.fg("error", "X");
1945
- const rProgress = rProg
1946
- ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}`
1947
- : "";
1948
- c.addChild(new Text(`${rIcon} ${theme.bold(r.agent)}${rProgress}`, 0, 0));
1949
-
1950
- if (rRunning && rProg) {
1951
- if (rProg.currentTool) {
1952
- const toolLine = rProg.currentToolArgs
1953
- ? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 50)}${rProg.currentToolArgs.length > 50 ? "..." : ""}`
1954
- : rProg.currentTool;
1955
- c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
1956
- }
1957
- for (const line of rProg.recentOutput.slice(-2)) {
1958
- c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 70)}${line.length > 70 ? "..." : ""}`), 0, 0));
1959
- }
1960
- } else {
1961
- const out = r.truncation?.text || getFinalOutput(r.messages);
1962
- if (out) c.addChild(new Markdown(out, 0, 0, mdTheme));
1963
- c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
1964
- if (r.sessionFile) {
1965
- c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
1966
- }
1967
- // Sharing disabled - session file path shown above
1968
- }
1969
- }
1970
-
1971
- if (d.artifacts) {
1972
- c.addChild(new Spacer(1));
1973
- c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
1974
- }
1975
- return c;
1976
- }
1977
-
1978
- const lines = [`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`];
1979
- // Show chain visualization if available
1980
- if (chainVis) {
1981
- lines.push(` ${chainVis}`);
1982
- }
1983
- // Find running progress from d.progress array (more reliable) or d.results
1984
- const runningProgress = d.progress?.find((p) => p.status === "running")
1985
- || d.results.find((r) => r.progress?.status === "running")?.progress;
1986
- if (runningProgress) {
1987
- if (runningProgress.currentTool) {
1988
- const toolLine = runningProgress.currentToolArgs
1989
- ? `${runningProgress.currentTool}: ${runningProgress.currentToolArgs.slice(0, 50)}${runningProgress.currentToolArgs.length > 50 ? "..." : ""}`
1990
- : runningProgress.currentTool;
1991
- lines.push(theme.fg("warning", ` > ${toolLine}`));
1992
- }
1993
- for (const line of runningProgress.recentOutput.slice(-2)) {
1994
- lines.push(theme.fg("dim", ` ${line.slice(0, 70)}${line.length > 70 ? "..." : ""}`));
1995
- }
1996
- lines.push(theme.fg("dim", "(ctrl+o to expand)"));
1997
- } else if (hasRunning) {
1998
- // Fallback: we know something is running but can't find details
1999
- lines.push(theme.fg("dim", "(ctrl+o to expand)"));
2000
- }
2001
- return new Text(lines.join("\n"), 0, 0);
458
+ renderResult(result, options, theme) {
459
+ return renderSubagentResult(result, options, theme);
2002
460
  },
2003
461
 
2004
462
  };