pi-subagents 0.47.1 → 0.49.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 (66) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +1 -0
  4. package/docs/configuration.md +74 -7
  5. package/docs/missions.md +4 -2
  6. package/docs/observability.md +27 -3
  7. package/docs/tool-reference.md +1 -1
  8. package/package.json +1 -1
  9. package/src/agents/agents.ts +36 -15
  10. package/src/api/preflight.ts +1 -1
  11. package/src/extension/config.ts +26 -0
  12. package/src/extension/doctor.ts +40 -0
  13. package/src/extension/index.ts +39 -8
  14. package/src/extension/public-execution.ts +35 -4
  15. package/src/extension/rpc.ts +3 -9
  16. package/src/extension/schemas.ts +10 -9
  17. package/src/extension/tool-description.ts +10 -8
  18. package/src/inspectors/herdr/actions.ts +11 -2
  19. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  20. package/src/intercom/intercom-bridge.ts +7 -2
  21. package/src/missions/lifecycle.ts +4 -7
  22. package/src/missions/store.ts +12 -7
  23. package/src/missions/workflow-state.ts +6 -2
  24. package/src/runs/background/active-async-capacity.ts +431 -0
  25. package/src/runs/background/active-run-index.ts +9 -5
  26. package/src/runs/background/async-execution.ts +141 -65
  27. package/src/runs/background/async-job-tracker.ts +4 -0
  28. package/src/runs/background/async-resume.ts +18 -2
  29. package/src/runs/background/async-status.ts +11 -5
  30. package/src/runs/background/chain-append.ts +33 -15
  31. package/src/runs/background/fleet-view.ts +18 -5
  32. package/src/runs/background/owned-process-tree.ts +104 -0
  33. package/src/runs/background/process-terminal.ts +17 -3
  34. package/src/runs/background/resume-guidance.ts +27 -7
  35. package/src/runs/background/retained-children.ts +14 -6
  36. package/src/runs/background/run-status.ts +101 -5
  37. package/src/runs/background/stale-run-reconciler.ts +3 -3
  38. package/src/runs/background/subagent-runner.ts +60 -32
  39. package/src/runs/foreground/chain-execution.ts +37 -2
  40. package/src/runs/foreground/execution.ts +90 -19
  41. package/src/runs/foreground/foreground-control.ts +12 -0
  42. package/src/runs/foreground/prompt-audit.ts +172 -0
  43. package/src/runs/foreground/subagent-executor.ts +829 -203
  44. package/src/runs/shared/acceptance.ts +13 -4
  45. package/src/runs/shared/completion-guard.ts +17 -1
  46. package/src/runs/shared/llm-intent-arbiter.ts +302 -0
  47. package/src/runs/shared/parallel-utils.ts +2 -0
  48. package/src/runs/shared/pi-args.ts +44 -1
  49. package/src/runs/shared/run-fanout-budget.ts +280 -0
  50. package/src/runs/shared/single-output.ts +4 -2
  51. package/src/runs/shared/task-intent.ts +19 -3
  52. package/src/runs/shared/worktree.ts +17 -5
  53. package/src/shared/agent-stream-options.ts +5 -0
  54. package/src/shared/artifacts.ts +2 -6
  55. package/src/shared/display-text.ts +50 -0
  56. package/src/shared/node-executable.ts +21 -0
  57. package/src/shared/types.ts +115 -4
  58. package/src/shared/utils.ts +3 -1
  59. package/src/tui/fleet-status.ts +7 -5
  60. package/src/tui/fleet-transcript.ts +1 -48
  61. package/src/tui/fleet.ts +228 -13
  62. package/src/tui/render.ts +86 -39
  63. package/src/watchdog/permission-arbiter.ts +2 -1
  64. package/src/watchdog/review.ts +4 -3
  65. package/src/workflows/chat-progress.ts +8 -2
  66. package/src/workflows/scripted-workflow.ts +66 -10
@@ -328,6 +328,7 @@ export interface FileMutationEffect {
328
328
  expected: boolean;
329
329
  attempted: boolean;
330
330
  message?: string;
331
+ resolvedBy?: "llm-intent-arbiter";
331
332
  }
332
333
 
