pi-subagents 0.23.0 → 0.24.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +17 -79
  3. package/agents/reviewer.md +2 -2
  4. package/package.json +1 -1
  5. package/prompts/parallel-cleanup.md +11 -1
  6. package/prompts/parallel-review.md +11 -1
  7. package/skills/pi-subagents/SKILL.md +29 -13
  8. package/src/agents/agent-serializer.ts +0 -42
  9. package/src/agents/agents.ts +1 -1
  10. package/src/extension/index.ts +14 -8
  11. package/src/extension/schemas.ts +1 -1
  12. package/src/intercom/intercom-bridge.ts +4 -1
  13. package/src/intercom/result-intercom.ts +8 -3
  14. package/src/runs/background/async-execution.ts +10 -5
  15. package/src/runs/background/async-resume.ts +57 -31
  16. package/src/runs/background/async-status.ts +16 -50
  17. package/src/runs/background/result-watcher.ts +3 -1
  18. package/src/runs/background/run-status.ts +28 -26
  19. package/src/runs/background/stale-run-reconciler.ts +3 -0
  20. package/src/runs/background/subagent-runner.ts +21 -7
  21. package/src/runs/foreground/chain-clarify.ts +183 -218
  22. package/src/runs/foreground/chain-execution.ts +55 -21
  23. package/src/runs/foreground/execution.ts +6 -3
  24. package/src/runs/foreground/subagent-executor.ts +152 -20
  25. package/src/runs/shared/single-output.ts +21 -6
  26. package/src/shared/settings.ts +19 -0
  27. package/src/shared/status-format.ts +49 -0
  28. package/src/shared/types.ts +18 -5
  29. package/src/slash/slash-commands.ts +1 -74
  30. package/src/tui/render.ts +37 -61
  31. package/src/agents/agent-templates.ts +0 -60
  32. package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
  33. package/src/manager-ui/agent-manager-detail.ts +0 -235
  34. package/src/manager-ui/agent-manager-edit.ts +0 -456
  35. package/src/manager-ui/agent-manager-list.ts +0 -283
  36. package/src/manager-ui/agent-manager-parallel.ts +0 -302
  37. package/src/manager-ui/agent-manager.ts +0 -732
  38. package/src/tui/subagents-status.ts +0 -621
  39. package/src/tui/text-editor.ts +0 -286
@@ -22,6 +22,8 @@ import {
22
22
  getStepAgents,
23
23
  isParallelStep,
24
24
  resolveStepBehavior,
25
+ suppressProgressForReadOnlyTask,
26
+ taskDisallowsFileUpdates,
25
27
  type ChainStep,
26
28
  type ResolvedStepBehavior,
27
29
  type SequentialStep,
@@ -206,6 +208,115 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
206
208
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
207
209
  }
208
210
 
211
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
212
+ state.foregroundRuns ??= new Map();
213
+ state.foregroundRuns.set(input.runId, {
214
+ runId: input.runId,
215
+ mode: input.mode,
216
+ cwd: input.cwd,
217
+ updatedAt: Date.now(),
218
+ children: input.results.map((result, index) => ({
219
+ agent: result.agent,
220
+ index,
221
+ status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
222
+ ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
223
+ })),
224
+ });
225
+ while (state.foregroundRuns.size > 50) {
226
+ const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
227
+ if (!oldest) break;
228
+ state.foregroundRuns.delete(oldest.runId);
229
+ }
230
+ }
231
+
232
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
233
+ const requested = (params.id ?? params.runId)?.trim();
234
+ if (!requested || !state.foregroundRuns?.size) return undefined;
235
+ const direct = state.foregroundRuns.get(requested);
236
+ const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
237
+ if (matches.length === 0) return undefined;
238
+ if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
239
+ const run = matches[0]!;
240
+ if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
241
+ const index = params.index ?? 0;
242
+ if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
243
+ if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
244
+ const child = run.children[index]!;
245
+ if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
246
+ if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
247
+ if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
248
+ const sessionFile = path.resolve(child.sessionFile);
249
+ if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
250
+ return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
251
+ }
252
+
253
+ type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
254
+ type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
255
+ type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
256
+
257
+ function isAsyncRunNotFound(error: unknown): boolean {
258
+ return error instanceof Error && error.message.startsWith("Async run not found.");
259
+ }
260
+
261
+ function isResumeAmbiguity(error: unknown): boolean {
262
+ return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
263
+ }
264
+
265
+ function resumeTargetExact(target: { runId: string } | undefined, requested: string): boolean {
266
+ return target?.runId === requested;
267
+ }
268
+
269
+ function escapeRegExp(value: string): string {
270
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
271
+ }
272
+
273
+ function isExactResumeError(error: unknown, source: "async" | "foreground", requested: string): boolean {
274
+ if (!(error instanceof Error) || !requested) return false;
275
+ return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
276
+ }
277
+
278
+ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
279
+ const requested = (params.id ?? params.runId)?.trim() ?? "";
280
+ let foregroundTarget: ForegroundResumeSourceTarget | undefined;
281
+ let foregroundError: unknown;
282
+ let asyncTarget: AsyncResumeSourceTarget | undefined;
283
+ let asyncError: unknown;
284
+
285
+ try {
286
+ const target = resolveForegroundResumeTarget(params, state);
287
+ if (target) foregroundTarget = { kind: "revive", source: "foreground", ...target };
288
+ } catch (error) {
289
+ foregroundError = error;
290
+ }
291
+ try {
292
+ asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
293
+ } catch (error) {
294
+ asyncError = error;
295
+ }
296
+
297
+ if (foregroundTarget && asyncTarget) {
298
+ const foregroundExact = resumeTargetExact(foregroundTarget, requested);
299
+ const asyncExact = resumeTargetExact(asyncTarget, requested);
300
+ if (foregroundExact && !asyncExact) return foregroundTarget;
301
+ if (asyncExact && !foregroundExact) return asyncTarget;
302
+ throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
303
+ }
304
+ if (foregroundTarget) {
305
+ if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
306
+ if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
307
+ return foregroundTarget;
308
+ }
309
+ if (asyncTarget) {
310
+ if (isExactResumeError(foregroundError, "foreground", requested)) throw foregroundError;
311
+ if (isResumeAmbiguity(foregroundError) && !resumeTargetExact(asyncTarget, requested)) throw foregroundError;
312
+ return asyncTarget;
313
+ }
314
+ if (foregroundError && !isAsyncRunNotFound(asyncError)) throw foregroundError;
315
+ if (foregroundError) throw foregroundError;
316
+ if (asyncError) throw asyncError;
317
+ throw new Error("Run not found. Provide id or runId.");
318
+ }
319
+
209
320
  function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
