pi-subagents 0.56.0 → 0.58.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 (108) hide show
  1. package/CHANGELOG.md +92 -0
  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 +124 -21
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +41 -2
  11. package/docs/models.md +3 -3
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +14 -3
  14. package/docs/workflows.md +44 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/SKILL.md +2 -0
  17. package/skills/pi-subagents/references/execution-controls.md +2 -2
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  19. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  20. package/src/agents/agent-management.ts +36 -5
  21. package/src/agents/agent-refinements.ts +4 -4
  22. package/src/agents/agent-serializer.ts +5 -0
  23. package/src/agents/agents.ts +257 -51
  24. package/src/agents/builtin-names.ts +6 -0
  25. package/src/agents/runtime-agent-events.ts +70 -0
  26. package/src/agents/runtime-agent-registry.ts +18 -4
  27. package/src/api/agents.ts +10 -5
  28. package/src/api/preflight.ts +28 -3
  29. package/src/extension/config.ts +70 -0
  30. package/src/extension/doctor.ts +3 -3
  31. package/src/extension/index.ts +33 -8
  32. package/src/extension/public-execution.ts +29 -13
  33. package/src/extension/rpc.ts +55 -19
  34. package/src/extension/schemas.ts +6 -5
  35. package/src/extension/tool-description.ts +18 -12
  36. package/src/inspectors/herdr/actions.ts +2 -1
  37. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  38. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  39. package/src/integrations/herdr-status.ts +51 -3
  40. package/src/runs/background/active-async-capacity.ts +77 -10
  41. package/src/runs/background/async-execution.ts +127 -19
  42. package/src/runs/background/async-job-tracker.ts +5 -0
  43. package/src/runs/background/async-resume.ts +6 -2
  44. package/src/runs/background/async-retention.ts +20 -3
  45. package/src/runs/background/async-status.ts +7 -0
  46. package/src/runs/background/chain-append.ts +2 -0
  47. package/src/runs/background/chain-root-attachment.ts +15 -1
  48. package/src/runs/background/fleet-view.ts +16 -10
  49. package/src/runs/background/inspect-rpc.ts +8 -8
  50. package/src/runs/background/notify.ts +26 -3
  51. package/src/runs/background/result-delivery-ownership.ts +45 -0
  52. package/src/runs/background/result-files.ts +27 -14
  53. package/src/runs/background/result-watcher.ts +36 -15
  54. package/src/runs/background/run-status.ts +33 -6
  55. package/src/runs/background/scheduled-runs.ts +7 -1
  56. package/src/runs/background/subagent-runner.ts +239 -56
  57. package/src/runs/background/wait-completions.ts +4 -0
  58. package/src/runs/foreground/execution.ts +92 -11
  59. package/src/runs/foreground/foreground-control.ts +6 -0
  60. package/src/runs/foreground/foreground-history.ts +22 -1
  61. package/src/runs/foreground/subagent-executor.ts +322 -102
  62. package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
  63. package/src/runs/shared/child-protocol.ts +21 -7
  64. package/src/runs/shared/claude-code-adapter.ts +129 -0
  65. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  66. package/src/runs/shared/completion-guard.ts +4 -3
  67. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  68. package/src/runs/shared/dynamic-fanout.ts +3 -3
  69. package/src/runs/shared/external-cli-contract.ts +167 -0
  70. package/src/runs/shared/external-cli-preflight.ts +122 -0
  71. package/src/runs/shared/external-cli-runner.ts +348 -55
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/launch-cwd.ts +16 -0
  74. package/src/runs/shared/long-running-guard.ts +2 -1
  75. package/src/runs/shared/mcp-config-sources.ts +386 -0
  76. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  77. package/src/runs/shared/model-exclusions.ts +69 -7
  78. package/src/runs/shared/model-fallback.ts +39 -5
  79. package/src/runs/shared/mutation-evidence.ts +7 -2
  80. package/src/runs/shared/nested-events.ts +3 -1
  81. package/src/runs/shared/nested-render.ts +2 -2
  82. package/src/runs/shared/parallel-utils.ts +8 -1
  83. package/src/runs/shared/pi-args.ts +61 -6
  84. package/src/runs/shared/process-signal.ts +13 -0
  85. package/src/runs/shared/run-history.ts +21 -1
  86. package/src/runs/shared/single-output.ts +17 -0
  87. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  88. package/src/shared/fork-context.ts +21 -0
  89. package/src/shared/formatters.ts +13 -1
  90. package/src/shared/launch-contract.ts +4 -0
  91. package/src/shared/pruned-fork.ts +450 -0
  92. package/src/shared/session-file-trust.ts +19 -0
  93. package/src/shared/session-tokens.ts +14 -3
  94. package/src/shared/settings.ts +10 -2
  95. package/src/shared/shortcuts.ts +17 -0
  96. package/src/shared/types.ts +160 -10
  97. package/src/shared/utils.ts +6 -29
  98. package/src/shared/workflow-child-permit.ts +116 -0
  99. package/src/slash/delegation-adapters.ts +0 -1
  100. package/src/slash/slash-commands.ts +8 -6
  101. package/src/slash/subagents-admin.ts +3 -0
  102. package/src/tui/fleet-status.ts +27 -10
  103. package/src/tui/fleet-transcript.ts +11 -5
  104. package/src/tui/fleet.ts +28 -13
  105. package/src/tui/render.ts +55 -21
  106. package/src/workflows/scripted-workflow.ts +299 -31
  107. package/src/workflows/workflow-child-summary.ts +117 -0
  108. package/src/workflows/workflow-receipt.ts +155 -5
@@ -89,6 +89,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
89
89
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
90
90
  systemPromptMode: agent.systemPromptMode,
91
91
  inheritProjectContext: agent.inheritProjectContext,
