pi-subagents 0.22.0 → 0.23.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.
@@ -207,6 +207,7 @@ export interface SingleResult {
207
207
 
208
208
  export interface Details {
209
209
  mode: SubagentRunMode | "management";
210
+ runId?: string;
210
211
  context?: "fresh" | "fork";
211
212
  results: SingleResult[];
212
213
  controlEvents?: ControlEvent[];
@@ -296,11 +297,15 @@ export interface AsyncStatus {
296
297
  steps?: Array<{
297
298
  agent: string;
298
299
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
300
+ sessionFile?: string;
299
301
  activityState?: ActivityState;
300
302
  lastActivityAt?: number;
301
303
  currentTool?: string;
304
+ currentToolArgs?: string;
302
305
  currentToolStartedAt?: number;
303
306
  currentPath?: string;
307
+ recentTools?: Array<{ tool: string; args: string; endMs: number }>;
308
+ recentOutput?: string[];
304
309
  turnCount?: number;
305
310
  toolCount?: number;
306
311
  startedAt?: number;
@@ -320,6 +325,10 @@ export interface AsyncStatus {
320
325
  sessionFile?: string;
321
326
  }
322
327
 
328
+ export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
329
+ index?: number;
330
+ };
331
+
323
332
  export interface AsyncJobState {
324
333
  asyncId: string;
325
334
  asyncDir: string;
@@ -338,7 +347,7 @@ export interface AsyncJobState {
338
347
  currentStep?: number;
339
348
  chainStepCount?: number;
340
349
  parallelGroups?: AsyncParallelGroupStatus[];
341
- steps?: AsyncStatus["steps"];
350
+ steps?: AsyncJobStep[];
342
351
  stepsTotal?: number;
343
352
  runningSteps?: number;
344
353
  completedSteps?: number;
@@ -353,10 +362,26 @@ export interface AsyncJobState {
353
362
  controlEventCursor?: number;
354
363
  }
355
364
 
365
+ export interface ForegroundResumeChild {
366
+ agent: string;
367
+ index: number;
368
+ sessionFile?: string;
369
+ status: SubagentResultStatus;
370
+ }
371
+
372
+ export interface ForegroundResumeRun {
373
+ runId: string;
374
+ mode: SubagentRunMode;
375
+ cwd: string;
376
+ updatedAt: number;
377
+ children: ForegroundResumeChild[];
378
+ }
379
+
356
380
  export interface SubagentState {
357
381
  baseCwd: string;
358
382
  currentSessionId: string | null;
359
383
  asyncJobs: Map<string, AsyncJobState>;
384
+ foregroundRuns?: Map<string, ForegroundResumeRun>;
360
385
  foregroundControls: Map<string, {
361
386
  runId: string;
362
387
  mode: SubagentRunMode;
@@ -279,6 +279,7 @@ async function runSlashSubagent(
279
279
  ctx: ExtensionContext,
280
280
  params: SubagentParamsLike,
281
281
  ): Promise<void> {
282
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
282
283
  const requestId = randomUUID();
283
284
  const initialDetails = buildSlashInitialResult(requestId, params);
284
285
  const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
package/src/tui/render.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Rendering functions for subagent results
3
3
  */
4
4
 
5
+ import * as path from "node:path";
5
6
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
6
7
  import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
8
  import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
@@ -14,6 +15,7 @@ import {
14
15
  } from "../shared/types.ts";
15
16
  import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "../shared/formatters.ts";
16
17
  import { getDisplayItems, getLastActivity, getSingleResultOutput } from "../shared/utils.ts";
18
+ import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
17
19
 
18
20
  type Theme = ExtensionContext["ui"]["theme"];
19
21
 
@@ -264,10 +266,10 @@ function formatWidgetAgents(agents: string[]): string {
264
266
  }
265
267
 
266
268
  function widgetJobName(job: AsyncJobState): string {
267
- const agents = job.agents?.length ? formatWidgetAgents(job.agents) : undefined;
268
- if (job.mode === "parallel") return agents ? `parallel · ${agents}` : "parallel";
269
- if (job.activeParallelGroup) return agents ? `parallel group · ${agents}` : "parallel group";
270
- if (job.agents?.length) return job.agents.join(" → ");
269
+ if (job.mode === "parallel") return "parallel";
270
+ if (job.mode === "chain") return "chain";
271
+ if (job.mode === "single" && job.agents?.length === 1) return job.agents[0]!;
272
+ if (job.agents?.length) return formatWidgetAgents(job.agents);
271
273
  return job.mode ?? "subagent";
272
274
  }
273
275
 
@@ -293,6 +295,7 @@ function widgetActivity(job: AsyncJobState): string {
293
295
  if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
294
296
  if (activity) return activity;
295
297
  if (facts.length) return facts.join(" · ");
298
+ if (job.status === "running") return "thinking…";
296
299
  if (job.status === "queued") return "queued…";
297
300
  if (job.status === "paused") return "Paused";
298
301
  if (job.status === "failed") return "Failed";
@@ -308,7 +311,7 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
308
311
  }
309
312
 
310
313
  function widgetStepGlyph(status: string, theme: Theme): string {
311
- if (status === "running") return theme.fg("accent", "▶");
314
+ if (status === "running") return theme.fg("accent", spinnerFrame());
312
315
  if (status === "complete" || status === "completed") return theme.fg("success", "✓");
313
316
  if (status === "failed") return theme.fg("error", "✗");
314
317
  if (status === "paused") return theme.fg("warning", "■");
@@ -337,14 +340,62 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]):
337
340
  return facts.join(" · ");
338
341
  }
339
342
 
343
+ function widgetAggregateStepStatus(steps: NonNullable<AsyncJobState["steps"]>): string {
344
+ if (steps.some((step) => step.status === "running")) return "running";
345
+ if (steps.some((step) => step.status === "failed")) return "failed";
346
+ if (steps.some((step) => step.status === "paused")) return "paused";
347
+ if (steps.length > 0 && steps.every((step) => step.status === "complete" || step.status === "completed")) return "complete";
348
+ return "pending";
349
+ }
350
+
351
+ function widgetParallelOutcome(steps: NonNullable<AsyncJobState["steps"]>, total: number): string {
352
+ const running = steps.filter((step) => step.status === "running").length;
353
+ const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
354
+ const failed = steps.filter((step) => step.status === "failed").length;
355
+ const paused = steps.filter((step) => step.status === "paused").length;
356
+ const parts = [`${done}/${total} done`];
357
+ if (running > 0) parts.unshift(formatAgentRunningLabel(running));
358
+ if (failed > 0) parts.push(`${failed} failed`);
359
+ if (paused > 0) parts.push(`${paused} paused`);
360
+ return parts.join(" · ");
361
+ }
362
+
363
+ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
364
+ if (!job.steps?.length) return [];
365
+ const total = job.chainStepCount ?? job.steps.length;
366
+ const groups = job.parallelGroups ?? [];
367
+ const lines: string[] = [];
368
+ let flatIndex = 0;
369
+ for (let stepIndex = 0; stepIndex < total; stepIndex++) {
370
+ const group = groups.find((candidate) => candidate.stepIndex === stepIndex);
371
+ if (group) {
372
+ const steps = job.steps.slice(group.start, group.start + group.count);
373
+ const status = widgetAggregateStepStatus(steps);
374
+ lines.push(` ${widgetStepGlyph(status, theme)} Step ${stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", widgetParallelOutcome(steps, group.count))}`);
375
+ flatIndex = Math.max(flatIndex, group.start + group.count);
376
+ continue;
377
+ }
378
+ const step = job.steps[flatIndex];
379
+ if (!step) {
380
+ lines.push(` ${theme.fg("dim", `◦ Step ${stepIndex + 1}/${total}: pending`)}`);
381
+ continue;
382
+ }
383
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", stepIndex + 1, total, expanded, width));
384
+ flatIndex++;
385
+ }
386
+ return lines;
387
+ }
388
+
340
389
  function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
341
- if (!job.activeParallelGroup || !job.steps?.length) return [];
390
+ if (!job.steps?.length) return [];
342
391
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
392
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
343
393
  const total = job.stepsTotal ?? job.steps.length;
344
394
  return job.steps.map((step, index) => {
345
395
  const marker = index === job.steps!.length - 1 ? "└" : "├";
346
396
  const activity = widgetStepActivity(step);
347
- return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} Agent ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${activity ? ` · ${activity}` : ""}`)}`;
397
+ const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
398
+ return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${activity ? ` · ${activity}` : ""}`)}`;
348
399
  });
349
400
  }
350
401
 
@@ -513,7 +564,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
513
564
  const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
514
565
  const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
515
566
  if (job.mode === "parallel") {
516
- if (job.status === "running") parts.push(formatAgentRunningLabel(running));
567
+ if (job.status === "running" && running > 0) parts.push(formatAgentRunningLabel(running));
517
568
  if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
518
569
  } else {
519
570
  const activeGroup = job.currentStep !== undefined
@@ -521,24 +572,159 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
521
572
  : job.parallelGroups?.find((group) => group.start === 0);
522
573
  const logicalStep = activeGroup?.stepIndex ?? job.currentStep ?? 0;
523
574
  const total = job.chainStepCount ?? stepsTotal;
524
- const groupProgress = job.status === "running"
525
- ? `${formatAgentRunningLabel(running)} · ${done}/${stepsTotal} done`
526
- : `${done}/${stepsTotal} done`;
527
- parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupProgress}`);
575
+ const groupParts = [`${done}/${stepsTotal} done`];
576
+ if (job.status === "running" && running > 0) groupParts.unshift(formatAgentRunningLabel(running));
577
+ parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`);
528
578
  }
529
579
  } else if (job.currentStep !== undefined) {
530
- parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
580
+ if (job.mode === "chain" && job.parallelGroups?.length) {
581
+ const total = job.chainStepCount ?? stepsTotal;
582
+ parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`);
583
+ } else {
584
+ parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
585
+ }
531
586
  } else if (stepsTotal > 1) {
532
587
  parts.push(`steps ${stepsTotal}`);
533
588
  }
589
+ if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
534
590
  if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
535
591
  const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
536
592
  if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
537
593
  return statJoin(theme, parts);
538
594
  }
539
595
 
540
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth()): string[] {
596
+ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>[number]): string {
597
+ return statJoin(theme, [
598
+ step.turnCount !== undefined ? `${step.turnCount} turns` : "",
599
+ step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
600
+ step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
601
+ step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
602
+ ]);
603
+ }
604
+
605
+ function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean): string {
606
+ const toolLine = formatCurrentToolLine(step, width, expanded);
607
+ if (toolLine) return toolLine;
608
+ const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
609
+ if (activity) return activity;
610
+ if (step.status === "running") return "thinking…";
611
+ return "";
612
+ }
613
+
614
+ function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["steps"]>[number]): string | undefined {
615
+ if (typeof step.index !== "number") return undefined;
616
+ return path.join(job.asyncDir, `output-${step.index}.log`);
617
+ }
618
+
619
+ function foregroundStyleWidgetStepLines(
620
+ job: AsyncJobState,
621
+ theme: Theme,
622
+ step: NonNullable<AsyncJobState["steps"]>[number],
623
+ itemTitle: "Agent" | "Step",
624
+ index: number,
625
+ total: number,
626
+ expanded: boolean,
627
+ width: number,
628
+ ): string[] {
629
+ const status = widgetStepStatus(step.status, theme);
630
+ const stats = widgetStepStats(theme, step);
631
+ const lines = [` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
632
+ const activity = widgetStepActivityLine(step, width, expanded);
633
+ if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
634
+ if (step.status === "running") {
635
+ if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
636
+ const output = widgetOutputPath(job, step);
637
+ if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
638
+ if (expanded) {
639
+ const liveStatus = buildLiveStatusLine(step);
640
+ if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
641
+ for (const tool of step.recentTools?.slice(-3) ?? []) {
642
+ const maxArgsLen = Math.max(40, width - 30);
643
+ const argsPreview = tool.args.length <= maxArgsLen ? tool.args : `${tool.args.slice(0, maxArgsLen)}...`;
644
+ lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
645
+ }
646
+ for (const line of step.recentOutput?.slice(-5) ?? []) {
647
+ lines.push(` ${theme.fg("dim", line)}`);
648
+ }
649
+ }
650
+ }
651
+ return lines;
652
+ }
653
+
654
+ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
655
+ if (!job.steps?.length) return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
656
+ if (job.mode !== "parallel" && job.mode !== "chain") return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
657
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
658
+ const total = job.stepsTotal ?? job.steps.length;
659
+ const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
660
+ const lines: string[] = [];
661
+ for (const [index, step] of job.steps.entries()) {
662
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
663
+ }
664
+ return lines;
665
+ }
666
+
667
+ function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
668
+ const stats = widgetStats(job, theme);
669
+ const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
670
+ const mode = widgetJobName(job);
671
+ const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
672
+ return [
673
+ `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background · /subagents-status")}`,
674
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
675
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width),
676
+ ].map((line) => truncLine(line, width));
677
+ }
678
+
679
+ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
680
+ const fullLines = buildSingleWidgetLines(job, theme, width, false);
681
+ if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
682
+
683
+ const total = job.stepsTotal ?? job.steps.length;
684
+ const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
685
+ const lines = fullLines.slice(0, 2);
686
+ for (const [index, step] of job.steps.entries()) {
687
+ const status = widgetStepStatus(step.status, theme);
688
+ const activity = widgetStepActivityLine(step, width, false);
689
+ const stepStats = widgetStepStats(theme, step);
690
+ const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
691
+ lines.push(` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
692
+ }
693
+ if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail · /subagents-status for output paths"));
694
+ return lines.map((line) => truncLine(line, width));
695
+ }
696
+
697
+ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
698
+ const rows = process.stdout.rows || 30;
699
+ const budget = expanded
700
+ ? Math.max(12, Math.min(24, Math.floor(rows * 0.55)))
701
+ : Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
702
+ if (lines.length <= budget) return lines;
703
+ const visibleLines = Math.max(1, budget - 1);
704
+ const hiddenCount = lines.length - visibleLines;
705
+ const hint = expanded
706
+ ? `… ${hiddenCount} live-detail lines hidden · /subagents-status for full detail`
707
+ : `… ${hiddenCount} lines hidden · Ctrl+O expands · /subagents-status for full detail`;
708
+ return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
709
+ }
710
+
711
+ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
712
+ return (_tui, theme) => {
713
+ const width = getTermWidth();
714
+ const lines = expanded
715
+ ? buildWidgetLines(jobs, theme, width, true)
716
+ : jobs.length === 1
717
+ ? compactSingleWidgetLines(jobs[0]!, theme, width)
718
+ : buildWidgetLines(jobs, theme, width, false);
719
+ const container = new Container();
720
+ for (const line of fitWidgetLineBudget(lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
721
+ return container;
722
+ };
723
+ }
724
+
725
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
541
726
  if (jobs.length === 0) return [];
727
+ if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
542
728
  const running = jobs.filter((job) => job.status === "running");
543
729
  const queued = jobs.filter((job) => job.status === "queued");
544
730
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
@@ -608,7 +794,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
608
794
  function refreshAnimatedWidget(): void {
609
795
  try {
610
796
  if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
611
- latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetLines(latestWidgetJobs, latestWidgetCtx.ui.theme));
797
+ latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(latestWidgetJobs, latestWidgetCtx.ui.getToolsExpanded?.() ?? false));
612
798
  latestWidgetCtx.ui.requestRender?.();
613
799
  } catch (error) {
614
800
  if (!isStaleExtensionContextError(error)) throw error;
@@ -662,7 +848,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
662
848
  latestWidgetCtx = ctx;
663
849
  latestWidgetJobs = [...jobs];
664
850
 
665
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(jobs, ctx.ui.theme));
851
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
666
852
  if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
667
853
  else stopWidgetAnimation();
668
854
  }
@@ -62,11 +62,20 @@ function stepGlyph(theme: Theme, status: string): string {
62
62
  return theme.fg("dim", "◦");
63
63
  }
64
64
 
65
+ function runGlyph(theme: Theme, status: AsyncRunSummary["state"]): string {
66
+ if (status === "running") return theme.fg("accent", "▶");
67
+ if (status === "queued") return theme.fg("dim", "◦");
68
+ if (status === "complete") return theme.fg("success", "✓");
69
+ if (status === "paused") return theme.fg("warning", "■");
70
+ return theme.fg("error", "✗");
71
+ }
72
+
65
73
  function runLabel(theme: Theme, run: AsyncRunSummary, selected: boolean): string {
66
74
  const prefix = selected ? theme.fg("accent", ">") : " ";
67
75
  const stepLabel = formatAsyncRunProgressLabel(run);
68
76
  const cwd = shortenPath(run.cwd ?? run.asyncDir);
69
- return `${prefix} ${run.id.slice(0, 8)} ${statusColor(theme, run.state)} | ${run.mode} | ${stepLabel} | ${cwd}`;
77
+ const mode = ((theme as { bold?: (value: string) => string }).bold?.(run.mode)) ?? run.mode;
78
+ return `${prefix} ${runGlyph(theme, run.state)} ${mode} · ${stepLabel} · ${run.id.slice(0, 8)} · ${cwd}`;
70
79
  }
71
80
 
72
81
  function selectedIndex(rows: StatusRow[], cursor: number): number {
@@ -332,7 +341,13 @@ export class SubagentsStatusComponent implements Component {
332
341
  if (run.sessionFile) {
333
342
  lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
334
343
  }
335
- lines.push(...this.renderStepRows(run, width, innerW));
344
+ if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
345
+ lines.push(...this.renderChainProgressRows(run, width, innerW));
346
+ } else if (run.mode === "parallel") {
347
+ lines.push(...this.renderAgentRows(run, width, innerW));
348
+ } else {
349
+ lines.push(...this.renderStepRows(run, width, innerW));
350
+ }
336
351
  return lines;
337
352
  }
338
353
 
@@ -438,7 +453,7 @@ export class SubagentsStatusComponent implements Component {
438
453
  : undefined;
439
454
 
440
455
  const body: string[] = [];
441
- body.push(...detailRows(`${run.id} | ${statusColor(this.theme, run.state)} | ${run.mode} | ${stepLabel} | ${duration}`, width, innerW, this.theme));
456
+ body.push(...detailRows(`${runGlyph(this.theme, run.state)} ${run.mode} · ${stepLabel} · ${statusColor(this.theme, run.state)} · ${duration}`, width, innerW, this.theme));
442
457
  if (activity) body.push(...detailRows(activity, width, innerW, this.theme));
443
458
  body.push(row("", width, this.theme));
444
459
  if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {