pi-subagents 0.46.0 → 0.47.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/agents/reviewer.md +3 -4
  3. package/docs/agents.md +1 -1
  4. package/docs/configuration.md +14 -6
  5. package/docs/extension-api.md +1 -1
  6. package/docs/missions.md +5 -3
  7. package/docs/models.md +3 -1
  8. package/docs/observability.md +3 -3
  9. package/docs/tool-reference.md +2 -2
  10. package/docs/watchdog.md +1 -1
  11. package/package.json +1 -1
  12. package/skills/pi-subagents/references/execution-controls.md +4 -4
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  14. package/src/extension/config.ts +13 -0
  15. package/src/extension/fanout-child.ts +5 -4
  16. package/src/extension/index.ts +39 -14
  17. package/src/extension/rpc.ts +7 -6
  18. package/src/extension/schemas.ts +27 -7
  19. package/src/extension/tool-description.ts +26 -8
  20. package/src/inspectors/herdr/project-panes.ts +2 -1
  21. package/src/intercom/native-supervisor-channel.ts +102 -4
  22. package/src/missions/store.ts +2 -1
  23. package/src/missions/workflow-state.ts +19 -13
  24. package/src/runs/background/active-run-index.ts +42 -0
  25. package/src/runs/background/async-execution.ts +10 -5
  26. package/src/runs/background/async-job-tracker.ts +293 -133
  27. package/src/runs/background/async-resume.ts +19 -3
  28. package/src/runs/background/async-status.ts +33 -5
  29. package/src/runs/background/control-channel.ts +91 -17
  30. package/src/runs/background/result-watcher.ts +102 -7
  31. package/src/runs/background/run-status.ts +18 -0
  32. package/src/runs/background/scheduled-runs.ts +25 -1
  33. package/src/runs/background/stale-run-reconciler.ts +12 -24
  34. package/src/runs/background/subagent-runner.ts +68 -10
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +3 -0
  38. package/src/runs/foreground/execution.ts +3 -0
  39. package/src/runs/foreground/subagent-executor.ts +129 -20
  40. package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
  41. package/src/runs/shared/dynamic-fanout.ts +1 -1
  42. package/src/runs/shared/model-fallback.ts +8 -4
  43. package/src/runs/shared/model-scope.ts +12 -2
  44. package/src/runs/shared/parallel-utils.ts +1 -0
  45. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  46. package/src/runs/shared/worktree.ts +3 -2
  47. package/src/shared/artifacts.ts +15 -15
  48. package/src/shared/display-text.ts +100 -0
  49. package/src/shared/file-coalescer.ts +9 -0
  50. package/src/shared/formatters.ts +4 -6
  51. package/src/shared/settings.ts +15 -2
  52. package/src/shared/types.ts +16 -2
  53. package/src/shared/utils.ts +43 -33
  54. package/src/slash/slash-commands.ts +3 -1
  55. package/src/tui/fleet-status.ts +14 -10
  56. package/src/tui/render.ts +30 -26
  57. package/src/watchdog/change-signature.ts +4 -3
  58. package/src/workflows/scripted-workflow.ts +4 -1