210
321
  if (runId) {
211
322
  const direct = state.asyncJobs.get(runId);
@@ -296,9 +407,9 @@ async function resumeAsyncRun(input: {
296
407
  };
297
408
  }
298
409
 
299
- let target: ReturnType<typeof resolveAsyncResumeTarget>;
410
+ let target: ResumeSourceTarget;
300
411
  try {
301
- target = resolveAsyncResumeTarget(input.params);
412
+ target = resolveResumeTarget(input.params, input.deps.state);
302
413
  } catch (error) {
303
414
  const message = error instanceof Error ? error.message : String(error);
304
415
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -352,7 +463,7 @@ async function resumeAsyncRun(input: {
352
463
  const agentConfig = agents.find((agent) => agent.name === target.agent);
353
464
  if (!agentConfig) {
354
465
  return {
355
- content: [{ type: "text", text: `Unknown agent for async resume: ${target.agent}` }],
466
+ content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
356
467
  isError: true,
357
468
  details: { mode: "management", results: [] },
358
469
  };
@@ -390,8 +501,9 @@ async function resumeAsyncRun(input: {
390
501
 
391
502
  const revivedId = result.details.asyncId ?? runId;
392
503
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
504
+ const sourceLabel = target.source === "foreground" ? "foreground" : "async";
393
505
  const lines = [
394
- `Revived async subagent from ${target.runId}.`,
506
+ `Revived ${sourceLabel} subagent from ${target.runId}.`,
395
507
  `Revived run: ${revivedId}`,
396
508
  `Agent: ${target.agent}`,
397
509
  `Session: ${target.sessionFile}`,
@@ -836,6 +948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
836
948
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
837
949
  return executeAsyncChain(id, {
838
950
  chain,
951
+ task: params.task,
839
952
  agents,
840
953
  ctx: asyncCtx,
841
954
  availableModels,
@@ -972,6 +1085,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
972
1085
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
973
1086
  return executeAsyncChain(id, {
974
1087
  chain: asyncChain,
1088
+ task: params.task,
975
1089
  agents,
976
1090
  ctx: asyncCtx,
977
1091
  availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
@@ -992,8 +1106,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
992
1106
  });
993
1107
  }
994
1108
 
995
- const chainDetails = chainResult.details ? compactForegroundDetails(chainResult.details) : undefined;
996
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted)
1109
+ const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1110
+ if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1111
+ const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
997
1112
  ? await maybeBuildForegroundIntercomReceipt({
998
1113
  pi: deps.pi,
999
1114
  intercomBridge: data.intercomBridge,
@@ -1010,7 +1125,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1010
1125
  };
1011
1126
  }
1012
1127
 
1013
- return chainResult;
1128
+ return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
1014
1129
  }
1015
1130
 
1016
1131
  interface ForegroundParallelRunInput {
@@ -1376,17 +1491,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1376
1491
  currentSessionId: deps.state.currentSessionId!,
1377
1492
  currentModelProvider: ctx.model?.provider,
1378
1493
  };
1379
- const parallelTasks = tasks.map((t, i) => ({
1380
- agent: t.agent,
1381
- task: params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!,
1382
- cwd: t.cwd,
1383
- ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1384
- ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1385
- ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1386
- ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1387
- ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1388
- ...(behaviorOverrides[i]?.progress !== undefined ? { progress: behaviorOverrides[i]!.progress } : {}),
1389
- }));
1494
+ const parallelTasks = tasks.map((t, i) => {
1495
+ const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
1496
+ const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
1497
+ return {
1498
+ agent: t.agent,
1499
+ task: taskText,
1500
+ cwd: t.cwd,
1501
+ ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1502
+ ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1503
+ ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1504
+ ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1505
+ ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1506
+ ...(progress !== undefined ? { progress } : {}),
1507
+ };
1508
+ });
1390
1509
  return executeAsyncChain(id, {
1391
1510
  chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
1392
1511
  resultMode: "parallel",
@@ -1411,7 +1530,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1411
1530
  }
1412
1531
  }
1413
1532
 
1414
- const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
1533
+ const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
1415
1534
  const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
1416
1535
  const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
1417
1536
  const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
@@ -1495,16 +1614,26 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1495
1614
  const interrupted = results.find((result) => result.interrupted);
1496
1615
  const details = compactForegroundDetails({
1497
1616
  mode: "parallel",
1617
+ runId,
1498
1618
  results,
1499
1619
  progress: params.includeProgress ? allProgress : undefined,
1500
1620
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1501
1621
  });
1622
+ rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1502
1623
  if (interrupted) {
1503
1624
  return {
1504
1625
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
1505
1626
  details,
1506
1627
  };
1507
1628
  }
1629
+ const detachedIndex = results.findIndex((result) => result.detached);
1630
+ const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
1631
+ if (detached) {
1632
+ return {
1633
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
1634
+ details,
1635
+ };
1636
+ }
1508
1637
 
1509
1638
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1510
1639
  pi: deps.pi,
@@ -1776,11 +1905,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1776
1905
  });
1777
1906
  const details = compactForegroundDetails({
1778
1907
  mode: "single",
1908
+ runId,
1779
1909
  results: [r],
1780
1910
  progress: params.includeProgress ? allProgress : undefined,
1781
1911
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1782
1912
  truncation: r.truncation,
1783
1913
  });
1914
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
1784
1915
 
1785
1916
  if (!r.detached && !r.interrupted) {
1786
1917
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
@@ -1801,7 +1932,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1801
1932
 
1802
1933
  if (r.detached) {
1803
1934
  return {
1804
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}` }],
1935
+ content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
1805
1936
  details,
1806
1937
  };
1807
1938
  }
@@ -1842,6 +1973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1842
1973
  ctx: ExtensionContext,
1843
1974
  ): Promise<AgentToolResult<Details>> => {
1844
1975
  deps.state.baseCwd = ctx.cwd;
1976
+ deps.state.foregroundRuns ??= new Map();
1845
1977
  deps.state.foregroundControls ??= new Map();
1846
1978
  deps.state.lastForegroundControlId ??= null;
1847
1979
  const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
@@ -69,6 +69,7 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
69
69
  const stat = fs.statSync(outputPath);
70
70
  return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
71
71
  } catch {
72
+ // The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
72
73
  return { exists: false };
73
74
  }
74
75
  }
@@ -94,18 +95,32 @@ export function resolveSingleOutput(
94
95
  ): { fullOutput: string; savedPath?: string; saveError?: string } {
95
96
  if (!outputPath) return { fullOutput: fallbackOutput };
96
97
 
98
+ let changedSinceStart = false;
97
99
  try {
98
100
  const stat = fs.statSync(outputPath);
99
- const changedSinceStart = !beforeRun?.exists
101
+ changedSinceStart = !beforeRun?.exists
100
102
  || stat.mtimeMs !== beforeRun.mtimeMs
101
103
  || stat.size !== beforeRun.size;
102
- if (changedSinceStart) {
104
+ } catch (error) {
105
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
106
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
103
107
  return {
104
- fullOutput: fs.readFileSync(outputPath, "utf-8"),
105
- savedPath: outputPath,
108
+ fullOutput: fallbackOutput,
109
+ saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
106
110
  };
107
111
  }
108
- } catch {}
112
+ }
113
+
114
+ if (changedSinceStart) {
115
+ try {
116
+ return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
117
+ } catch (error) {
118
+ return {
119
+ fullOutput: fallbackOutput,
120
+ saveError: `Failed to read changed output file: ${error instanceof Error ? error.message : String(error)}`,
121
+ };
122
+ }
123
+ }
109
124
 
110
125
  const save = persistSingleOutput(outputPath, fallbackOutput);
111
126
  if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
@@ -132,7 +147,7 @@ export function finalizeSingleOutput(params: {
132
147
  return { displayOutput, savedPath: params.savedPath, outputReference };
133
148
  }
134
149
  if (params.exitCode === 0 && params.saveError && params.outputPath) {
135
- displayOutput += `\n\nFailed to save output to: ${params.outputPath}\n${params.saveError}`;
150
+ displayOutput += `\n\nOutput file error: ${params.outputPath}\n${params.saveError}`;
136
151
  return { displayOutput, saveError: params.saveError };
137
152
  }
138
153
  return { displayOutput };
@@ -220,6 +220,25 @@ export function resolveStepBehavior(
220
220
  return { output, outputMode, reads, progress, skills, model };
221
221
  }
222
222
 
223
+ export function resolveTaskTextForFileUpdatePolicy(task: string | undefined, originalTask?: string): string | undefined {
224
+ if (!task) return originalTask;
225
+ return originalTask ? task.replaceAll("{task}", originalTask) : task;
226
+ }
227
+
228
+ export function taskDisallowsFileUpdates(task: string | undefined): boolean {
229
+ if (!task) return false;
230
+ return /\breview[- ]only\b/i.test(task)
231
+ || /\bread[- ]only\s+(?:review|audit|inspection|pass)\b/i.test(task)
232
+ || /\b(?:no|without)\s+(?:file\s+)?edits?\b/i.test(task)
233
+ || /\b(?:do not|don't|must not)\s+(?:edit|modify|write|touch)\b/i.test(task)
234
+ || /\bleave\s+files?\s+unchanged\b/i.test(task);
235
+ }
236
+
237
+ export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior, task: string | undefined, originalTask?: string): ResolvedStepBehavior {
238
+ const policyTask = resolveTaskTextForFileUpdatePolicy(task, originalTask);
239
+ return behavior.progress && taskDisallowsFileUpdates(policyTask) ? { ...behavior, progress: false } : behavior;
240
+ }
241
+
223
242
  // =============================================================================
224
243
  // Chain Instruction Injection
225
244
  // =============================================================================
@@ -0,0 +1,49 @@
1
+ import type { ActivityState, AsyncJobStep } from "./types.ts";
2
+
3
+ type StepStatusLike = Pick<AsyncJobStep, "status">;
4
+
5
+ function formatActivityAge(ms: number): string {
6
+ if (ms < 1000) return "now";
7
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
8
+ return `${Math.floor(ms / 60000)}m`;
9
+ }
10
+
11
+ export function formatActivityLabel(lastActivityAt: number | undefined, activityState?: ActivityState, now = Date.now()): string | undefined {
12
+ if (lastActivityAt === undefined) {
13
+ if (activityState === "needs_attention") return "needs attention";
14
+ if (activityState === "active_long_running") return "active but long-running";
15
+ return undefined;
16
+ }
17
+ const age = formatActivityAge(Math.max(0, now - lastActivityAt));
18
+ if (activityState === "needs_attention") return `no activity for ${age}`;
19
+ if (activityState === "active_long_running") return `active but long-running · last activity ${age} ago`;
20
+ return age === "now" ? "active now" : `active ${age} ago`;
21
+ }
22
+
23
+ function isCompletedStepStatus(status: AsyncJobStep["status"]): boolean {
24
+ return status === "complete" || status === "completed";
25
+ }
26
+
27
+ export function aggregateStepStatus(steps: StepStatusLike[]): AsyncJobStep["status"] {
28
+ if (steps.some((step) => step.status === "running")) return "running";
29
+ if (steps.some((step) => step.status === "failed")) return "failed";
30
+ if (steps.some((step) => step.status === "paused")) return "paused";
31
+ if (steps.length > 0 && steps.every((step) => isCompletedStepStatus(step.status))) return "complete";
32
+ return "pending";
33
+ }
34
+
35
+ export function formatAgentRunningLabel(count: number): string {
36
+ return count === 1 ? "1 agent running" : `${count} agents running`;
37
+ }
38
+
39
+ export function formatParallelOutcome(steps: StepStatusLike[], total: number, options: { showRunning?: boolean } = {}): string {
40
+ const running = steps.filter((step) => step.status === "running").length;
41
+ const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
42
+ const failed = steps.filter((step) => step.status === "failed").length;
43
+ const paused = steps.filter((step) => step.status === "paused").length;
44
+ const parts = [`${done}/${total} done`];
45
+ if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
46
+ if (failed > 0) parts.push(`${failed} failed`);
47
+ if (paused > 0) parts.push(`${paused} paused`);
48
+ return parts.join(" · ");
49
+ }
@@ -207,6 +207,7 @@ export interface SingleResult {
207
207
 
208
208
  export interface Details {
209
209
  mode: SubagentRunMode | "management";
210
+ runId?: string;
210
211
  context?: "fresh" | "fork";
211
212
  results: SingleResult[];
212
213
  controlEvents?: ControlEvent[];
@@ -296,6 +297,7 @@ export interface AsyncStatus {
296
297
  steps?: Array<{
297
298
  agent: string;
298
299
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
300
+ sessionFile?: string;
299
301
  activityState?: ActivityState;
300
302
  lastActivityAt?: number;
301
303
  currentTool?: string;
@@ -360,10 +362,26 @@ export interface AsyncJobState {
360
362
  controlEventCursor?: number;
361
363
  }
362
364
 
365
+ export interface ForegroundResumeChild {
366
+ agent: string;
367
+ index: number;
368
+ sessionFile?: string;
369
+ status: SubagentResultStatus;
370
+ }
371
+
372
+ export interface ForegroundResumeRun {
373
+ runId: string;
374
+ mode: SubagentRunMode;
375
+ cwd: string;
376
+ updatedAt: number;
377
+ children: ForegroundResumeChild[];
378
+ }
379
+
363
380
  export interface SubagentState {
364
381
  baseCwd: string;
365
382
  currentSessionId: string | null;
366
383
  asyncJobs: Map<string, AsyncJobState>;
384
+ foregroundRuns?: Map<string, ForegroundResumeRun>;
367
385
  foregroundControls: Map<string, {
368
386
  runId: string;
369
387
  mode: SubagentRunMode;
@@ -476,10 +494,6 @@ interface TopLevelParallelConfig {
476
494
  concurrency?: number;
477
495
  }
478
496
 
479
- interface AgentManagerConfig {
480
- newShortcut?: string;
481
- }
482
-
483
497
  export interface ExtensionConfig {
484
498
  asyncByDefault?: boolean;
485
499
  forceTopLevelAsync?: boolean;
@@ -487,7 +501,6 @@ export interface ExtensionConfig {
487
501
  maxSubagentDepth?: number;
488
502
  control?: ControlConfig;
489
503
  parallel?: TopLevelParallelConfig;
490
- agentManager?: AgentManagerConfig;
491
504
  worktreeSetupHook?: string;
492
505
  worktreeSetupHookTimeoutMs?: number;
493
506
  intercomBridge?: IntercomBridgeConfig;
@@ -4,14 +4,9 @@ import * as path from "node:path";
4
4
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
  import { Key, matchesKey } from "@mariozechner/pi-tui";
6
6
  import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
- import { AgentManagerComponent, type ManagerResult } from "../manager-ui/agent-manager.ts";
8
- import { SubagentsStatusComponent } from "../tui/subagents-status.ts";
9
- import { discoverAvailableSkills } from "../agents/skills.ts";
10
7
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
11
- import { resolveCurrentSessionId } from "../shared/session-identity.ts";
12
8
  import { isParallelStep, type ChainStep } from "../shared/settings.ts";
13
9
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
14
- import { toModelInfo } from "../shared/model-info.ts";
15
10
  import {
16
11
  applySlashUpdate,
17
12
  buildSlashInitialResult,
@@ -25,7 +20,6 @@ import {
25
20
  SLASH_SUBAGENT_RESPONSE_EVENT,
26
21
  SLASH_SUBAGENT_STARTED_EVENT,
27
22
  SLASH_SUBAGENT_UPDATE_EVENT,
28
- type ExtensionConfig,
29
23
  type SingleResult,
30
24
  type SubagentState,
31
25
  } from "../shared/types.ts";
@@ -279,6 +273,7 @@ async function runSlashSubagent(
279
273
  ctx: ExtensionContext,
280
274
  params: SubagentParamsLike,
281
275
  ): Promise<void> {
276
+ if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
282
277
  const requestId = randomUUID();
283
278
  const initialDetails = buildSlashInitialResult(requestId, params);
284
279
  const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
@@ -327,49 +322,6 @@ async function runSlashSubagent(
327
322
  }
328
323
  }
329
324
 
330
- async function openAgentManager(
331
- pi: ExtensionAPI,
332
- ctx: ExtensionContext,
333
- config: ExtensionConfig = {},
334
- ): Promise<void> {
335
- const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
336
- const models = ctx.modelRegistry.getAvailable().map(toModelInfo);
337
- const skills = discoverAvailableSkills(ctx.cwd);
338
-
339
- const result = await ctx.ui.custom<ManagerResult>(
340
- (tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut, preferredModelProvider: ctx.model?.provider }),
341
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
342
- );
343
- if (!result) return;
344
-
345
- const launchOptions: SubagentParamsLike = {
346
- clarify: !result.skipClarify && !result.background,
347
- agentScope: "both",
348
- ...(result.fork ? { context: "fork" as const } : {}),
349
- ...(result.background ? { async: true } : {}),
350
- };
351
-
352
- if (result.action === "chain") {
353
- const chain = result.agents.map((name, i) => ({
354
- agent: name,
355
- ...(i === 0 ? { task: result.task } : {}),
356
- }));
357
- await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
358
- return;
359
- }
360
-
361
- if (result.action === "launch") {
362
- await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
363
- } else if (result.action === "launch-chain") {
364
- await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
365
- } else if (result.action === "parallel") {
366
- await runSlashSubagent(pi, ctx, {
367
- tasks: result.tasks,
368
- ...launchOptions,
369
- ...(result.worktree ? { worktree: true } : {}),
370
- });
371
- }
372
- }
373
325
 
374
326
  interface ParsedStep { name: string; config: InlineConfig; task?: string }
375
327
 
@@ -451,15 +403,7 @@ const parseAgentArgs = (
451
403
  export function registerSlashCommands(
452
404
  pi: ExtensionAPI,
453
405
  state: SubagentState,
454
- config: ExtensionConfig = {},
455
406
  ): void {
456
- pi.registerCommand("agents", {
457
- description: "Open the Agents Manager",
458
- handler: async (_args, ctx) => {
459
- await openAgentManager(pi, ctx, config);
460
- },
461
- });
462
-
463
407
  pi.registerCommand("run", {
464
408
  description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
465
409
  getArgumentCompletions: makeAgentCompletions(state, false),
@@ -566,18 +510,6 @@ export function registerSlashCommands(
566
510
  },
567
511
  });
568
512
 
569
- pi.registerCommand("subagents-status", {
570
- description: "Show active and recent async subagent runs",
571
- handler: async (_args, ctx) => {
572
- const sessionId = resolveCurrentSessionId(ctx.sessionManager);
573
- state.baseCwd = ctx.cwd;
574
- state.currentSessionId = sessionId;
575
- await ctx.ui.custom<void>(
576
- (tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
577
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
578
- );
579
- },
580
- });
581
513
 
582
514
  pi.registerCommand("subagents-doctor", {
583
515
  description: "Show subagent diagnostics",
@@ -586,9 +518,4 @@ export function registerSlashCommands(
586
518
  },
587
519
  });
588
520
 
589
- pi.registerShortcut("ctrl+shift+a", {
590
- handler: async (ctx) => {
591
- await openAgentManager(pi, ctx, config);
592
- },
593
- });
594
521
  }