pi-subagents 0.24.4 → 0.25.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/src/tui/render.ts CHANGED
@@ -12,12 +12,15 @@ import {
12
12
  type AsyncJobStep,
13
13
  type AsyncParallelGroupStatus,
14
14
  type Details,
15
+ type NestedRunSummary,
16
+ type NestedStepSummary,
15
17
  MAX_WIDGET_JOBS,
16
18
  WIDGET_KEY,
17
19
  } from "../shared/types.ts";
18
20
  import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
19
21
  import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
20
22
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
23
+ import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
21
24
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
22
25
 
23
26
  type Theme = ExtensionContext["ui"]["theme"];
@@ -257,6 +260,7 @@ export function widgetRenderKey(job: AsyncJobState): string {
257
260
  chainStepCount: job.chainStepCount,
258
261
  parallelGroups: job.parallelGroups,
259
262
  steps: job.steps,
263
+ nestedChildren: job.nestedChildren,
260
264
  stepsTotal: job.stepsTotal,
261
265
  runningSteps: job.runningSteps,
262
266
  completedSteps: job.completedSteps,
@@ -399,18 +403,21 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
399
403
  return lines;
400
404
  }
401
405
 
402
- function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
406
+ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
403
407
  if (!job.steps?.length) return [];
404
408
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
405
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
409
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
406
410
  const total = job.stepsTotal ?? job.steps.length;
