pi-subagents 0.15.0 → 0.16.0

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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.0] - 2026-04-16
6
+
7
+ ### Added
8
+ - Top-level parallel `tasks` mode now supports a per-call `concurrency` override, matching the existing chain parallel-step concurrency control. This ships part of issue `#91`. Thanks @Gabrielgvl.
9
+
10
+ ### Changed
11
+ - Top-level parallel defaults and limits can now be configured through `~/.pi/agent/extensions/subagent/config.json` under `parallel.maxTasks` and `parallel.concurrency`, while keeping the existing defaults of 8 tasks and concurrency 4 when unset. This completes issue `#91`. Thanks @Gabrielgvl.
12
+
13
+ ### Fixed
14
+ - `context: "fork"` sync runs now create child sessions from a throwaway session-manager instance opened on the persisted parent session file, instead of mutating the live parent session manager. This keeps the parent session writing to its own file so the matching `toolResult(subagent)` no longer lands in a descendant session by accident. This fixes issue `#87`. Thanks @asmisha.
15
+ - Project agent and chain discovery now reads both `.agents/` and `.pi/agents/`, while preferring `.pi/agents/` when both locations define the same parsed name and keeping manager writes on the `.pi/agents/` path. This fixes issue `#88`. Thanks @desek.
16
+ - Ctrl+O expanded subagent results now actually show expanded content. Previously the `expanded` flag was received but ignored, so task text and tool-call args were identically truncated in both views. Now expanded mode shows the full task and longer (but still bounded) tool-call previews. Additionally, tool calls are no longer lost after foreground compaction: compact display summaries are preserved and shown in expanded view even after `messages` are stripped. This addresses issue `#90`. Thanks @asagajda.
17
+
5
18
  ## [0.15.0] - 2026-04-16
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -42,6 +42,8 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
42
42
  | User | `~/.pi/agent/agents/{name}.md` | Medium |
43
43
  | Project | `.pi/agents/{name}.md` (searches up directory tree) | Highest |
44
44
 
45
+ Project discovery also reads legacy `.agents/{name}.md` files. If both `.agents/` and `.pi/agents/` contain a project agent with the same parsed `name`, `.pi/agents/` wins and the agent manager still writes to `.pi/agents/`.
46
+
45
47
  Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
46
48
 
47
49
  **Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them.
@@ -255,7 +257,7 @@ Append `[key=value,...]` to any agent name to override its defaults:
255
257
  ```
256
258
  /chain scout[output=context.md] "scan code" -> planner[reads=context.md] "analyze auth"
257
259
  /run scout[model=anthropic/claude-sonnet-4] summarize this codebase
258
- /parallel scanner[output=scan.md] "find issues" -> reviewer[output=review.md] "check style"
260
+ /parallel scanner[model=anthropic/claude-sonnet-4] "find issues" -> reviewer[skills=code-review+security] "check style"
259
261
  ```
260
262
 
261
263
  | Key | Example | Description |
@@ -359,6 +361,8 @@ Chains are `.chain.md` files stored alongside agent files. They define reusable
359
361
  | User | `~/.pi/agent/agents/{name}.chain.md` |
360
362
  | Project | `.pi/agents/{name}.chain.md` |
361
363
 
364
+ Project chain discovery also reads legacy `.agents/{name}.chain.md` files. If both locations define the same parsed chain `name`, `.pi/agents/` wins and manager writes stay in `.pi/agents/`.
365
+
362
366
  **Format:**
363
367
 
364
368
  ```markdown
@@ -545,6 +549,9 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
545
549
  // Parallel
546
550
  { tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
547
551
 
552
+ // Parallel with top-level concurrency override
553
+ { tasks: [{ agent: "scout", task: "a" }, { agent: "reviewer", task: "b" }], concurrency: 2 }
554
+
548
555
  // Parallel with count shorthand (run the same task 3 times)
549
556
  { tasks: [{ agent: "scout", task: "audit auth", count: 3 }] }
550
557
 
@@ -711,6 +718,7 @@ Notes:
711
718
  | `model` | string | agent default | Override model for single agent |
712
719
  | `fallbackModels` | `string \| string[]` | agent default | Management/config-only field for ordered backup models. Markdown frontmatter uses a comma-separated string. |
713
720
  | `tasks` | `{agent, task, cwd?, count?, skill?}[]` | - | Parallel tasks. Foreground runs directly; background requests are converted to an equivalent chain. `count` repeats one task entry N times with the same settings. |
721
+ | `concurrency` | number | `config.parallel.concurrency` or `4` | Top-level `tasks` mode only: max concurrent tasks. |
714
722
  | `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
715
723
  | `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
716
724
  | `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
@@ -845,6 +853,25 @@ This aggregated output becomes `{previous}` for the next step.
845
853
 
846
854
  `pi-subagents` reads optional JSON config from `~/.pi/agent/extensions/subagent/config.json`.
847
855
 
856
+ ### `parallel`
857
+
858
+ `parallel` controls top-level `tasks` mode defaults and limits.
859
+
860
+ ```json
861
+ {
862
+ "parallel": {
863
+ "maxTasks": 12,
864
+ "concurrency": 6
865
+ }
866
+ }
867
+ ```
868
+
869
+ Fields:
870
+ - `maxTasks` defaults to `8` when unset or invalid
871
+ - `concurrency` defaults to `4` when unset or invalid
872
+
873
+ Per-call `concurrency` on the `subagent` tool takes precedence over `config.parallel.concurrency` for top-level `tasks` runs.
874
+
848
875
  ### `defaultSessionDir`
849
876
 
850
877
  `defaultSessionDir` sets the fallback directory used for session logs. Eg:
package/agents.ts CHANGED
@@ -606,13 +606,20 @@ function isDirectory(p: string): boolean {
606
606
  }
607
607
  }
608
608
 
609
- function findNearestProjectAgentsDir(cwd: string): string | null {
609
+ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
610
610
  const projectRoot = findNearestProjectRoot(cwd);
611
- if (!projectRoot) return null;
612
- const candidateAlt = path.join(projectRoot, ".agents");
613
- if (isDirectory(candidateAlt)) return candidateAlt;
614
- const candidate = path.join(projectRoot, ".pi", "agents");
615
- return isDirectory(candidate) ? candidate : null;
611
+ if (!projectRoot) return { readDirs: [], preferredDir: null };
612
+
613
+ const legacyDir = path.join(projectRoot, ".agents");
614
+ const preferredDir = path.join(projectRoot, ".pi", "agents");
615
+ const readDirs: string[] = [];
616
+ if (isDirectory(legacyDir)) readDirs.push(legacyDir);
617
+ if (isDirectory(preferredDir)) readDirs.push(preferredDir);
618
+
619
+ return {
620
+ readDirs,
621
+ preferredDir,
622
+ };
616
623
  }
617
624
 
618
625
  const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
@@ -620,7 +627,7 @@ const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)
620
627
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
621
628
  const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
622
629
  const userDirNew = path.join(os.homedir(), ".agents");
623
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
630
+ const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
624
631
  const userSettingsPath = getUserAgentSettingsPath();
625
632
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
626
633
 
@@ -631,12 +638,12 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
631
638
  userSettingsPath,
632
639
  projectSettingsPath,
633
640
  );
634
-
641
+
635
642
  const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
636
643
  const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
637
644
  const userAgents = [...userAgentsOld, ...userAgentsNew];
638
645
 
639
- const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
646
+ const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
640
647
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents);
641
648
 
642
649
  return { agents, projectAgentsDir };
