pi-subagents 0.18.1 → 0.19.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 +17 -0
- package/README.md +2 -2
- package/agent-manager-chain-detail.ts +50 -6
- package/agent-manager-detail.ts +15 -2
- package/agent-manager.ts +76 -23
- package/async-execution.ts +45 -18
- package/chain-execution.ts +12 -2
- package/doctor.ts +198 -0
- package/execution.ts +2 -4
- package/index.ts +5 -2
- package/intercom-bridge.ts +61 -9
- package/package.json +5 -1
- package/prompts/parallel-review.md +8 -0
- package/schemas.ts +5 -2
- package/settings.ts +5 -0
- package/slash-commands.ts +95 -42
- package/slash-live-state.ts +3 -3
- package/subagent-executor.ts +137 -19
- package/subagent-runner.ts +8 -0
- package/subagents-status.ts +229 -9
- package/worktree.ts +19 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.19.1] - 2026-04-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `subagent({ action: "doctor" })` and `/subagents-doctor` for read-only subagent environment diagnostics.
|
|
9
|
+
- Added `/run-chain` to launch saved `.chain.md` workflows directly from slash commands with completion, shared task input, and `--bg`/`--fork` support.
|
|
10
|
+
|
|
11
|
+
## [0.19.0] - 2026-04-26
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Added top-level parallel task support for per-task `output`, `reads`, and `progress`, including `/parallel` inline forwarding and async preservation.
|
|
15
|
+
- Added `/agents` launch toggles for forked context, background execution, and worktree-isolated parallel runs.
|
|
16
|
+
- Added a read-only detail view to `/subagents-status` for inspecting selected async runs, including recent events, output tails, and useful run paths.
|
|
17
|
+
- Added a packaged `/parallel-review` prompt template for launching fresh-context adversarial review subagents.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Parallel and chain child runs now detach cleanly when a child uses intercom, preventing incoming handoff messages from aborting the parent foreground run.
|
|
21
|
+
|
|
5
22
|
## [0.18.1] - 2026-04-25
|
|
6
23
|
|
|
7
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -977,9 +977,9 @@ The inherited thread is reference-only. Do not continue that conversation or sen
|
|
|
977
977
|
Use `intercom` only to coordinate with the orchestrator session `{orchestratorTarget}`.
|
|
978
978
|
|
|
979
979
|
- Need a decision or you're blocked: `intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })`
|
|
980
|
-
-
|
|
980
|
+
- Blocked or explicitly asked to send progress: `intercom({ action: "send", to: "{orchestratorTarget}", message: "UPDATE: <summary>" })`
|
|
981
981
|
|
|
982
|
-
If no
|
|
982
|
+
Do not send routine completion handoffs through intercom. If no coordination is needed, return a focused task result.
|
|
983
983
|
```
|
|
984
984
|
|
|
985
985
|
Bridge activation also requires all of the following:
|
|
@@ -2,6 +2,7 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
3
|
import type { ChainConfig, ChainStepConfig } from "./agents.ts";
|
|
4
4
|
import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.ts";
|
|
5
|
+
import { isParallelStep, type ChainStep } from "./settings.ts";
|
|
5
6
|
|
|
6
7
|
export interface ChainDetailState {
|
|
7
8
|
scrollOffset: number;
|
|
@@ -14,11 +15,24 @@ export type ChainDetailAction =
|
|
|
14
15
|
|
|
15
16
|
const CHAIN_DETAIL_VIEWPORT_HEIGHT = 12;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
type DetailChainStep = ChainStepConfig | ChainStep;
|
|
19
|
+
|
|
20
|
+
export function buildDependencyMap(steps: DetailChainStep[]): Map<number, number[]> {
|
|
18
21
|
const outputMap = new Map<string, number>();
|
|
19
22
|
const deps = new Map<number, number[]>();
|
|
20
23
|
for (let i = 0; i < steps.length; i++) {
|
|
21
24
|
const step = steps[i]!;
|
|
25
|
+
if (isParallelStep(step as ChainStep)) {
|
|
26
|
+
const reads = step.parallel.flatMap((task) => Array.isArray(task.reads) ? task.reads : []);
|
|
27
|
+
const sources = reads
|
|
28
|
+
.map((file) => outputMap.get(file))
|
|
29
|
+
.filter((idx): idx is number => idx !== undefined);
|
|
30
|
+
if (sources.length > 0) deps.set(i, [...new Set(sources)]);
|
|
31
|
+
for (const task of step.parallel) {
|
|
32
|
+
if (typeof task.output === "string" && task.output.length > 0) outputMap.set(task.output, i);
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
22
36
|
if (typeof step.output === "string" && step.output.length > 0) outputMap.set(step.output, i);
|
|
23
37
|
if (Array.isArray(step.reads) && step.reads.length > 0) {
|
|
24
38
|
const sources = step.reads
|
|
@@ -33,21 +47,51 @@ export function buildDependencyMap(steps: ChainStepConfig[]): Map<number, number
|
|
|
33
47
|
function buildChainDetailLines(chain: ChainConfig, width: number): string[] {
|
|
34
48
|
const contentWidth = width - 3;
|
|
35
49
|
const lines: string[] = [];
|
|
36
|
-
const
|
|
50
|
+
const steps = chain.steps as DetailChainStep[];
|
|
51
|
+
const dependencyMap = buildDependencyMap(steps);
|
|
37
52
|
lines.push(truncateToWidth(chain.description, contentWidth));
|
|
38
53
|
lines.push("");
|
|
39
54
|
lines.push(truncateToWidth(`File: ${formatPath(chain.filePath)}`, contentWidth));
|
|
40
55
|
lines.push("");
|
|
41
56
|
lines.push(truncateToWidth("── Flow ──", contentWidth));
|
|
42
57
|
|
|
43
|
-
for (let i = 0; i <
|
|
44
|
-
const step =
|
|
58
|
+
for (let i = 0; i < steps.length; i++) {
|
|
59
|
+
const step = steps[i]!;
|
|
60
|
+
const sources = dependencyMap.get(i);
|
|
61
|
+
const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
|
|
62
|
+
if (isParallelStep(step as ChainStep)) {
|
|
63
|
+
lines.push(truncateToWidth(` ${i + 1} Parallel: ${step.parallel.map((task) => task.agent).join(" + ")}`, contentWidth));
|
|
64
|
+
if (step.concurrency !== undefined) lines.push(truncateToWidth(` concurrency: ${step.concurrency}`, contentWidth));
|
|
65
|
+
if (step.failFast !== undefined) lines.push(truncateToWidth(` fail fast: ${step.failFast ? "on" : "off"}`, contentWidth));
|
|
66
|
+
if (step.worktree !== undefined) lines.push(truncateToWidth(` worktree: ${step.worktree ? "on" : "off"}`, contentWidth));
|
|
67
|
+
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
68
|
+
const task = step.parallel[taskIndex]!;
|
|
69
|
+
lines.push(truncateToWidth(` ${taskIndex + 1}. ${task.agent}`, contentWidth));
|
|
70
|
+
const taskPreview = (task.task ?? "").split("\n")[0] ?? "";
|
|
71
|
+
if (taskPreview) lines.push(truncateToWidth(` task: ${taskPreview}`, contentWidth));
|
|
72
|
+
if (Array.isArray(task.reads) && task.reads.length > 0) lines.push(truncateToWidth(` ← reads: ${task.reads.join(", ")}${fromText}`, contentWidth));
|
|
73
|
+
else if (task.reads === false) lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
|
|
74
|
+
if (typeof task.output === "string" && task.output.length > 0) lines.push(truncateToWidth(` → output: ${task.output}`, contentWidth));
|
|
75
|
+
else if (task.output === false) lines.push(truncateToWidth(" → output: (disabled)", contentWidth));
|
|
76
|
+
if (task.model) lines.push(truncateToWidth(` model: ${task.model}`, contentWidth));
|
|
77
|
+
if (task.skill !== undefined) {
|
|
78
|
+
const skillsText =
|
|
79
|
+
task.skill === false
|
|
80
|
+
? "(disabled)"
|
|
81
|
+
: Array.isArray(task.skill)
|
|
82
|
+
? (task.skill.length > 0 ? task.skill.join(", ") : "(none)")
|
|
83
|
+
: task.skill;
|
|
84
|
+
lines.push(truncateToWidth(` skills: ${skillsText}`, contentWidth));
|
|
85
|
+
}
|
|
86
|
+
if (task.progress !== undefined) lines.push(truncateToWidth(` progress: ${task.progress ? "on" : "off"}`, contentWidth));
|
|
87
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
45
91
|
lines.push(truncateToWidth(` ${i + 1} ${step.agent}`, contentWidth));
|
|
46
92
|
const taskPreview = step.task.split("\n")[0] ?? "";
|
|
47
93
|
lines.push(truncateToWidth(` task: ${taskPreview || "(none)"}`, contentWidth));
|
|
48
94
|
if (Array.isArray(step.reads) && step.reads.length > 0) {
|
|
49
|
-
const sources = dependencyMap.get(i);
|
|
50
|
-
const fromText = sources && sources.length > 0 ? ` (from ${sources.map((s) => s + 1).join(", ")})` : "";
|
|
51
95
|
lines.push(truncateToWidth(` ← reads: ${step.reads.join(", ")}${fromText}`, contentWidth));
|
|
52
96
|
} else if (step.reads === false) {
|
|
53
97
|
lines.push(truncateToWidth(" ← reads: (disabled)", contentWidth));
|
package/agent-manager-detail.ts
CHANGED
|
@@ -180,12 +180,19 @@ export function renderDetail(
|
|
|
180
180
|
return lines;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
export interface LaunchToggleState {
|
|
184
|
+
fork: boolean;
|
|
185
|
+
background: boolean;
|
|
186
|
+
worktree?: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
export function renderTaskInput(
|
|
184
190
|
title: string,
|
|
185
191
|
editor: TextEditorState,
|
|
186
192
|
skipClarify: boolean,
|
|
187
193
|
width: number,
|
|
188
194
|
theme: Theme,
|
|
195
|
+
launchToggles?: LaunchToggleState,
|
|
189
196
|
): string[] {
|
|
190
197
|
const lines: string[] = [];
|
|
191
198
|
lines.push(renderHeader(` ${title} `, width, theme));
|
|
@@ -209,8 +216,14 @@ export function renderTaskInput(
|
|
|
209
216
|
lines.push(row(` ${bottom}`, width, theme));
|
|
210
217
|
|
|
211
218
|
lines.push(row("", width, theme));
|
|
212
|
-
const enterLabel = skipClarify ? "quick run" : "run";
|
|
213
219
|
const quickLabel = skipClarify ? "on" : "off";
|
|
214
|
-
|
|
220
|
+
const footerParts = ["[enter] run", `[tab] quick:${quickLabel}`];
|
|
221
|
+
if (launchToggles) {
|
|
222
|
+
footerParts.push(`[ctrl+f] fork:${launchToggles.fork ? "on" : "off"}`);
|
|
223
|
+
footerParts.push(`[ctrl+b] bg:${launchToggles.background ? "on" : "off"}`);
|
|
224
|
+
if (launchToggles.worktree !== undefined) footerParts.push(`[ctrl+w] worktree:${launchToggles.worktree ? "on" : "off"}`);
|
|
225
|
+
}
|
|
226
|
+
footerParts.push("[esc]");
|
|
227
|
+
lines.push(renderFooter(` ${footerParts.join(" ")} `, width, theme));
|
|
215
228
|
return lines;
|
|
216
229
|
}
|
package/agent-manager.ts
CHANGED
|
@@ -20,19 +20,20 @@ import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-t
|
|
|
20
20
|
import { parseChain, serializeChain } from "./chain-serializer.ts";
|
|
21
21
|
import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
|
|
22
22
|
import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
|
|
23
|
-
import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction } from "./agent-manager-detail.ts";
|
|
23
|
+
import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
|
|
24
24
|
import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
|
|
25
25
|
import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
|
|
26
26
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
|
|
27
27
|
import type { TextEditorState } from "./text-editor.ts";
|
|
28
28
|
import { loadRunsForAgent } from "./run-history.ts";
|
|
29
29
|
import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
|
|
30
|
+
import { isParallelStep, type ChainStep } from "./settings.ts";
|
|
30
31
|
|
|
31
32
|
export type ManagerResult =
|
|
32
|
-
| { action: "launch"; agent: string; task: string; skipClarify?: boolean }
|
|
33
|
-
| { action: "chain"; agents: string[]; task: string; skipClarify?: boolean }
|
|
34
|
-
| { action: "parallel"; tasks: Array<{ agent: string; task: string }>; skipClarify?: boolean }
|
|
35
|
-
| { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean }
|
|
33
|
+
| { action: "launch"; agent: string; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
|
|
34
|
+
| { action: "chain"; agents: string[]; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
|
|
35
|
+
| { action: "parallel"; tasks: Array<{ agent: string; task: string }>; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
|
|
36
|
+
| { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
|
|
36
37
|
| undefined;
|
|
37
38
|
|
|
38
39
|
export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
|
|
@@ -69,7 +70,27 @@ function cloneConfig(config: AgentConfig): AgentConfig {
|
|
|
69
70
|
: undefined,
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
function cloneChainConfig(config: ChainConfig): ChainConfig {
|
|
73
|
+
function cloneChainConfig(config: ChainConfig): ChainConfig {
|
|
74
|
+
const steps = (config.steps as unknown as ChainStep[]).map((step) => {
|
|
75
|
+
if (isParallelStep(step)) {
|
|
76
|
+
return {
|
|
77
|
+
...step,
|
|
78
|
+
parallel: step.parallel.map((task) => ({
|
|
79
|
+
...task,
|
|
80
|
+
reads: Array.isArray(task.reads) ? [...task.reads] : task.reads,
|
|
81
|
+
skill: Array.isArray(task.skill) ? [...task.skill] : task.skill,
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...step,
|
|
87
|
+
reads: Array.isArray(step.reads) ? [...step.reads] : step.reads,
|
|
88
|
+
...(Array.isArray((step as typeof step & { skills?: string[] | false }).skills) ? { skills: [...(step as typeof step & { skills: string[] }).skills] } : { skills: (step as typeof step & { skills?: false }).skills }),
|
|
89
|
+
...(Array.isArray(step.skill) ? { skill: [...step.skill] } : { skill: step.skill }),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
return { ...config, steps: steps as unknown as ChainConfig["steps"], extraFields: config.extraFields ? { ...config.extraFields } : undefined };
|
|
93
|
+
}
|
|
73
94
|
function slugTemplateName(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
|
|
74
95
|
function nextSelectableIndex(items: TemplateItem[], current: number, direction: 1 | -1): number { let next = current + direction; while (next >= 0 && next < items.length && items[next]!.type === "separator") next += direction; if (next < 0 || next >= items.length) return current; return next; }
|
|
75
96
|
const CHAIN_EDIT_VIEWPORT = 10;
|
|
@@ -90,6 +111,9 @@ export class AgentManagerComponent implements Component {
|
|
|
90
111
|
private chainEditState: { editor: TextEditorState; error?: string } | null = null;
|
|
91
112
|
private taskEditor: TextEditorState = createEditorState();
|
|
92
113
|
private skipClarify = false;
|
|
114
|
+
private launchFork = false;
|
|
115
|
+
private launchBackground = false;
|
|
116
|
+
private launchWorktree = false;
|
|
93
117
|
private chainAgentIds: string[] = [];
|
|
94
118
|
private chainLaunchId: string | null = null;
|
|
95
119
|
private parallelMode = false;
|
|
@@ -187,10 +211,21 @@ export class AgentManagerComponent implements Component {
|
|
|
187
211
|
this.parallelState = createParallelState(names);
|
|
188
212
|
this.screen = "parallel-builder";
|
|
189
213
|
}
|
|
214
|
+
private resetLaunchToggles(): void { this.launchFork = false; this.launchBackground = false; this.launchWorktree = false; }
|
|
215
|
+
private enterParallelTaskInput(): void {
|
|
216
|
+
this.chainAgentIds = [];
|
|
217
|
+
this.chainLaunchId = null;
|
|
218
|
+
this.parallelMode = true;
|
|
219
|
+
this.taskBackScreen = "parallel-builder";
|
|
220
|
+
this.taskEditor = createEditorState();
|
|
221
|
+
this.skipClarify = true;
|
|
222
|
+
this.resetLaunchToggles();
|
|
223
|
+
this.screen = "task-input";
|
|
224
|
+
}
|
|
190
225
|
private enterTaskInput(ids: string[], backScreen: ManagerScreen = "list"): void {
|
|
191
|
-
this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input";
|
|
226
|
+
this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input";
|
|
192
227
|
}
|
|
193
|
-
private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input"; }
|
|
228
|
+
private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.resetLaunchToggles(); this.screen = "task-input"; }
|
|
194
229
|
private enterTemplateSelect(): void { this.templateCursor = TEMPLATE_ITEMS.findIndex((item) => item.type !== "separator"); if (this.templateCursor < 0) this.templateCursor = 0; this.screen = "template-select"; }
|
|
195
230
|
|
|
196
231
|
private enterChainEdit(entry: ChainEntry): void {
|
|
@@ -276,6 +311,27 @@ export class AgentManagerComponent implements Component {
|
|
|
276
311
|
catch (err) { state.error = err instanceof Error ? err.message : "Failed to save chain."; return false; }
|
|
277
312
|
}
|
|
278
313
|
|
|
314
|
+
private canToggleLaunchWorktree(): boolean {
|
|
315
|
+
if (this.parallelMode && this.parallelState) return true;
|
|
316
|
+
if (!this.chainLaunchId) return false;
|
|
317
|
+
const chainEntry = this.getChainEntry(this.chainLaunchId);
|
|
318
|
+
return chainEntry ? (chainEntry.config.steps as unknown as ChainStep[]).some(isParallelStep) : false;
|
|
319
|
+
}
|
|
320
|
+
private launchFlags(): { fork?: boolean; background?: boolean; worktree?: boolean } {
|
|
321
|
+
return {
|
|
322
|
+
...(this.launchFork ? { fork: true } : {}),
|
|
323
|
+
...(this.launchBackground ? { background: true } : {}),
|
|
324
|
+
...(this.launchWorktree && this.canToggleLaunchWorktree() ? { worktree: true } : {}),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
private launchToggleState(): LaunchToggleState {
|
|
328
|
+
return {
|
|
329
|
+
fork: this.launchFork,
|
|
330
|
+
background: this.launchBackground,
|
|
331
|
+
...(this.canToggleLaunchWorktree() ? { worktree: this.launchWorktree } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
279
335
|
private handleTemplateSelectInput(data: string): void {
|
|
280
336
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
281
337
|
if (matchesKey(data, "up")) { this.templateCursor = nextSelectableIndex(TEMPLATE_ITEMS, this.templateCursor, -1); this.tui.requestRender(); return; }
|
|
@@ -476,13 +532,7 @@ export class AgentManagerComponent implements Component {
|
|
|
476
532
|
const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
|
|
477
533
|
const pAction = handleParallelInput(this.parallelState, agentOptions, data, this.overlayWidth);
|
|
478
534
|
if (pAction?.type === "proceed") {
|
|
479
|
-
this.
|
|
480
|
-
this.chainLaunchId = null;
|
|
481
|
-
this.parallelMode = true;
|
|
482
|
-
this.taskBackScreen = "parallel-builder";
|
|
483
|
-
this.taskEditor = createEditorState();
|
|
484
|
-
this.skipClarify = true;
|
|
485
|
-
this.screen = "task-input";
|
|
535
|
+
this.enterParallelTaskInput();
|
|
486
536
|
} else if (pAction?.type === "back") {
|
|
487
537
|
this.parallelState = null;
|
|
488
538
|
this.parallelMode = false;
|
|
@@ -493,28 +543,31 @@ export class AgentManagerComponent implements Component {
|
|
|
493
543
|
}
|
|
494
544
|
case "task-input": {
|
|
495
545
|
if (matchesKey(data, "tab")) { this.skipClarify = !this.skipClarify; this.tui.requestRender(); return; }
|
|
546
|
+
if (matchesKey(data, "ctrl+f")) { this.launchFork = !this.launchFork; this.tui.requestRender(); return; }
|
|
547
|
+
if (matchesKey(data, "ctrl+b")) { this.launchBackground = !this.launchBackground; this.tui.requestRender(); return; }
|
|
548
|
+
if (matchesKey(data, "ctrl+w") && this.canToggleLaunchWorktree()) { this.launchWorktree = !this.launchWorktree; this.tui.requestRender(); return; }
|
|
496
549
|
const innerW = this.overlayWidth - 2; const boxInnerWidth = Math.max(10, innerW - 4); const nextState = handleEditorInput(this.taskEditor, data, boxInnerWidth);
|
|
497
550
|
if (nextState) { this.taskEditor = nextState; this.tui.requestRender(); return; }
|
|
498
551
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.screen = this.taskBackScreen; this.tui.requestRender(); return; }
|
|
499
552
|
if (matchesKey(data, "return")) {
|
|
500
553
|
if (this.chainLaunchId) {
|
|
501
554
|
const chainEntry = this.getChainEntry(this.chainLaunchId); if (!chainEntry) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
502
|
-
this.done({ action: "launch-chain", chain: cloneChainConfig(chainEntry.config), task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
|
|
555
|
+
this.done({ action: "launch-chain", chain: cloneChainConfig(chainEntry.config), task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
|
|
503
556
|
} else if (this.parallelMode && this.parallelState) {
|
|
504
557
|
const sharedTask = this.taskEditor.buffer;
|
|
505
558
|
const tasks = this.parallelState.slots.map((slot) => ({ agent: slot.agentName, task: slot.customTask || sharedTask }));
|
|
506
|
-
this.done({ action: "parallel", tasks, skipClarify: this.skipClarify }); return;
|
|
559
|
+
this.done({ action: "parallel", tasks, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
|
|
507
560
|
}
|
|
508
561
|
if (this.chainAgentIds.length > 1) {
|
|
509
562
|
const agents = this.chainAgentIds
|
|
510
563
|
.map((id) => this.getAgentEntry(id)?.config.name)
|
|
511
564
|
.filter((name): name is string => Boolean(name));
|
|
512
565
|
if (agents.length !== this.chainAgentIds.length) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
513
|
-
this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
|
|
566
|
+
this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
|
|
514
567
|
}
|
|
515
568
|
const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name;
|
|
516
569
|
if (!name) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
517
|
-
this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
|
|
570
|
+
this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify, ...this.launchFlags() }); return;
|
|
518
571
|
}
|
|
519
572
|
return;
|
|
520
573
|
}
|
|
@@ -628,16 +681,16 @@ export class AgentManagerComponent implements Component {
|
|
|
628
681
|
return renderParallel(this.parallelState, agentOptions, w, this.theme);
|
|
629
682
|
}
|
|
630
683
|
case "task-input": {
|
|
631
|
-
if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme); }
|
|
632
|
-
if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme);
|
|
684
|
+
if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState()); }
|
|
685
|
+
if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
|
|
633
686
|
if (this.chainAgentIds.length > 1) {
|
|
634
687
|
const names = this.chainAgentIds
|
|
635
688
|
.map((id) => this.getAgentEntry(id)?.config.name)
|
|
636
689
|
.filter((name): name is string => Boolean(name));
|
|
637
|
-
return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme);
|
|
690
|
+
return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
|
|
638
691
|
}
|
|
639
692
|
const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name ?? "Agent";
|
|
640
|
-
return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme);
|
|
693
|
+
return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme, this.launchToggleState());
|
|
641
694
|
}
|
|
642
695
|
case "confirm-delete": return this.renderConfirmDelete(w);
|
|
643
696
|
case "name-input": return this.renderNameInput(w);
|
package/async-execution.ts
CHANGED
|
@@ -12,12 +12,13 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
12
12
|
import type { AgentConfig } from "./agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "./pi-args.ts";
|
|
14
14
|
import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
15
|
-
import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.ts";
|
|
15
|
+
import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "./settings.ts";
|
|
16
16
|
import type { RunnerStep } from "./parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "./skills.ts";
|
|
19
19
|
import { resolveChildCwd } from "./utils.ts";
|
|
20
20
|
import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
|
|
21
|
+
import { resolveExpectedWorktreeAgentCwd } from "./worktree.ts";
|
|
21
22
|
import {
|
|
22
23
|
type ArtifactConfig,
|
|
23
24
|
type Details,
|
|
@@ -221,12 +222,22 @@ export function executeAsyncChain(
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
|
|
225
|
+
let progressInstructionCreated = false;
|
|
226
|
+
const buildStepOverrides = (s: SequentialStep): StepOverrides => {
|
|
227
|
+
const stepSkillInput = normalizeSkillInput(s.skill);
|
|
228
|
+
return {
|
|
229
|
+
...(s.output !== undefined ? { output: s.output } : {}),
|
|
230
|
+
...(s.reads !== undefined ? { reads: s.reads } : {}),
|
|
231
|
+
...(s.progress !== undefined ? { progress: s.progress } : {}),
|
|
232
|
+
...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
|
|
233
|
+
...(s.model ? { model: s.model } : {}),
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior) => {
|
|
225
237
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
226
238
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const behavior = resolveStepBehavior(a, stepOverrides, chainSkills);
|
|
239
|
+
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
240
|
+
const behavior = resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills);
|
|
230
241
|
const skillNames = behavior.skills === false ? [] : behavior.skills;
|
|
231
242
|
const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
|
|
232
243
|
|
|
@@ -236,16 +247,20 @@ export function executeAsyncChain(
|
|
|
236
247
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
237
248
|
}
|
|
238
249
|
|
|
239
|
-
const
|
|
240
|
-
const
|
|
250
|
+
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
|
|
251
|
+
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
252
|
+
if (behavior.progress) progressInstructionCreated = true;
|
|
253
|
+
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, runnerCwd, isFirstProgressAgent);
|
|
254
|
+
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
255
|
+
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${s.task ?? "{previous}"}${progressInstructions.suffix}`, outputPath);
|
|
241
256
|
|
|
242
|
-
const primaryModel = resolveModelCandidate(
|
|
257
|
+
const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
|
|
243
258
|
return {
|
|
244
259
|
agent: s.agent,
|
|
245
260
|
task,
|
|
246
261
|
cwd: stepCwd,
|
|
247
262
|
model: applyThinkingSuffix(primaryModel, a.thinking),
|
|
248
|
-
modelCandidates: buildModelCandidates(
|
|
263
|
+
modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
249
264
|
applyThinkingSuffix(candidate, a.thinking),
|
|
250
265
|
),
|
|
251
266
|
tools: a.tools,
|
|
@@ -269,17 +284,29 @@ export function executeAsyncChain(
|
|
|
269
284
|
return sessionFile;
|
|
270
285
|
};
|
|
271
286
|
|
|
272
|
-
const steps: RunnerStep[] = chain.map((s) => {
|
|
287
|
+
const steps: RunnerStep[] = chain.map((s, stepIndex) => {
|
|
273
288
|
if (isParallelStep(s)) {
|
|
289
|
+
const parallelBehaviors = s.parallel.map((task) => {
|
|
290
|
+
const agent = agents.find((candidate) => candidate.name === task.agent)!;
|
|
291
|
+
return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
|
|
292
|
+
});
|
|
293
|
+
const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
|
|
294
|
+
if (progressPrecreated) {
|
|
295
|
+
if (!s.worktree) writeInitialProgressFile(runnerCwd);
|
|
296
|
+
progressInstructionCreated = true;
|
|
297
|
+
}
|
|
274
298
|
return {
|
|
275
|
-
parallel: s.parallel.map((t) =>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
parallel: s.parallel.map((t, taskIndex) => {
|
|
300
|
+
let behaviorCwd: string | undefined;
|
|
301
|
+
if (s.worktree) {
|
|
302
|
+
try {
|
|
303
|
+
behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
|
|
304
|
+
} catch {
|
|
305
|
+
behaviorCwd = undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
|
|
309
|
+
}),
|
|
283
310
|
concurrency: s.concurrency,
|
|
284
311
|
failFast: s.failFast,
|
|
285
312
|
worktree: s.worktree,
|
package/chain-execution.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveStepBehavior,
|
|
16
16
|
resolveParallelBehaviors,
|
|
17
17
|
buildChainInstructions,
|
|
18
|
+
writeInitialProgressFile,
|
|
18
19
|
createParallelDirs,
|
|
19
20
|
aggregateParallelOutputs,
|
|
20
21
|
isParallelStep,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
type ResolvedTemplates,
|
|
27
28
|
} from "./settings.ts";
|
|
28
29
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
30
|
+
import { INTERCOM_BRIDGE_MARKER } from "./intercom-bridge.ts";
|
|
29
31
|
import { runSync } from "./execution.ts";
|
|
30
32
|
import { buildChainSummary } from "./formatters.ts";
|
|
31
33
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
type ArtifactPaths,
|
|
47
49
|
type ControlEvent,
|
|
48
50
|
type Details,
|
|
51
|
+
type IntercomEventBus,
|
|
49
52
|
type ResolvedControlConfig,
|
|
50
53
|
type SingleResult,
|
|
51
54
|
MAX_CONCURRENCY,
|
|
@@ -75,6 +78,7 @@ interface ParallelChainRunInput {
|
|
|
75
78
|
prev: string;
|
|
76
79
|
originalTask: string;
|
|
77
80
|
ctx: ExtensionContext;
|
|
81
|
+
intercomEvents?: IntercomEventBus;
|
|
78
82
|
cwd?: string;
|
|
79
83
|
runId: string;
|
|
80
84
|
globalTaskIndex: number;
|
|
@@ -134,8 +138,7 @@ function ensureParallelProgressFile(
|
|
|
134
138
|
if (progressCreated || !parallelBehaviors.some((behavior) => behavior.progress)) {
|
|
135
139
|
return progressCreated;
|
|
136
140
|
}
|
|
137
|
-
|
|
138
|
-
fs.writeFileSync(progressPath, "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n");
|
|
141
|
+
writeInitialProgressFile(chainDir);
|
|
139
142
|
return true;
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -221,6 +224,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
221
224
|
cwd: taskCwd,
|
|
222
225
|
signal: input.signal,
|
|
223
226
|
interruptSignal: interruptController.signal,
|
|
227
|
+
allowIntercomDetach: taskAgentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
228
|
+
intercomEvents: input.intercomEvents,
|
|
224
229
|
runId: input.runId,
|
|
225
230
|
index: input.globalTaskIndex + taskIndex,
|
|
226
231
|
sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
|
|
@@ -287,6 +292,7 @@ export interface ChainExecutionParams {
|
|
|
287
292
|
task?: string;
|
|
288
293
|
agents: AgentConfig[];
|
|
289
294
|
ctx: ExtensionContext;
|
|
295
|
+
intercomEvents?: IntercomEventBus;
|
|
290
296
|
signal?: AbortSignal;
|
|
291
297
|
runId: string;
|
|
292
298
|
cwd?: string;
|
|
@@ -352,6 +358,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
352
358
|
controlConfig,
|
|
353
359
|
childIntercomTarget,
|
|
354
360
|
foregroundControl,
|
|
361
|
+
intercomEvents,
|
|
355
362
|
chainSkills: chainSkillsParam,
|
|
356
363
|
chainDir: chainDirBase,
|
|
357
364
|
} = params;
|
|
@@ -536,6 +543,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
536
543
|
prev,
|
|
537
544
|
originalTask,
|
|
538
545
|
ctx,
|
|
546
|
+
intercomEvents,
|
|
539
547
|
cwd,
|
|
540
548
|
runId,
|
|
541
549
|
globalTaskIndex,
|
|
@@ -709,6 +717,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
709
717
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
710
718
|
signal,
|
|
711
719
|
interruptSignal: interruptController.signal,
|
|
720
|
+
allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
721
|
+
intercomEvents,
|
|
712
722
|
runId,
|
|
713
723
|
index: globalTaskIndex,
|
|
714
724
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|