pi-subagents 0.45.1 → 0.46.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 (48) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +342 -0
  4. package/docs/configuration.md +320 -0
  5. package/docs/extension-api.md +308 -0
  6. package/docs/missions.md +117 -0
  7. package/docs/models.md +190 -0
  8. package/docs/observability.md +174 -0
  9. package/docs/tool-reference.md +343 -0
  10. package/docs/watchdog.md +176 -0
  11. package/docs/workflows.md +163 -0
  12. package/package.json +4 -2
  13. package/skills/pi-subagents/references/execution-controls.md +2 -2
  14. package/src/agents/agents.ts +17 -8
  15. package/src/agents/frontmatter.ts +7 -3
  16. package/src/agents/skills.ts +2 -9
  17. package/src/api/project-panes.ts +30 -0
  18. package/src/extension/config.ts +15 -1
  19. package/src/extension/index.ts +36 -16
  20. package/src/extension/schemas.ts +3 -2
  21. package/src/extension/subagent-guide.ts +39 -0
  22. package/src/extension/tool-description.ts +4 -4
  23. package/src/inspectors/herdr/project-panes.ts +457 -62
  24. package/src/missions/actions.ts +25 -2
  25. package/src/missions/lifecycle.ts +21 -2
  26. package/src/missions/store.ts +77 -1
  27. package/src/missions/types.ts +33 -0
  28. package/src/runs/background/async-execution.ts +7 -1
  29. package/src/runs/background/completion-replay.ts +267 -0
  30. package/src/runs/background/result-watcher.ts +12 -4
  31. package/src/runs/background/resume-guidance.ts +33 -0
  32. package/src/runs/background/subagent-wait.ts +5 -2
  33. package/src/runs/background/wait-completions.ts +39 -5
  34. package/src/runs/background/wait-subscriptions.ts +21 -5
  35. package/src/runs/foreground/execution.ts +4 -0
  36. package/src/runs/foreground/foreground-history.ts +137 -0
  37. package/src/runs/foreground/subagent-executor.ts +326 -38
  38. package/src/runs/shared/subagent-prompt-runtime.ts +71 -5
  39. package/src/shared/fork-context.ts +13 -0
  40. package/src/shared/prompt-resources.ts +51 -0
  41. package/src/shared/types.ts +30 -1
  42. package/src/shared/utf8.ts +11 -0
  43. package/src/slash/prompt-workflows.ts +2 -15
  44. package/src/slash/slash-commands.ts +19 -1
  45. package/src/tui/fleet-status.ts +8 -2
  46. package/src/tui/fleet.ts +135 -25
  47. package/src/tui/render.ts +120 -7
  48. package/src/workflows/scripted-workflow.ts +187 -11
@@ -30,6 +30,7 @@ import { drainOutstandingWork } from "../background/auto-drain.ts";
30
30
  const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
31
31
  const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
32
32
  export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
33
+ const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
33
34
 