@@ -654,7 +661,7 @@ export function discoverAgentsAll(cwd: string): {
654
661
  } {
655
662
  const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
656
663
  const userDirNew = path.join(os.homedir(), ".agents");
657
- const projectDir = findNearestProjectAgentsDir(cwd);
664
+ const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
658
665
  const userSettingsPath = getUserAgentSettingsPath();
659
666
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
660
667
 
@@ -669,11 +676,24 @@ export function discoverAgentsAll(cwd: string): {
669
676
  ...loadAgentsFromDir(userDirOld, "user"),
670
677
  ...loadAgentsFromDir(userDirNew, "user"),
671
678
  ];
672
- const project = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
679
+ const projectMap = new Map<string, AgentConfig>();
680
+ for (const dir of projectDirs) {
681
+ for (const agent of loadAgentsFromDir(dir, "project")) {
682
+ projectMap.set(agent.name, agent);
683
+ }
684
+ }
685
+ const project = Array.from(projectMap.values());
686
+
687
+ const chainMap = new Map<string, ChainConfig>();
688
+ for (const dir of projectDirs) {
689
+ for (const chain of loadChainsFromDir(dir, "project")) {
690
+ chainMap.set(chain.name, chain);
691
+ }
692
+ }
673
693
  const chains = [
674
694
  ...loadChainsFromDir(userDirOld, "user"),
675
695
  ...loadChainsFromDir(userDirNew, "user"),
676
- ...(projectDir ? loadChainsFromDir(projectDir, "project") : []),
696
+ ...Array.from(chainMap.values()),
677
697
  ];
678
698
 
679
699
  const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
package/fork-context.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export type SubagentExecutionContext = "fresh" | "fork";
2
2
 
3
+ interface ForkableSessionManagerStatic {
4
+ open(path: string): { createBranchedSession(leafId: string): string | undefined };
5
+ }
6
+
3
7
  export interface ForkableSessionManager {
4
8
  getSessionFile(): string | undefined;
5
9
  getLeafId(): string | null;
6
- createBranchedSession(leafId: string): string | undefined;
10
+ constructor: ForkableSessionManagerStatic;
7
11
  }
8
12
 
9
13
  export interface ForkContextResolver {
@@ -41,7 +45,8 @@ export function createForkContextResolver(
41
45
  const cached = cachedSessionFiles.get(index);
42
46
  if (cached) return cached;
43
47
  try {
44
- const sessionFile = sessionManager.createBranchedSession(leafId);
48
+ const sourceManager = sessionManager.constructor.open(parentSessionFile);
49
+ const sessionFile = sourceManager.createBranchedSession(leafId);
45
50
  if (!sessionFile) {
46
51
  throw new Error("Session manager did not return a session file.");
47
52
  }
package/formatters.ts CHANGED
@@ -83,11 +83,12 @@ export function buildChainSummary(
83
83
  /**
84
84
  * Format a tool call for display
85
85
  */
86
- export function formatToolCall(name: string, args: Record<string, unknown>): string {
86
+ export function formatToolCall(name: string, args: Record<string, unknown>, expanded = false): string {
87
87
  switch (name) {
88
88
  case "bash": {
89
89
  const command = typeof args.command === "string" ? args.command : "";
90
- return `$ ${command.slice(0, 60)}${command.length > 60 ? "..." : ""}`;
90
+ const maxLength = expanded ? 240 : 60;
91
+ return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
91
92
  }
92
93
  case "read":
93
94
  case "write":
@@ -101,7 +102,8 @@ export function formatToolCall(name: string, args: Record<string, unknown>): str
101
102
  }
102
103
  default: {
103
104
  const s = JSON.stringify(args);
104
- return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`;
105
+ const maxLength = expanded ? 160 : 40;
106
+ return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
105
107
  }
106
108
  }
107
109
  }
package/index.ts CHANGED
@@ -253,7 +253,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
253
253
  EXECUTION (use exactly ONE mode):
254
254
  • SINGLE: { agent, task } - one task
255
255
  • CHAIN: { chain: [{agent:"scout"}, {parallel:[{agent:"worker",count:3}]}] } - sequential pipeline with optional parallel fan-out
256
- • PARALLEL: { tasks: [{agent,task,count?}, ...], worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
256
+ • PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
257
257
  • Optional context: { context: "fresh" | "fork" } (default: "fresh")
258
258
 
259
259
  CHAIN TEMPLATE VARIABLES (use in task strings):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
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
@@ -101,6 +101,18 @@ function hasEmptyTextOutputWithoutOutputTarget(task: string, output: string): bo
101
101
  return !extractOutputTarget(task);
102
102
  }
103
103
 
104
+ function getToolCallLines(
105
+ result: Pick<Details["results"][number], "messages" | "toolCalls">,
106
+ expanded: boolean,
107
+ ): string[] {
108
+ if (result.messages) {
109
+ return getDisplayItems(result.messages)
110
+ .filter((item): item is { type: "tool"; name: string; args: Record<string, unknown> } => item.type === "tool")
111
+ .map((item) => formatToolCall(item.name, item.args, expanded));
112
+ }
113
+ return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
114
+ }
115
+
104
116
  /**
105
117
  * Render the async jobs widget
106
118
  */
@@ -165,7 +177,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
165
177
  */
166
178
  export function renderSubagentResult(
167
179
  result: AgentToolResult<Details>,
168
- _options: { expanded: boolean },
180
+ options: { expanded: boolean },
169
181
  theme: Theme,
170
182
  ): Component {
171
183
  const d = result.details;
@@ -176,6 +188,7 @@ export function renderSubagentResult(
176
188
  return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
177
189
  }
178
190
 
191
+ const expanded = options.expanded;
179
192
  const mdTheme = getMarkdownTheme();
180
193
 
181
194
  if (d.mode === "single" && d.results.length === 1) {
@@ -198,15 +211,17 @@ export function renderSubagentResult(
198
211
  : "";
199
212
 
200
213
  const w = getTermWidth() - 4;
214
+ const fit = (text: string) => expanded ? text : truncLine(text, w);
215
+ const toolCallLines = getToolCallLines(r, expanded);
201
216
  const c = new Container();
202
- c.addChild(new Text(truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`, w), 0, 0));
217
+ c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
203
218
  c.addChild(new Spacer(1));
204
219
  const taskMaxLen = Math.max(20, w - 8);
205
- const taskPreview = r.task.length > taskMaxLen
206
- ? `${r.task.slice(0, taskMaxLen)}...`
207
- : r.task;
220
+ const taskPreview = expanded || r.task.length <= taskMaxLen
221
+ ? r.task
222
+ : `${r.task.slice(0, taskMaxLen)}...`;
208
223
  c.addChild(
209
- new Text(truncLine(theme.fg("dim", `Task: ${taskPreview}`), w), 0, 0),
224
+ new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
210
225
  );
211
226
  c.addChild(new Spacer(1));
212
227
 
@@ -214,58 +229,58 @@ export function renderSubagentResult(
214
229
  if (r.progress.currentTool) {
215
230
  const maxToolArgsLen = Math.max(50, w - 20);
216
231
  const toolArgsPreview = r.progress.currentToolArgs
217
- ? (r.progress.currentToolArgs.length > maxToolArgsLen
218
- ? `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`
219
- : r.progress.currentToolArgs)
232
+ ? (expanded || r.progress.currentToolArgs.length <= maxToolArgsLen
233
+ ? r.progress.currentToolArgs
234
+ : `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
220
235
  : "";
221
236
  const toolLine = toolArgsPreview
222
237
  ? `${r.progress.currentTool}: ${toolArgsPreview}`
223
238
  : r.progress.currentTool;
224
- c.addChild(new Text(truncLine(theme.fg("warning", `> ${toolLine}`), w), 0, 0));
239
+ c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
225
240
  }
226
241
  if (r.progress.recentTools?.length) {
227
242
  for (const t of r.progress.recentTools.slice(-3)) {
228
243
  const maxArgsLen = Math.max(40, w - 24);
229
- const argsPreview = t.args.length > maxArgsLen
230
- ? `${t.args.slice(0, maxArgsLen)}...`
231
- : t.args;
232
- c.addChild(new Text(truncLine(theme.fg("dim", `${t.tool}: ${argsPreview}`), w), 0, 0));
244
+ const argsPreview = expanded || t.args.length <= maxArgsLen
245
+ ? t.args
246
+ : `${t.args.slice(0, maxArgsLen)}...`;
247
+ c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
233
248
  }
234
249
  }
235
250
  for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
236
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${line}`), w), 0, 0));
251
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
237
252
  }
238
253
  if (r.progress.currentTool || r.progress.recentTools?.length || r.progress.recentOutput?.length) {
239
254
  c.addChild(new Spacer(1));
240
255
  }
241
256
  }
242
257
 
243
- const items = getDisplayItems(r.messages);
244
- for (const item of items) {
245
- if (item.type === "tool")
246
- c.addChild(new Text(truncLine(theme.fg("muted", formatToolCall(item.name, item.args)), w), 0, 0));
258
+ if (expanded) {
259
+ for (const line of toolCallLines) {
260
+ c.addChild(new Text(fit(theme.fg("muted", line)), 0, 0));
261
+ }
262
+ if (toolCallLines.length) c.addChild(new Spacer(1));
247
263
  }
248
- if (items.length) c.addChild(new Spacer(1));
249
264
 
250
265
  if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
251
266
  c.addChild(new Spacer(1));
252
267
  if (r.skills?.length) {
253
- c.addChild(new Text(truncLine(theme.fg("dim", `Skills: ${r.skills.join(", ")}`), w), 0, 0));
268
+ c.addChild(new Text(fit(theme.fg("dim", `Skills: ${r.skills.join(", ")}`)), 0, 0));
254
269
  }
255
270
  if (r.skillsWarning) {
256
- c.addChild(new Text(truncLine(theme.fg("warning", `Warning: ${r.skillsWarning}`), w), 0, 0));
271
+ c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
257
272
  }
258
273
  if (r.attemptedModels && r.attemptedModels.length > 1) {
259
- c.addChild(new Text(truncLine(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
274
+ c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
260
275
  }
261
- c.addChild(new Text(truncLine(theme.fg("dim", formatUsage(r.usage, r.model)), w), 0, 0));
276
+ c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
262
277
  if (r.sessionFile) {
263
- c.addChild(new Text(truncLine(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), w), 0, 0));
278
+ c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
264
279
  }
265
280
 
266
281
  if (r.artifactPaths) {
267
282
  c.addChild(new Spacer(1));
268
- c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), w), 0, 0));
283
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
269
284
  }
270
285
  return c;
271
286
  }
