pi-subagents 0.3.3 → 0.4.1

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
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Changed
6
+ - Added `pi-package` keyword for npm discoverability (pi v0.50.0 package system)
7
+
8
+ ## [0.4.0] - 2026-01-25
9
+
10
+ ### Added
11
+ - **Clarify TUI for single and parallel modes** - Use `clarify: true` to preview/edit before execution
12
+ - Single mode: Edit task, model, thinking level, output file
13
+ - Parallel mode: Edit each task independently, model, thinking level
14
+ - Navigate between parallel tasks with ↑↓
15
+ - **Mode-aware TUI headers** - Header shows "Agent: X" for single, "Parallel Tasks (N)" for parallel, "Chain: X → Y" for chains
16
+ - **Model override for single/parallel** - TUI model selection now works for all modes
17
+
18
+ ### Fixed
19
+ - **MAX_PARALLEL error mode** - Now correctly returns `mode: 'parallel'` (was incorrectly `mode: 'single'`)
20
+ - **`output: true` handling** - Now correctly treats `true` as "use agent's default output" instead of creating a file literally named "true"
21
+
22
+ ### Changed
23
+ - **Schema description** - `clarify` parameter now documents all modes: "default: true for chains, false for single/parallel"
24
+
3
25
  ## [0.3.3] - 2026-01-25
4
26
 
5
27
  ### Added
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  Pi extension for delegating tasks to subagents with chains, parallel execution, TUI clarification, and async support.
8
8
 
9
+ https://github.com/user-attachments/assets/702554ec-faaf-4635-80aa-fb5d6e292fd1
10
+
9
11
  ## Installation
10
12
 
