pi-subagents 0.55.0 → 0.57.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 (97) hide show
  1. package/CHANGELOG.md +81 -3
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +111 -15
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +2 -2
  11. package/docs/models.md +6 -0
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +11 -1
  14. package/docs/workflows.md +20 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/references/execution-controls.md +1 -1
  17. package/src/agents/agent-management.ts +13 -5
  18. package/src/agents/agent-refinements.ts +4 -4
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +242 -49
  21. package/src/agents/builtin-names.ts +6 -0
  22. package/src/agents/runtime-agent-registry.ts +8 -2
  23. package/src/api/preflight.ts +22 -3
  24. package/src/extension/config.ts +70 -0
  25. package/src/extension/doctor.ts +3 -3
  26. package/src/extension/index.ts +18 -4
  27. package/src/extension/public-execution.ts +29 -13
  28. package/src/extension/rpc.ts +20 -5
  29. package/src/extension/schemas.ts +9 -3
  30. package/src/extension/tool-description.ts +11 -9
  31. package/src/inspectors/herdr/actions.ts +2 -1
  32. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  33. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  34. package/src/integrations/herdr-status.ts +46 -3
  35. package/src/runs/background/active-async-capacity.ts +77 -10
  36. package/src/runs/background/async-execution.ts +148 -27
  37. package/src/runs/background/async-job-tracker.ts +5 -0
  38. package/src/runs/background/async-resume.ts +3 -0
  39. package/src/runs/background/async-retention.ts +20 -3
  40. package/src/runs/background/async-status.ts +5 -0
  41. package/src/runs/background/chain-root-attachment.ts +15 -1
  42. package/src/runs/background/fleet-view.ts +16 -10
  43. package/src/runs/background/inspect-rpc.ts +8 -8
  44. package/src/runs/background/result-files.ts +27 -14
  45. package/src/runs/background/result-watcher.ts +9 -5
  46. package/src/runs/background/run-status.ts +33 -6
  47. package/src/runs/background/scheduled-runs.ts +7 -1
  48. package/src/runs/background/subagent-runner.ts +254 -49
  49. package/src/runs/background/wait-completions.ts +4 -0
  50. package/src/runs/foreground/execution.ts +107 -16
  51. package/src/runs/foreground/foreground-control.ts +6 -0
  52. package/src/runs/foreground/foreground-history.ts +25 -3
  53. package/src/runs/foreground/subagent-executor.ts +324 -84
  54. package/src/runs/foreground/workflow-detach-reconcile.ts +31 -7
  55. package/src/runs/shared/acceptance.ts +10 -5
  56. package/src/runs/shared/agent-contract.ts +1 -1
  57. package/src/runs/shared/child-protocol.ts +21 -7
  58. package/src/runs/shared/claude-code-adapter.ts +129 -0
  59. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  60. package/src/runs/shared/completion-guard.ts +3 -2
  61. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  62. package/src/runs/shared/dynamic-fanout.ts +1 -1
  63. package/src/runs/shared/extension-bindings.ts +78 -0
  64. package/src/runs/shared/external-cli-contract.ts +167 -0
  65. package/src/runs/shared/external-cli-preflight.ts +122 -0
  66. package/src/runs/shared/external-cli-runner.ts +349 -54
  67. package/src/runs/shared/fast-mode-extension.ts +10 -0
  68. package/src/runs/shared/model-exclusions.ts +59 -8
  69. package/src/runs/shared/model-fallback.ts +25 -8
  70. package/src/runs/shared/mutation-evidence.ts +150 -0
  71. package/src/runs/shared/nested-events.ts +3 -1
  72. package/src/runs/shared/nested-render.ts +2 -2
  73. package/src/runs/shared/parallel-utils.ts +3 -0
  74. package/src/runs/shared/pi-args.ts +47 -0
  75. package/src/runs/shared/process-signal.ts +13 -0
  76. package/src/runs/shared/run-history.ts +21 -1
  77. package/src/runs/shared/structured-output.ts +18 -4
  78. package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
  79. package/src/shared/fork-context.ts +21 -0
  80. package/src/shared/formatters.ts +7 -1
  81. package/src/shared/launch-contract.ts +6 -0
  82. package/src/shared/pruned-fork.ts +450 -0
  83. package/src/shared/session-file-trust.ts +19 -0
  84. package/src/shared/session-tokens.ts +14 -3
  85. package/src/shared/settings.ts +16 -3
  86. package/src/shared/shortcuts.ts +17 -0
  87. package/src/shared/types.ts +166 -9
  88. package/src/shared/workflow-child-permit.ts +116 -0
  89. package/src/slash/delegation-adapters.ts +0 -1
  90. package/src/slash/slash-commands.ts +8 -6
  91. package/src/tui/fleet-status.ts +27 -10
  92. package/src/tui/fleet-transcript.ts +11 -5
  93. package/src/tui/fleet.ts +28 -13
  94. package/src/tui/render.ts +47 -21
  95. package/src/workflows/scripted-workflow.ts +298 -30
  96. package/src/workflows/workflow-child-summary.ts +117 -0
  97. package/src/workflows/workflow-receipt.ts +119 -4