@@ -341,16 +356,17 @@ export function renderSubagentResult(
341
356
  : null;
342
357
 
343
358
  const w = getTermWidth() - 4;
359
+ const fit = (text: string) => expanded ? text : truncLine(text, w);
344
360
  const c = new Container();
345
361
  c.addChild(
346
362
  new Text(
347
- truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge}${stepInfo}${summaryStr}`, w),
363
+ fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge}${stepInfo}${summaryStr}`),
348
364
  0,
349
365
  0,
350
366
  ),
351
367
  );
352
368
  if (chainVis) {
353
- c.addChild(new Text(truncLine(` ${chainVis}`, w), 0, 0));
369
+ c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
354
370
  }
355
371
 
356
372
  const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
@@ -365,7 +381,7 @@ export function renderSubagentResult(
365
381
  : (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
366
382
 
367
383
  if (!r) {
368
- c.addChild(new Text(truncLine(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), w), 0, 0));
384
+ c.addChild(new Text(fit(theme.fg("dim", ` Step ${i + 1}: ${agentName}`)), 0, 0));
369
385
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
370
386
  c.addChild(new Spacer(1));
371
387
  continue;
@@ -389,58 +405,66 @@ export function renderSubagentResult(
389
405
  const stepHeader = rRunning
390
406
  ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
391
407
  : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
392
- c.addChild(new Text(truncLine(stepHeader, w), 0, 0));
408
+ const toolCallLines = getToolCallLines(r, expanded);
409
+ c.addChild(new Text(fit(stepHeader), 0, 0));
393
410
 
394
411
  const taskMaxLen = Math.max(20, w - 12);
395
- const taskPreview = r.task.length > taskMaxLen
396
- ? `${r.task.slice(0, taskMaxLen)}...`
397
- : r.task;
398
- c.addChild(new Text(truncLine(theme.fg("dim", ` task: ${taskPreview}`), w), 0, 0));
412
+ const taskPreview = expanded || r.task.length <= taskMaxLen
413
+ ? r.task
414
+ : `${r.task.slice(0, taskMaxLen)}...`;
415
+ c.addChild(new Text(fit(theme.fg("dim", ` task: ${taskPreview}`)), 0, 0));
399
416
 
400
417
  const outputTarget = extractOutputTarget(r.task);
401
418
  if (outputTarget) {
402
- c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), w), 0, 0));
419
+ c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
403
420
  }