407
- return job.steps.map((step, index) => {
408
- const marker = index === job.steps!.length - 1 ? "└" : "├";
411
+ const lines: string[] = [];
412
+ for (const [index, step] of job.steps.entries()) {
413
+ const marker = index === job.steps.length - 1 ? "└" : "├";
409
414
  const activity = widgetStepActivity(step, job.updatedAt);
410
415
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
411
416
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
412
- return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
413
- });
417
+ 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}` : ""}`)}`);
418
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
419
+ }
420
+ return lines;
414
421
  }
415
422
 
416
423
  function parseParallelGroupAgentCount(label: string | undefined): number | undefined {
@@ -646,6 +653,84 @@ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["s
646
653
  return path.join(job.asyncDir, `output-${step.index}.log`);
647
654
  }
648
655
 
656
+ function nestedRunName(run: NestedRunSummary): string {
657
+ if (run.agent) return run.agent;
658
+ if (run.agents?.length) return formatWidgetAgents(run.agents);
659
+ return run.id;
660
+ }
661
+
662
+ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number): string {
663
+ if (state === "running") return theme.fg("accent", runningGlyph(seed));
664
+ if (state === "complete" || state === "completed") return theme.fg("success", "✓");
665
+ if (state === "failed") return theme.fg("error", "✗");
666
+ if (state === "paused") return theme.fg("warning", "■");
667
+ return theme.fg("muted", "◦");
668
+ }
669
+
670
+ function nestedRunSeed(run: NestedRunSummary): number | undefined {
671
+ return runningSeed(run.lastUpdate, run.lastActivityAt, run.currentStep, run.toolCount, run.turnCount, run.totalTokens?.total, run.currentToolStartedAt);
672
+ }
673
+
674
+ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount">, state: NestedRunSummary["state"] | NestedStepSummary["status"], snapshotNow?: number): string {
675
+ const facts: string[] = [];
676
+ if (input.currentTool && input.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${input.currentTool} ${formatDuration(Math.max(0, snapshotNow - input.currentToolStartedAt))}`);
677
+ else if (input.currentTool) facts.push(input.currentTool);
678
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
679
+ if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
680
+ if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
681
+ const activity = buildLiveStatusLine(input, snapshotNow);
682
+ if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
683
+ if (activity) return activity;
684
+ if (facts.length) return facts.join(" · ");
685
+ if (state === "running") return "thinking…";
686
+ if (state === "queued" || state === "pending") return "queued…";
687
+ if (state === "paused") return "Paused";
688
+ if (state === "failed") return "Failed";
689
+ return "Done";
690
+ }
691
+
692
+ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
693
+ if (!children?.length || lineBudget <= 0) return [];
694
+ if (!expanded) {
695
+ const aggregate = formatNestedAggregate(children);
696
+ return aggregate ? [theme.fg("dim", `↳ ${aggregate}`)] : [];
697
+ }
698
+ const lines: string[] = [];
699
+ const maxDepth = 2;
700
+ const append = (items: NestedRunSummary[] | undefined, depth: number, prefix: string): void => {
701
+ if (!items?.length || lines.length >= lineBudget) return;
702
+ if (depth > maxDepth) {
703
+ const aggregate = formatNestedAggregate(items);
704
+ if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix}↳ ${aggregate}`));
705
+ return;
706
+ }
707
+ for (let index = 0; index < items.length; index++) {
708
+ const child = items[index]!;
709
+ if (lines.length >= lineBudget) {
710
+ const aggregate = formatNestedAggregate(items.slice(index));
711
+ if (aggregate) lines[lines.length - 1] = theme.fg("dim", `${prefix}↳ ${aggregate}`);
712
+ return;
713
+ }
714
+ const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
715
+ const error = child.error ? ` · ${child.error}` : "";
716
+ lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
717
+ if (depth === maxDepth) {
718
+ const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
719
+ if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
720
+ continue;
721
+ }
722
+ for (const step of child.steps ?? []) {
723
+ if (lines.length >= lineBudget) return;
724
+ lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
725
+ append(step.children, depth + 1, `${prefix} `);
726
+ }
727
+ append(child.children, depth + 1, `${prefix} `);
728
+ }
729
+ };
730
+ append(children, 0, "");
731
+ return lines.map((line) => truncLine(line, width));
732
+ }
733
+
649
734
  function foregroundStyleWidgetStepLines(
650
735
  job: AsyncJobState,
651
736
  theme: Theme,
@@ -662,6 +747,9 @@ function foregroundStyleWidgetStepLines(
662
747
  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
748
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
664
749
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
750
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
751
+ lines.push(` ${nestedLine}`);
752
+ }
665
753
  if (step.status === "running") {
666
754
  if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
667
755
  const output = widgetOutputPath(job, step);
@@ -683,7 +771,10 @@ function foregroundStyleWidgetStepLines(
683
771
  }
684
772
 
685
773
  function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
686
- if (!job.steps?.length) return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
774
+ if (!job.steps?.length) return [
775
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
776
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
777
+ ];
687
778
  if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
688
779
  const total = job.stepsTotal ?? job.steps.length;
689
780
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
@@ -691,6 +782,11 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
691
782
  for (const [index, step] of job.steps.entries()) {
692
783
  lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
693
784
  }
785
+ const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
786
+ const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
787
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
788
+ lines.push(` ${nestedLine}`);
789
+ }
694
790
  return lines;
695
791
  }
696
792
 
@@ -720,6 +816,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
720
816
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
721
817
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
722
818
  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}` : ""}`);
819
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
723
820
  }
724
821
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
725
822
  return lines.map((line) => truncLine(line, width));
@@ -777,7 +874,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
777
874
  items.push([
778
875
  `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
779
876
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
780
- ...widgetParallelAgentDetails(job, theme),
877
+ ...widgetParallelAgentDetails(job, theme, expanded, width),
781
878
  ]);
782
879
  slots--;
783
880
  }
@@ -794,7 +891,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
794
891
  items.push([
795
892
  `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
796
893
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
797
- ...widgetParallelAgentDetails(job, theme),
894
+ ...widgetParallelAgentDetails(job, theme, expanded, width),
798
895
  ]);
799
896
  slots--;
800
897
  }
@@ -841,7 +938,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
841
938
  const isRunning = r.progress?.status === "running";
842
939
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
843
940
  const stats = statJoin(theme, [
844
- r.usage?.turns ? `⟳${r.usage.turns}` : "",
941
+ r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
845
942
  formatProgressStats(theme, progress),
846
943
  ]);
847
944
  const c = new Container();