92
+ inheritGlobalContext: agent.inheritGlobalContext,
92
93
  inheritSkills: agent.inheritSkills,
93
94
  ...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
94
95
  ...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
@@ -98,6 +99,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
98
99
  ...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
99
100
  ...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
100
101
  ...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
102
+ ...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
101
103
  ...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
102
104
  ...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
103
105
  };
@@ -189,6 +191,7 @@ function metadataFor(agent: AgentConfig): string {
189
191
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
190
192
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
191
193
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
194
+ lines.push(`Inherit global context: ${agent.inheritGlobalContext ? "true" : "false"}`);
192
195
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
193
196
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
194
197
  if (agent.output) lines.push(`Output: ${agent.output}`);
@@ -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 { formatContextUsage, 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,33 @@ 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 taskText = task?.trim();
271
+ const labelText = label?.trim();
272
+ const normalizedTask = taskText && taskText !== PROMPT_REDACTED ? oneLine(taskText) : "";
273
+ const normalizedLabel = labelText && labelText !== PROMPT_REDACTED ? oneLine(labelText) : "";
274
+ const normalized = normalizedLabel && normalizedTask && normalizedLabel !== normalizedTask
275
+ ? `${normalizedLabel} — ${normalizedTask}`
276
+ : normalizedLabel || normalizedTask;
277
+ if (!normalized) return undefined;
278
+ return previewDisplayText(normalized, COMPACT_TASK_MAX_CHARS);
279
+ }
280
+
281
+ function workflowLabelForResult(details: Details, resultIndex: number): string | undefined {
282
+ const flatIndex = details.results[resultIndex]?.progress?.index ?? resultIndex;
283
+ const visit = (nodes: NonNullable<Details["workflowGraph"]>["nodes"]): string | undefined => {
284
+ for (const node of nodes) {
285
+ if (node.flatIndex === flatIndex && node.label.trim()) return node.label;
286
+ const nested = node.children ? visit(node.children) : undefined;
287
+ if (nested) return nested;
288
+ }
289
+ return undefined;
290
+ };
291
+ return details.workflowGraph ? visit(details.workflowGraph.nodes) : undefined;
292
+ }
293
+
276
294
  function hasLiveOutputSignal(line: string): boolean {
277
295
  const clean = oneLine(line);
278
296
  return liveOutputWordSignalPattern.test(clean) || liveOutputCodeSignalPattern.test(clean);
@@ -740,7 +758,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
740
758
  if (step.currentPath) facts.push(shortenPath(step.currentPath));
741
759
  if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
742
760
  if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
743
- if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
761
+ if (step.tokens?.total) facts.push(formatTokenUsage(step.tokens, "token"));
744
762
  const activity = buildLiveStatusLine(step, snapshotNow);
745
763
  if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
746
764
  if (activity) return activity;
@@ -780,7 +798,9 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
780
798
  const activity = widgetStepActivity(step, job.updatedAt);
781
799
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
782
800
  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}` : ""}`)}`);
801
+ const label = compactTaskText(step.description, step.label);
802
+ const display = label ? `${label} (${step.agent})` : step.agent;
803
+ 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
804
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
785
805
  }
786
806
  return lines;
@@ -1058,7 +1078,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
1058
1078
  parts.push(`steps ${stepsTotal}`);
1059
1079
  }
1060
1080
  if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
1061
- if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
1081
+ if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
1062
1082
  if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
1063
1083
  return statJoin(theme, parts);
1064
1084
  }
@@ -1067,7 +1087,11 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
1067
1087
  return statJoin(theme, [
1068
1088
  step.turnCount !== undefined ? `${step.turnCount} turns` : "",
1069
1089
  step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
1070
- step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
1090
+ step.tokens
1091
+ ? step.contextLimit !== undefined
1092
+ ? formatContextUsage(step.tokens, step.contextLimit) ?? formatTokenUsage(step.tokens, "token")
1093
+ : step.tokens.total ? formatTokenUsage(step.tokens, "token") : ""
1094
+ : "",
1071
1095
  step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
1072
1096
  ]);
1073
1097
  }
@@ -1259,6 +1283,8 @@ function foregroundStyleWidgetStepLines(
1259
1283
  const stats = widgetStepStats(theme, step);
1260
1284
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1261
1285
  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}` : ""}`];
1286
+ const task = compactTaskText(step.description, step.label);
1287
+ if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
1262
1288
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
1263
1289
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
1264
1290
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 12 : 6)) {
@@ -1329,7 +1355,9 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
1329
1355
  const stepStats = widgetStepStats(theme, step);
1330
1356
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
1331
1357
  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}` : ""}`);
1358
+ const task = compactTaskText(step.description, step.label);
1359
+ const taskSuffix = task ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", `task: ${task}`)}` : "";
1360
+ 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
1361
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, 6)) lines.push(` ${nestedLine}`);
1334
1362
  }
1335
1363
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
@@ -1703,6 +1731,8 @@ function renderSingleCompact(
1703
1731
  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
1732
 
1705
1733
  if (isRunning && r.progress) {
1734
+ const task = compactTaskText(r.task);
1735
+ if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
1706
1736
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
1707
1737
  const activity = compactCurrentActivity(r.progress);
1708
1738
  c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
@@ -1872,6 +1902,10 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
1872
1902
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1873
1903
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1874
1904
  c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
1905
+ if (rRunning || rPending) {
1906
+ const task = compactTaskText(r.task, workflowLabelForResult(d, i));
1907
+ if (task) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}task: ${task}`), width), 0, 0));
1908
+ }
1875
1909
  if (rRunning && rProg && "status" in rProg) {
1876
1910
  const liveProgress = rProg as AgentProgress;
1877
1911
  const activity = compactCurrentActivity(liveProgress);