@@ -25,6 +25,7 @@ type FleetStatusEntry = {
25
25
  description?: string;
26
26
  startedAt: number;
27
27
  tokens: number;
28
+ window?: number;
28
29
  state: string;
29
30
  external?: true;
30
31
  projectPane?: HerdrProjectPaneSnapshot;
@@ -39,6 +40,7 @@ type FleetWorkflowRow = {
39
40
  activity?: string;
40
41
  startedAt?: number;
41
42
  tokens?: number;
43
+ window?: number;
42
44
  overflow?: number;
43
45
  };
44
46
 
@@ -72,12 +74,15 @@ export function formatFleetElapsed(ms: number): string {
72
74
  return `${Math.max(0, Math.round(ms / 1000))}s`;
73
75
  }
74
76
 
75
- export function formatFleetTokens(count: number): string {
76
- let compact: string;
77
- if (count >= 1_000_000) compact = `${(count / 1_000_000).toFixed(1)}M`;
78
- else if (count >= 1_000) compact = `${(count / 1_000).toFixed(1)}k`;
79
- else compact = `${Math.max(0, Math.round(count))}`;
80
- return `↓ ${compact} tokens`;
77
+ export function formatFleetTokens(count: number, window?: number): string {
78
+ const compact = (value: number): string => value >= 1_000_000
79
+ ? `${(value / 1_000_000).toFixed(1)}M`
80
+ : value >= 1_000
81
+ ? `${(value / 1_000).toFixed(1)}k`
82
+ : `${Math.max(0, Math.round(value))}`;
83
+ return window !== undefined
84
+ ? `↓ ${compact(window)} window · ${compact(count)} spent`
85
+ : `↓ ${compact(count)} tokens`;
81
86
  }
82
87
 
83
88
  function rightAlign(left: string, right: string, width: number): string {
@@ -134,6 +139,7 @@ function workflowFleetRows(steps: AsyncJobStep[] | undefined): FleetWorkflowRow[
134
139
  ...(activity ? { activity } : {}),
135
140
  ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
136
141
  ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
142
+ ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
137
143
  };
138
144
  });
139
145
  }
@@ -332,7 +338,8 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
332
338
  ...(modelThinking ? { modelThinking } : {}),
333
339
  description: foregroundDescription(control, child.description),
334
340
  startedAt: child.startedAt,
335
- tokens: child.tokens ?? 0,
341
+ tokens: child.tokens ?? 0,
342
+ ...(child.window !== undefined ? { window: child.window } : {}),
336
343
  state: "running",