34
35
  const STRUCTURED_OUTPUT_INSTRUCTIONS = [
35
36
  "This subagent step has a strict structured output contract.",
@@ -330,7 +331,7 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
330
331
 
331
332
  export function registerSteeringInbox(
332
333
  pi: ExtensionAPI,
333
- deps: { watch?: typeof fs.watch; nativeRealpath?: (filePath: string) => string } = {},
334
+ deps: { watch?: typeof fs.watch; nativeRealpath?: (filePath: string) => string; legacySettleFallbackMs?: number } = {},
334
335
  ): void {
335
336
  const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
336
337
  if (!steerInbox) return;
@@ -343,11 +344,14 @@ export function registerSteeringInbox(
343
344
  let disposed = false;
344
345
  let agentRunning = false;
345
346
  let inTurn = false;
347
+ let awaitingSettlement = false;
346
348
  let flushing = false;
347
349
  let started = false;
348
350
  let canSteer = typeof sendUserMessage === "function";
349
351
  let watcher: fs.FSWatcher | undefined;
350
352
  let interval: NodeJS.Timeout | undefined;
353
+ let settleFallback: NodeJS.Timeout | undefined;
354
+ const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
351
355
  const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
352
356
  if (!ackDir || !Number.isInteger(childIndex) || childIndex < 0) return;
353
357
  writeSteerAckAt(steerAckPathFromDir(ackDir, request.id), {
@@ -375,7 +379,8 @@ export function registerSteeringInbox(
375
379
  continue;
376
380
  }
377
381
  const requestedMode = request.mode ?? "steer";
378
- const delivery = requestedMode === "follow_up" || (requestedMode === "auto" && inTurn) ? "followUp" as const : "steer" as const;
382
+ const autoCanUseIdle = requestedMode === "auto" && !agentRunning && !awaitingSettlement;
383
+ const delivery = requestedMode === "follow_up" || (requestedMode === "auto" && (inTurn || awaitingSettlement)) ? "followUp" as const : "steer" as const;
379
384
  const pendingFollowUps = [...pending.values()].reduce((count, entries) => count + entries.filter((entry) => entry.deliveryStatus === "queued").length, 0);
380
385
  if (delivery === "followUp" && queued.length + pendingFollowUps >= MAX_STEER_QUEUE_SIZE) {
381
386
  acknowledge(request, "failed", `Follow-up queue is full (${MAX_STEER_QUEUE_SIZE} messages).`);
@@ -386,7 +391,7 @@ export function registerSteeringInbox(
386
391
  entries.push({ request, deliveryStatus: delivery === "followUp" ? "queued" : "delivered" });
387
392
  pending.set(formatted, entries);
388
393
  try {
389
- sendUserMessage(formatted, requestedMode === "auto" && !agentRunning ? undefined : { deliverAs: delivery });
394
+ sendUserMessage(formatted, autoCanUseIdle ? undefined : { deliverAs: delivery });
390
395
  } catch (error) {
391
396
  entries.pop();
392
397
  if (entries.length === 0) pending.delete(formatted);
@@ -440,14 +445,71 @@ export function registerSteeringInbox(
440
445
  flush();
441
446
  return undefined;
442
447
  };
448
+ const clearSettleFallback = (): void => {
449
+ if (!settleFallback) return;
450
+ clearTimeout(settleFallback);
451
+ settleFallback = undefined;
452
+ };
453
+ const markSettled = (): undefined => {
454
+ clearSettleFallback();
455
+ agentRunning = false;
456
+ inTurn = false;
457
+ awaitingSettlement = false;
458
+ return activate();
459
+ };
460
+ const armLegacySettleFallback = (): void => {
461
+ clearSettleFallback();
462
+ settleFallback = setTimeout(() => {
463
+ settleFallback = undefined;
464
+ if (disposed || !awaitingSettlement) return;
465
+ agentRunning = false;
466
+ inTurn = false;
467
+ awaitingSettlement = false;
468
+ activate();
469
+ }, legacySettleFallbackMs);
470
+ settleFallback.unref?.();
471
+ };
443
472
 
444
473
  const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: unknown) => unknown) => void;
445
474
  // Register input before the watcher so an accepted extension input cannot race request dispatch.
446
475
  onRuntimeEvent("input", onInput);
447
476
  onRuntimeEvent("session_start", () => start());
448
- onRuntimeEvent("agent_start", () => { agentRunning = true; return activate(); });
449
- onRuntimeEvent("agent_end", () => { agentRunning = false; inTurn = false; return activate(); });
477
+ onRuntimeEvent("agent_start", () => {
478
+ clearSettleFallback();
479
+ agentRunning = true;
480
+ awaitingSettlement = false;
481
+ return activate();
482
+ });
483
+ onRuntimeEvent("agent_end", (event) => {
484
+ inTurn = false;
485
+ if ((event as { willRetry?: unknown } | undefined)?.willRetry === true) {
486
+ clearSettleFallback();
487
+ agentRunning = true;
488
+ awaitingSettlement = true;
489
+ return activate();
490
+ }
491
+ agentRunning = true;
492
+ awaitingSettlement = true;
493
+ armLegacySettleFallback();
494
+ return activate();
495
+ });
496
+ onRuntimeEvent("agent_settled", markSettled);
497
+ onRuntimeEvent("session_compact", () => {
498
+ const unresolved = [...pending.values()].flat();
499
+ pending.clear();
500
+ for (const entry of unresolved) {
501
+ try {
502
+ writeSteerRequestToDir(steerInbox, { ...entry.request, mode: "follow_up" });
503
+ } catch (error) {
504
+ acknowledge(entry.request, "failed", `Could not retry steering after compaction: ${error instanceof Error ? error.message : String(error)}`);
505
+ }
506
+ }
507
+ return activate();
508
+ });
450
509
  onRuntimeEvent("turn_start", () => {
510
+ clearSettleFallback();
511
+ agentRunning = true;
512
+ awaitingSettlement = false;
451
513
  inTurn = true;
452
514
  const next = queued.findIndex((entry) => entry.ready);
453
515
  if (next >= 0) {
@@ -466,7 +528,11 @@ export function registerSteeringInbox(
466
528
  }
467
529
  onRuntimeEvent("session_shutdown", () => {
468
530
  for (const entry of queued) acknowledge(entry.request, "failed", "Run ended before queued follow-up delivery.", "queued");
531
+ for (const entries of pending.values()) {
532
+ for (const entry of entries) acknowledge(entry.request, "failed", "Run ended before Pi confirmed steering input delivery.");
533
+ }
469
534
  disposed = true;
535
+ clearSettleFallback();
470
536
  try { watcher?.close(); } catch {}
471
537
  if (interval) clearInterval(interval);
472
538
  });
@@ -9,6 +9,7 @@ type SubagentExecutionContext = "fresh" | "fork";
9
9
  interface BranchSessionEntry {
10
10
  type: string;
11
11
  id?: string;
12
+ cwd?: string;
12
13
  parentId?: string | null;
13
14
  timestamp?: string;
14
15
  message?: {
@@ -128,6 +129,18 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
128
129
  });
129
130
  }
130
131
 
132
+ /** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
133
+ export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
134
+ const entries = readSessionEntries(sessionFile);
135
+ const header = entries[0];
136
+ if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
137
+ const resolvedCwd = path.resolve(cwd);
138
+ const effectiveCwd = fs.realpathSync.native(resolvedCwd);
139
+ if (header.cwd === effectiveCwd) return;
140
+ header.cwd = effectiveCwd;
141
+ fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
142
+ }
143
+
131
144
  export function createForkContextResolver(
132
145
  sessionManager: ForkableSessionManager,
133
146
  requestedContext: unknown,
@@ -0,0 +1,51 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseFrontmatter } from "../agents/frontmatter.ts";
5
+ import { getAgentDir, getProjectConfigDir } from "./utils.ts";
6
+
7
+ const PROMPT_REF_PATTERN = /^(package|user|project):([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
8
+ const PROMPT_VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
9
+
10
+ type PromptVariable = string | number | boolean;
11
+
12
+ export function getPromptDirectories(cwd: string) {
13
+ return {
14
+ package: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts"),
15
+ user: path.join(getAgentDir(), "prompts"),
16
+ project: path.join(getProjectConfigDir(cwd), "prompts"),
17
+ };
18
+ }
19
+
20
+ function promptVariables(vars: unknown): Record<string, PromptVariable> {
21
+ if (vars === undefined) return {};
22
+ if (!vars || typeof vars !== "object" || Array.isArray(vars)) throw new Error("prompts.render vars must be a plain object.");
23
+ const prototype = Object.getPrototypeOf(vars);
24
+ if (prototype !== null && prototype !== Object.prototype) throw new Error("prompts.render vars must be a plain object.");
25
+ for (const [name, value] of Object.entries(vars)) {
26
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
27
+ throw new Error(`prompts.render variable '${name}' must be a string, number, or boolean.`);
28
+ }
29
+ }
30
+ return vars as Record<string, PromptVariable>;
31
+ }
32
+
33
+ export function renderWorkflowPrompt(ref: string, vars: unknown, cwd: string): string {
34
+ const match = ref.match(PROMPT_REF_PATTERN);
35
+ if (!match) throw new Error("prompts.render ref must use package:<name>, user:<name>, or project:<name>.");
36
+ const scope = match[1] as keyof ReturnType<typeof getPromptDirectories>;
37
+ const name = match[2]!;
38
+ const filePath = path.join(getPromptDirectories(cwd)[scope], `${name}.md`);
39
+ let content: string;
40
+ try {
41
+ if (!fs.lstatSync(filePath).isFile()) throw new Error("not a regular file");
42
+ content = fs.readFileSync(filePath, "utf-8");
43
+ } catch (error) {
44
+ const detail = error instanceof Error ? error.message : String(error);
45
+ throw new Error(`Could not read prompt fragment '${ref}': ${detail}`);
46
+ }
47
+ const variables = promptVariables(vars);
48
+ return parseFrontmatter(content).body.trim().replace(PROMPT_VARIABLE_PATTERN, (placeholder, variable: string) => {
49
+ return Object.hasOwn(variables, variable) ? String(variables[variable]) : placeholder;
50
+ });
51
+ }
@@ -969,6 +969,8 @@ export interface WaitCompletion {
969
969
  mode?: string;
970
970
  state?: string;
971
971
  success?: boolean;
972
+ /** Versioned bounded output archive retained with the durable completion replay. */
973
+ archivePath?: string;
972
974
  results?: WaitCompletionChild[];
973
975
  }
974
976
 
@@ -1045,6 +1047,7 @@ export interface Details {
1045
1047
  operation: "run" | "status";
1046
1048
  key: string;
1047
1049
  state: "started" | "completed" | "failed" | "reused";
1050
+ agent?: string;
1048
1051
  runId?: string;
1049
1052
  phase?: string;
1050
1053
  label?: string;
@@ -1538,6 +1541,10 @@ export interface ForegroundChildControl {
1538
1541
 
1539
1542
  export interface ForegroundRunControl {
1540
1543
  runId: string;
1544
+ /** Workflow shell that owns this live foreground child, when applicable. */
1545
+ parentWorkflowRunId?: string;
1546
+ /** Stable workflow lane key for this live foreground child. */
1547
+ workflowKey?: string;
1541
1548
  /** Originating parent session; required for public fleet projection. */
1542
1549
  sessionId?: string;
1543
1550
  mode: SubagentRunMode;
@@ -1780,12 +1787,34 @@ export interface ScheduledRunsConfig {
1780
1787
 
1781
1788
  export type FleetViewPlacement = "aboveEditor" | "belowEditor";
1782
1789
 
1790
+ export const FLEET_KEYBINDING_ACTIONS = [
1791
+ "close",
1792
+ "scrollUp",
1793
+ "scrollDown",
1794
+ "selectUp",
1795
+ "selectDown",
1796
+ "selectFirst",
1797
+ "selectLast",
1798
+ "pageUp",
1799
+ "pageDown",
1800
+ "refresh",
1801
+ "steer",
1802
+ "inspect",
1803
+ "stop",
1804
+ "toggleTools",
1805
+ ] as const;
1806
+
1807
+ export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
1808
+ export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
1809
+
1783
1810
  export interface ExtensionConfig {
1784
1811
  asyncByDefault?: boolean;
1785
1812
  /** Show the Claude Code-style navigable fleet. Defaults to true. */
1786
1813
  fleetView?: boolean;
1787
1814
  /** Place the persistent FleetView above or below the editor. Defaults to belowEditor. */
1788
1815
  fleetViewPlacement?: FleetViewPlacement;
1816
+ /** Local keybindings for the full Fleet inspector. */
1817
+ fleetKeybindings?: FleetKeybindingsConfig;
1789
1818
  /** Show the under-editor async runs widget. Defaults to true, including when FleetView is enabled. */
1790
1819
  asyncWidget?: boolean;
1791
1820
  /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
@@ -1922,7 +1951,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
1922
1951
  export const POLL_INTERVAL_MS = 250;
1923
1952
  export const MAX_WIDGET_JOBS = 4;
1924
1953
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
1925
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
1954
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
1926
1955
 
1927
1956
  export const DEFAULT_FORK_PREAMBLE =
1928
1957
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -0,0 +1,11 @@
1
+ export function decodeUtf8Tail(bytes: Buffer): string {
2
+ let start = 0;
3
+ while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
4
+ return bytes.subarray(start).toString("utf-8");
5
+ }
6
+
7
+ export function utf8Tail(value: string, maxBytes: number): { text: string; truncated: boolean } {
8
+ const bytes = Buffer.from(value, "utf-8");
9
+ if (bytes.length <= maxBytes) return { text: value, truncated: false };
10
+ return { text: decodeUtf8Tail(bytes.subarray(bytes.length - maxBytes)), truncated: true };
11
+ }
@@ -1,10 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
4
  import { parseFrontmatter } from "../agents/frontmatter.ts";
6
5
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
7
- import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
6
+ import { getPromptDirectories } from "../shared/prompt-resources.ts";
8
7
 
9
8
  interface PromptWorkflow {
10
9
  name: string;
@@ -32,21 +31,9 @@ const RESERVED_COMMAND_NAMES = new Set([
32
31
  "subagents-models",
33
32
  ]);
34
33
 
35
- function packagePromptsDir(): string {
36
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts");
37
- }
38
-
39
- function promptDirs(cwd: string): string[] {
40
- return [
41
- packagePromptsDir(),
42
- path.join(getAgentDir(), "prompts"),
43
- path.join(getProjectConfigDir(cwd), "prompts"),
44
- ];
45
- }
46
-
47
34
  function readPromptFiles(cwd: string): string[] {
48
35
  const files: string[] = [];
49
- for (const dir of promptDirs(cwd)) {
36
+ for (const dir of Object.values(getPromptDirectories(cwd))) {
50
37
  let entries: fs.Dirent[];
51
38
  try {
52
39
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -22,6 +22,7 @@ import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
22
22
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
23
23
  import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
24
24
  import { openSubagentsAdmin } from "./subagents-admin.ts";
25
+ import { SUBAGENT_GUIDE_TOPICS } from "../extension/subagent-guide.ts";
25
26
  import { openSubagentFleet } from "../tui/fleet.ts";
26
27
  import {
27
28
  applySlashUpdate,
@@ -40,6 +41,7 @@ import {
40
41
  SLASH_SUBAGENT_UPDATE_EVENT,
41
42
  DIRS,
42
43
  type Details,
44
+ type FleetKeybindingsConfig,
43
45
  type JsonSchemaObject,
44
46
  type SingleResult,
45
47
  type SubagentState,
@@ -628,6 +630,7 @@ function slashRunWorkflowScript(key: string, child: Record<string, unknown>): st
628
630
  export function registerSlashCommands(
629
631
  pi: ExtensionAPI,
630
632
  state: SubagentState,
633
+ options: { fleetKeybindings?: FleetKeybindingsConfig } = {},
631
634
  ): void {
632
635
  let fleetOpen = false;
633
636
  const showFleet = async (ctx: ExtensionContext) => {
@@ -642,7 +645,7 @@ export function registerSlashCommands(
642
645
  }
643
646
  fleetOpen = true;
644
647
  try {
645
- await openSubagentFleet(ctx, state, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
648
+ await openSubagentFleet(ctx, state, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: options.fleetKeybindings });
646
649
  } finally {
647
650
  fleetOpen = false;
648
651
  }
@@ -698,6 +701,21 @@ export function registerSlashCommands(
698
701
  },
699
702
  });
700
703
 
704
+ pi.registerCommand("subagents-guide", {
705
+ description: "Show a packaged subagents guide topic",
706
+ getArgumentCompletions: (prefix) => prefix.includes(" ") ? null : SUBAGENT_GUIDE_TOPICS
707
+ .filter((topic) => topic.startsWith(prefix))
708
+ .map((topic) => ({ value: topic, label: topic })),
709
+ handler: async (args, ctx) => {
710
+ const topic = args.trim();
711
+ if (topic.includes(" ")) {
712
+ ctx.ui.notify("Usage: /subagents-guide [topic]", "error");
713
+ return;
714
+ }
715
+ await runSlashSubagent(pi, ctx, { action: "guide", ...(topic ? { topic } : {}) });
716
+ },
717
+ });
718
+
701
719
  pi.registerCommand("subagents-refine", {
702
720
  description: "Generate a bounded project-local refinement overlay for one subagent",
703
721
  getArgumentCompletions: makeAgentCompletions(state),
@@ -144,6 +144,12 @@ function isStaleExtensionContextError(error: unknown): boolean {
144
144
  || error.message.includes("Extension context no longer active"));
145
145
  }
146
146
 
147
+ function foregroundDescription(control: { parentWorkflowRunId?: string; workflowKey?: string }, description: string | undefined): string | undefined {
148
+ if (!control.parentWorkflowRunId) return description;
149
+ const workflow = `workflow child: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}`;
150
+ return description ? `${workflow} · ${description}` : workflow;
151
+ }
152
+
147
153
  export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
148
154
  const entries: FleetStatusEntry[] = [];
149
155
  for (const control of state.foregroundControls.values()) {
@@ -154,7 +160,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
154
160
  key: `foreground-active:${control.runId}:${child.index}`,
155
161
  agent: child.agent,
156
162
  ...(modelThinking ? { modelThinking } : {}),
157
- description: child.description,
163
+ description: foregroundDescription(control, child.description),
158
164
  startedAt: child.startedAt,
159
165
  tokens: child.tokens ?? 0,
160
166
  state: "running",
@@ -170,7 +176,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
170
176
  key: `foreground-active:${control.runId}:${control.currentIndex ?? 0}`,
171
177
  agent: control.currentAgent ?? control.mode,
172
178
  ...(modelThinking ? { modelThinking } : {}),
173
- description: control.description,
179
+ description: foregroundDescription(control, control.description),
174
180
  startedAt: control.startedAt,
175
181
  tokens: control.tokens ?? 0,
176
182
  state: "running",