pi-subagents 0.9.1 → 0.9.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,12 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.2] - 2026-02-19
6
+
7
+ ### Fixed
8
+ - TUI crash on async subagent completion: "Rendered line exceeds terminal width." `render.ts` never truncated output to fit the terminal — widget lines (`agents.join(" -> ")`), chain visualizations, skills lists, and task previews could all exceed the terminal width. Added `truncLine` helper using pi-tui's `truncateToWidth`/`visibleWidth` and applied it to every `Text` widget and widget string. Task preview lengths are now dynamic based on terminal width instead of hardcoded.
9
+ - Agent Manager scope badge showed `[built]` instead of `[builtin]` in list and detail views. Widened scope column to fit.
10
+
5
11
  ## [0.9.1] - 2026-02-17
6
12
 
7
13
  ### Fixed
8
14
  - Builtin agents were silently excluded from management listings, chain validation, and agent resolution. Added `allAgents()` helper that includes all three tiers (builtin, user, project) and applied it to `handleList`, `findAgents`, `availableNames`, and `unknownChainAgents`.
9
15
  - `resolveTarget` now blocks mutation of builtin agents with a clear error message suggesting the user create a same-named override, instead of allowing `fs.unlinkSync` or `fs.writeFileSync` on extension files.
10
- - Agent Manager TUI guards: delete and edit actions on builtin agents are blocked with an error status. Detail screen hides `[e]dit` from the footer for builtins. Scope badge shows `[built]` instead of falling through to `[proj]`.
16
+ - Agent Manager TUI guards: delete and edit actions on builtin agents are blocked with an error status. Detail screen hides `[e]dit` from the footer for builtins. Scope badge shows `[builtin]` instead of falling through to `[proj]`.
11
17
  - Cloning a builtin agent set the scope to `"builtin"` at runtime (violating the `"user" | "project"` type), causing wrong badge display and the clone inheriting builtin protections until session reload. Now maps to `"user"`.
12
18
  - Agent Manager `loadEntries` suppresses builtins overridden by user/project agents, preventing duplicate entries in the TUI list.
13
19
  - `BUILTIN_AGENTS_DIR` resolved via `import.meta.url` instead of hardcoded `~/.pi/agent/extensions/subagent/agents` path. Works regardless of where the extension is installed.