333
334
  export interface EffectsProjection {
@@ -343,6 +344,7 @@ export type ProcessTerminalReason =
343
344
  | "runner-candidate-missing"
344
345
  | "runner-instance-mismatch"
345
346
  | "writer-close-unverified"
347
+ | "process-tree-unverified"
346
348
  | "canonical-session-unavailable"
347
349
  | "canonical-session-lease-active"
348
350
  | "canonical-session-release-unverified"
@@ -357,6 +359,19 @@ export interface RunnerProcessInstanceExitV1 {
357
359
  signal: string | null;
358
360
  }
359
361
 
362
+ export type ProcessTreeTerminalV1 =
363
+ | {
364
+ state: "observed";
365
+ mechanism: "posix-process-group";
366
+ processGroupId: number;
367
+ verifiedAt: number;
368
+ }
369
+ | {
370
+ state: "unknown";
371
+ reason: "unsupported-platform" | "signal-failed" | "verification-failed";
372
+ diagnostic?: string;
373
+ };
374
+
360
375
  export interface PiWriterProcessInstanceExitV1 {
361
376
  processInstanceId: string;
362
377
  kind: "pi-writer";
@@ -364,6 +379,7 @@ export interface PiWriterProcessInstanceExitV1 {
364
379
  closeObservedAt: number;
365
380
  exitCode: number | null;
366
381
  signal: string | null;
382
+ processTree: ProcessTreeTerminalV1;
367
383
  }
368
384
 
369
385
  export type ProcessInstanceExitV1 = RunnerProcessInstanceExitV1 | PiWriterProcessInstanceExitV1;
@@ -462,9 +478,30 @@ export interface SteeringNotice {
462
478
  currentSessionId?: string;
463
479
  }
464
480
 
481
+ export interface RunFanoutBudgetDescriptor {
482
+ version: 1;
483
+ rootRunId: string;
484
+ directory: string;
485
+ limit: number;
486
+ parentPath?: string;
487
+ }
488
+
489
+ export interface RunFanoutBudgetSnapshot {
490
+ used: number;
491
+ limit: number;
492
+ remaining: number;
493
+ }
494
+
495
+ export interface RunFanoutRejection extends RunFanoutBudgetSnapshot {
496
+ code: "RUN_FANOUT_LIMIT";
497
+ path: string;
498
+ requested: number;
499
+ }
500
+
465
501
  export interface SteeringRecoveryDescriptor {
466
502
  version: 1;
467
503
  launchContractDigest?: string;
504
+ runFanoutBudget: RunFanoutBudgetDescriptor;
468
505
  sourceRunId: string;
469
506
  agentContract?: AgentContract;
470
507
  agent: string;
@@ -491,6 +528,8 @@ export interface SteeringRecoveryDescriptor {
491
528
  structuredOutputSchema?: JsonSchemaObject;
492
529
  acceptance?: AcceptanceInput;
493
530
  controlConfig?: ResolvedControlConfig;
531
+ /** Raw per-run bridge override. Omitted descriptors continue to use global config. */
532
+ intercomBridge?: IntercomBridgeConfig;
494
533
  absoluteDeadlineAt?: number;
495
534
  initialTurnBudget?: ResolvedTurnBudget;
496
535
  initialToolBudget?: ResolvedToolBudget;
@@ -1024,6 +1063,9 @@ export interface Details {
1024
1063
  // Aggregated cost across all agents in the run
1025
1064
  totalCost?: CostSummary;
1026
1065
  spawnBudget?: SpawnBudgetSnapshot;
1066
+ runFanoutBudget?: RunFanoutBudgetSnapshot;
1067
+ runFanoutRejection?: RunFanoutRejection;
1068
+ activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1027
1069
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1028
1070
  capabilityAudit?: SubagentCapabilityAudit;
1029
1071
  parallelHandoff?: ParallelHandoffReference;
@@ -1046,7 +1088,7 @@ export interface Details {
1046
1088
  trace: Array<{
1047
1089
  operation: "run" | "status";
1048
1090
  key: string;
1049
- state: "started" | "completed" | "failed" | "reused";
1091
+ state: "started" | "completed" | "failed" | "stopped" | "reused";
1050
1092
  agent?: string;
1051
1093
  runId?: string;
1052
1094
  phase?: string;
@@ -1219,6 +1261,8 @@ export interface AsyncStartedEvent {
1219
1261
  asyncDir?: string;
1220
1262
  /** Parent-resolved launch directory, used as a trusted artifact root while this session is live. */
1221
1263
  cwd?: string;
1264
+ /** Parent-resolved child session root, used only while this session owns the live run. */
1265
+ sessionRoot?: string;
1222
1266
  pid?: number;
1223
1267
  sessionId?: string;
1224
1268
  mode?: SubagentRunMode;
@@ -1323,6 +1367,8 @@ export interface AsyncStatus {
1323
1367
  workflowGraph?: WorkflowGraphSnapshot;
1324
1368
  checkpoint?: ChainCheckpointState;
1325
1369
  processTerminal?: ProcessTerminalV1;
1370
+ runFanoutBudget?: RunFanoutBudgetSnapshot;
1371
+ runFanoutBudgetDescriptor?: RunFanoutBudgetDescriptor;
1326
1372
  launchContractDigest?: string;
1327
1373
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1328
1374
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
@@ -1342,6 +1388,10 @@ export interface AsyncStatus {
1342
1388
  phase?: string;
1343
1389
  label?: string;
1344
1390
  workflowKey?: string;
1391
+ /** Child run identity for workflow capacity reconciliation. */
1392
+ runId?: string;
1393
+ /** True only when this workflow child owns a detached async runner. */
1394
+ async?: boolean;
1345
1395
  parentWorkflowRunId?: string;
1346
1396
  outputName?: string;
1347
1397
  structured?: boolean;
@@ -1416,6 +1466,8 @@ export interface AsyncJobState {
1416
1466
  asyncDir: string;
1417
1467
  /** Parent-resolved launch directory retained for trusted live artifact lookup. */
1418
1468
  cwd?: string;
1469
+ /** Parent-resolved child session root retained for trusted live transcript lookup. */
1470
+ sessionRoot?: string;
1419
1471
  status: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
1420
1472
  /** Short caller-facing task/goal shown in fleet surfaces when available. */
1421
1473
  description?: string;
@@ -1574,6 +1626,12 @@ export interface ForegroundRunControl {
1574
1626
  toolCount?: number;
1575
1627
  /** Independently tracked children for foreground parallel work and fleet inspection. */
1576
1628
  activeChildren?: Map<number, ForegroundChildControl>;
1629
+ /** Live Prompt Audit redo callback. It is current-session memory only. */
1630
+ promptAuditRedo?: (index: number, guidance: string) => Promise<{ text: string; isError?: boolean }>;
1631
+ /** Memory-only source run for Prompt Audit redo. */
1632
+ sourceRunId?: string;
1633
+ /** Memory-only replacement run started by Prompt Audit redo. */
1634
+ supersededByRunId?: string;
1577
1635
  /** Scheduling owners that may still launch another child. Removal is safe only at zero. */
1578
1636
  schedulingOwners?: number;
1579
1637
  nestedRoute?: NestedRouteInfo;
@@ -1593,6 +1651,12 @@ export interface WaitSubscriptionRecord {
1593
1651
  expiresAt: number;
1594
1652
  }
1595
1653
 
1654
+ export interface ActiveAsyncCapacitySnapshot {
1655
+ used: number;
1656
+ /** Zero means the opt-in cap is disabled. */
1657
+ limit: number;
1658
+ }
1659
+
1596
1660
  export interface SubagentState {
1597
1661
  baseCwd: string;
1598
1662
  currentSessionId: string | null;
@@ -1603,6 +1667,10 @@ export interface SubagentState {
1603
1667
  /** Runtime mission-store snapshot used by optional inspector context. */
1604
1668
  missionStoreConfig?: MissionStoreConfig;
1605
1669
  parentSessionFile?: string | null;
1670
+ /** Extension-owned roots trusted for child session transcript reads. */
1671
+ trustedSessionRoots?: string[];
1672
+ /** Live async session roots created by this parent executor, keyed by run id. */
1673
+ liveAsyncSessionRoots?: Map<string, string>;
1606
1674
  /** Last valid parent session model observed for this session; used when continuation contexts omit ctx.model. */
1607
1675
  lastParentModel?: { provider: string; id: string };
1608
1676
  subagentInProgress?: boolean;
@@ -1613,6 +1681,8 @@ export interface SubagentState {
1613
1681
  granted?: number;
1614
1682
  grantHistory?: SpawnBudgetGrant[];
1615
1683
  };
1684
+ /** Current-session top-level async capacity projection. */
1685
+ activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1616
1686
  asyncJobs: Map<string, AsyncJobState>;
1617
1687
  /** Current-session active and recent async runs for the native fleet inspector. */
1618
1688
  fleetJobs?: Map<string, AsyncJobState>;
@@ -1730,9 +1800,12 @@ export interface RunSyncOptions {
1730
1800
  /** Effective parent wait-tool setting propagated to the child runtime. */
1731
1801
  waitToolEnabled?: boolean;
1732
1802
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1803
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
1733
1804
  nestedRoute?: NestedRouteInfo;
1734
1805
  /** Override the agent's default model (format: "provider/id" or just "id") */
1735
1806
  modelOverride?: string;
1807
+ /** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
1808
+ llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
1736
1809
  /** Override the agent's default thinking level for this run */
1737
1810
  thinkingOverride?: AgentConfig["thinking"];
1738
1811
  /** Registry models available for heuristic bare-model resolution */
@@ -1756,6 +1829,8 @@ export interface RunSyncOptions {
1756
1829
  dynamic?: boolean;
1757
1830
  dynamicGroup?: boolean;
1758
1831
  };
1832
+ /** Private live callback for the exact child prompt after runtime acceptance injection. */
1833
+ onEffectivePrompt?: (prompt: string) => void;
1759
1834
  }
1760
1835
 
1761
1836
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -1817,6 +1892,13 @@ export const FLEET_KEYBINDING_ACTIONS = [
1817
1892
  export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
1818
1893
  export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
1819
1894
 
1895
+ export interface MainWindowRendererConfig {
1896
+ /** Unit of horizontal space in main chat subagent call/result rows. Omit to preserve current spacing. Set 0 for no extra padding. */
1897
+ horizontalSpacing?: number;
1898
+ /** Maximum collapsed rich-result rows. Expanded output is not capped. */
1899
+ compactResultMaxLines?: number;
1900
+ }
1901
+
1820
1902
  export interface ExtensionConfig {
1821
1903
  asyncByDefault?: boolean;
1822
1904
  /** Show the Claude Code-style navigable fleet. Defaults to true. */
@@ -1833,6 +1915,8 @@ export interface ExtensionConfig {
1833
1915
  legacyChainControls?: boolean;
1834
1916
  /** Inline chat rendering for the subagent tool. Defaults to rich. */
1835
1917
  inlineToolDisplay?: InlineToolDisplay;
1918
+ /** Density controls for the main chat subagent call/result renderer. */
1919
+ mainWindowRenderer?: MainWindowRendererConfig;
1836
1920
  forceTopLevelAsync?: boolean;
1837
1921
  waitTool?: WaitToolConfig;
1838
1922
  defaultSessionDir?: string;
@@ -1840,8 +1924,22 @@ export interface ExtensionConfig {
1840
1924
  maxSubagentDepth?: number;
1841
1925
  /** Optional cumulative session cap. Unset or 0 means unlimited. */
1842
1926
  maxSubagentSpawnsPerSession?: number;
1927
+ /** Cumulative logical-child cap for one top-level run tree. Defaults to 64. */
1928
+ maxSubagentSpawnsPerRun?: number;
1929
+ /** Optional active top-level async run cap per parent session. Unset or 0 means unlimited. */
1930
+ maxActiveAsyncRunsPerSession?: number;
1843
1931
  /** Global cap on simultaneously-running subagent tasks within a single run. Defaults to 20. */
1844
1932
  globalConcurrencyLimit?: number;
1933
+ /**
1934
+ * Global default runtime deadline in milliseconds. It replaces the built-in
1935
+ * 30-minute backstop for single, parallel, and chain launches (foreground, plus
1936
+ * plain single-agent async runs) when neither the call (`timeoutMs`/`maxRuntimeMs`)
1937
+ * nor the selected agent provides a timeout. Explicit call values and agent
1938
+ * frontmatter defaults still win. Composite async runs (chain/parallel/workflow)
1939
+ * stay unbounded at the top level by design — their children are bounded individually.
1940
+ * Must be a positive integer; invalid values are ignored.
1941
+ */
1942
+ timeoutMs?: number;
1845
1943
  control?: ControlConfig;
1846
1944
  completionBatch?: CompletionBatchConfig;
1847
1945
  turnBudget?: TurnBudgetConfig;
@@ -1854,7 +1952,7 @@ export interface ExtensionConfig {
1854
1952
  worktreeSetupHook?: string;
1855
1953
  worktreeSetupHookTimeoutMs?: number;
1856
1954
  worktreeBaseDir?: string;
1857
- /** Where to store subagent artifact files. Defaults to "project" (cwd/.pi/subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
1955
+ /** Where to store subagent artifact files. Defaults to "session" (the pi session directory, or OS temp when unavailable). Set to "project" for cwd/.pi/subagents. */
1858
1956
  artifactDir?: ArtifactDirPreference;
1859
1957
  /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
1860
1958
  artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
@@ -1878,7 +1976,7 @@ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
1878
1976
 
1879
1977
  export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
1880
1978
  enabled: true,
1881
- dir: "project",
1979
+ dir: "session",
1882
1980
  includeInput: true,
1883
1981
  includeOutput: true,
1884
1982
  includeJsonl: false,
@@ -1965,7 +2063,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
1965
2063
  export const POLL_INTERVAL_MS = 250;
1966
2064
  export const MAX_WIDGET_JOBS = 4;
1967
2065
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
1968
- 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", "dismiss", "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;
2066
+ 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", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "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;
1969
2067
 
1970
2068
  export const DEFAULT_FORK_PREAMBLE =
1971
2069
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -2057,6 +2155,19 @@ export function resolveMaxSubagentSpawnsPerSession(configMaxSpawns?: number): nu
2057
2155
  return configuredMaxSpawns === 0 ? undefined : configuredMaxSpawns;
2058
2156
  }
2059
2157
 
2158
+ export const DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN = 64;
2159
+
2160
+ export function normalizeMaxSubagentSpawnsPerRun(value: unknown): number | undefined {
2161
+ const normalized = normalizeNonNegativeInteger(value);
2162
+ return normalized !== undefined && normalized > 0 ? normalized : undefined;
2163
+ }
2164
+
2165
+ export function resolveMaxSubagentSpawnsPerRun(configMaxSpawns?: number): number {
2166
+ return normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN)
2167
+ ?? normalizeMaxSubagentSpawnsPerRun(configMaxSpawns)
2168
+ ?? DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN;
2169
+ }
2170
+
2060
2171
  // ============================================================================
2061
2172
  // Utility Functions
2062
2173
  // ============================================================================
@@ -17,6 +17,7 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Neste
17
17
  const DEFAULT_CONFIG_DIR_NAME = ".pi";
18
18
  const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
19
19
  export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
20
+ export const PROMPT_REDACTED = "[prompt redacted]";
20
21
 
21
22
  export function resolveWatchPath(
22
23
  watchPath: string,
@@ -354,7 +355,7 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
354
355
  agent: progress.agent,
355
356
  status: progress.status,
356
357
  activityState: progress.activityState,
357
- task: progress.task,
358
+ task: "[prompt redacted]",
358
359
  skills: progress.skills,
359
360
  toolCount: progress.toolCount,
360
361
  tokens: progress.tokens,
@@ -428,6 +429,7 @@ export function compactForegroundResult(result: SingleResult): SingleResult {
428
429
  const toolCalls = result.toolCalls?.length ? result.toolCalls : extractToolCallSummaries(result.messages);
429
430
  return {
430
431
  ...result,
432
+ task: "[prompt redacted]",
431
433
  messages: undefined,
432
434
  progress: undefined,
433
435
  toolCalls: toolCalls.length ? toolCalls : undefined,
@@ -318,7 +318,7 @@ export class SubagentFleetStatus {
318
318
  this.clearWidget();
319
319
  return;
320
320
  }
321
- if (this.entries.length === 0) {
321
+ if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) {
322
322
  this.active = false;
323
323
  this.selectedKey = "main";
324
324
  this.lastRenderKey = "";
@@ -413,10 +413,13 @@ export class SubagentFleetStatus {
413
413
  }
414
414
 
415
415
  render(width: number, theme: Theme): string[] {
416
- if (this.entries.length === 0) return [];
416
+ if (this.entries.length === 0 && !(this.state.activeAsyncCapacity?.used)) return [];
417
417
  if (!this.active) {
418
418
  const tokens = this.entries.reduce((total, entry) => total + entry.tokens, 0);
419
- const label = `${this.entries.length} active ${this.entries.length === 1 ? "agent" : "agents"}`;
419
+ const capacity = this.state.activeAsyncCapacity;
420
+ const asyncRuns = capacity ? `Async runs ${capacity.used}/${capacity.limit || "∞"}` : "";
421
+ const agents = this.entries.length > 0 ? `${this.entries.length} active ${this.entries.length === 1 ? "agent" : "agents"}` : "";
422
+ const label = [agents, asyncRuns].filter(Boolean).join(" · ");
420
423
  return [truncateToWidth(` ${theme.fg("muted", label)} · ${theme.fg("dim", `${formatFleetTokens(tokens)} · ↓/← to inspect`)}`, width)];
421
424
  }
422
425
  const roster = this.rosterKeys();
@@ -444,9 +447,8 @@ export class SubagentFleetStatus {
444
447
  }
445
448
 
446
449
  private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme): string {
447
- const description = entry.description?.replace(/\s+/g, " ").trim();
448
450
  const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
449
- const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${description ? ` ${description}` : ""}`;
451
+ const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}`;
450
452
  const elapsed = Date.now() - entry.startedAt;
451
453
  const right = theme.fg("dim", `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`);
452
454
  return rightAlign(left, right, width);
@@ -2,59 +2,12 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getLanguageFromPath, highlightCode, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi, type MarkdownTheme } from "@earendil-works/pi-tui";
5
+ import { safeTerminalText as safeDisplayText } from "../shared/display-text.ts";
5
6
 
6
7
  const DEFAULT_MAX_RECORDS = 240;
7
8
  const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
8
9
  const MAX_MESSAGE_CHARS = 64 * 1024;
9
10
  const TOOL_PREVIEW_LINES = 7;
10
- const BINARY_CONTENT_PLACEHOLDER = "[binary content omitted for safe display]";
11
-
12
- function isUnsafeDisplayCodePoint(codePoint: number): boolean {
13
- const terminalControl = (codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a)
14
- || (codePoint >= 0x7f && codePoint <= 0x9f);
15
- const bidiControl = codePoint === 0x061c
16
- || codePoint === 0x200e
17
- || codePoint === 0x200f
18
- || (codePoint >= 0x202a && codePoint <= 0x202e)
19
- || (codePoint >= 0x2066 && codePoint <= 0x2069);
20
- const privateUse = (codePoint >= 0xe000 && codePoint <= 0xf8ff)
21
- || (codePoint >= 0xf0000 && codePoint <= 0xffffd)
22
- || (codePoint >= 0x100000 && codePoint <= 0x10fffd);
23
- const invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;
24
- const nonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef)
25
- || (codePoint & 0xffff) === 0xfffe
26
- || (codePoint & 0xffff) === 0xffff;
27
- return terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;
28
- }
29
-
30
- function looksLikeBinaryContent(text: string): boolean {
31
- if (text.includes("\0")) return true;
32
- let suspiciousControls = 0;
33
- let replacementCharacters = 0;
34
- let codePoints = 0;
35
- for (const character of text) {
36
- codePoints++;
37
- const codePoint = character.codePointAt(0) ?? 0;
38
- if ((codePoint <= 0x08) || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;
39
- if (codePoint === 0xfffd) replacementCharacters++;
40
- }
41
- if (codePoints === 0) return false;
42
- return (suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1)
43
- || (replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1);
44
- }
45
-
46
- function safeDisplayText(text: string): string {
47
- const normalized = text.replace(/\r\n/g, "\n");
48
- if (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;
49
- let safe = "";
50
- for (const character of normalized) {
51
- const codePoint = character.codePointAt(0) ?? 0;
52
- safe += isUnsafeDisplayCodePoint(codePoint)
53
- ? `[U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}]`
54
- : character;
55
- }
56
- return safe;
57
- }
58
11
 
59
12
  function sanitizeJsonDisplayValue(value: unknown): { value: unknown; changed: boolean } {
60
13
  if (typeof value === "string") {