pi-subagents 0.24.4 → 0.27.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 +29 -0
- package/README.md +145 -27
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +71 -20
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +171 -0
- package/src/extension/index.ts +7 -2
- package/src/extension/schemas.ts +138 -5
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +185 -10
- package/src/runs/background/async-job-tracker.ts +41 -6
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +71 -31
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +89 -4
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +648 -42
- package/src/runs/foreground/chain-execution.ts +331 -118
- package/src/runs/foreground/execution.ts +226 -10
- package/src/runs/foreground/subagent-executor.ts +377 -14
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +73 -5
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +345 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +268 -43
package/src/tui/render.ts
CHANGED
|
@@ -12,12 +12,16 @@ import {
|
|
|
12
12
|
type AsyncJobStep,
|
|
13
13
|
type AsyncParallelGroupStatus,
|
|
14
14
|
type Details,
|
|
15
|
+
type NestedRunSummary,
|
|
16
|
+
type NestedStepSummary,
|
|
17
|
+
type WorkflowNodeStatus,
|
|
15
18
|
MAX_WIDGET_JOBS,
|
|
16
19
|
WIDGET_KEY,
|
|
17
20
|
} from "../shared/types.ts";
|
|
18
21
|
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
19
22
|
import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
|
|
20
23
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
24
|
+
import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
|
|
21
25
|
import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
|
|
22
26
|
|
|
23
27
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
@@ -218,10 +222,21 @@ function firstOutputLine(text: string): string {
|
|
|
218
222
|
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
|
|
226
|
+
const acceptance = result.acceptance;
|
|
227
|
+
if (!acceptance?.status || acceptance.status === "not-required") return undefined;
|
|
228
|
+
const finalization = acceptance.finalization
|
|
229
|
+
? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
|
|
230
|
+
: "";
|
|
231
|
+
return `acceptance: ${acceptance.status}${finalization}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
221
234
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
222
235
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
223
236
|
if (result.interrupted) return "Paused";
|
|
224
237
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
238
|
+
const acceptance = formatAcceptanceStatus(result);
|
|
239
|
+
if (acceptance) return `Done · ${acceptance}`;
|
|
225
240
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
226
241
|
return "Done";
|
|
227
242
|
}
|
|
@@ -257,6 +272,7 @@ export function widgetRenderKey(job: AsyncJobState): string {
|
|
|
257
272
|
chainStepCount: job.chainStepCount,
|
|
258
273
|
parallelGroups: job.parallelGroups,
|
|
259
274
|
steps: job.steps,
|
|
275
|
+
nestedChildren: job.nestedChildren,
|
|
260
276
|
stepsTotal: job.stepsTotal,
|
|
261
277
|
runningSteps: job.runningSteps,
|
|
262
278
|
completedSteps: job.completedSteps,
|
|
@@ -399,18 +415,21 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
399
415
|
return lines;
|
|
400
416
|
}
|
|
401
417
|
|
|
402
|
-
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
|
|
418
|
+
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
403
419
|
if (!job.steps?.length) return [];
|
|
404
420
|
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
405
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
|
|
421
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
406
422
|
const total = job.stepsTotal ?? job.steps.length;
|
|
407
|
-
|
|
408
|
-
|
|
423
|
+
const lines: string[] = [];
|
|
424
|
+
for (const [index, step] of job.steps.entries()) {
|
|
425
|
+
const marker = index === job.steps.length - 1 ? "└" : "├";
|
|
409
426
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
410
427
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
411
428
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
412
|
-
|
|
413
|
-
|
|
429
|
+
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
430
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
|
|
431
|
+
}
|
|
432
|
+
return lines;
|
|
414
433
|
}
|
|
415
434
|
|
|
416
435
|
function parseParallelGroupAgentCount(label: string | undefined): number | undefined {
|
|
@@ -420,26 +439,44 @@ function parseParallelGroupAgentCount(label: string | undefined): number | undef
|
|
|
420
439
|
return inner.split("+").map((part) => part.trim()).filter(Boolean).length;
|
|
421
440
|
}
|
|
422
441
|
|
|
423
|
-
function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">): boolean {
|
|
424
|
-
if (details.mode !== "chain") return false;
|
|
425
|
-
if (details.currentStepIndex === undefined) return false;
|
|
426
|
-
const currentLabel = details.chainAgents?.[details.currentStepIndex];
|
|
427
|
-
return parseParallelGroupAgentCount(currentLabel) !== undefined;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
442
|
interface ChainStepSpan {
|
|
431
443
|
stepIndex: number;
|
|
432
444
|
start: number;
|
|
433
445
|
count: number;
|
|
434
446
|
isParallel: boolean;
|
|
447
|
+
status?: WorkflowNodeStatus;
|
|
448
|
+
label?: string;
|
|
449
|
+
error?: string;
|
|
435
450
|
}
|
|
436
451
|
|
|
437
|
-
function buildChainStepSpans(
|
|
438
|
-
if (
|
|
452
|
+
function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGraph">): ChainStepSpan[] {
|
|
453
|
+
if (details.workflowGraph?.nodes?.length) {
|
|
454
|
+
const spans: ChainStepSpan[] = [];
|
|
455
|
+
let flatCursor = 0;
|
|
456
|
+
for (const node of details.workflowGraph.nodes) {
|
|
457
|
+
if (node.stepIndex === undefined) continue;
|
|
458
|
+
if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
|
|
459
|
+
const childFlatIndexes = (node.children ?? [])
|
|
460
|
+
.map((child) => child.flatIndex)
|
|
461
|
+
.filter((value): value is number => typeof value === "number");
|
|
462
|
+
const start = childFlatIndexes.length ? Math.min(...childFlatIndexes) : flatCursor;
|
|
463
|
+
const count = node.children?.length ?? 0;
|
|
464
|
+
spans.push({ stepIndex: node.stepIndex, start, count, isParallel: true, status: node.status, label: node.label, error: node.error });
|
|
465
|
+
flatCursor = Math.max(flatCursor, start + count);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const start = node.flatIndex ?? flatCursor;
|
|
469
|
+
spans.push({ stepIndex: node.stepIndex, start, count: 1, isParallel: false, status: node.status, label: node.label, error: node.error });
|
|
470
|
+
flatCursor = Math.max(flatCursor, start + 1);
|
|
471
|
+
}
|
|
472
|
+
if (spans.length) return spans.sort((left, right) => left.stepIndex - right.stepIndex);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!details.chainAgents?.length) return [];
|
|
439
476
|
const spans: ChainStepSpan[] = [];
|
|
440
477
|
let start = 0;
|
|
441
|
-
for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
|
|
442
|
-
const label = chainAgents[stepIndex]!;
|
|
478
|
+
for (let stepIndex = 0; stepIndex < details.chainAgents.length; stepIndex++) {
|
|
479
|
+
const label = details.chainAgents[stepIndex]!;
|
|
443
480
|
const parsedCount = parseParallelGroupAgentCount(label);
|
|
444
481
|
const count = parsedCount ?? 1;
|
|
445
482
|
spans.push({ stepIndex, start, count, isParallel: parsedCount !== undefined });
|
|
@@ -448,6 +485,12 @@ function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[]
|
|
|
448
485
|
return spans;
|
|
449
486
|
}
|
|
450
487
|
|
|
488
|
+
function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
|
|
489
|
+
if (details.mode !== "chain") return false;
|
|
490
|
+
if (details.currentStepIndex === undefined) return false;
|
|
491
|
+
return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
|
|
492
|
+
}
|
|
493
|
+
|
|
451
494
|
function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
|
|
452
495
|
const spans: ChainStepSpan[] = [];
|
|
453
496
|
let flatIndex = 0;
|
|
@@ -472,6 +515,55 @@ function isDoneResult(result: Details["results"][number]): boolean {
|
|
|
472
515
|
return result.exitCode === 0;
|
|
473
516
|
}
|
|
474
517
|
|
|
518
|
+
function workflowGraphHasStatus(details: Pick<Details, "workflowGraph">, statuses: WorkflowNodeStatus[]): boolean {
|
|
519
|
+
return details.workflowGraph?.nodes.some((node) => statuses.includes(node.status)) ?? false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
interface ChainRenderResultEntry {
|
|
523
|
+
kind: "result";
|
|
524
|
+
resultIndex: number;
|
|
525
|
+
rowNumber: number;
|
|
526
|
+
agentName: string;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
interface ChainRenderPlaceholderEntry {
|
|
530
|
+
kind: "placeholder";
|
|
531
|
+
rowNumber: number;
|
|
532
|
+
stepLabel: string;
|
|
533
|
+
agentName: string;
|
|
534
|
+
status: WorkflowNodeStatus;
|
|
535
|
+
error?: string;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
type ChainRenderEntry = ChainRenderResultEntry | ChainRenderPlaceholderEntry;
|
|
539
|
+
|
|
540
|
+
function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
|
|
541
|
+
if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
|
|
542
|
+
const entries: ChainRenderEntry[] = [];
|
|
543
|
+
for (const span of buildChainStepSpans(details)) {
|
|
544
|
+
if (span.isParallel && span.count === 0) {
|
|
545
|
+
entries.push({
|
|
546
|
+
kind: "placeholder",
|
|
547
|
+
rowNumber: span.stepIndex + 1,
|
|
548
|
+
stepLabel: `Step ${span.stepIndex + 1}`,
|
|
549
|
+
agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
|
|
550
|
+
status: span.status ?? "pending",
|
|
551
|
+
error: span.error,
|
|
552
|
+
});
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
for (let index = span.start; index < span.start + span.count; index++) {
|
|
556
|
+
entries.push({
|
|
557
|
+
kind: "result",
|
|
558
|
+
resultIndex: index,
|
|
559
|
+
rowNumber: index + 1,
|
|
560
|
+
agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return entries;
|
|
565
|
+
}
|
|
566
|
+
|
|
475
567
|
interface MultiProgressLabel {
|
|
476
568
|
headerLabel: string;
|
|
477
569
|
itemTitle: "Step" | "Agent";
|
|
@@ -483,8 +575,8 @@ interface MultiProgressLabel {
|
|
|
483
575
|
showActiveGroupOnly: boolean;
|
|
484
576
|
}
|
|
485
577
|
|
|
486
|
-
function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents">, hasRunning: boolean): MultiProgressLabel {
|
|
487
|
-
const stepSpans = buildChainStepSpans(details
|
|
578
|
+
function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
|
|
579
|
+
const stepSpans = buildChainStepSpans(details);
|
|
488
580
|
const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
|
|
489
581
|
const activeParallelGroup = isChainParallelGroupActive(details);
|
|
490
582
|
const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
|
|
@@ -548,11 +640,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
548
640
|
if (details.mode === "chain" && details.chainAgents?.length) {
|
|
549
641
|
const totalCount = details.totalSteps ?? details.chainAgents.length;
|
|
550
642
|
const doneLogical = stepSpans.filter((span) => {
|
|
643
|
+
if (span.status && span.status !== "completed") return false;
|
|
644
|
+
if (span.count === 0) return span.status === "completed";
|
|
551
645
|
for (let index = span.start; index < span.start + span.count; index++) {
|
|
552
646
|
const progressEntry = details.progress?.find((progress) => progress.index === index);
|
|
553
647
|
const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
|
|
554
|
-
if (progressEntry?.status === "running" || progressEntry?.status === "pending") return false;
|
|
555
|
-
if (resultEntry
|
|
648
|
+
if (progressEntry?.status === "running" || progressEntry?.status === "pending" || progressEntry?.status === "failed") return false;
|
|
649
|
+
if (!resultEntry || !isDoneResult(resultEntry)) return false;
|
|
556
650
|
}
|
|
557
651
|
return true;
|
|
558
652
|
}).length;
|
|
@@ -568,9 +662,9 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
568
662
|
return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
|
|
569
663
|
}
|
|
570
664
|
|
|
571
|
-
function resultRowLabel(details: Pick<Details, "mode" | "chainAgents">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
|
|
665
|
+
function resultRowLabel(details: Pick<Details, "mode" | "chainAgents" | "workflowGraph">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
|
|
572
666
|
if (details.mode === "chain" && label.hasParallelInChain) {
|
|
573
|
-
const span = buildChainStepSpans(details
|
|
667
|
+
const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
|
|
574
668
|
if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
|
|
575
669
|
if (span) return `Step ${span.stepIndex + 1}`;
|
|
576
670
|
}
|
|
@@ -646,6 +740,84 @@ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["s
|
|
|
646
740
|
return path.join(job.asyncDir, `output-${step.index}.log`);
|
|
647
741
|
}
|
|
648
742
|
|
|
743
|
+
function nestedRunName(run: NestedRunSummary): string {
|
|
744
|
+
if (run.agent) return run.agent;
|
|
745
|
+
if (run.agents?.length) return formatWidgetAgents(run.agents);
|
|
746
|
+
return run.id;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number): string {
|
|
750
|
+
if (state === "running") return theme.fg("accent", runningGlyph(seed));
|
|
751
|
+
if (state === "complete" || state === "completed") return theme.fg("success", "✓");
|
|
752
|
+
if (state === "failed") return theme.fg("error", "✗");
|
|
753
|
+
if (state === "paused") return theme.fg("warning", "■");
|
|
754
|
+
return theme.fg("muted", "◦");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function nestedRunSeed(run: NestedRunSummary): number | undefined {
|
|
758
|
+
return runningSeed(run.lastUpdate, run.lastActivityAt, run.currentStep, run.toolCount, run.turnCount, run.totalTokens?.total, run.currentToolStartedAt);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount">, state: NestedRunSummary["state"] | NestedStepSummary["status"], snapshotNow?: number): string {
|
|
762
|
+
const facts: string[] = [];
|
|
763
|
+
if (input.currentTool && input.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${input.currentTool} ${formatDuration(Math.max(0, snapshotNow - input.currentToolStartedAt))}`);
|
|
764
|
+
else if (input.currentTool) facts.push(input.currentTool);
|
|
765
|
+
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
766
|
+
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
767
|
+
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
768
|
+
const activity = buildLiveStatusLine(input, snapshotNow);
|
|
769
|
+
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
770
|
+
if (activity) return activity;
|
|
771
|
+
if (facts.length) return facts.join(" · ");
|
|
772
|
+
if (state === "running") return "thinking…";
|
|
773
|
+
if (state === "queued" || state === "pending") return "queued…";
|
|
774
|
+
if (state === "paused") return "Paused";
|
|
775
|
+
if (state === "failed") return "Failed";
|
|
776
|
+
return "Done";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
|
|
780
|
+
if (!children?.length || lineBudget <= 0) return [];
|
|
781
|
+
if (!expanded) {
|
|
782
|
+
const aggregate = formatNestedAggregate(children);
|
|
783
|
+
return aggregate ? [theme.fg("dim", `↳ ${aggregate}`)] : [];
|
|
784
|
+
}
|
|
785
|
+
const lines: string[] = [];
|
|
786
|
+
const maxDepth = 2;
|
|
787
|
+
const append = (items: NestedRunSummary[] | undefined, depth: number, prefix: string): void => {
|
|
788
|
+
if (!items?.length || lines.length >= lineBudget) return;
|
|
789
|
+
if (depth > maxDepth) {
|
|
790
|
+
const aggregate = formatNestedAggregate(items);
|
|
791
|
+
if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix}↳ ${aggregate}`));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
for (let index = 0; index < items.length; index++) {
|
|
795
|
+
const child = items[index]!;
|
|
796
|
+
if (lines.length >= lineBudget) {
|
|
797
|
+
const aggregate = formatNestedAggregate(items.slice(index));
|
|
798
|
+
if (aggregate) lines[lines.length - 1] = theme.fg("dim", `${prefix}↳ ${aggregate}`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
|
|
802
|
+
const error = child.error ? ` · ${child.error}` : "";
|
|
803
|
+
lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
|
|
804
|
+
if (depth === maxDepth) {
|
|
805
|
+
const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
|
|
806
|
+
if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
for (const step of child.steps ?? []) {
|
|
810
|
+
if (lines.length >= lineBudget) return;
|
|
811
|
+
lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
|
|
812
|
+
append(step.children, depth + 1, `${prefix} `);
|
|
813
|
+
}
|
|
814
|
+
append(child.children, depth + 1, `${prefix} `);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
append(children, 0, "");
|
|
818
|
+
return lines.map((line) => truncLine(line, width));
|
|
819
|
+
}
|
|
820
|
+
|
|
649
821
|
function foregroundStyleWidgetStepLines(
|
|
650
822
|
job: AsyncJobState,
|
|
651
823
|
theme: Theme,
|
|
@@ -662,6 +834,9 @@ function foregroundStyleWidgetStepLines(
|
|
|
662
834
|
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
663
835
|
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
664
836
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
837
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
|
|
838
|
+
lines.push(` ${nestedLine}`);
|
|
839
|
+
}
|
|
665
840
|
if (step.status === "running") {
|
|
666
841
|
if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
|
|
667
842
|
const output = widgetOutputPath(job, step);
|
|
@@ -683,7 +858,10 @@ function foregroundStyleWidgetStepLines(
|
|
|
683
858
|
}
|
|
684
859
|
|
|
685
860
|
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
|
|
686
|
-
if (!job.steps?.length) return [
|
|
861
|
+
if (!job.steps?.length) return [
|
|
862
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
863
|
+
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
|
|
864
|
+
];
|
|
687
865
|
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
688
866
|
const total = job.stepsTotal ?? job.steps.length;
|
|
689
867
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
@@ -691,6 +869,11 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
691
869
|
for (const [index, step] of job.steps.entries()) {
|
|
692
870
|
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
|
|
693
871
|
}
|
|
872
|
+
const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
873
|
+
const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
874
|
+
for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
|
|
875
|
+
lines.push(` ${nestedLine}`);
|
|
876
|
+
}
|
|
694
877
|
return lines;
|
|
695
878
|
}
|
|
696
879
|
|
|
@@ -720,6 +903,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
720
903
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
721
904
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
722
905
|
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
906
|
+
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
|
|
723
907
|
}
|
|
724
908
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
|
|
725
909
|
return lines.map((line) => truncLine(line, width));
|
|
@@ -777,7 +961,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
777
961
|
items.push([
|
|
778
962
|
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
779
963
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
780
|
-
...widgetParallelAgentDetails(job, theme),
|
|
964
|
+
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
781
965
|
]);
|
|
782
966
|
slots--;
|
|
783
967
|
}
|
|
@@ -794,7 +978,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
794
978
|
items.push([
|
|
795
979
|
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
796
980
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
797
|
-
...widgetParallelAgentDetails(job, theme),
|
|
981
|
+
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
798
982
|
]);
|
|
799
983
|
slots--;
|
|
800
984
|
}
|
|
@@ -841,7 +1025,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
841
1025
|
const isRunning = r.progress?.status === "running";
|
|
842
1026
|
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
843
1027
|
const stats = statJoin(theme, [
|
|
844
|
-
r.usage?.turns ?
|
|
1028
|
+
r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
|
|
845
1029
|
formatProgressStats(theme, progress),
|
|
846
1030
|
]);
|
|
847
1031
|
const c = new Container();
|
|
@@ -873,9 +1057,12 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
873
1057
|
|
|
874
1058
|
function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
875
1059
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
876
|
-
|| d.results.some((r) => r.progress?.status === "running")
|
|
877
|
-
|
|
878
|
-
const
|
|
1060
|
+
|| d.results.some((r) => r.progress?.status === "running")
|
|
1061
|
+
|| workflowGraphHasStatus(d, ["running"]);
|
|
1062
|
+
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1063
|
+
|| workflowGraphHasStatus(d, ["failed"]);
|
|
1064
|
+
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1065
|
+
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
879
1066
|
let totalSummary = d.progressSummary;
|
|
880
1067
|
if (!totalSummary) {
|
|
881
1068
|
let sawProgress = false;
|
|
@@ -908,13 +1095,29 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
908
1095
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
909
1096
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
910
1097
|
const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
|
|
911
|
-
|
|
1098
|
+
const chainEntries = buildChainRenderEntries(d, multiLabel);
|
|
1099
|
+
const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
|
|
1100
|
+
const i = displayStart + offset;
|
|
912
1101
|
const r = d.results[i];
|
|
913
1102
|
const fallbackLabel = itemTitle.toLowerCase();
|
|
914
1103
|
const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
|
|
915
|
-
|
|
1104
|
+
return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
|
|
1105
|
+
});
|
|
1106
|
+
for (const entry of renderEntries) {
|
|
1107
|
+
if (entry.kind === "placeholder") {
|
|
1108
|
+
const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
|
|
1109
|
+
const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
|
|
1110
|
+
c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
|
|
1111
|
+
if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const i = entry.resultIndex;
|
|
1115
|
+
const r = d.results[i];
|
|
1116
|
+
const rowNumber = entry.rowNumber;
|
|
1117
|
+
const agentName = entry.agentName;
|
|
916
1118
|
if (!r) {
|
|
917
|
-
|
|
1119
|
+
const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
|
|
1120
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
|
|
918
1121
|
continue;
|
|
919
1122
|
}
|
|
920
1123
|
const output = getSingleResultOutput(r);
|
|
@@ -1062,20 +1265,27 @@ export function renderSubagentResult(
|
|
|
1062
1265
|
if (!expanded) return renderMultiCompact(d, theme);
|
|
1063
1266
|
|
|
1064
1267
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1065
|
-
|| d.results.some((r) => r.progress?.status === "running")
|
|
1268
|
+
|| d.results.some((r) => r.progress?.status === "running")
|
|
1269
|
+
|| workflowGraphHasStatus(d, ["running"]);
|
|
1066
1270
|
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
1067
1271
|
const hasEmptyWithoutTarget = d.results.some((r) =>
|
|
1068
1272
|
r.exitCode === 0
|
|
1069
1273
|
&& r.progress?.status !== "running"
|
|
1070
1274
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1071
1275
|
);
|
|
1276
|
+
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1277
|
+
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1072
1278
|
const icon = hasRunning
|
|
1073
1279
|
? theme.fg("warning", "running")
|
|
1074
1280
|
: hasEmptyWithoutTarget
|
|
1075
1281
|
? theme.fg("warning", "warning")
|
|
1076
|
-
:
|
|
1077
|
-
? theme.fg("
|
|
1078
|
-
:
|
|
1282
|
+
: hasWorkflowFailure
|
|
1283
|
+
? theme.fg("error", "failed")
|
|
1284
|
+
: hasWorkflowPause
|
|
1285
|
+
? theme.fg("warning", "paused")
|
|
1286
|
+
: ok === d.results.length
|
|
1287
|
+
? theme.fg("success", "ok")
|
|
1288
|
+
: theme.fg("error", "failed");
|
|
1079
1289
|
|
|
1080
1290
|
const totalSummary =
|
|
1081
1291
|
d.progressSummary ||
|
|
@@ -1146,18 +1356,33 @@ export function renderSubagentResult(
|
|
|
1146
1356
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
1147
1357
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
1148
1358
|
const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
|
|
1359
|
+
const chainEntries = buildChainRenderEntries(d, multiLabel);
|
|
1360
|
+
const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
|
|
1361
|
+
const i = displayStart + offset;
|
|
1362
|
+
const r = d.results[i];
|
|
1363
|
+
const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
|
|
1364
|
+
return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
|
|
1365
|
+
});
|
|
1149
1366
|
|
|
1150
1367
|
c.addChild(new Spacer(1));
|
|
1151
1368
|
|
|
1152
|
-
for (
|
|
1369
|
+
for (const entry of renderEntries) {
|
|
1370
|
+
if (entry.kind === "placeholder") {
|
|
1371
|
+
const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
|
|
1372
|
+
c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}: ${theme.bold(entry.agentName)}`), 0, 0));
|
|
1373
|
+
c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
|
|
1374
|
+
if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
|
|
1375
|
+
c.addChild(new Spacer(1));
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const i = entry.resultIndex;
|
|
1153
1379
|
const r = d.results[i];
|
|
1154
|
-
const rowNumber =
|
|
1155
|
-
const agentName =
|
|
1156
|
-
? (r?.agent || `step-${rowNumber}`)
|
|
1157
|
-
: (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
|
|
1380
|
+
const rowNumber = entry.rowNumber;
|
|
1381
|
+
const agentName = entry.agentName;
|
|
1158
1382
|
|
|
1159
1383
|
if (!r) {
|
|
1160
|
-
|
|
1384
|
+
const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
|
|
1385
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
|
|
1161
1386
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
1162
1387
|
c.addChild(new Spacer(1));
|
|
1163
1388
|
continue;
|