@@ -129,7 +129,7 @@ export function renderDetail(
129
129
  theme: Theme,
130
130
  ): string[] {
131
131
  const lines: string[] = [];
132
- const scopeBadge = agent.source === "builtin" ? "[built]" : agent.source === "project" ? "[proj]" : "[user]";
132
+ const scopeBadge = agent.source === "builtin" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
133
133
  const headerText = ` ${agent.name} ${scopeBadge} ${formatPath(agent.filePath)} `;
134
134
  lines.push(renderHeader(headerText, width, theme));
135
135
  lines.push(row("", width, theme));
@@ -190,7 +190,7 @@ export function renderList(
190
190
  const innerW = width - 2;
191
191
  const nameWidth = 16;
192
192
  const modelWidth = 12;
193
- const scopeWidth = 7;
193
+ const scopeWidth = 9;
194
194
 
195
195
  for (let i = 0; i < visible.length; i++) {
196
196
  const agent = visible[i]!;
@@ -208,7 +208,7 @@ export function renderList(
208
208
  const modelDisplay = modelRaw.includes("/") ? modelRaw.split("/").pop() ?? modelRaw : modelRaw;
209
209
  const nameText = isCursor ? theme.fg("accent", agent.name) : agent.name;
210
210
  const modelText = theme.fg("dim", modelDisplay);
211
- const scopeLabel = agent.kind === "chain" ? "[chain]" : agent.source === "builtin" ? "[built]" : agent.source === "project" ? "[proj]" : "[user]";
211
+ const scopeLabel = agent.kind === "chain" ? "[chain]" : agent.source === "builtin" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
212
212
  const scopeBadge = theme.fg("dim", scopeLabel);
213
213
  const descText = theme.fg("dim", agent.description);
214
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.9.1",
3
+ "version": "0.9.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",
package/render.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
6
6
  import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
- import { Container, Markdown, Spacer, Text, type Widget } from "@mariozechner/pi-tui";
7
+ import { Container, Markdown, Spacer, Text, truncateToWidth, visibleWidth, type Widget } from "@mariozechner/pi-tui";
8
8
  import {
9
9
  type AsyncJobState,
10
10
  type Details,
@@ -16,6 +16,15 @@ import { getFinalOutput, getDisplayItems, getOutputTail, getLastActivity } from
16
16
 
17
17
  type Theme = ExtensionContext["ui"]["theme"];
18
18
 
19
+ function getTermWidth(): number {
20
+ return process.stdout.columns || 120;
21
+ }
22
+
23
+ function truncLine(text: string, maxWidth: number): string {
24
+ if (visibleWidth(text) <= maxWidth) return text;
25
+ return truncateToWidth(text, maxWidth - 1) + "…";
26
+ }
27
+
19
28
  // Track last rendered widget state to avoid no-op re-renders
20
29
  let lastWidgetHash = "";
21
30
 
@@ -67,6 +76,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
67
76
  lastWidgetHash = newHash;
68
77
 
69
78
  const theme = ctx.ui.theme;
79
+ const w = getTermWidth();
70
80
  const lines: string[] = [];
71
81
  lines.push(theme.fg("accent", "Async subagents"));
72
82
 
@@ -90,12 +100,12 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
90
100
  const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
91
101
  const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
92
102
 
93
- lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
103
+ lines.push(truncLine(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`, w));
94
104
 
95
105
  if (job.status === "running" && job.outputFile) {
96
106
  const tail = getOutputTail(job.outputFile, 3);
97
107
  for (const line of tail) {
98
- lines.push(theme.fg("dim", ` > ${line}`));
108
+ lines.push(truncLine(theme.fg("dim", ` > ${line}`), w));
99
109
  }
100
110
  }
101
111
  }
@@ -114,7 +124,8 @@ export function renderSubagentResult(
114
124
  const d = result.details;
115
125
  if (!d || !d.results.length) {
116
126
  const t = result.content[0];
117
- return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
127
+ const text = t?.type === "text" ? t.text : "(no output)";
128
+ return new Text(truncLine(text, getTermWidth() - 4), 0, 0);
118
129
  }
119
130
 
120
131
  const mdTheme = getMarkdownTheme();
@@ -135,37 +146,42 @@ export function renderSubagentResult(
135
146
  ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
136
147
  : "";
137
148
 
149
+ const w = getTermWidth() - 4;
138
150
  const c = new Container();
139
- c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
151
+ c.addChild(new Text(truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, w), 0, 0));
140
152
  c.addChild(new Spacer(1));
153
+ const taskMaxLen = Math.max(20, w - 8);
154
+ const taskPreview = r.task.length > taskMaxLen
155
+ ? `${r.task.slice(0, taskMaxLen)}...`
156
+ : r.task;
141
157
  c.addChild(
142
- new Text(theme.fg("dim", `Task: ${r.task.slice(0, 150)}${r.task.length > 150 ? "..." : ""}`), 0, 0),
158
+ new Text(truncLine(theme.fg("dim", `Task: ${taskPreview}`), w), 0, 0),
143
159
  );
144
160
  c.addChild(new Spacer(1));
145
161
 
146
162
  const items = getDisplayItems(r.messages);
147
163
  for (const item of items) {
148
164
  if (item.type === "tool")
149
- c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
165
+ c.addChild(new Text(truncLine(theme.fg("muted", formatToolCall(item.name, item.args)), w), 0, 0));
150
166
  }
151
167
  if (items.length) c.addChild(new Spacer(1));
152
168
 
153
169
  if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
154
170
  c.addChild(new Spacer(1));
155
171
  if (r.skills?.length) {
156
- c.addChild(new Text(theme.fg("dim", `Skills: ${r.skills.join(", ")}`), 0, 0));
172
+ c.addChild(new Text(truncLine(theme.fg("dim", `Skills: ${r.skills.join(", ")}`), w), 0, 0));
157
173
  }
158
174
  if (r.skillsWarning) {
159
- c.addChild(new Text(theme.fg("warning", `⚠️ ${r.skillsWarning}`), 0, 0));
175
+ c.addChild(new Text(truncLine(theme.fg("warning", `⚠️ ${r.skillsWarning}`), w), 0, 0));
160
176
  }
161
- c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
177
+ c.addChild(new Text(truncLine(theme.fg("dim", formatUsage(r.usage, r.model)), w), 0, 0));
162
178
  if (r.sessionFile) {
163
- c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
179
+ c.addChild(new Text(truncLine(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), w), 0, 0));
164
180
  }
165
181
 
166
182
  if (r.artifactPaths) {
167
183
  c.addChild(new Spacer(1));
168
- c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
184
+ c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), w), 0, 0));
169
185
  }
170
186
  return c;
171
187
  }
@@ -231,7 +247,7 @@ export function renderSubagentResult(
231
247
  && Boolean(isComplete)
232
248
  && hasEmptyTextOutputWithoutOutputTarget(result.task, getFinalOutput(result.messages));
233
249
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
234
- const icon = isFailed
250
+ const stepIcon = isFailed
235
251
  ? theme.fg("error", "✗")
236
252
  : isEmptyWithoutTarget
237
253
  ? theme.fg("warning", "⚠")
@@ -240,22 +256,23 @@ export function renderSubagentResult(
240
256
  : isCurrent && hasRunning
241
257
  ? theme.fg("warning", "●")
242
258
  : theme.fg("dim", "○");
243
- return `${icon} ${agent}`;
259
+ return `${stepIcon} ${agent}`;
244
260
  })
245
261
  .join(theme.fg("dim", " → "))
246
262
  : null;
247
263
 
264
+ const w = getTermWidth() - 4;
248
265
  const c = new Container();
249
266
  c.addChild(
250
267
  new Text(
251
- `${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
268
+ truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`, w),
252
269
  0,
253
270
  0,
254
271
  ),
255
272
  );
256
273
  // Show chain visualization
257
274
  if (chainVis) {
258
- c.addChild(new Text(` ${chainVis}`, 0, 0));
275
+ c.addChild(new Text(truncLine(` ${chainVis}`, w), 0, 0));
259
276
  }
260
277
 
261
278
  // === STATIC STEP LAYOUT (like clarification UI) ===
@@ -275,7 +292,7 @@ export function renderSubagentResult(
275
292
 
276
293
  if (!r) {
277
294
  // Pending step
278
- c.addChild(new Text(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), 0, 0));
295
+ c.addChild(new Text(truncLine(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), w), 0, 0));
279
296
  c.addChild(new Text(theme.fg("dim", ` status: ○ pending`), 0, 0));
280
297
  c.addChild(new Spacer(1));
281
298
  continue;
@@ -299,45 +316,46 @@ export function renderSubagentResult(
299
316
  const stepHeader = rRunning
300
317
  ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
301
318
  : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
302
- c.addChild(new Text(stepHeader, 0, 0));
319
+ c.addChild(new Text(truncLine(stepHeader, w), 0, 0));
303
320
 
304
- const taskPreview = r.task.slice(0, 120) + (r.task.length > 120 ? "..." : "");
305
- c.addChild(new Text(theme.fg("dim", ` task: ${taskPreview}`), 0, 0));
321
+ const taskMaxLen = Math.max(20, w - 12);
322
+ const taskPreview = r.task.slice(0, taskMaxLen) + (r.task.length > taskMaxLen ? "..." : "");
323
+ c.addChild(new Text(truncLine(theme.fg("dim", ` task: ${taskPreview}`), w), 0, 0));
306
324
 
307
325
  const outputTarget = extractOutputTarget(r.task);
308
326
  if (outputTarget) {
309
- c.addChild(new Text(theme.fg("dim", ` output: ${outputTarget}`), 0, 0));
327
+ c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), w), 0, 0));
310
328
  }
311
329
 
312
330
  if (r.skills?.length) {
313
- c.addChild(new Text(theme.fg("dim", ` skills: ${r.skills.join(", ")}`), 0, 0));
331
+ c.addChild(new Text(truncLine(theme.fg("dim", ` skills: ${r.skills.join(", ")}`), w), 0, 0));
314
332
  }
315
333
  if (r.skillsWarning) {
316
- c.addChild(new Text(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), 0, 0));
334
+ c.addChild(new Text(truncLine(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), w), 0, 0));
317
335
  }
318
336
 
319
337
  if (rRunning && rProg) {
320
338
  if (rProg.skills?.length) {
321
- c.addChild(new Text(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`), 0, 0));
339
+ c.addChild(new Text(truncLine(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`), w), 0, 0));
322
340
  }
323
341
  // Current tool for running step
324
342
  if (rProg.currentTool) {
325
343
  const toolLine = rProg.currentToolArgs
326
344
  ? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 100)}${rProg.currentToolArgs.length > 100 ? "..." : ""}`
327
345
  : rProg.currentTool;
328
- c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
346
+ c.addChild(new Text(truncLine(theme.fg("warning", ` > ${toolLine}`), w), 0, 0));
329
347
  }
330
348
  // Recent tools
331
349
  if (rProg.recentTools?.length) {
332
350
  for (const t of rProg.recentTools.slice(0, 3)) {
333
351
  const args = t.args.slice(0, 90) + (t.args.length > 90 ? "..." : "");
334
- c.addChild(new Text(theme.fg("dim", ` ${t.tool}: ${args}`), 0, 0));
352
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${t.tool}: ${args}`), w), 0, 0));
335
353
  }
336
354
  }
337
355
  // Recent output (limited)
338
356
  const recentLines = (rProg.recentOutput ?? []).slice(-5);
339
357
  for (const line of recentLines) {
340
- c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), 0, 0));
358
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), w), 0, 0));
341
359
  }
342
360
  }
343
361
 
@@ -346,7 +364,7 @@ export function renderSubagentResult(
346
364
 
347
365
  if (d.artifacts) {
348
366
  c.addChild(new Spacer(1));
349
- c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
367
+ c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), w), 0, 0));
350
368
  }
351
369
  return c;
352
370
  }