@@ -1290,6 +1290,8 @@ export interface AsyncStatus {
1290
1290
  context?: "fresh" | "fork" | "mixed";
1291
1291
  isNested?: boolean;
1292
1292
  state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
1293
+ /** Display-only dismissal marker for a reload-orphaned workflow. */
1294
+ displayDismissedAt?: number;
1293
1295
  error?: string;
1294
1296
  activityState?: ActivityState;
1295
1297
  lastActivityAt?: number;
@@ -1545,6 +1547,8 @@ export interface ForegroundRunControl {
1545
1547
  parentWorkflowRunId?: string;
1546
1548
  /** Stable workflow lane key for this live foreground child. */
1547
1549
  workflowKey?: string;
1550
+ /** Private control root used to steer a live workflow-owned child. */
1551
+ workflowSteeringDir?: string;
1548
1552
  /** Originating parent session; required for public fleet projection. */
1549
1553
  sessionId?: string;
1550
1554
  mode: SubagentRunMode;
@@ -1614,6 +1618,8 @@ export interface SubagentState {
1614
1618
  fleetJobs?: Map<string, AsyncJobState>;
1615
1619
  /** Suppress dynamic status widgets while the fleet overlay owns the viewport. */
1616
1620
  fleetInspectorOpen?: boolean;
1621
+ /** Temporarily suppress dynamic widgets while Pi compacts the session. */
1622
+ widgetsSuspended?: boolean;
1617
1623
  foregroundRuns?: Map<string, ForegroundResumeRun>;
1618
1624
  foregroundControls: Map<string, ForegroundRunControl>;
1619
1625
  lastForegroundControlId: string | null;
@@ -1680,6 +1686,10 @@ export interface RunSyncOptions {
1680
1686
  permissions?: import("../runs/shared/permissions.ts").PermissionConfig;
1681
1687
  /** Session id of the direct parent session for permission-system ask forwarding. */
1682
1688
  parentSessionId?: string;
1689
+ /** Private prompt-runtime steering transport for workflow-owned foreground children. */
1690
+ steerInboxDir?: string;
1691
+ steerCapabilityPath?: string;
1692
+ steerAckDir?: string;
1683
1693
  /** Resolved launch context for this child. */
1684
1694
  context?: "fresh" | "fork";
1685
1695
  cwd?: string;
@@ -1819,6 +1829,8 @@ export interface ExtensionConfig {
1819
1829
  asyncWidget?: boolean;
1820
1830
  /** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
1821
1831
  toolDescriptionMode?: ToolDescriptionMode;
1832
+ /** Include legacy append-step and checkpoint controls in the registered tool schema. Defaults to false. */
1833
+ legacyChainControls?: boolean;
1822
1834
  /** Inline chat rendering for the subagent tool. Defaults to rich. */
1823
1835
  inlineToolDisplay?: InlineToolDisplay;
1824
1836
  forceTopLevelAsync?: boolean;
@@ -1842,8 +1854,10 @@ export interface ExtensionConfig {
1842
1854
  worktreeSetupHook?: string;
1843
1855
  worktreeSetupHookTimeoutMs?: number;
1844
1856
  worktreeBaseDir?: string;
1845
- /** Where to store subagent artifact files. Defaults to "project" (cwd/.pi-subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
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. */
1846
1858
  artifactDir?: ArtifactDirPreference;
1859
+ /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
1860
+ artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
1847
1861
  intercomBridge?: IntercomBridgeConfig;
1848
1862
  proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
1849
1863
  scheduledRuns?: ScheduledRunsConfig;
@@ -1951,7 +1965,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
1951
1965
  export const POLL_INTERVAL_MS = 250;
1952
1966
  export const MAX_WIDGET_JOBS = 4;
1953
1967
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
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;
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;
1955
1969
 
1956
1970
  export const DEFAULT_FORK_PREAMBLE =
1957
1971
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -6,6 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
+ import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "./display-text.ts";
9
10
  import { formatToolCall } from "./formatters.ts";
10
11
  import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
11
12
 
@@ -101,6 +102,23 @@ export function getAgentDir(): string {
101
102
 
102
103
  const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
103
104
 
105
+ export function pruneStatusCacheForAsyncRoot(asyncDirRoot: string, runIds: Iterable<string>): number {
106
+ const root = path.resolve(asyncDirRoot);
107
+ const currentStatusPaths = new Set(
108
+ Array.from(runIds, (runId) => path.resolve(root, runId, "status.json")),
109
+ );
110
+ let removed = 0;
111
+ for (const statusPath of statusCache.keys()) {
112
+ const resolved = path.resolve(statusPath);
113
+ const relative = path.relative(root, resolved);
114
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative) && !currentStatusPaths.has(resolved)) {
115
+ statusCache.delete(statusPath);
116
+ removed++;
117
+ }
118
+ }
119
+ return removed;
120
+ }
121
+
104
122
  function getErrorMessage(error: unknown): string {
105
123
  return error instanceof Error ? error.message : String(error);
106
124
  }
@@ -127,7 +145,10 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
127
145
  try {
128
146
  stat = fs.statSync(statusPath);
129
147
  } catch (error) {
130
- if (isNotFoundError(error)) return null;
148
+ if (isNotFoundError(error)) {
149
+ statusCache.delete(statusPath);
150
+ return null;
151
+ }
131
152
  throw new Error(`Failed to inspect async status file '${statusPath}': ${getErrorMessage(error)}`, {
132
153
  cause: error instanceof Error ? error : undefined,
133
154
  });
@@ -148,7 +169,10 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
148
169
  try {
149
170
  content = fs.readFileSync(statusPath, "utf-8");
150
171
  } catch (error) {
151
- if (isNotFoundError(error)) return null;
172
+ if (isNotFoundError(error)) {
173
+ statusCache.delete(statusPath);
174
+ return null;
175
+ }
152
176
  throw new Error(`Failed to read async status file '${statusPath}': ${getErrorMessage(error)}`, {
153
177
  cause: error instanceof Error ? error : undefined,
154
178
  });
@@ -170,10 +194,6 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
170
194
  ino: stat.ino,
171
195
  status,
172
196
  });
173
- if (statusCache.size > 50) {
174
- const firstKey = statusCache.keys().next().value;
175
- if (firstKey) statusCache.delete(firstKey);
176
- }
177
197
  return status;
178
198
  }
179
199
 
@@ -522,11 +542,8 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
522
542
  * Extract a preview of tool arguments for display
523
543
  */
524
544
  export function extractToolArgsPreview(args: Record<string, unknown>): string {
525
- const truncatePreview = (value: string, maxLength: number): string =>
526
- value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
527
-
528
545
  const stringifyPreviewValue = (value: unknown): string | undefined => {
529
- if (typeof value === "string" && value.trim().length > 0) return value;
546
+ if (typeof value === "string" && value.trim().length > 0) return sanitizeDisplayText(value);
530
547
  if (typeof value === "number" || typeof value === "boolean") return String(value);
531
548
  return undefined;
532
549
  };
@@ -539,39 +556,32 @@ export function extractToolArgsPreview(args: Record<string, unknown>): string {
539
556
  return `${first}${suffix}`;
540
557
  };
541
558
 
542
- // Handle MCP tool calls - show server/tool info
543
- if (args.tool && typeof args.tool === "string") {
544
- const server = args.server && typeof args.server === "string" ? `${args.server}/` : "";
545
- const toolArgs = args.args && typeof args.args === "string" ? ` ${args.args.slice(0, 40)}` : "";
546
- return `${server}${args.tool}${toolArgs}`;
559
+ if (typeof args.tool === "string") {
560
+ const server = typeof args.server === "string" ? `${sanitizeDisplayText(args.server)}/` : "";
561
+ const toolArgs = typeof args.args === "string" ? ` ${truncateDisplayText(sanitizeDisplayText(args.args), 40)}` : "";
562
+ return sanitizeDisplayText(`${server}${args.tool}${toolArgs}`);
547
563
  }
548
564
 
549
565
  const queriesPreview = previewArray(args.queries);
550
- if (queriesPreview) return truncatePreview(queriesPreview, 60);
551
- if (typeof args.query === "string" && args.query.trim().length > 0) return truncatePreview(args.query, 60);
552
- if (typeof args.workflow === "string" && args.workflow.trim().length > 0) return `workflow=${truncatePreview(args.workflow, 48)}`;
566
+ if (queriesPreview) return previewDisplayText(queriesPreview, 60);
567
+ if (typeof args.query === "string" && args.query.trim().length > 0) return previewDisplayText(args.query, 60);
568
+ if (typeof args.workflow === "string" && args.workflow.trim().length > 0) return `workflow=${previewDisplayText(args.workflow, 48)}`;
553
569
 
554
- if (typeof args.url === "string" && args.url.trim().length > 0) return truncatePreview(args.url, 60);
570
+ if (typeof args.url === "string" && args.url.trim().length > 0) return previewDisplayText(args.url, 60);
555
571
  const urlsPreview = previewArray(args.urls);
556
- if (urlsPreview) return truncatePreview(urlsPreview, 60);
557
- if (typeof args.prompt === "string" && args.prompt.trim().length > 0) return truncatePreview(args.prompt, 60);
558
-
572
+ if (urlsPreview) return previewDisplayText(urlsPreview, 60);
573
+ if (typeof args.prompt === "string" && args.prompt.trim().length > 0) return previewDisplayText(args.prompt, 60);
574
+
559
575
  const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task", "describe", "search"];
560
576
  for (const key of previewKeys) {
561
- if (args[key] && typeof args[key] === "string") {
562
- const value = args[key] as string;
563
- return truncatePreview(value, 60);
564
- }
577
+ if (typeof args[key] === "string") return previewDisplayText(args[key], 60);
565
578
  }
566
-
567
- // Fallback: show first string value found
579
+
568
580
  for (const [key, value] of Object.entries(args)) {
581
+ const displayKey = sanitizeDisplayText(key);
569
582
  const arrayPreview = previewArray(value);
570
- if (arrayPreview) return `${key}=${truncatePreview(arrayPreview, 50)}`;
571
- if (typeof value === "string" && value.length > 0) {
572
- const preview = truncatePreview(value, 50);
573
- return `${key}=${preview}`;
574
- }
583
+ if (arrayPreview) return `${displayKey}=${previewDisplayText(arrayPreview, 50)}`;
584
+ if (typeof value === "string" && value.length > 0) return `${displayKey}=${previewDisplayText(value, 50)}`;
575
585
  }
576
586
  return "";
577
587
  }
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
6
6
  import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
7
+ import { resolveExistingReadPaths } from "../shared/settings.ts";
7
8
  import {
8
9
  DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
9
10
  applySubagentProfile,
@@ -675,7 +676,8 @@ export function registerSlashCommands(
675
676
 
676
677
  let finalTask = task;
677
678
  if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
678
- finalTask = `[Read from: ${inline.reads.join(", ")}]\n\n${finalTask}`;
679
+ const existingReads = inline.reads.filter((read) => resolveExistingReadPaths([read], state.baseCwd).length > 0);
680
+ if (existingReads.length > 0) finalTask = `[Read from: ${existingReads.join(", ")}]\n\n${finalTask}`;
679
681
  }
680
682
  const child: Record<string, unknown> = { agent: agentName, task: finalTask, agentScope: "both" };
681
683
  if (inline.output !== undefined) child.output = inline.output;
@@ -307,26 +307,22 @@ export class SubagentFleetStatus {
307
307
  refresh(): void {
308
308
  const ctx = this.getActiveUiContext();
309
309
  if (!ctx) return;
310
+ if (this.state.widgetsSuspended) {
311
+ this.clearWidget();
312
+ return;
313
+ }
310
314
  this.entries = collectFleetStatusEntries(this.state);
311
315
  this.clampSelection();
312
316
  if (this.inspectorOpen || this.state.fleetInspectorOpen) {
313
317
  this.lastRenderKey = "";
314
- if (this.widgetRegistered) {
315
- ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
316
- this.widgetRegistered = false;
317
- this.tui = undefined;
318
- }
318
+ this.clearWidget();
319
319
  return;
320
320
  }
321
321
  if (this.entries.length === 0) {
322
322
  this.active = false;
323
323
  this.selectedKey = "main";
324
324
  this.lastRenderKey = "";
325
- if (this.widgetRegistered) {
326
- ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
327
- this.widgetRegistered = false;
328
- this.tui = undefined;
329
- }
325
+ this.clearWidget();
330
326
  return;
331
327
  }
332
328
 
@@ -356,6 +352,7 @@ export class SubagentFleetStatus {
356
352
  }
357
353
 
358
354
  handleKey(data: string): { consume?: boolean; data?: string } | undefined {
355
+ if (this.state.widgetsSuspended) return undefined;
359
356
  const ctx = this.getActiveUiContext();
360
357
  if (!ctx || this.entries.length === 0 || isKeyRelease(data)) return undefined;
361
358
  if (this.inspectorOpen) return undefined;
@@ -536,6 +533,13 @@ export class SubagentFleetStatus {
536
533
  }
537
534
  }
538
535
 
536
+ private clearWidget(): void {
537
+ if (!this.widgetRegistered) return;
538
+ this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
539
+ this.widgetRegistered = false;
540
+ this.tui = undefined;
541
+ }
542
+
539
543
  private clearUiRegistration(): void {
540
544
  if (this.timer) clearInterval(this.timer);
541
545
  this.timer = undefined;
package/src/tui/render.ts CHANGED
@@ -16,8 +16,10 @@ import {
16
16
  type NestedStepSummary,
17
17
  type WorkflowNodeStatus,
18
18
  MAX_WIDGET_JOBS,
19
+ POLL_INTERVAL_MS,
19
20
  WIDGET_KEY,
20
21
  } from "../shared/types.ts";
22
+ import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
21
23
  import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
22
24
  import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
23
25
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
@@ -320,6 +322,12 @@ function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStarte
320
322
  return progress.lastActivityAt;
321
323
  }
322
324
 
325
+ function renderToolArgsPreview(value: string, maxLength: number, expanded: boolean): string {
326
+ const normalized = sanitizeDisplayText(value);
327
+ if (expanded || normalized.length <= maxLength) return normalized;
328
+ return `${truncateDisplayText(normalized, maxLength)}...`;
329
+ }
330
+
323
331
  function formatCurrentToolLine(
324
332
  progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">,
325
333
  availableWidth: number,
@@ -329,9 +337,7 @@ function formatCurrentToolLine(
329
337
  if (!progress.currentTool) return undefined;
330
338
  const maxToolArgsLen = Math.max(50, availableWidth - 20);
331
339
  const toolArgsPreview = progress.currentToolArgs
332
- ? (expanded || progress.currentToolArgs.length <= maxToolArgsLen
333
- ? progress.currentToolArgs
334
- : `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
340
+ ? renderToolArgsPreview(progress.currentToolArgs, maxToolArgsLen, expanded)
335
341
  : "";
336
342
  const durationSuffix = progress.currentToolStartedAt !== undefined && snapshotNow !== undefined
337
343
  ? ` | ${formatDuration(Math.max(0, snapshotNow - progress.currentToolStartedAt))}`
@@ -622,7 +628,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
622
628
  }
623
629
 
624
630
 
625
- function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
631
+ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
626
632
  if (!job.steps?.length) return [];
627
633
  const total = job.chainStepCount ?? job.steps.length;
628
634
  const lines: string[] = [];
@@ -630,7 +636,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
630
636
  const steps = job.steps.slice(span.start, span.start + span.count);
631
637
  if (span.isParallel) {
632
638
  const status = aggregateStepStatus(steps);
633
- lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
639
+ lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), frame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
634
640
  continue;
635
641
  }
636
642
  const step = steps[0];
@@ -638,15 +644,15 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
638
644
  lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
639
645
  continue;
640
646
  }
641
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
647
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, frame));
642
648
  }
643
649
  return lines;
644
650
  }
645
651
 
646
- function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
652
+ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
647
653
  if (!job.steps?.length) return [];
648
654
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
649
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
655
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
650
656
  const total = job.stepsTotal ?? job.steps.length;
651
657
  const lines: string[] = [];
652
658
  for (const [index, step] of job.steps.entries()) {
@@ -654,7 +660,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
654
660
  const activity = widgetStepActivity(step, job.updatedAt);
655
661
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
656
662
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
657
- lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
663
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
658
664
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
659
665
  }
660
666
  return lines;
@@ -1147,7 +1153,7 @@ function foregroundStyleWidgetStepLines(
1147
1153
  if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
1148
1154
  for (const tool of step.recentTools?.slice(-3) ?? []) {
1149
1155
  const maxArgsLen = Math.max(40, width - 30);
1150
- const argsPreview = tool.args.length <= maxArgsLen ? tool.args : `${tool.args.slice(0, maxArgsLen)}...`;
1156
+ const argsPreview = renderToolArgsPreview(tool.args, maxArgsLen, expanded);
1151
1157
  lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
1152
1158
  }
1153
1159
  for (const line of compactRecentOutputLines(step.recentOutput)) {
@@ -1163,7 +1169,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
1163
1169
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1164
1170
  ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
1165
1171
  ];
1166
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
1172
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
1167
1173
  const total = job.stepsTotal ?? job.steps.length;
1168
1174
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
1169
1175
  const lines: string[] = [];
@@ -1411,10 +1417,10 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
1411
1417
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
1412
1418
  }
1413
1419
 
1414
- function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
1420
+ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
1415
1421
  if (expanded) {
1416
1422
  resetWidgetLayoutSession();
1417
- return fitWidgetLineBudget(lines, theme, width, true);
1423
+ return fitWidgetLineBudget(buildLines(), theme, width, true);
1418
1424
  }
1419
1425
 
1420
1426
  const hasMatchingSession = widgetSessionMatches(expanded);
@@ -1432,6 +1438,7 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
1432
1438
  return rendered.lines;
1433
1439
  }
1434
1440
 
1441
+ const lines = buildLines();
1435
1442
  if (lines.length <= availableRows) {
1436
1443
  widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
1437
1444
  return fitWidgetLineBudget(lines, theme, width, false);
@@ -1453,12 +1460,13 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
1453
1460
  const container = new Container();
1454
1461
  container.render = (renderWidth: number): string[] => {
1455
1462
  const width = Math.max(0, renderWidth - 2);
1456
- const lines = expanded
1457
- ? buildWidgetLines(jobs, theme, width, true)
1463
+ const frame = Math.floor(Date.now() / POLL_INTERVAL_MS);
1464
+ const buildLines = (): string[] => expanded
1465
+ ? buildWidgetLines(jobs, theme, width, true, frame)
1458
1466
  : jobs.length === 1
1459
- ? compactSingleWidgetLines(jobs[0]!, theme, width)
1460
- : buildWidgetLines(jobs, theme, width, false);
1461
- return fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded).map((line) => paddedWidgetLine(line, renderWidth));
1467
+ ? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
1468
+ : buildWidgetLines(jobs, theme, width, false, frame);
1469
+ return fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
1462
1470
  };
1463
1471
  return container;
1464
1472
  };
@@ -1488,7 +1496,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1488
1496
  items.push([
1489
1497
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1490
1498
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1491
- ...widgetParallelAgentDetails(job, theme, expanded, width),
1499
+ ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1492
1500
  ]);
1493
1501
  slots--;
1494
1502
  }
@@ -1505,7 +1513,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1505
1513
  items.push([
1506
1514
  `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1507
1515
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1508
- ...widgetParallelAgentDetails(job, theme, expanded, width),
1516
+ ...widgetParallelAgentDetails(job, theme, expanded, width, frame),
1509
1517
  ]);
1510
1518
  slots--;
1511
1519
  }
@@ -1870,9 +1878,7 @@ export function renderSubagentResult(
1870
1878
  if (r.progress.recentTools?.length) {
1871
1879
  for (const t of r.progress.recentTools.slice(-3)) {
1872
1880
  const maxArgsLen = Math.max(40, w - 24);
1873
- const argsPreview = expanded || t.args.length <= maxArgsLen
1874
- ? t.args
1875
- : `${t.args.slice(0, maxArgsLen)}...`;
1881
+ const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
1876
1882
  c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
1877
1883
  }
1878
1884
  }
@@ -2103,9 +2109,7 @@ export function renderSubagentResult(
2103
2109
  if (rProg.recentTools?.length) {
2104
2110
  for (const t of rProg.recentTools.slice(-3)) {
2105
2111
  const maxArgsLen = Math.max(40, w - 30);
2106
- const argsPreview = expanded || t.args.length <= maxArgsLen
2107
- ? t.args
2108
- : `${t.args.slice(0, maxArgsLen)}...`;
2112
+ const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
2109
2113
  c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
2110
2114
  }
2111
2115
  }
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
5
6
 
6
- const IGNORED_CHANGE_PREFIXES = [".pi-subagents/", "tmp/", "node_modules/"];
7
- const IGNORED_CHANGE_PATHS = new Set([".pi-subagents", "tmp", "node_modules"]);
8
- const IGNORED_CHANGE_SEGMENTS = new Set([".git", ".pi-subagents", "node_modules"]);
7
+ const IGNORED_CHANGE_PREFIXES = [`${PROJECT_SUBAGENTS_RELATIVE_DIR}/`, "tmp/", "node_modules/"];
8
+ const IGNORED_CHANGE_PATHS = new Set([PROJECT_SUBAGENTS_RELATIVE_DIR, "tmp", "node_modules"]);
9
+ const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
9
10
 
10
11
  const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
11
12
  const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
@@ -469,10 +469,13 @@ function validateKey(value: unknown, owner = "runs.run"): string {
469
469
  return value;
470
470
  }
471
471
 
472
- function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label"> {
472
+ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
473
473
  return {
474
474
  ...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
475
475
  ...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
476
+ // Requested agent name, so a child is identifiable while it runs. Launch
477
+ // resolution overwrites this with the canonical name on the terminal entry.
478
+ ...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
476
479
  };
477
480
  }
478
481