404
421
 
405
422
  if (r.skills?.length) {
406
- c.addChild(new Text(truncLine(theme.fg("dim", ` skills: ${r.skills.join(", ")}`), w), 0, 0));
423
+ c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));
407
424
  }
408
425
  if (r.skillsWarning) {
409
- c.addChild(new Text(truncLine(theme.fg("warning", ` Warning: ${r.skillsWarning}`), w), 0, 0));
426
+ c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
410
427
  }
411
428
  if (r.attemptedModels && r.attemptedModels.length > 1) {
412
- c.addChild(new Text(truncLine(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
429
+ c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
413
430
  }
414
431
 
415
432
  if (rRunning && rProg) {
416
433
  if (rProg.skills?.length) {
417
- c.addChild(new Text(truncLine(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`), w), 0, 0));
434
+ c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
418
435
  }
419
436
  if (rProg.currentTool) {
420
437
  const maxToolArgsLen = Math.max(50, w - 20);
421
438
  const toolArgsPreview = rProg.currentToolArgs
422
- ? (rProg.currentToolArgs.length > maxToolArgsLen
423
- ? `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`
424
- : rProg.currentToolArgs)
439
+ ? (expanded || rProg.currentToolArgs.length <= maxToolArgsLen
440
+ ? rProg.currentToolArgs
441
+ : `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`)
425
442
  : "";
426
443
  const toolLine = toolArgsPreview
427
444
  ? `${rProg.currentTool}: ${toolArgsPreview}`
428
445
  : rProg.currentTool;
429
- c.addChild(new Text(truncLine(theme.fg("warning", ` > ${toolLine}`), w), 0, 0));
446
+ c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
430
447
  }
431
448
  if (rProg.recentTools?.length) {
432
449
  for (const t of rProg.recentTools.slice(-3)) {
433
450
  const maxArgsLen = Math.max(40, w - 30);
434
- const argsPreview = t.args.length > maxArgsLen
435
- ? `${t.args.slice(0, maxArgsLen)}...`
436
- : t.args;
437
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${t.tool}: ${argsPreview}`), w), 0, 0));
451
+ const argsPreview = expanded || t.args.length <= maxArgsLen
452
+ ? t.args
453
+ : `${t.args.slice(0, maxArgsLen)}...`;
454
+ c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
438
455
  }
439
456
  }
440
457
  const recentLines = (rProg.recentOutput ?? []).slice(-5);
441
458
  for (const line of recentLines) {
442
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${line}`), w), 0, 0));
459
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
460
+ }
461
+ }
462
+
463
+ if (expanded && !rRunning) {
464
+ for (const line of toolCallLines) {
465
+ c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
443
466
  }
467
+ if (toolCallLines.length) c.addChild(new Spacer(1));
444
468
  }
445
469
 
446
470
  c.addChild(new Spacer(1));
@@ -448,7 +472,7 @@ export function renderSubagentResult(
448
472
 
449
473
  if (d.artifacts) {
450
474
  c.addChild(new Spacer(1));
451
- c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), w), 0, 0));
475
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)), 0, 0));
452
476
  }
453
477
  return c;
454
478
  }
package/schemas.ts CHANGED
@@ -73,6 +73,7 @@ export const SubagentParams = Type.Object({
73
73
  description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
74
74
  })),
75
75
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
76
+ concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
76
77
  worktree: Type.Optional(Type.Boolean({
77
78
  description: "Create isolated git worktrees for each parallel task. " +
78
79
  "Prevents filesystem conflicts. Requires clean git state. " +
package/slash-commands.ts CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  finalizeSlashResult,
15
15
  } from "./slash-live-state.ts";
16
16
  import {
17
- MAX_PARALLEL,
18
17
  SLASH_RESULT_TYPE,
19
18
  SLASH_SUBAGENT_CANCEL_EVENT,
20
19
  SLASH_SUBAGENT_REQUEST_EVENT,
@@ -453,22 +452,18 @@ export function registerSlashCommands(
453
452
  pi.registerCommand("parallel", {
454
453
  description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
455
454
  getArgumentCompletions: makeAgentCompletions(state, true),
456
- handler: async (args, ctx) => {
457
- const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
458
- const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
459
- if (!parsed) return;
460
- if (parsed.steps.length > MAX_PARALLEL) { ctx.ui.notify(`Max ${MAX_PARALLEL} parallel tasks`, "error"); return; }
461
- const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
462
- agent: name,
463
- task: stepTask ?? parsed.task,
464
- ...(config.output !== undefined ? { output: config.output } : {}),
465
- ...(config.reads !== undefined ? { reads: config.reads } : {}),
466
- ...(config.model ? { model: config.model } : {}),
467
- ...(config.skill !== undefined ? { skill: config.skill } : {}),
468
- ...(config.progress !== undefined ? { progress: config.progress } : {}),
469
- }));
470
- const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
471
- if (bg) params.async = true;
455
+ handler: async (args, ctx) => {
456
+ const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
457
+ const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
458
+ if (!parsed) return;
459
+ const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
460
+ agent: name,
461
+ task: stepTask ?? parsed.task,
462
+ ...(config.model ? { model: config.model } : {}),
463
+ ...(config.skill !== undefined ? { skill: config.skill } : {}),
464
+ }));
465
+ const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
466
+ if (bg) params.async = true;
472
467
  if (fork) params.context = "fork";
473
468
  await runSlashSubagent(pi, ctx, params);
474
469
  },
@@ -45,9 +45,9 @@ import {
45
45
  type SingleResult,
46
46
  type SubagentState,
47
47
  DEFAULT_ARTIFACT_CONFIG,
48
- MAX_CONCURRENCY,
49
- MAX_PARALLEL,
50
48
  checkSubagentDepth,
49
+ resolveTopLevelParallelConcurrency,
50
+ resolveTopLevelParallelMaxTasks,
51
51
  resolveChildMaxSubagentDepth,
52
52
  resolveCurrentMaxSubagentDepth,
53
53
  wrapForkTask,
@@ -60,9 +60,6 @@ interface TaskParam {
60
60
  count?: number;
61
61
  model?: string;
62
62
  skill?: string | string[] | boolean;
63
- output?: string | false;
64
- reads?: string[] | false;
65
- progress?: boolean;
66
63
  }
67
64
 
68
65
  export interface SubagentParamsLike {
@@ -71,6 +68,7 @@ export interface SubagentParamsLike {
71
68
  task?: string;
72
69
  chain?: ChainStep[];
73
70
  tasks?: TaskParam[];
71
+ concurrency?: number;
74
72
  worktree?: boolean;
75
73
  context?: "fresh" | "fork";
76
74
  async?: boolean;
@@ -113,7 +111,7 @@ interface ExecutionContextData {
113
111
  sessionFileForIndex: (idx?: number) => string | undefined;
114
112
  artifactConfig: ArtifactConfig;
115
113
  artifactsDir: string;
116
- parallelDowngraded: boolean;
114
+ backgroundRequestedWhileClarifying: boolean;
117
115
  effectiveAsync: boolean;
118
116
  }
119
117
 
@@ -349,6 +347,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
349
347
  effectiveAsync,
350
348
  } = data;
351
349
  const hasChain = (params.chain?.length ?? 0) > 0;
350
+ const hasTasks = (params.tasks?.length ?? 0) > 0;
352
351
  const hasSingle = Boolean(params.agent && params.task);
353
352
  if (!effectiveAsync) return null;
354
353
 
@@ -363,6 +362,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
363
362
  }
364
363
  }
365
364
 
365
+ if (hasTasks && params.tasks) {
366
+ const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
367
+ if (params.tasks.length > maxParallelTasks) {
368
+ return buildParallelModeError(`Max ${maxParallelTasks} tasks`);
369
+ }
370
+ if (params.worktree) {
371
+ const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd);
372
+ if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
373
+ }
374
+ }
375
+
366
376
  if (!isAsyncAvailable()) {
367
377
  return {
368
378
  content: [{ type: "text", text: "Async mode requires jiti for TypeScript execution but it could not be found. Install globally: npm install -g jiti" }],
@@ -383,6 +393,43 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
383
393
  fullId: `${m.provider}/${m.id}`,
384
394
  }));
385
395
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
396
+ const currentProvider = ctx.model?.provider;
397
+
398
+ if (hasTasks && params.tasks) {
399
+ const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
400
+ const modelOverrides = params.tasks.map((task, index) =>
401
+ resolveModelCandidate(task.model ?? agentConfigs[index]?.model, availableModels, currentProvider),
402
+ );
403
+ const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
404
+ const parallelTasks = params.tasks.map((task, index) => ({
405
+ agent: task.agent,
406
+ task: params.context === "fork" ? wrapForkTask(task.task) : task.task,
407
+ cwd: task.cwd,
408
+ ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
409
+ ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
410
+ }));
411
+ return executeAsyncChain(id, {
412
+ chain: [{
413
+ parallel: parallelTasks,
414
+ concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
415
+ worktree: params.worktree,
416
+ }],
417
+ agents,
418
+ ctx: asyncCtx,
419
+ availableModels,
420
+ cwd: effectiveCwd,
421
+ maxOutput: params.maxOutput,
422
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
423
+ artifactConfig,
424
+ shareEnabled,
425
+ sessionRoot,
426
+ chainSkills: [],
427
+ sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)),
428
+ maxSubagentDepth: currentMaxSubagentDepth,
429
+ worktreeSetupHook: deps.config.worktreeSetupHook,
430
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
431
+ });
432
+ }
386
433
 
387
434
  if (hasChain && params.chain) {
388
435
  const normalized = normalizeSkillInput(params.skill);
@@ -421,7 +468,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
421
468
  const normalizedSkills = normalizeSkillInput(params.skill);
422
469
  const skills = normalizedSkills === false ? [] : normalizedSkills;
423
470
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
424
- const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, ctx.model?.provider);
471
+ const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
425
472
  return executeAsyncSingle(id, {
426
473
  agent: params.agent!,
427
474
  task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
@@ -551,6 +598,7 @@ interface ForegroundParallelRunInput {
551
598
  modelOverrides: (string | undefined)[];
552
599
  skillOverrides: (string[] | false | undefined)[];
553
600
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
601
+ concurrencyLimit: number;
554
602
  liveResults: (SingleResult | undefined)[];
555
603
  liveProgress: (AgentProgress | undefined)[];
556
604
  onUpdate?: (r: AgentToolResult<Details>) => void;
@@ -634,7 +682,7 @@ function buildParallelWorktreeSuffix(
634
682
  }
635
683
 
636
684
  async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
637
- return mapConcurrent(input.tasks, MAX_CONCURRENCY, async (task, index) => {
685
+ return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
638
686
  const overrideSkills = input.skillOverrides[index];
639
687
  const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
640
688
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
@@ -690,17 +738,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
690
738
  shareEnabled,
691
739
  artifactConfig,
692
740
  artifactsDir,
693
- parallelDowngraded,
741
+ backgroundRequestedWhileClarifying,
694
742
  onUpdate,
695
743
  sessionRoot,
696
744
  } = data;
697
745
  const allProgress: AgentProgress[] = [];
698
746
  const allArtifactPaths: ArtifactPaths[] = [];
699
747
  const tasks = params.tasks!;
748
+ const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
749
+ const parallelConcurrency = resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency);
700
750
 
701
- if (tasks.length > MAX_PARALLEL)
751
+ if (tasks.length > maxParallelTasks)
702
752
  return {
703
- content: [{ type: "text", text: `Max ${MAX_PARALLEL} tasks` }],
753
+ content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }],
704
754
  isError: true,
705
755
  details: { mode: "parallel" as const, results: [] },
706
756
  };
@@ -800,7 +850,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
800
850
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
801
851
  }));
802
852
  return executeAsyncChain(id, {
803
- chain: [{ parallel: parallelTasks, worktree: params.worktree }],
853
+ chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
804
854
  agents,
805
855
  ctx: asyncCtx,
806
856
  availableModels,
@@ -857,6 +907,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
857
907
  modelOverrides,
858
908
  skillOverrides,
859
909
  behaviors,
910
+ concurrencyLimit: parallelConcurrency,
860
911
  maxSubagentDepths,
861
912
  liveResults,
862
913
  liveProgress,
@@ -875,7 +926,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
875
926
 
876
927
  const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
877
928
  const ok = results.filter((result) => result.exitCode === 0).length;
878
- const downgradeNote = parallelDowngraded ? " (async not supported for parallel)" : "";
929
+ const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
879
930
  const aggregatedOutput = aggregateParallelOutputs(
880
931
  results.map((result) => ({
881
932
  agent: result.agent,
@@ -1200,11 +1251,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1200
1251
  }
1201
1252
 
1202
1253
  const requestedAsync = normalizedParams.async ?? deps.asyncByDefault;
1203
- const parallelDowngraded = hasTasks && requestedAsync;
1204
- let effectiveAsync = false;
1205
- if (requestedAsync && !hasTasks) {
1206
- effectiveAsync = hasChain ? normalizedParams.clarify === false : normalizedParams.clarify !== true;
1207
- }
1254
+ const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && normalizedParams.clarify === true;
1255
+ const effectiveAsync = requestedAsync
1256
+ && (hasChain ? normalizedParams.clarify === false : normalizedParams.clarify !== true);
1208
1257
 
1209
1258
  const artifactConfig: ArtifactConfig = {
1210
1259
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -1251,7 +1300,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1251
1300
  sessionFileForIndex,
1252
1301
  artifactConfig,
1253
1302
  artifactsDir,
1254
- parallelDowngraded,
1303
+ backgroundRequestedWhileClarifying,
1255
1304
  effectiveAsync,
1256
1305
  };
1257
1306
 
package/types.ts CHANGED
@@ -61,6 +61,11 @@ export interface AgentProgress {
61
61
  failedTool?: string;
62
62
  }
63
63
 
64
+ export interface ToolCallSummary {
65
+ text: string;
66
+ expandedText: string;
67
+ }
68
+
64
69
  export interface ProgressSummary {
65
70
  toolCount: number;
66
71
  tokens: number;
@@ -96,6 +101,7 @@ export interface SingleResult {
96
101
  skillsWarning?: string;
97
102
  progress?: AgentProgress;
98
103
  progressSummary?: ProgressSummary;
104
+ toolCalls?: ToolCallSummary[];
99
105
  artifactPaths?: ArtifactPaths;
100
106
  truncation?: TruncationResult;
101
107
  finalOutput?: string;
@@ -273,10 +279,16 @@ export interface IntercomBridgeConfig {
273
279
  instructionFile?: string;
274
280
  }
275
281
 
282
+ export interface TopLevelParallelConfig {
283
+ maxTasks?: number;
284
+ concurrency?: number;
285
+ }
286
+
276
287
  export interface ExtensionConfig {
277
288
  asyncByDefault?: boolean;
278
289
  defaultSessionDir?: string;
279
290
  maxSubagentDepth?: number;
291
+ parallel?: TopLevelParallelConfig;
280
292
  worktreeSetupHook?: string;
281
293
  worktreeSetupHookTimeoutMs?: number;
282
294
  intercomBridge?: IntercomBridgeConfig;
@@ -376,6 +388,25 @@ export const DEFAULT_FORK_PREAMBLE =
376
388
  "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
377
389
  "— focus exclusively on completing this task using your tools.";
378
390
 
391
+ function normalizeTopLevelParallelValue(value: unknown): number | undefined {
392
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
393
+ if (!Number.isInteger(parsed) || parsed < 1) return undefined;
394
+ return parsed;
395
+ }
396
+
397
+ export function resolveTopLevelParallelMaxTasks(value: unknown): number {
398
+ return normalizeTopLevelParallelValue(value) ?? MAX_PARALLEL;
399
+ }
400
+
401
+ export function resolveTopLevelParallelConcurrency(
402
+ override: unknown,
403
+ configValue: unknown,
404
+ ): number {
405
+ return normalizeTopLevelParallelValue(override)
406
+ ?? normalizeTopLevelParallelValue(configValue)
407
+ ?? MAX_CONCURRENCY;
408
+ }
409
+
379
410
  export function getAsyncConfigPath(suffix: string): string {
380
411
  return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
381
412
  }
package/utils.ts CHANGED
@@ -6,7 +6,8 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@mariozechner/pi-ai";
9
- import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult } from "./types.ts";
9
+ import { formatToolCall } from "./formatters.ts";
10
+ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult, ToolCallSummary } from "./types.ts";
10
11
 
11
12
  // ============================================================================
12
13
  // File System Utilities
@@ -242,12 +243,33 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
242
243
  };
243
244
  }
244
245
 
246
+ export function extractToolCallSummaries(messages: Message[] | undefined): ToolCallSummary[] {
247
+ if (!messages?.length) return [];
248
+ const summaries: ToolCallSummary[] = [];
249
+ for (const msg of messages) {
250
+ if (msg.role !== "assistant") continue;
251
+ for (const part of msg.content) {
252
+ if (part.type !== "toolCall") continue;
253
+ const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
254
+ ? part.arguments
255
+ : {};
256
+ summaries.push({
257
+ text: formatToolCall(part.name, args),
258
+ expandedText: formatToolCall(part.name, args, true),
259
+ });
260
+ }
261
+ }
262
+ return summaries;
263
+ }
264
+
245
265
  export function compactForegroundResult(result: SingleResult): SingleResult {
246
266
  if (result.progress?.status === "running") return result;
267
+ const toolCalls = result.toolCalls?.length ? result.toolCalls : extractToolCallSummaries(result.messages);
247
268
  return {
248
269
  ...result,
249
270
  messages: undefined,
250
271
  progress: undefined,
272
+ toolCalls: toolCalls.length ? toolCalls : undefined,
251
273
  };
252
274
  }
253
275