pi-subagents 0.37.1 → 0.37.2

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.37.2] - 2026-07-28
6
+
7
+ ### Changed
8
+ - Reduced repeated scanning and file reads in live TUI rendering and skill loading.
9
+
10
+ ### Fixed
11
+ - Passed `--no-context-files` to child Pi runs when an agent disables inherited project context, avoiding stale prompt-header parsing as Pi's context block format changes. Thanks to @KorenKrita for #667.
12
+
5
13
  ## [0.37.1] - 2026-07-27
6
14
 
7
15
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.37.1",
3
+ "version": "0.37.2",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -379,19 +379,21 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
379
379
  return candidate.order < existing.order ? candidate : existing;
380
380
  }
381
381
 
382
- function maybeReadSkillDescription(filePath: string): string | undefined {
383
- try {
384
- const content = fs.readFileSync(filePath, "utf-8");
385
- const normalized = content.replace(/\r\n/g, "\n");
386
- if (!normalized.startsWith("---")) return undefined;
382
+ function parseSkillDescription(content: string): string | undefined {
383
+ const normalized = content.replace(/\r\n/g, "\n");
384
+ if (!normalized.startsWith("---")) return undefined;
385
+
386
+ const endIndex = normalized.indexOf("\n---", 3);
387
+ if (endIndex === -1) return undefined;
387
388
 
388
- const endIndex = normalized.indexOf("\n---", 3);
389
- if (endIndex === -1) return undefined;
389
+ const frontmatter = normalized.slice(3, endIndex).trim();
390
+ const match = frontmatter.match(/^description:\s*(.+)$/m);
391
+ return match?.[1]?.trim().replace(/^['\"]|['\"]$/g, "");
392
+ }
390
393
 
391
- const frontmatter = normalized.slice(3, endIndex).trim();
392
- const match = frontmatter.match(/^description:\s*(.+)$/m);
393
- if (!match) return undefined;
394
- return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
394
+ function maybeReadSkillDescription(filePath: string): string | undefined {
395
+ try {
396
+ return parseSkillDescription(fs.readFileSync(filePath, "utf-8"));
395
397
  } catch {
396
398
  // Description parsing is best-effort metadata extraction.
397
399
  return undefined;
@@ -585,7 +587,7 @@ function readSkill(
585
587
 
586
588
  const raw = fs.readFileSync(skillPath, "utf-8");
587
589
  const content = stripSkillFrontmatter(raw);
588
- const description = maybeReadSkillDescription(skillPath);
590
+ const description = parseSkillDescription(raw);
589
591
  const skill: ResolvedSkill = {
590
592
  name: skillName,
591
593
  path: skillPath,
@@ -253,6 +253,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
253
253
  }
254
254
  for (const extPath of toolPlan.extensionArgs) args.push("--extension", extPath);
255
255
 
256
+ if (!input.inheritProjectContext) {
257
+ args.push("--no-context-files");
258
+ }
256
259
  if (!input.inheritSkills) {
257
260
  args.push("--no-skills");
258
261
  }
package/src/tui/render.ts CHANGED
@@ -40,6 +40,7 @@ function getTermWidth(): number {
40
40
  }
41
41
 
42
42
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
43
+ const ansiStylePattern = /\x1b\[[0-9;]*m/y;
43
44
 
44
45
  /**
45
46
  * Truncate a line to maxWidth, preserving ANSI styling through the ellipsis.
@@ -50,7 +51,7 @@ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
50
51
  *
51
52
  * Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
52
53
  */
53
- function truncLine(text: string, maxWidth: number): string {
54
+ export function truncLine(text: string, maxWidth: number): string {
54
55
  if (visibleWidth(text) <= maxWidth) return text;
55
56
 
56
57
  const targetWidth = maxWidth - 1;
@@ -60,7 +61,8 @@ function truncLine(text: string, maxWidth: number): string {
60
61
  let i = 0;
61
62
 
62
63
  while (i < text.length) {
63
- const ansiMatch = text.slice(i).match(/^\x1b\[[0-9;]*m/);
64
+ ansiStylePattern.lastIndex = i;
65
+ const ansiMatch = ansiStylePattern.exec(text);
64
66
  if (ansiMatch) {
65
67
  const code = ansiMatch[0];
66
68
  result += code;
@@ -74,11 +76,9 @@ function truncLine(text: string, maxWidth: number): string {
74
76
  continue;
75
77
  }
76
78
 
77
- let end = i;
78
- while (end < text.length && !text.slice(end).match(/^\x1b\[[0-9;]*m/)) {
79
- end++;
80
- }
81
-
79
+ let end = text.indexOf("\x1b[", i);
80
+ if (end === i) end = text.indexOf("\x1b[", i + 2);
81
+ if (end === -1) end = text.length;
82
82
  const textPortion = text.slice(i, end);
83
83
  for (const seg of segmenter.segment(textPortion)) {
84
84
  const grapheme = seg.segment;
@@ -529,17 +529,15 @@ function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGra
529
529
  return spans;
530
530
  }
531
531
 
532
- function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
533
- if (details.mode !== "chain") return false;
534
- if (details.currentStepIndex === undefined) return false;
535
- return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
536
- }
537
-
538
532
  function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
533
+ const groupsByStep = new Map<number, AsyncParallelGroupStatus>();
534
+ for (const group of parallelGroups) {
535
+ if (!groupsByStep.has(group.stepIndex)) groupsByStep.set(group.stepIndex, group);
536
+ }
539
537
  const spans: ChainStepSpan[] = [];
540
538
  let flatIndex = 0;
541
539
  for (let stepIndex = 0; stepIndex < total; stepIndex++) {
542
- const group = parallelGroups.find((candidate) => candidate.stepIndex === stepIndex);
540
+ const group = groupsByStep.get(stepIndex);
543
541
  if (group) {
544
542
  spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
545
543
  flatIndex = Math.max(flatIndex, group.start + group.count);
@@ -567,6 +565,7 @@ interface ChainRenderResultEntry {
567
565
  kind: "result";
568
566
  resultIndex: number;
569
567
  rowNumber: number;
568
+ rowLabel?: string;
570
569
  agentName: string;
571
570
  }
572
571
 
@@ -601,6 +600,7 @@ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): C
601
600
  kind: "result",
602
601
  resultIndex: index,
603
602
  rowNumber: index + 1,
603
+ rowLabel: span.isParallel ? `Agent ${index - span.start + 1}/${span.count}` : `Step ${span.stepIndex + 1}`,
604
604
  agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
605
605
  });
606
606
  }
@@ -622,7 +622,9 @@ interface MultiProgressLabel {
622
622
  function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
623
623
  const stepSpans = buildChainStepSpans(details);
624
624
  const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
625
- const activeParallelGroup = isChainParallelGroupActive(details);
625
+ const activeParallelGroup = details.mode === "chain"
626
+ && details.currentStepIndex !== undefined
627
+ && stepSpans.some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
626
628
  const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
627
629
 
628
630
  if (details.mode === "parallel") {
@@ -708,15 +710,10 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
708
710
  return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
709
711
  }
710
712
 
711
- function resultRowLabel(details: Pick<Details, "mode" | "chainAgents" | "workflowGraph">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
712
- if (details.mode === "chain" && label.hasParallelInChain) {
713
- const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
714
- if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
715
- if (span) return `Step ${span.stepIndex + 1}`;
716
- }
713
+ function resultRowLabel(label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
717
714
  if (label.itemTitle === "Agent") {
718
715
  const localStepNumber = label.activeParallelGroup
719
- ? Math.max(1, stepNumber - label.groupStartIndex)
716
+ ? resultIndex - label.groupStartIndex + 1
720
717
  : stepNumber;
721
718
  return `Agent ${localStepNumber}/${label.totalCount}`;
722
719
  }
@@ -1417,7 +1414,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1417
1414
  const rowNumber = entry.rowNumber;
1418
1415
  const agentName = entry.agentName;
1419
1416
  if (!r) {
1420
- const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1417
+ const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
1421
1418
  c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1422
1419
  continue;
1423
1420
  }
@@ -1430,7 +1427,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1430
1427
  const stepStats = formatProgressStats(theme, rProg);
1431
1428
  const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
1432
1429
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1433
- const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1430
+ const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
1434
1431
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1435
1432
  c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1436
1433
  if (rRunning && rProg && "status" in rProg) {
@@ -1717,7 +1714,7 @@ export function renderSubagentResult(
1717
1714
  const agentName = entry.agentName;
1718
1715
 
1719
1716
  if (!r) {
1720
- const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1717
+ const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
1721
1718
  c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
1722
1719
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
1723
1720
  c.addChild(new Spacer(1));
@@ -1740,7 +1737,7 @@ export function renderSubagentResult(
1740
1737
  : theme.fg("success", "done");
1741
1738
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
1742
1739
  const modelDisplay = modelThinkingBadge(theme, r.model);
1743
- const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1740
+ const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
1744
1741
  const contextBadge = contextModeBadge(theme, r.context);
1745
1742
  const stepHeader = rRunning
1746
1743
  ? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats}`