pi-subagents 0.11.10 → 0.11.12

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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.12] - 2026-03-28
6
+
7
+ ### Changed
8
+ - Tool history (`recentTools`) in execution progress is now chronological (oldest first) and uncapped, replacing the old newest-first order with a 5-entry cap. Affects all execution paths (tool, slash commands, chains, parallel, async, delegation). Both single-task and chain-step render paths in `render.ts` now consistently use `slice(-3)` for most-recent display.
9
+ - Removed 50ms throttle on execution progress updates. `onUpdate` now fires immediately on every tool start, tool end, message end, and tool result. Affects all execution paths.
10
+ - Delegation bridge now passes through full `recentOutputLines` arrays, `recentTools` history, and resolved `model` to prompt-template consumers, replacing the old stripped-down single-line updates.
11
+
12
+ ## [0.11.11] - 2026-03-23
13
+
14
+ ### Changed
15
+ - Updated for pi 0.62.0 compatibility. `Skill.source` replaced with `Skill.sourceInfo` for skill provenance, `Widget` type replaced with `Component`. Bumped devDependencies to `^0.62.0`.
16
+
5
17
  ## [0.11.10] - 2026-03-21
6
18
 
7
19
  ### Changed
package/execution.ts CHANGED
@@ -139,48 +139,15 @@ export async function runSync(
139
139
  closeJsonlWriter = () => jsonlWriter.close();
140
140
  let buf = "";
141
141
 
142
- // Throttled update mechanism - consolidates all updates
143
- let lastUpdateTime = 0;
144
- let updatePending = false;
145
- let pendingTimer: ReturnType<typeof setTimeout> | null = null;
146
142
  let processClosed = false;
147
- const UPDATE_THROTTLE_MS = 50; // Reduced from 75ms for faster responsiveness
148
143
 
149
- const scheduleUpdate = () => {
144
+ const fireUpdate = () => {
150
145
  if (!onUpdate || processClosed) return;
151
- const now = Date.now();
152
- const elapsed = now - lastUpdateTime;
153
-
154
- if (elapsed >= UPDATE_THROTTLE_MS) {
155
- // Enough time passed, update immediately
156
- // Clear any pending timer to avoid double-updates
157
- if (pendingTimer) {
158
- clearTimeout(pendingTimer);
159
- pendingTimer = null;
160
- }
161
- lastUpdateTime = now;
162
- updatePending = false;
163
- progress.durationMs = now - startTime;
164
- onUpdate({
165
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
166
- details: { mode: "single", results: [result], progress: [progress] },
167
- });
168
- } else if (!updatePending) {
169
- // Schedule update for later
170
- updatePending = true;
171
- pendingTimer = setTimeout(() => {
172
- pendingTimer = null;
173
- if (updatePending && !processClosed) {
174
- updatePending = false;
175
- lastUpdateTime = Date.now();
176
- progress.durationMs = Date.now() - startTime;
177
- onUpdate({
178
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
179
- details: { mode: "single", results: [result], progress: [progress] },
180
- });
181
- }
182
- }, UPDATE_THROTTLE_MS - elapsed);
183
- }
146
+ progress.durationMs = Date.now() - startTime;
147
+ onUpdate({
148
+ content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
149
+ details: { mode: "single", results: [result], progress: [progress] },
150
+ });
184
151
  };
185
152
 
186
153
  const processLine = (line: string) => {
@@ -195,25 +162,20 @@ export async function runSync(
195
162
  progress.toolCount++;
196
163
  progress.currentTool = evt.toolName;
197
164
  progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
198
- // Tool start is important - update immediately by forcing throttle reset
199
- lastUpdateTime = 0;
200
- scheduleUpdate();
165
+ fireUpdate();
201
166
  }
202
167
 
203
168
  if (evt.type === "tool_execution_end") {
204
169
  if (progress.currentTool) {
205
- progress.recentTools.unshift({
170
+ progress.recentTools.push({
206
171
  tool: progress.currentTool,
207
172
  args: progress.currentToolArgs || "",
208
173
  endMs: now,
209
174
  });
210
- if (progress.recentTools.length > 5) {
211
- progress.recentTools.pop();
212
- }
213
175
  }
214
176
  progress.currentTool = undefined;
215
177
  progress.currentToolArgs = undefined;
216
- scheduleUpdate();
178
+ fireUpdate();
217
179
  }
218
180
 
219
181
  if (evt.type === "message_end" && evt.message) {
@@ -245,7 +207,7 @@ export async function runSync(
245
207
  }
246
208
  }
247
209
  }
248
- scheduleUpdate();
210
+ fireUpdate();
249
211
  }
250
212
  if (evt.type === "tool_result_end" && evt.message) {
251
213
  result.messages.push(evt.message);
@@ -262,7 +224,7 @@ export async function runSync(
262
224
  progress.recentOutput.splice(0, progress.recentOutput.length - 50);
263
225
  }
264
226
  }
265
- scheduleUpdate();
227
+ fireUpdate();
266
228
  }
267
229
  } catch {
268
230
  // Non-JSON stdout lines are expected; only structured events are parsed.
@@ -276,19 +238,12 @@ export async function runSync(
276
238
  const lines = buf.split("\n");
277
239
  buf = lines.pop() || "";
278
240
  lines.forEach(processLine);
279
-
280
- // Also schedule an update on data received (handles streaming output)
281
- scheduleUpdate();
282
241
  });
283
242
  proc.stderr.on("data", (d) => {
284
243
  stderrBuf += d.toString();
285
244
  });
286
245
  proc.on("close", (code) => {
287
246
  processClosed = true;
288
- if (pendingTimer) {
289
- clearTimeout(pendingTimer);
290
- pendingTimer = null;
291
- }
292
247
  if (buf.trim()) processLine(buf);
293
248
  if (code !== 0 && stderrBuf.trim() && !result.error) {
294
249
  result.error = stderrBuf.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.11.10",
3
+ "version": "0.11.12",
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",
@@ -47,8 +47,8 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@marcfargas/pi-test-harness": "^0.5.0",
50
- "@mariozechner/pi-agent-core": "^0.54.0",
51
- "@mariozechner/pi-ai": "^0.54.0",
52
- "@mariozechner/pi-coding-agent": "^0.54.0"
50
+ "@mariozechner/pi-agent-core": "^0.62.0",
51
+ "@mariozechner/pi-ai": "^0.62.0",
52
+ "@mariozechner/pi-coding-agent": "^0.62.0"
53
53
  }
54
54
  }
@@ -41,6 +41,9 @@ export interface PromptTemplateDelegationTaskProgress {
41
41
  currentTool?: string;
42
42
  currentToolArgs?: string;
43
43
  recentOutput?: string;
44
+ recentOutputLines?: string[];
45
+ recentTools?: Array<{ tool: string; args: string }>;
46
+ model?: string;
44
47
  toolCount?: number;
45
48
  durationMs?: number;
46
49
  tokens?: number;
@@ -51,6 +54,9 @@ export interface PromptTemplateDelegationUpdate {
51
54
  currentTool?: string;
52
55
  currentToolArgs?: string;
53
56
  recentOutput?: string;
57
+ recentOutputLines?: string[];
58
+ recentTools?: Array<{ tool: string; args: string }>;
59
+ model?: string;
54
60
  toolCount?: number;
55
61
  durationMs?: number;
56
62
  tokens?: number;
@@ -71,6 +77,7 @@ interface PromptTemplateBridgeResult {
71
77
  messages?: unknown[];
72
78
  exitCode?: number;
73
79
  error?: string;
80
+ model?: string;
74
81
  }>;
75
82
  progress?: Array<{
76
83
  index?: number;
@@ -79,6 +86,7 @@ interface PromptTemplateBridgeResult {
79
86
  currentTool?: string;
80
87
  currentToolArgs?: string;
81
88
  recentOutput?: string[];
89
+ recentTools?: Array<{ tool?: string; args?: string }>;
82
90
  toolCount?: number;
83
91
  durationMs?: number;
84
92
  tokens?: number;
@@ -154,17 +162,62 @@ export function firstTextContent(content: unknown): string | undefined {
154
162
  return undefined;
155
163
  }
156
164
 
165
+ function filterRecentOutput(lines: string[] | undefined): string[] | undefined {
166
+ if (!lines || lines.length === 0) return undefined;
167
+ const filtered = lines.filter((line) => typeof line === "string" && line.trim() && line.trim() !== "(running...)");
168
+ if (filtered.length === 0) return undefined;
169
+ return filtered;
170
+ }
171
+
172
+ function sanitizeRecentTools(
173
+ tools: Array<{ tool?: string; args?: string }> | undefined,
174
+ ): Array<{ tool: string; args: string }> | undefined {
175
+ if (!tools || tools.length === 0) return undefined;
176
+ const sanitized = tools
177
+ .filter((entry) => typeof entry.tool === "string" && entry.tool.trim().length > 0)
178
+ .map((entry) => ({
179
+ tool: entry.tool as string,
180
+ args: typeof entry.args === "string" ? entry.args : String(entry.args ?? ""),
181
+ }));
182
+ return sanitized.length > 0 ? sanitized : undefined;
183
+ }
184
+
185
+ function resolveProgressModel(
186
+ update: PromptTemplateBridgeResult,
187
+ entry: { index?: number; agent?: string },
188
+ ): string | undefined {
189
+ const results = update.details?.results;
190
+ if (!results || results.length === 0) return undefined;
191
+ if (typeof entry.index === "number" && entry.index >= 0) {
192
+ const byIndex = results[entry.index];
193
+ if (typeof byIndex?.model === "string") return byIndex.model;
194
+ }
195
+ if (entry.agent) {
196
+ const byAgent = results.find((result) => result.agent === entry.agent && typeof result.model === "string");
197
+ if (byAgent?.model) return byAgent.model;
198
+ }
199
+ const firstWithModel = results.find((result) => typeof result.model === "string");
200
+ return firstWithModel?.model;
201
+ }
202
+
157
203
  function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
158
204
  const progress = update.details?.progress?.[0];
159
205
  const taskProgress = update.details?.progress?.map((entry) => {
160
206
  const lastOutput = entry.recentOutput?.[entry.recentOutput.length - 1];
207
+ const safeLastOutput =
208
+ typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
209
+ ? lastOutput
210
+ : undefined;
161
211
  return {
162
212
  index: entry.index,
163
213
  agent: entry.agent ?? "delegate",
164
214
  status: entry.status,
165
215
  currentTool: entry.currentTool,
166
216
  currentToolArgs: entry.currentToolArgs,
167
- recentOutput: lastOutput && lastOutput !== "(running...)" ? lastOutput : undefined,
217
+ recentOutput: safeLastOutput,
218
+ recentOutputLines: filterRecentOutput(entry.recentOutput),
219
+ recentTools: sanitizeRecentTools(entry.recentTools),
220
+ model: resolveProgressModel(update, entry),
168
221
  toolCount: entry.toolCount,
169
222
  durationMs: entry.durationMs,
170
223
  tokens: entry.tokens,
@@ -172,11 +225,18 @@ function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResul
172
225
  });
173
226
  if (!progress && (!taskProgress || taskProgress.length === 0)) return undefined;
174
227
  const lastOutput = progress?.recentOutput?.[progress.recentOutput.length - 1];
228
+ const safeLastOutput =
229
+ typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
230
+ ? lastOutput
231
+ : undefined;
175
232
  return {
176
233
  requestId,
177
234
  currentTool: progress?.currentTool,
178
235
  currentToolArgs: progress?.currentToolArgs,
179
- recentOutput: lastOutput && lastOutput !== "(running...)" ? lastOutput : undefined,
236
+ recentOutput: safeLastOutput,
237
+ recentOutputLines: filterRecentOutput(progress?.recentOutput),
238
+ recentTools: sanitizeRecentTools(progress?.recentTools),
239
+ model: progress ? resolveProgressModel(update, progress) : undefined,
180
240
  toolCount: progress?.toolCount,
181
241
  durationMs: progress?.durationMs,
182
242
  tokens: progress?.tokens,
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, truncateToWidth, visibleWidth, type Widget } from "@mariozechner/pi-tui";
7
+ import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
8
8
  import {
9
9
  type AsyncJobState,
10
10
  type Details,
@@ -179,7 +179,7 @@ export function renderSubagentResult(
179
179
  result: AgentToolResult<Details>,
180
180
  _options: { expanded: boolean },
181
181
  theme: Theme,
182
- ): Widget {
182
+ ): Component {
183
183
  const d = result.details;
184
184
  if (!d || !d.results.length) {
185
185
  const t = result.content[0];
@@ -447,7 +447,7 @@ export function renderSubagentResult(
447
447
  }
448
448
  // Recent tools
449
449
  if (rProg.recentTools?.length) {
450
- for (const t of rProg.recentTools.slice(0, 3)) {
450
+ for (const t of rProg.recentTools.slice(-3)) {
451
451
  const maxArgsLen = Math.max(40, w - 30);
452
452
  const argsPreview = t.args.length > maxArgsLen
453
453
  ? `${t.args.slice(0, maxArgsLen)}...`
package/skills.ts CHANGED
@@ -194,32 +194,24 @@ function buildSkillPaths(cwd: string): string[] {
194
194
  return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
195
195
  }
196
196
 
197
- function inferSkillSource(rawSource: unknown, filePath: string, cwd: string): SkillSource {
198
- const source = typeof rawSource === "string" ? rawSource : "";
197
+ function inferSkillSource(sourceInfo: { source: string; scope: string }, filePath: string, cwd: string): SkillSource {
198
+ const { scope, source } = sourceInfo;
199
+
200
+ if (scope === "project" && source === "local") return "project";
201
+ if (scope === "user" && source === "local") return "user";
202
+
203
+ // Fallback: infer from file path when sourceInfo isn't specific enough
204
+ // (e.g. scope === "temporary" for skills loaded via explicit skillPaths)
199
205
  const projectRoot = path.resolve(cwd, CONFIG_DIR);
200
206
  const isProjectScoped = isWithinPath(filePath, projectRoot);
207
+ if (isProjectScoped) return "project";
208
+
201
209
  const isUserScoped = isWithinPath(filePath, AGENT_DIR);
210
+ if (isUserScoped) return "user";
211
+
202
212
  const globalRoot = getGlobalNpmRoot();
203
- const isGlobalPackage = globalRoot ? isWithinPath(filePath, globalRoot) : false;
204
-
205
- if (source === "project") return "project";
206
- if (source === "user") return "user";
207
- if (source === "settings") {
208
- if (isProjectScoped) return "project-settings";
209
- if (isUserScoped) return "user-settings";
210
- return "unknown";
211
- }
212
- if (source === "package") {
213
- if (isProjectScoped) return "project-package";
214
- if (isUserScoped || isGlobalPackage) return "user-package";
215
- return "unknown";
216
- }
217
- if (source === "extension") return "extension";
218
- if (source === "builtin") return "builtin";
213
+ if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
219
214
 
220
- if (isProjectScoped) return "project";
221
- if (isUserScoped) return "user";
222
- if (isGlobalPackage) return "user-package";
223
215
  return "unknown";
224
216
  }
225
217
 
@@ -247,7 +239,7 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
247
239
  const entry: CachedSkillEntry = {
248
240
  name: skill.name,
249
241
  filePath: skill.filePath,
250
- source: inferSkillSource((skill as { source?: unknown }).source, skill.filePath, cwd),
242
+ source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
251
243
  description: skill.description,
252
244
  order: i,
253
245
  };