337
344
  ...((control.nestedChildren?.filter((nested) => nested.parentStepIndex === child.index).length ?? 0) > 0
338
345
  ? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
@@ -350,6 +357,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
350
357
  description: foregroundDescription(control, control.description),
351
358
  startedAt: control.startedAt,
352
359
  tokens: control.tokens ?? 0,
360
+ ...(control.window !== undefined ? { window: control.window } : {}),
353
361
  state: "running",
354
362
  ...(control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
355
363
  });
@@ -367,6 +375,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
367
375
  description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
368
376
  startedAt,
369
377
  tokens: job.totalTokens?.total ?? 0,
378
+ ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
370
379
  state: job.status,
371
380
  ...(job.steps?.length ? { workflowRows: workflowFleetRows(job.steps) } : {}),
372
381
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
@@ -387,6 +396,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
387
396
  description: job.description,
388
397
  startedAt,
389
398
  tokens: job.totalTokens?.total ?? 0,
399
+ ...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
390
400
  state: job.status,
391
401
  ...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
392
402
  });
@@ -404,6 +414,9 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
404
414
  description: step.description ?? job.description,
405
415
  startedAt: step.startedAt ?? startedAt,
406
416
  tokens: step.tokens?.total ?? (steps.length === 1 ? job.totalTokens?.total ?? 0 : 0),
417
+ ...((step.tokens?.window ?? (steps.length === 1 ? job.totalTokens?.window : undefined)) !== undefined
418
+ ? { window: step.tokens?.window ?? job.totalTokens?.window }
419
+ : {}),
407
420
  state: step.status,