11
13
  ```bash
@@ -43,18 +45,30 @@ npx pi-subagents --remove
43
45
 
44
46
  *Chain defaults to sync with TUI clarification. Use `clarify: false` to enable async (sequential-only chains; parallel-in-chain requires sync mode).
45
47
 
46
- **Chain clarification TUI keybindings:**
48
+ **Clarify TUI for single/parallel:**
49
+
50
+ Single and parallel modes also support the clarify TUI for previewing/editing parameters before execution. Unlike chains, they default to no TUI - use `clarify: true` to enable:
51
+
52
+ ```typescript
53
+ // Single agent with clarify TUI
54
+ { agent: "scout", task: "Analyze the codebase", clarify: true }
55
+
56
+ // Parallel tasks with clarify TUI
57
+ { tasks: [{agent: "scout", task: "Analyze frontend"}, ...], clarify: true }
58
+ ```
59
+
60
+ **Clarification TUI keybindings:**
47
61
 
48
62
  *Navigation mode:*
49
- - `Enter` - Run the chain
63
+ - `Enter` - Run
50
64
  - `Esc` - Cancel
51
- - `↑↓` - Navigate between steps
52
- - `e` - Edit task/template
53
- - `m` - Select model (fuzzy search with all available models)
54
- - `t` - Select thinking level (off, minimal, low, medium, high, xhigh)
55
- - `w` - Edit writes (output file path)
56
- - `r` - Edit reads list
57
- - `p` - Toggle progress tracking for ALL steps (chains share one progress.md)
65
+ - `↑↓` - Navigate between steps/tasks (parallel, chain)
66
+ - `e` - Edit task/template (all modes)
67
+ - `m` - Select model (all modes)
68
+ - `t` - Select thinking level (all modes)
69
+ - `w` - Edit writes/output file (single, chain only)
70
+ - `r` - Edit reads list (chain only)
71
+ - `p` - Toggle progress tracking (chain only)
58
72
 
59
73
  *Model selector mode:*
60
74
  - `↑↓` - Navigate model list
package/chain-clarify.ts CHANGED
@@ -11,6 +11,9 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui"
11
11
  import type { AgentConfig } from "./agents.js";
12
12
  import type { ResolvedStepBehavior } from "./settings.js";
13
13
 
14
+ /** Clarify TUI mode */
15
+ export type ClarifyMode = 'single' | 'parallel' | 'chain';
16
+
14
17
  /** Model info for display */
15
18
  export interface ModelInfo {
16
19
  provider: string;
@@ -76,10 +79,11 @@ export class ChainClarifyComponent implements Component {
76
79
  private agentConfigs: AgentConfig[],
77
80
  private templates: string[],
78
81
  private originalTask: string,
79
- private chainDir: string,
82
+ private chainDir: string | undefined, // undefined for single/parallel modes
80
83
  private resolvedBehaviors: ResolvedStepBehavior[],
81
84
  private availableModels: ModelInfo[],
82
85
  private done: (result: ChainClarifyResult) => void,
86
+ private mode: ClarifyMode = 'chain', // Mode: 'single', 'parallel', or 'chain'
83
87
  ) {
84
88
  // Initialize filtered models
85
89
  this.filteredModels = [...availableModels];
@@ -222,11 +226,17 @@ export class ChainClarifyComponent implements Component {
222
226
  // Header (truncate agent name to prevent overflow)
223
227
  const fieldName = this.editMode === "template" ? "task" : this.editMode;
224
228
  const rawAgentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
225
- const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step N: ) "
229
+ const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step/Task N: ) "
226
230
  const agentName = rawAgentName.length > maxAgentLen
227
231
  ? rawAgentName.slice(0, maxAgentLen - 1) + "…"
228
232
  : rawAgentName;
229
- const headerText = ` Editing ${fieldName} (Step ${this.editingStep! + 1}: ${agentName}) `;
233
+ // Use mode-appropriate terminology
234
+ const stepLabel = this.mode === 'single'
235
+ ? agentName
236
+ : this.mode === 'parallel'
237
+ ? `Task ${this.editingStep! + 1}: ${agentName}`
238
+ : `Step ${this.editingStep! + 1}: ${agentName}`;
239
+ const headerText = ` Editing ${fieldName} (${stepLabel}) `;
230
240
  lines.push(this.renderHeader(headerText));
231
241
  lines.push(this.row(""));
232
242
 
@@ -365,26 +375,38 @@ export class ChainClarifyComponent implements Component {
365
375
  return;
366
376
  }
367
377
 
368
- // 'e' to edit template
378
+ // 'e' to edit template (all modes)
369
379
  if (data === "e") {
370
380
  this.enterEditMode("template");
371
381
  return;
372
382
  }
373
383
 
374
- // 'w' to edit writes (output file)
375
- if (data === "w") {
384
+ // 'm' to select model (all modes)
385
+ if (data === "m") {
386
+ this.enterModelSelector();
387
+ return;
388
+ }
389
+
390
+ // 't' to select thinking level (all modes)
391
+ if (data === "t") {
392
+ this.enterThinkingSelector();
393
+ return;
394
+ }
395
+
396
+ // 'w' to edit writes (single and chain only - not parallel)
397
+ if (data === "w" && this.mode !== 'parallel') {
376
398
  this.enterEditMode("output");
377
399
  return;
378
400
  }
379
401
 
380
- // 'r' to edit reads
381
- if (data === "r") {
402
+ // 'r' to edit reads (chain only)
403
+ if (data === "r" && this.mode === 'chain') {
382
404
  this.enterEditMode("reads");
383
405
  return;
384
406
  }
385
407
 
386
- // 'p' to toggle progress for ALL steps (chains share a single progress.md)
387
- if (data === "p") {
408
+ // 'p' to toggle progress for ALL steps (chain only - chains share a single progress.md)
409
+ if (data === "p" && this.mode === 'chain') {
388
410
  // Check if any step has progress enabled
389
411
  const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
390
412
  // Toggle all steps to the opposite state
@@ -395,18 +417,6 @@ export class ChainClarifyComponent implements Component {
395
417
  this.tui.requestRender();
396
418
  return;
397
419
  }
398
-
399
- // 'm' to select model
400
- if (data === "m") {
401
- this.enterModelSelector();
402
- return;
403
- }
404
-
405
- // 't' to select thinking level
406
- if (data === "t") {
407
- this.enterThinkingSelector();
408
- return;
409
- }
410
420
  }
411
421
 
412
422
  private enterEditMode(mode: EditMode): void {
@@ -809,7 +819,12 @@ export class ChainClarifyComponent implements Component {
809
819
  }
810
820
  return this.renderFullEditMode();
811
821
  }
812
- return this.renderNavigationMode();
822
+ // Mode-based navigation rendering
823
+ switch (this.mode) {
824
+ case 'single': return this.renderSingleMode();
825
+ case 'parallel': return this.renderParallelMode();
826
+ case 'chain': return this.renderChainMode();
827
+ }
813
828
  }
814
829
 
815
830
  /** Render the model selector view */
@@ -818,9 +833,14 @@ export class ChainClarifyComponent implements Component {
818
833
  const th = this.theme;
819
834
  const lines: string[] = [];
820
835
 
821
- // Header
836
+ // Header (mode-aware terminology)
822
837
  const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
823
- const headerText = ` Select Model (Step ${this.editingStep! + 1}: ${agentName}) `;
838
+ const stepLabel = this.mode === 'single'
839
+ ? agentName
840
+ : this.mode === 'parallel'
841
+ ? `Task ${this.editingStep! + 1}: ${agentName}`
842
+ : `Step ${this.editingStep! + 1}: ${agentName}`;
843
+ const headerText = ` Select Model (${stepLabel}) `;
824
844
  lines.push(this.renderHeader(headerText));
825
845
  lines.push(this.row(""));
826
846
 
@@ -898,9 +918,14 @@ export class ChainClarifyComponent implements Component {
898
918
  const th = this.theme;
899
919
  const lines: string[] = [];
900
920
 
901
- // Header
921
+ // Header (mode-aware terminology)
902
922
  const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
903
- const headerText = ` Thinking Level (Step ${this.editingStep! + 1}: ${agentName}) `;
923
+ const stepLabel = this.mode === 'single'
924
+ ? agentName
925
+ : this.mode === 'parallel'
926
+ ? `Task ${this.editingStep! + 1}: ${agentName}`
927
+ : `Step ${this.editingStep! + 1}: ${agentName}`;
928
+ const headerText = ` Thinking Level (${stepLabel}) `;
904
929
  lines.push(this.renderHeader(headerText));
905
930
  lines.push(this.row(""));
906
931
 
@@ -947,8 +972,122 @@ export class ChainClarifyComponent implements Component {
947
972
  return lines;
948
973
  }
949
974
 
950
- /** Render navigation mode (step selection, preview) */
951
- private renderNavigationMode(): string[] {
975
+ /** Get footer text based on mode */
976
+ private getFooterText(): string {
977
+ switch (this.mode) {
978
+ case 'single':
979
+ return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites ';
980
+ case 'parallel':
981
+ return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking • ↑↓ Navigate ';
982
+ case 'chain':
983
+ return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ';
984
+ }
985
+ }
986
+
987
+ /** Render single agent mode (simplified view) */
988
+ private renderSingleMode(): string[] {
989
+ const innerW = this.width - 2;
990
+ const th = this.theme;
991
+ const lines: string[] = [];
992
+
993
+ // Header with agent name
994
+ const agentName = this.agentConfigs[0]?.name ?? "unknown";
995
+ const maxHeaderLen = innerW - 4;
996
+ const headerText = ` Agent: ${truncateToWidth(agentName, maxHeaderLen - 9)} `;
997
+ lines.push(this.renderHeader(headerText));
998
+ lines.push(this.row(""));
999
+
1000
+ // Single step - always index 0, always selected
1001
+ const config = this.agentConfigs[0]!;
1002
+ const behavior = this.getEffectiveBehavior(0);
1003
+
1004
+ // Agent name with selection indicator
1005
+ const stepLabel = config.name;
1006
+ lines.push(this.row(` ${th.fg("accent", "▶ " + stepLabel)}`));
1007
+
1008
+ // Task line
1009
+ const template = (this.templates[0] ?? "").split("\n")[0] ?? "";
1010
+ const taskLabel = th.fg("dim", "task: ");
1011
+ lines.push(this.row(` ${taskLabel}${truncateToWidth(template, innerW - 12)}`));
1012
+
1013
+ // Model line
1014
+ const effectiveModel = this.getEffectiveModel(0);
1015
+ const override = this.behaviorOverrides.get(0);
1016
+ const isOverridden = override?.model !== undefined;
1017
+ const modelValue = isOverridden
1018
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1019
+ : effectiveModel;
1020
+ const modelLabel = th.fg("dim", "model: ");
1021
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1022
+
1023
+ // Writes line (output file)
1024
+ const writesValue = behavior.output === false
1025
+ ? th.fg("dim", "(disabled)")
1026
+ : (behavior.output || th.fg("dim", "(none)"));
1027
+ const writesLabel = th.fg("dim", "writes: ");
1028
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1029
+
1030
+ lines.push(this.row(""));
1031
+
1032
+ // Footer
1033
+ lines.push(this.renderFooter(this.getFooterText()));
1034
+
1035
+ return lines;
1036
+ }
1037
+
1038
+ /** Render parallel mode (multi-task view without chain features) */
1039
+ private renderParallelMode(): string[] {
1040
+ const innerW = this.width - 2;
1041
+ const th = this.theme;
1042
+ const lines: string[] = [];
1043
+
1044
+ // Header with task count
1045
+ const headerText = ` Parallel Tasks (${this.agentConfigs.length}) `;
1046
+ lines.push(this.renderHeader(headerText));
1047
+ lines.push(this.row(""));
1048
+
1049
+ // Each task
1050
+ for (let i = 0; i < this.agentConfigs.length; i++) {
1051
+ const config = this.agentConfigs[i]!;
1052
+ const isSelected = i === this.selectedStep;
1053
+
1054
+ // Task header (truncate agent name to prevent overflow)
1055
+ const color = isSelected ? "accent" : "dim";
1056
+ const prefix = isSelected ? "▶ " : " ";
1057
+ const taskPrefix = `Task ${i + 1}: `;
1058
+ const maxNameLen = innerW - 4 - prefix.length - taskPrefix.length;
1059
+ const agentName = config.name.length > maxNameLen
1060
+ ? config.name.slice(0, maxNameLen - 1) + "…"
1061
+ : config.name;
1062
+ const taskLabel = `${taskPrefix}${agentName}`;
1063
+ lines.push(this.row(` ${th.fg(color, prefix + taskLabel)}`));
1064
+
1065
+ // Task line
1066
+ const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
1067
+ const taskTextLabel = th.fg("dim", "task: ");
1068
+ lines.push(this.row(` ${taskTextLabel}${truncateToWidth(template, innerW - 12)}`));
1069
+
1070
+ // Model line
1071
+ const effectiveModel = this.getEffectiveModel(i);
1072
+ const override = this.behaviorOverrides.get(i);
1073
+ const isOverridden = override?.model !== undefined;
1074
+ const modelValue = isOverridden
1075
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1076
+ : effectiveModel;
1077
+ const modelLabel = th.fg("dim", "model: ");
1078
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1079
+
1080
+ lines.push(this.row(""));
1081
+ }
1082
+
1083
+ // Footer
1084
+ lines.push(this.renderFooter(this.getFooterText()));
1085
+
1086
+ return lines;
1087
+ }
1088
+
1089
+ /** Render chain mode (step selection, preview) */
1090
+ private renderChainMode(): string[] {
952
1091
  const innerW = this.width - 2;
953
1092
  const th = this.theme;
954
1093
  const lines: string[] = [];
@@ -964,7 +1103,8 @@ export class ChainClarifyComponent implements Component {
964
1103
  // Original task (truncated) and chain dir
965
1104
  const taskPreview = truncateToWidth(this.originalTask, innerW - 16);
966
1105
  lines.push(this.row(` Original Task: ${taskPreview}`));
967
- const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
1106
+ // chainDir is guaranteed to be defined in chain mode
1107
+ const chainDirPreview = truncateToWidth(this.chainDir ?? "", innerW - 12);
968
1108
  lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
969
1109
 
970
1110
  // Chain-wide progress setting
@@ -1053,8 +1193,7 @@ export class ChainClarifyComponent implements Component {
1053
1193
  }
1054
1194
 
1055
1195
  // Footer with keybindings
1056
- const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ";
1057
- lines.push(this.renderFooter(footerText));
1196
+ lines.push(this.renderFooter(this.getFooterText()));
1058
1197
 
1059
1198
  return lines;
1060
1199
  }
package/index.ts CHANGED
@@ -19,7 +19,8 @@ import * as path from "node:path";
19
19
  import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
20
20
  import { Text } from "@mariozechner/pi-tui";
21
21
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
22
- import { cleanupOldChainDirs, getStepAgents, isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
22
+ import { cleanupOldChainDirs, getStepAgents, isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep } from "./settings.js";
23
+ import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.js";
23
24
  import { cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
24
25
  import {
25
26
  type AgentProgress,
@@ -188,8 +189,13 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
188
189
  const requestedAsync = params.async ?? asyncByDefault;
189
190
  const parallelDowngraded = hasTasks && requestedAsync;
190
191
  // clarify implies sync mode (TUI is blocking)
191
- // If user requested async without explicit clarify: false, downgrade to sync for chains
192
- const effectiveAsync = requestedAsync && !hasTasks && (hasChain ? params.clarify === false : true);
192
+ // - Chains default to TUI (clarify: true), so async requires explicit clarify: false
193
+ // - Single defaults to no TUI, so async is allowed unless clarify: true is passed
194
+ const effectiveAsync = requestedAsync && !hasTasks && (
195
+ hasChain
196
+ ? params.clarify === false // chains: only async if TUI explicitly disabled
197
+ : params.clarify !== true // single: async unless TUI explicitly enabled
198
+ );
193
199
 
194
200
  const artifactConfig: ArtifactConfig = {
195
201
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -336,14 +342,74 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
336
342
  }
337
343
 
338
344
  if (hasTasks && params.tasks) {
345
+ // MAX_PARALLEL check first (fail fast before TUI)
339
346
  if (params.tasks.length > MAX_PARALLEL)
340
347
  return {
341
348
  content: [{ type: "text", text: `Max ${MAX_PARALLEL} tasks` }],
342
349
  isError: true,
343
- details: { mode: "single" as const, results: [] },
350
+ details: { mode: "parallel" as const, results: [] },
344
351
  };
352
+
353
+ // Validate all agents exist
354
+ const agentConfigs: AgentConfig[] = [];
355
+ for (const t of params.tasks) {
356
+ const config = agents.find(a => a.name === t.agent);
357
+ if (!config) {
358
+ return {
359
+ content: [{ type: "text", text: `Unknown agent: ${t.agent}` }],
360
+ isError: true,
361
+ details: { mode: "parallel" as const, results: [] },
362
+ };
363
+ }
364
+ agentConfigs.push(config);
365
+ }
366
+
367
+ // Mutable copies for TUI modifications
368
+ let tasks = params.tasks.map(t => t.task);
369
+ const modelOverrides: (string | undefined)[] = new Array(params.tasks.length).fill(undefined);
370
+
371
+ // Show clarify TUI if requested
372
+ if (params.clarify === true && ctx.hasUI) {
373
+ // Get available models (same pattern as chain-execution.ts)
374
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
375
+ provider: m.provider,
376
+ id: m.id,
377
+ fullId: `${m.provider}/${m.id}`,
378
+ }));
379
+
380
+ const behaviors = agentConfigs.map(c => resolveStepBehavior(c, {}));
381
+
382
+ const result = await ctx.ui.custom<ChainClarifyResult>(
383
+ (tui, theme, _kb, done) =>
384
+ new ChainClarifyComponent(
385
+ tui, theme,
386
+ agentConfigs,
387
+ tasks,
388
+ '', // no originalTask for parallel (each task is independent)
389
+ undefined, // no chainDir for parallel
390
+ behaviors,
391
+ availableModels,
392
+ done,
393
+ 'parallel', // mode
394
+ ),
395
+ { overlay: true, overlayOptions: { anchor: 'center', width: 84, maxHeight: '80%' } },
396
+ );
397
+
398
+ if (!result || !result.confirmed) {
399
+ return { content: [{ type: 'text', text: 'Cancelled' }], details: { mode: 'parallel', results: [] } };
400
+ }
401
+
402
+ // Apply TUI overrides
403
+ tasks = result.templates;
404
+ for (let i = 0; i < result.behaviorOverrides.length; i++) {
405
+ const override = result.behaviorOverrides[i];
406
+ if (override?.model) modelOverrides[i] = override.model;
407
+ }
408
+ }
409
+
410
+ // Execute with overrides (tasks array has same length as params.tasks)
345
411
  const results = await mapConcurrent(params.tasks, MAX_CONCURRENCY, async (t, i) =>
346
- runSync(ctx.cwd, agents, t.agent, t.task, {
412
+ runSync(ctx.cwd, agents, t.agent, tasks[i]!, {
347
413
  cwd: t.cwd ?? params.cwd,
348
414
  signal,
349
415
  runId,
@@ -353,6 +419,7 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
353
419
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
354
420
  artifactConfig,
355
421
  maxOutput: params.maxOutput,
422
+ modelOverride: modelOverrides[i],
356
423
  }),
357
424
  );
358
425
 
@@ -377,24 +444,67 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
377
444
  if (hasSingle) {
378
445
  // Look up agent config for output handling
379
446
  const agentConfig = agents.find((a) => a.name === params.agent);
380
- // Note: runSync already handles unknown agent, but we need config for output
447
+ if (!agentConfig) {
448
+ return {
449
+ content: [{ type: 'text', text: `Unknown agent: ${params.agent}` }],
450
+ isError: true,
451
+ details: { mode: 'single', results: [] },
452
+ };
453
+ }
381
454
 
382
455
  let task = params.task!;
383
- let outputPath: string | undefined;
456
+ let modelOverride: string | undefined;
457
+ // Normalize output: true means "use default" (same as undefined), false means disable
458
+ const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
459
+ let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : rawOutput;
460
+
461
+ // Show clarify TUI if requested
462
+ if (params.clarify === true && ctx.hasUI) {
463
+ // Get available models (same pattern as chain-execution.ts)
464
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
465
+ provider: m.provider,
466
+ id: m.id,
467
+ fullId: `${m.provider}/${m.id}`,
468
+ }));
469
+
470
+ const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput });
471
+
472
+ const result = await ctx.ui.custom<ChainClarifyResult>(
473
+ (tui, theme, _kb, done) =>
474
+ new ChainClarifyComponent(
475
+ tui, theme,
476
+ [agentConfig],
477
+ [task],
478
+ task,
479
+ undefined, // no chainDir for single
480
+ [behavior],
481
+ availableModels,
482
+ done,
483
+ 'single', // mode
484
+ ),
485
+ { overlay: true, overlayOptions: { anchor: 'center', width: 84, maxHeight: '80%' } },
486
+ );
487
+
488
+ if (!result || !result.confirmed) {
489
+ return { content: [{ type: 'text', text: 'Cancelled' }], details: { mode: 'single', results: [] } };
490
+ }
384
491
 
385
- // Check if agent has output and it's not disabled
386
- if (agentConfig) {
387
- const effectiveOutput =
388
- params.output !== undefined ? params.output : agentConfig.output;
492
+ // Apply TUI overrides
493
+ task = result.templates[0]!;
494
+ const override = result.behaviorOverrides[0];
495
+ if (override?.model) modelOverride = override.model;
496
+ if (override?.output !== undefined) effectiveOutput = override.output;
497
+ }
389
498
 
390
- if (effectiveOutput && effectiveOutput !== false) {
391
- const outputDir = `/tmp/pi-${agentConfig.name}-${runId}`;
392
- fs.mkdirSync(outputDir, { recursive: true });
393
- outputPath = `${outputDir}/${effectiveOutput}`;
499
+ // Compute output path at runtime (uses effectiveOutput which may be TUI-modified)
500
+ let outputPath: string | undefined;
501
+ if (effectiveOutput && effectiveOutput !== false) {
502
+ const outputDir = `/tmp/pi-${agentConfig.name}-${runId}`;
503
+ fs.mkdirSync(outputDir, { recursive: true });
504
+ outputPath = `${outputDir}/${effectiveOutput}`;
394
505
 
395
- // Inject output instruction into task
396
- task += `\n\n---\n**Output:** Write your findings to: ${outputPath}`;
397
- }
506
+ // Inject output instruction into task
507
+ task += `\n\n---\n**Output:** Write your findings to: ${outputPath}`;
398
508
  }
399
509
 
400
510
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
@@ -407,6 +517,7 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
407
517
  artifactConfig,
408
518
  maxOutput: params.maxOutput,
409
519
  onUpdate,
520
+ modelOverride,
410
521
  });
411
522
 
412
523
  if (r.progress) allProgress.push(r.progress);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
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",
@@ -13,6 +13,7 @@
13
13
  "url": "https://github.com/nicobailon/pi-subagents/issues"
14
14
  },
15
15
  "keywords": [
16
+ "pi-package",
16
17
  "pi",
17
18
  "pi-coding-agent",
18
19
  "subagents",
package/schemas.ts CHANGED
@@ -77,8 +77,8 @@ export const SubagentParams = Type.Object({
77
77
  sessionDir: Type.Optional(
78
78
  Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
79
79
  ),
80
- // Chain clarification TUI
81
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
80
+ // Clarification TUI
81
+ clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
82
82
  // Solo agent output override
83
83
  output: Type.Optional(Type.Union([
84
84
  Type.String(),