@esso0428/pi-subagents 0.17.31 → 0.17.32

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.
@@ -91,6 +91,15 @@ export interface AgentActivity {
91
91
  }
92
92
 
93
93
  /** Metadata attached to Agent tool results for custom rendering. */
94
+ type WidgetRow = {
95
+ record: AgentRecord;
96
+ depth: number;
97
+ ancestorContinues: boolean[];
98
+ isLast: boolean;
99
+ /** True when the row's parent is gone, so it keeps its original indent. */
100
+ orphaned: boolean;
101
+ };
102
+
94
103
  export interface AgentDetails {
95
104
  displayName: string;
96
105
  description: string;
@@ -245,12 +254,22 @@ export function describeActivity(activeTools: Map<string, string>, responseText?
245
254
  * which is not simply `body.length - 1`: the bounded viewport pads the body
246
255
  * with blank lines, and two-line rows carry a `│` continuation underneath.
247
256
  */
248
- function markLastBranch(body: string[], bodyIndex: number): void {
257
+ function markLastBranch(body: string[], bodyIndex: number, depth = 0): void {
249
258
  const header = body[bodyIndex];
250
259
  if (header === undefined) return;
251
260
  body[bodyIndex] = header.replace("├─", "└─");
252
261
  const continuation = body[bodyIndex + 1];
253
- if (continuation?.includes("│ ")) body[bodyIndex + 1] = continuation.replace("│ ", " ");
262
+ if (!continuation) return;
263
+ let from = 0;
264
+ for (let level = 0; level <= depth; level++) {
265
+ const index = continuation.indexOf("│ ", from);
266
+ if (index < 0) return;
267
+ if (level === depth) {
268
+ body[bodyIndex + 1] = continuation.slice(0, index) + " " + continuation.slice(index + 3);
269
+ return;
270
+ }
271
+ from = index + 3;
272
+ }
254
273
  }
255
274
 
256
275
  // ---- Widget manager ----
@@ -306,7 +325,7 @@ export class AgentWidget {
306
325
  const all = this.manager.listAgents();
307
326
  switch (this.mode()) {
308
327
  case "off": return [];
309
- case "background": return all.filter(a => a.isBackground !== false);
328
+ case "background": return all.filter(a => a.parentAgentId !== undefined || a.isBackground !== false);
310
329
  default: return all;
311
330
  }
312
331
  }
@@ -378,16 +397,65 @@ export class AgentWidget {
378
397
  * navigation. `listAgents()` is newest-first; active rows come first so the
379
398
  * panel exposes currently useful work before terminal history.
380
399
  */
381
- private roster(): AgentRecord[] {
400
+ private rosterRows(): WidgetRow[] {
382
401
  const agents = this.widgetAgents();
383
- const finished = agents.filter(record =>
384
- record.status !== "running" && record.status !== "queued"
385
- && record.completedAt !== undefined
386
- && this.options.canOpenHistory(record),
402
+ const visible = agents.filter(record =>
403
+ record.status === "running" || record.status === "queued"
404
+ || (record.completedAt !== undefined && this.options.canOpenHistory(record)),
387
405
  );
388
- const running = agents.filter(record => record.status === "running");
389
- const queued = agents.filter(record => record.status === "queued");
390
- return [...running, ...queued, ...finished];
406
+ const rank = (record: AgentRecord): number =>
407
+ record.status === "running" ? 0 : record.status === "queued" ? 1 : 2;
408
+ const order = new Map(agents.map((record, index) => [record.id, index]));
409
+ const compare = (a: AgentRecord, b: AgentRecord) => rank(a) - rank(b)
410
+ || (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0);
411
+ const visibleIds = new Set(visible.map(record => record.id));
412
+ const children = new Map<string, AgentRecord[]>();
413
+ const roots: AgentRecord[] = [];
414
+ const orphans: AgentRecord[] = [];
415
+ for (const record of visible) {
416
+ const parent = record.parentAgentId;
417
+ if (parent && visibleIds.has(parent)) {
418
+ const siblings = children.get(parent) ?? [];
419
+ siblings.push(record);
420
+ children.set(parent, siblings);
421
+ } else if (parent) {
422
+ // The parent record is gone. Keep the row at its original depth so the
423
+ // subtree does not silently flatten into a top-level agent, and mark it
424
+ // so the render can say why it sits there alone.
425
+ orphans.push(record);
426
+ } else {
427
+ roots.push(record);
428
+ }
429
+ }
430
+ roots.sort(compare);
431
+ for (const siblings of children.values()) siblings.sort(compare);
432
+
433
+ const rows: WidgetRow[] = [];
434
+ const visit = (
435
+ record: AgentRecord,
436
+ depth: number,
437
+ ancestorContinues: boolean[],
438
+ isLast: boolean,
439
+ orphaned = false,
440
+ ) => {
441
+ rows.push({ record, depth, ancestorContinues, isLast, orphaned });
442
+ const nested = children.get(record.id) ?? [];
443
+ nested.forEach((child, index) => {
444
+ visit(child, depth + 1, [...ancestorContinues, !isLast], index === nested.length - 1);
445
+ });
446
+ };
447
+ roots.forEach((record, index) => {
448
+ visit(record, 0, [], index === roots.length - 1);
449
+ });
450
+ orphans.sort(compare);
451
+ orphans.forEach((record, index) => {
452
+ visit(record, Math.max(1, record.depth ?? 1), new Array(Math.max(0, (record.depth ?? 1) - 1)).fill(true), index === orphans.length - 1, true);
453
+ });
454
+ return rows;
455
+ }
456
+
457
+ private roster(): AgentRecord[] {
458
+ return this.rosterRows().map(row => row.record);
391
459
  }
392
460
 
393
461
  /**
@@ -533,7 +601,7 @@ export class AgentWidget {
533
601
  }
534
602
 
535
603
  /** Render a finished agent line. */
536
- private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
604
+ private renderFinishedLine(a: AgentRecord, theme: Theme): string {
537
605
  const name = getDisplayName(a.type);
538
606
  const modeLabel = getPromptModeLabel(a.type);
539
607
  const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
@@ -561,12 +629,19 @@ export class AgentWidget {
561
629
 
562
630
  const parts: string[] = [];
563
631
  const activity = this.agentActivity.get(a.id);
564
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
632
+ const live = activity ?? a.liveActivity;
633
+ if (live) parts.push(formatTurns(live.turnCount, live.maxTurns));
565
634
  if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
635
+ const tokens = getLifetimeTotal(a.lifetimeUsage);
636
+ if (tokens > 0) {
637
+ parts.push(formatSessionTokens(tokens, getSessionContextPercent(activity?.session ?? a.liveActivity?.session ?? a.session), theme, a.compactionCount));
638
+ if (a.parentAgentId) parts.push(theme.fg("dim", "already counted in parent"));
639
+ }
566
640
  parts.push(duration);
567
641
 
568
642
  const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
569
- return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", truncateLine(a.description))} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
643
+ const issue = a.nestedIssue ? theme.fg("warning", ` · nested blocked: ${truncateLine(a.nestedIssue)}`) : "";
644
+ return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", truncateLine(a.description))} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}${issue}`;
570
645
  }
571
646
 
572
647
  /**
@@ -600,59 +675,69 @@ export class AgentWidget {
600
675
  // Build sections separately for overflow-aware assembly.
601
676
  // Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
602
677
 
603
- const finishedLines: { record: AgentRecord; lines: string[] }[] = [];
604
- for (const a of finished) {
678
+ const rosterRows = this.rosterRows();
679
+ const ancestorPrefix = (row: WidgetRow): string => row.ancestorContinues
680
+ .map(continues => continues ? "│ " : " ")
681
+ .join("");
682
+ const orphanNote = (row: WidgetRow): string =>
683
+ row.orphaned ? theme.fg("dim", ` · parent ${row.record.parentDescription ?? row.record.parentAgentId} is gone`) : "";
684
+ const rowPrefix = (row: WidgetRow): string => ancestorPrefix(row) + (row.isLast ? "└─ " : "├─ ");
685
+ const detailPrefix = (row: WidgetRow): string => ancestorPrefix(row) + "│ ";
686
+ const renderedRows: { record: AgentRecord; lines: string[]; depth: number }[] = [];
687
+
688
+ for (const row of rosterRows) {
689
+ const a = row.record;
690
+ const note = orphanNote(row);
605
691
  const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
606
- finishedLines.push({
607
- record: a,
608
- lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
609
- });
610
- }
611
-
612
- const runningLines: { record: AgentRecord; lines: string[] }[] = []; // each entry is [header, activity]
613
- for (const a of running) {
614
- const name = getDisplayName(a.type);
615
- const modeLabel = getPromptModeLabel(a.type);
616
- const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
617
- const elapsed = formatMs(Date.now() - a.startedAt);
618
-
619
- const bg = this.agentActivity.get(a.id);
620
- const toolUses = bg?.toolUses ?? a.toolUses;
621
- const tokens = getLifetimeTotal(bg?.lifetimeUsage);
622
- const contextPercent = getSessionContextPercent(bg?.session);
623
- const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
624
-
625
- const parts: string[] = [];
626
- if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
627
- if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
628
- if (tokenText) parts.push(tokenText);
629
- parts.push(elapsed);
630
- const statsText = parts.join(" · ");
631
-
632
- const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
633
-
634
- const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
635
- runningLines.push({
636
- record: a,
637
- lines: [
638
- truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
639
- truncate(theme.fg("dim", "│ ") + ` ${theme.fg("dim", `⎿ ${activity}`)}`),
640
- ],
641
- });
692
+ const prefix = theme.fg("dim", rowPrefix(row));
693
+ if (a.status === "running") {
694
+ const name = getDisplayName(a.type);
695
+ const modeLabel = getPromptModeLabel(a.type);
696
+ const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
697
+ const elapsed = formatMs(Date.now() - a.startedAt);
698
+ const activityState = this.agentActivity.get(a.id);
699
+ const live = activityState ?? a.liveActivity;
700
+ const toolUses = activityState?.toolUses ?? a.toolUses;
701
+ const tokens = getLifetimeTotal(a.lifetimeUsage);
702
+ const contextPercent = getSessionContextPercent(activityState?.session ?? a.liveActivity?.session ?? a.session);
703
+ const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount)
704
+ + (a.parentAgentId ? theme.fg("dim", " (in parent)") : "") : "";
705
+ const parts: string[] = [];
706
+ if (live) parts.push(formatTurns(live.turnCount, live.maxTurns));
707
+ if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
708
+ if (tokenText) parts.push(tokenText);
709
+ parts.push(elapsed);
710
+ const statsText = parts.join(" · ");
711
+ const activity = a.nestedIssue
712
+ ? `nested blocked: ${a.nestedIssue}`
713
+ : live ? describeActivity(live.activeTools, live.responseText) : "thinking…";
714
+ renderedRows.push({
715
+ record: a,
716
+ depth: row.depth,
717
+ lines: [
718
+ truncate(prefix + `${marker} ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}${note}`),
719
+ truncate(theme.fg("dim", detailPrefix(row)) + theme.fg("dim", `⎿ ${activity}`)),
720
+ ],
721
+ });
722
+ } else if (a.status === "queued") {
723
+ renderedRows.push({
724
+ record: a,
725
+ depth: row.depth,
726
+ lines: [truncate(prefix + `${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}${note}`)],
727
+ });
728
+ } else {
729
+ renderedRows.push({
730
+ record: a,
731
+ depth: row.depth,
732
+ lines: [truncate(prefix + `${marker} ` + this.renderFinishedLine(a, theme) + note)],
733
+ });
734
+ }
642
735
  }
643
736
 
644
- const queuedLines: { record: AgentRecord; lines: string[] }[] = queued.map(a => {
645
- const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
646
- return {
647
- record: a,
648
- lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
649
- };
650
- });
651
-
652
737
  // Assemble with a responsive cap (heading + overflow indicator = 2
653
738
  // reserved lines when content exceeds the available body budget).
654
739
  const maxBody = maxLines - 1; // heading takes 1 line
655
- const rows = [...runningLines, ...queuedLines, ...finishedLines];
740
+ const rows = renderedRows;
656
741
  const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
657
742
 
658
743
  const heading = "Agents ↑↓ select · enter view · esc back";
@@ -668,7 +753,7 @@ export class AgentWidget {
668
753
  for (const row of rows) lines.push(...row.lines);
669
754
  if (rows.length > 0) {
670
755
  const lastRow = rows[rows.length - 1];
671
- markLastBranch(lines, lines.length - lastRow.lines.length);
756
+ markLastBranch(lines, lines.length - lastRow.lines.length, lastRow.depth);
672
757
  }
673
758
  } else {
674
759
  // Reserve one line for a directional overflow summary. The viewport is
@@ -722,7 +807,7 @@ export class AgentWidget {
722
807
  visibleBody.push(rows[start].lines[0]);
723
808
  end = start + 1;
724
809
  }
725
- markLastBranch(visibleBody, lastRowLine);
810
+ markLastBranch(visibleBody, lastRowLine, rows[Math.max(start, end - 1)]?.depth ?? 0);
726
811
 
727
812
  const hiddenBefore = start;
728
813
  const hiddenAfter = Math.max(0, rows.length - end);
@@ -549,12 +549,16 @@ export class ConversationViewer implements Component {
549
549
  const headerParts: string[] = [duration];
550
550
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
551
551
  if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
552
- const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
552
+ const tokens = getLifetimeTotal(this.record.lifetimeUsage);
553
553
  if (tokens > 0) {
554
- const percent = getSessionContextPercent(this.activity?.session);
554
+ const percent = getSessionContextPercent(this.activity?.session ?? this.record.session);
555
555
  headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
556
+ if (this.record.parentAgentId) headerParts.push(th.fg("dim", "already counted in parent"));
556
557
  }
557
- const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
558
+ const owner = this.record.parentAgentId
559
+ ? ` · child of ${this.record.parentDescription ?? this.record.parentAgentId}`
560
+ : "";
561
+ const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)}${th.fg("dim", owner)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
558
562
  const previewLabel = this.hoveredPreview ? th.fg("text", th.bold("[preview]")) : th.fg("dim", "[preview]");
559
563
  const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[Esc]")) : th.fg("dim", "[Esc]");
560
564
  const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);