408
421
  ...((step.children?.length ?? 0) > 0
409
422
  ? { nestedChildren: step.children }
@@ -611,6 +624,10 @@ export class SubagentFleetStatus {
611
624
  const workEntries = this.entries.filter((entry) => !entry.surface);
612
625
  const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
613
626
  const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
627
+ const nativeEntries = workEntries.filter((entry) => !entry.external);
628
+ const window = nativeEntries.length > 0 && nativeEntries.every((entry) => entry.window !== undefined)
629
+ ? nativeEntries.reduce((total, entry) => total + entry.window!, 0)
630
+ : undefined;
614
631
  const capacity = this.state.activeAsyncCapacity;
615
632
  const hasNativeRows = workEntries.some((entry) => !entry.external);
616
633
  const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
@@ -621,7 +638,7 @@ export class SubagentFleetStatus {
621
638
  const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
622
639
  const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
623
640
  const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
624
- const detail = [showNativeSummary ? formatFleetTokens(tokens) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
641
+ const detail = [showNativeSummary ? formatFleetTokens(tokens, window) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
625
642
  return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
626
643
  }
627
644
  const roster = this.rosterKeys();
@@ -670,7 +687,7 @@ export class SubagentFleetStatus {
670
687
  const elapsed = Date.now() - entry.startedAt;
671
688
  const rightText = entry.projectPane
672
689
  ? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
673
- : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`;
690
+ : entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens, entry.window)}`;
674
691
  const right = theme.fg("dim", rightText);
675
692
  return rightAlign(left, right, width);
676
693
  }
@@ -695,7 +712,7 @@ export class SubagentFleetStatus {
695
712
  const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
696
713
  const details = [
697
714
  row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
698
- row.tokens !== undefined ? formatFleetTokens(row.tokens) : undefined,
715
+ row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
699
716
  ].filter(Boolean).join(" · ");
700
717
  return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
701
718
  }
@@ -3,6 +3,7 @@ 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
5
  import { safeTerminalText as safeDisplayText } from "../shared/display-text.ts";
6
+ import { isTrustedRecordedSessionFile } from "../shared/session-file-trust.ts";
6
7
 
7
8
  const DEFAULT_MAX_RECORDS = 240;
8
9
  const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
@@ -61,6 +62,8 @@ export interface FleetTranscript {
61
62
 
62
63
  interface FleetTranscriptReadOptions {
63
64
  trustedRoots: string[];
65
+ trustedFiles?: string[];
66
+ trustedFileRoot?: string;
64
67
  maxRecords?: number;
65
68
  maxBytes?: number;
66
69
  }
@@ -109,10 +112,13 @@ function isNotFoundError(error: unknown): boolean {
109
112
  return Boolean(objectValue(error)?.code === "ENOENT");
110
113
  }
111
114
 
112
- function validateTranscriptPath(filePath: string, trustedRoots: string[]): { resolvedPath?: string; warning?: string } {
113
- if (trustedRoots.length === 0) return { warning: `Transcript preview has no trusted root: ${filePath}` };
115
+ function validateTranscriptPath(filePath: string, trustedRoots: string[], trustedFiles: string[] = [], trustedFileRoot?: string): { resolvedPath?: string; warning?: string } {
116
+ if (trustedRoots.length === 0 && (!trustedFileRoot || trustedFiles.length === 0)) return { warning: `Transcript preview has no trusted root: ${filePath}` };
114
117
  const resolvedPath = path.resolve(filePath);
115
- if (!trustedRoots.some((root) => pathWithin(root, resolvedPath))) {
118
+ const recordedCandidate = trustedFileRoot
119
+ && pathWithin(trustedFileRoot, resolvedPath)
120
+ && trustedFiles.some((file) => path.resolve(file) === resolvedPath);
121
+ if (!trustedRoots.some((root) => pathWithin(root, resolvedPath)) && !recordedCandidate) {
116
122
  return { warning: `Transcript is outside trusted roots: ${filePath}` };
117
123
  }
118
124
  let stat: fs.Stats;
@@ -129,7 +135,7 @@ function validateTranscriptPath(filePath: string, trustedRoots: string[]): { res
129
135
  const realRoots = trustedRoots
130
136
  .filter((root) => fs.existsSync(root))
131
137
  .map((root) => fs.realpathSync(root));
132
- if (!realRoots.some((root) => pathWithin(root, realPath))) {
138
+ if (!realRoots.some((root) => pathWithin(root, realPath)) && !isTrustedRecordedSessionFile(realPath, trustedFiles, trustedFileRoot)) {
133
139
  return { warning: `Transcript resolves outside trusted roots: ${filePath}` };
134
140
  }
135
141
  return { resolvedPath: realPath };
@@ -335,7 +341,7 @@ function parseTranscriptLines(lines: string[], conversationStarted = false): { e
335
341
  }
336
342
 
337
343
  export function readFleetTranscript(filePath: string, options: FleetTranscriptReadOptions): FleetTranscript {
338
- const validated = validateTranscriptPath(filePath, options.trustedRoots);
344
+ const validated = validateTranscriptPath(filePath, options.trustedRoots, options.trustedFiles, options.trustedFileRoot);
339
345
  if (!validated.resolvedPath) {
340
346
  return { path: filePath, events: [], truncated: false, ...(validated.warning ? { warning: safeDisplayText(validated.warning) } : {}) };
341
347
  }
package/src/tui/fleet.ts CHANGED
@@ -5,7 +5,7 @@ import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-codi
5
5
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
6
6
  import { snapshotExternalRuns, type ExternalRun } from "../api/external-runs.ts";
7
7
  import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
8
- import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
8
+ import { formatDuration, formatModelThinking, formatTokens, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
9
9
  import { DIRS, type AsyncJobState, type AsyncJobStep, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
10
10
  import { decodeUtf8Tail } from "../shared/utf8.ts";
11
11
  import { readStatus } from "../shared/utils.ts";
@@ -403,7 +403,9 @@ function foregroundActiveDetail(item: Extract<FleetItem, { kind: "foreground-act
403
403
  live.currentTool ? `Current tool: ${live.currentTool}${live.currentPath ? ` · ${shortenPath(live.currentPath)}` : ""}` : undefined,
404
404
  live.turnCount !== undefined ? `Turns: ${live.turnCount}` : undefined,
405
405
  live.toolCount !== undefined ? `Tools: ${live.toolCount}` : undefined,
406
- live.tokens !== undefined ? `Tokens: ${formatTokens(live.tokens)}` : undefined,
406
+ live.tokens !== undefined
407
+ ? `Tokens: ${formatTokenUsage({ input: live.inputTokens ?? 0, output: live.outputTokens ?? 0, total: live.tokens, ...(live.window !== undefined ? { window: live.window } : {}), ...(live.windowPeak !== undefined ? { windowPeak: live.windowPeak } : {}) }, "tokens")}`
408
+ : undefined,
407
409
  promptAuditCount > 0 ? `Prompt audit: ${promptAuditCount} live · 3 views · p opens` : undefined,
408
410
  "",
409
411
  "Transcript",
@@ -533,7 +535,7 @@ function workflowProgressLines(steps: AsyncJobStep[] | undefined): string[] {
533
535
  }
534
536
  const activity = workflowStepActivity(row.step);
535
537
  const context = contextModeLabel(row.step.context);
536
- const details = [row.step.status, activity, context, row.step.tokens?.total !== undefined ? formatTokens(row.step.tokens.total) : undefined].filter(Boolean).join(" · ");
538
+ const details = [row.step.status, activity, context, row.step.tokens ? formatTokenUsage(row.step.tokens) : undefined].filter(Boolean).join(" · ");
537
539
  lines.push(` ${row.index + 1}. ${workflowStepLabel(row.step, row.index)}${details ? ` — ${details}` : ""}`);
538
540
  }
539
541
  return lines;
@@ -547,6 +549,8 @@ function asyncDetail(item: Extract<FleetItem, { kind: "async" }>, state: Subagen
547
549
  index: item.index,
548
550
  lines: TRANSCRIPT_LINES,
549
551
  sessionRoots: uniquePaths([...(state.trustedSessionRoots ?? []), trackedJob?.sessionRoot]),
552
+ trustedSessionFiles: [item.step?.sessionFile ?? item.run.sessionFile].filter((value): value is string => Boolean(value)),
553
+ trustedSessionFileRoot: state.trustedSessionFileRoot,
550
554
  }).split("\n");
551
555
  if (status.mode === "workflow" && item.index === undefined) {
552
556
  const progress = workflowProgressLines((status.steps ?? item.run.steps) as AsyncJobStep[]);
@@ -628,7 +632,7 @@ function fleetArtifactsRoot(state: SubagentState, cwd: string): string {
628
632
  );
629
633
  }
630
634
 
631
- function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[] } | undefined {
635
+ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string; trustedRoots: string[]; trustedFiles?: string[]; trustedFileRoot?: string } | undefined {
632
636
  if (item.kind === "external") return undefined;
633
637
  if (item.kind === "foreground-active") {
634
638
  const artifactsRoot = fleetArtifactsRoot(state, item.control.cwd ?? state.baseCwd);
@@ -651,10 +655,12 @@ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string
651
655
  };
652
656
  }
653
657
  const step = item.step ?? (item.run.steps.length === 1 ? item.run.steps[0] : undefined);
654
- if (!step?.transcriptPath) return undefined;
655
- const transcriptPath = path.isAbsolute(step.transcriptPath)
656
- ? step.transcriptPath
657
- : path.resolve(item.run.asyncDir, step.transcriptPath);
658
+ const recordedSessionFile = step?.sessionFile ?? item.run.sessionFile;
659
+ const recordedPath = step?.transcriptPath ?? recordedSessionFile;
660
+ if (!recordedPath) return undefined;
661
+ const transcriptPath = path.isAbsolute(recordedPath)
662
+ ? recordedPath
663
+ : path.resolve(item.run.asyncDir, recordedPath);
658
664
  const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
659
665
  return {
660
666
  path: transcriptPath,
@@ -664,6 +670,7 @@ function transcriptTarget(item: FleetItem, state: SubagentState): { path: string
664
670
  trackedJob?.cwd ? fleetArtifactsRoot(state, trackedJob.cwd) : undefined,
665
671
  item.run.sessionFile ? getArtifactsDir(item.run.sessionFile, item.run.cwd ?? state.baseCwd, state.artifactDirPreference) : undefined,
666
672
  ]),
673
+ ...(!step?.transcriptPath && recordedSessionFile ? { trustedFiles: [recordedSessionFile], trustedFileRoot: state.trustedSessionFileRoot } : {}),
667
674
  };
668
675
  }
669
676
 
@@ -687,23 +694,27 @@ function itemSource(item: FleetItem): string {
687
694
  function itemStats(item: FleetItem): string[] {
688
695
  let model: string | undefined;
689
696
  let tokens: number | undefined;
697
+ let tokenUsage: AsyncJobStep["tokens"];
690
698
  let tools: number | undefined;
691
699
  let durationMs: number | undefined;
692
700
  if (item.kind === "foreground-active") {
693
701
  const live = item.activeChild ?? item.control;
694
702
  model = formatModelThinking(live.model, live.thinking) || undefined;
695
703
  tokens = live.tokens;
704
+ if (tokens !== undefined) tokenUsage = { input: live.inputTokens ?? 0, output: live.outputTokens ?? 0, total: tokens, ...(live.window !== undefined ? { window: live.window } : {}), ...(live.windowPeak !== undefined ? { windowPeak: live.windowPeak } : {}) };
696
705
  tools = live.toolCount;
697
706
  durationMs = Math.max(0, Date.now() - live.startedAt);
698
707
  } else if (item.kind === "foreground-recent") {
699
708
  model = formatModelThinking(item.child.model, item.child.thinking) || undefined;
700
709
  tokens = item.child.tokens;
710
+ if (tokens !== undefined) tokenUsage = { input: 0, output: 0, total: tokens, ...(item.child.window !== undefined ? { window: item.child.window } : {}), ...(item.child.windowPeak !== undefined ? { windowPeak: item.child.windowPeak } : {}) };
701
711
  tools = item.child.toolCount;
702
712
  } else if (item.kind === "external") {
703
713
  durationMs = Math.max(0, externalElapsedEnd(item.run) - item.run.startedAt);
704
714
  } else {
705
715
  model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
706
- tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
716
+ tokenUsage = item.step?.tokens ?? (item.index === undefined ? item.run.totalTokens : undefined);
717
+ tokens = tokenUsage?.total;
707
718
  tools = item.step?.toolCount ?? (item.index === undefined ? item.run.toolCount : undefined);
708
719
  const terminalRun = item.state !== "queued" && item.state !== "running" && item.state !== "pending";
709
720
  const endTime = item.run.endedAt ?? (terminalRun ? item.run.lastUpdate : undefined) ?? Date.now();
@@ -711,7 +722,7 @@ function itemStats(item: FleetItem): string[] {
711
722
  }
712
723
  return [
713
724
  model,
714
- tokens !== undefined ? `${formatTokens(tokens)} tok` : undefined,
725
+ tokenUsage ? formatTokenUsage(tokenUsage) : tokens !== undefined ? `${formatTokens(tokens)} tok` : undefined,
715
726
  tools !== undefined ? `${tools} tool${tools === 1 ? "" : "s"}` : undefined,
716
727
  durationMs !== undefined ? formatDuration(durationMs) : undefined,
717
728
  ].filter((value): value is string => Boolean(value));
@@ -1201,8 +1212,8 @@ export class SubagentFleetComponent implements Component {
1201
1212
  });
1202
1213
  }
1203
1214
 
1204
- private renderedTranscript(target: { path: string; trustedRoots: string[] }, width: number): { transcript: FleetTranscript; body: string[] } {
1205
- const fingerprint = `${target.trustedRoots.join("\0")}|${transcriptFingerprint(target.path)}`;
1215
+ private renderedTranscript(target: { path: string; trustedRoots: string[]; trustedFiles?: string[]; trustedFileRoot?: string }, width: number): { transcript: FleetTranscript; body: string[] } {
1216
+ const fingerprint = `${target.trustedRoots.join("\0")}|${target.trustedFiles?.join("\0") ?? ""}|${target.trustedFileRoot ?? ""}|${transcriptFingerprint(target.path)}`;
1206
1217
  if (this.transcriptCache
1207
1218
  && this.transcriptCache.path === target.path
1208
1219
  && this.transcriptCache.fingerprint === fingerprint
@@ -1210,7 +1221,11 @@ export class SubagentFleetComponent implements Component {
1210
1221
  && this.transcriptCache.expandedTools === this.expandedTools) {
1211
1222
  return { transcript: this.transcriptCache.transcript, body: [...this.transcriptCache.body] };
1212
1223
  }
1213
- const transcript = readFleetTranscript(target.path, { trustedRoots: target.trustedRoots });
1224
+ const transcript = readFleetTranscript(target.path, {
1225
+ trustedRoots: target.trustedRoots,
1226
+ ...(target.trustedFiles ? { trustedFiles: target.trustedFiles } : {}),
1227
+ ...(target.trustedFileRoot ? { trustedFileRoot: target.trustedFileRoot } : {}),
1228
+ });
1214
1229
  const body = transcript.events.length > 0
1215
1230
  ? renderFleetTranscript(transcript, width, this.theme, this.markdownTheme, { expandedTools: this.expandedTools })
1216
1231
  : [];
package/src/tui/render.ts CHANGED
@@ -21,9 +21,10 @@ import {
21
21
  WIDGET_ANIMATION_INTERVAL_MS,
22
22
  WIDGET_KEY,
23
23
  } from "../shared/types.ts";
24
- import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
25
- import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
26
- import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
24
+ import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
25
+ import { FLEET_OPEN_SHORTCUT, formatShortcutLabel } from "../shared/shortcuts.ts";
26
+ import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
27
+ import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
27
28
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
28
29
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
29
30
  import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
@@ -69,23 +70,13 @@ function liveDetailKeyText(): string {
69
70
  return keyText("app.tools.expand");
70
71
  }
71
72
 
72
- function liveDetailHintText(): string {
73
- return `Press ${liveDetailKeyText()} for live detail`;
73
+ export function liveDetailHintText(): string {
74
+ return `Press ${liveDetailKeyText()} for live detail · ${formatShortcutLabel(FLEET_OPEN_SHORTCUT)} Fleet`;
74
75
  }
75
76
 
76
77
  function foregroundSingleHintText(shortcut?: string): string {
77
78
  if (!shortcut) return liveDetailHintText();
78
- const label = shortcut
79
- .split("+")
80
- .map((part) => {
81
- const normalized = part.trim().toLowerCase();
82
- if (normalized === "ctrl") return "Ctrl";
83
- if (normalized === "alt") return "Alt";
84
- if (normalized === "shift") return "Shift";
85
- if (normalized === "super") return "Super";
86
- return normalized.length === 1 ? normalized.toUpperCase() : part.trim();
87
- })
88
- .join("+");
79
+ const label = formatShortcutLabel(shortcut);
89
80
  return `${liveDetailHintText()} · ${label} to run in background`;
90
81
  }
91
82
 
@@ -273,6 +264,29 @@ function oneLine(text: string): string {
273
264
  return text.replace(ansiEscapePattern, "").replace(/\s+/g, " ").trim();
274
265
  }
275
266
 
267
+ const COMPACT_TASK_MAX_CHARS = 96;
268
+
269
+ export function compactTaskText(task: string | undefined, label?: string): string | undefined {
270
+ const source = label?.trim() || task?.trim();
271
+ if (!source || source === PROMPT_REDACTED) return undefined;
272
+ const normalized = oneLine(source);
273
+ if (!normalized) return undefined;
274
+ return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
275
+ }
276
+
277
+ function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
278
+ const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
279
+ const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
280
+ for (const node of nodes) {
281
+ if (node.flatIndex === flatIndex && node.label.trim()) return node.label;
282
+ const nested = node.children ? visit(node.children) : undefined;
283
+ if (nested) return nested;
284
+ }
285
+ return undefined;
286
+ };
287
+ return details.workflowGraph ? visit(details.workflowGraph.nodes) : undefined;
288
+ }
289
+
276
290
  function hasLiveOutputSignal(line: string): boolean {
277
291
  const clean = oneLine(line);
278
292
  return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
@@ -740,7 +754,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
740
754
  if (step.currentPath) facts.push(shortenPath(step.currentPath));
741
755
  if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
742
756
  if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
743
- if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
757
+ if (step.tokens?.total) facts.push(formatTokenUsage(step.tokens, "token"));
744
758
  const activity = buildLiveStatusLine(step, snapshotNow);
745
759
  if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
746
760
  if (activity) return activity;
@@ -780,7 +794,9 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
780
794
  const activity = widgetStepActivity(step, job.updatedAt);
781
795
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
782
796
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
783
- 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}` : ""}`)}`);
797
+ const label = compactTaskText(undefined, step.label);
798
+ const display = label ? `${label} (${step.agent})` : step.agent;
799
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${display} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
784
800
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
785
801
  }
786
802
  return lines;
@@ -1058,7 +1074,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
1058
1074
  parts.push(`steps ${stepsTotal}`);
1059
1075
  }
1060
1076
  if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
1061
- if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
1077
+ if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
1062
1078
  if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
1063
1079
  return statJoin(theme, parts);
1064
1080
  }
@@ -1067,7 +1083,7 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
1067
1083
  return statJoin(theme, [
1068
1084
  step.turnCount !== undefined ? `${step.turnCount} turns` : "",
1069
1085
  step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
1070
- step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
1086
+ step.tokens?.total ? formatTokenUsage(step.tokens, "token") : "",
1071
1087
  step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
1072
1088
  ]);
1073
1089
  }
@@ -1259,6 +1275,8 @@ function foregroundStyleWidgetStepLines(
1259
1275
  const stats = widgetStepStats(theme, step);
1260
1276
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1261
1277
  const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), frame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
1278
+ const task = compactTaskText(step.description, step.label);
1279
+ if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
1262
1280
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
1263
1281
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
1264
1282
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
@@ -1329,7 +1347,9 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1329
1347
  const stepStats = widgetStepStats(theme, step);
1330
1348
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
1331
1349
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1332
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1350
+ const task = compactTaskText(step.description, step.label);
1351
+ const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
1352
+ lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${contextModeBadge(theme, step.context)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${taskSuffix}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
1333
1353
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
1334
1354
  }
1335
1355
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
@@ -1703,6 +1723,8 @@ function renderSingleCompact(
1703
1723
  c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1704
1724
 
1705
1725
  if (isRunning && r.progress) {
1726
+ const task = compactTaskText(r.task);
1727
+ if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
1706
1728
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
1707
1729
  const activity = compactCurrentActivity(r.progress);
1708
1730
  c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
@@ -1872,6 +1894,10 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1872
1894
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1873
1895
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1874
1896
  c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
1897
+ if (rRunning || rPending) {
1898
+ const task = compactTaskText(r.task, workflowLabelForResult(d, i));
1899
+ if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
1900
+ }
1875
1901
  if (rRunning && rProg && "status" in rProg) {
1876
1902
  const liveProgress = rProg as AgentProgress;
1877
1903
  const activity = compactCurrentActivity(liveProgress);