@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.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 (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
@@ -1,4 +1,4 @@
1
- import type { AgentToolResult, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { SubagentRunRequest, SubagentStatus } from "./contracts.js";
3
3
  type SubagentRenderArgs = Partial<SubagentRunRequest> & {
4
4
  id?: string;
@@ -37,13 +37,35 @@ export declare function renderSubagentResult(result: AgentToolResult<unknown>, o
37
37
  render(width: number): string[];
38
38
  invalidate(): void;
39
39
  };
40
- export declare function createSubagentBackgroundWidget(): {
40
+ type SubagentReceipt = {
41
+ readonly id: string;
42
+ readonly label: string;
43
+ readonly state: string;
44
+ readonly model?: string;
45
+ readonly role?: string;
46
+ readonly tools?: readonly string[];
47
+ readonly input: number;
48
+ readonly output: number;
49
+ readonly cacheRead: number;
50
+ readonly costUsd: number;
51
+ readonly durationMs: number;
52
+ readonly attempts: number;
53
+ readonly error?: string;
54
+ };
55
+ /** The transcript line a finished background run leaves behind, in the shape of the workflow receipt. */
56
+ export declare function renderSubagentReceipt(data: SubagentReceipt, expanded: boolean, theme: Theme): string[];
57
+ type ReceiptHost = {
58
+ appendEntry?: (customType: string, data: SubagentReceipt) => void;
59
+ registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"];
60
+ };
61
+ export declare function createSubagentBackgroundWidget(host?: ReceiptHost): {
41
62
  start(next: ExtensionContext): void;
42
63
  update(status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>): void;
43
64
  dispose(): void;
44
65
  };
45
66
  export declare function renderSubagentInspectCall(args: {
46
67
  id?: string;
68
+ scope?: string;
47
69
  }, theme: Theme): {
48
70
  render(width: number): string[];
49
71
  invalidate(): void;
@@ -1,16 +1,12 @@
1
1
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
- import { formatCost, formatStalledDuration, sanitizeDisplayText, WORKFLOW_AGENT_STALL_THRESHOLD_MS } from "../../src/index.js";
2
+ import { formatCost, formatStalledDuration, formatWorkflowRuntime, sanitizeDisplayText, WORKFLOW_AGENT_STALL_THRESHOLD_MS } from "../../src/index.js";
3
+ import { agentActivityLabel, stalledDuration, textBlock } from "../../src/host-view.js";
4
+ import { drawFrame, formatElapsed, formatTokens, mark, QUIET_MS, REPAINT_MS } from "../../src/background-widget.js";
3
5
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
6
  const TERMINAL_STATES = new Set(["completed", "failed", "stopped"]);
5
7
  const MAX_INSPECT_VALUE_CHARS = 4000;
6
8
  const MAX_INSPECT_TOOL_CALLS = 32;
7
9
  const MAX_BACKGROUND_WIDGET_ROWS = 10;
8
- function textBlock(text) {
9
- return {
10
- render(width) { return text.split("\n").map((line) => truncateToWidth(line, Math.max(1, width), "…")); },
11
- invalidate() { },
12
- };
13
- }
14
10
  function roleName(value) {
15
11
  if (typeof value === "string")
16
12
  return value.trim() || undefined;
@@ -59,20 +55,6 @@ function statusValue(value) {
59
55
  return undefined;
60
56
  return value;
61
57
  }
62
- function runtime(startedAt, finishedAt, now) {
63
- if (startedAt === undefined)
64
- return "";
65
- const seconds = Math.max(0, Math.floor(((finishedAt ?? now) - startedAt) / 1000));
66
- if (seconds < 60)
67
- return `${String(seconds)}s`;
68
- const minutes = Math.floor(seconds / 60);
69
- const remainingSeconds = seconds % 60;
70
- if (minutes < 60)
71
- return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
72
- const hours = Math.floor(minutes / 60);
73
- const remainingMinutes = minutes % 60;
74
- return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
75
- }
76
58
  function stateGlyph(state, spinner) {
77
59
  if (state === "running")
78
60
  return spinner;
@@ -83,32 +65,14 @@ function stateColor(state) {
83
65
  return "accent";
84
66
  return state === "completed" ? "success" : "error";
85
67
  }
86
- function activity(status) {
87
- if (status.state !== "running")
88
- return undefined;
89
- const current = status.progress?.activity;
90
- if (current?.kind === "reasoning" || current?.kind === "text") {
91
- const name = current.kind === "reasoning" ? "reasoning" : "responding";
92
- const text = sanitizeDisplayText(current.text);
93
- return text && text !== name ? `${name} · ${text}` : name;
94
- }
95
- if (current?.kind === "tool")
96
- return sanitizeDisplayText(current.text);
97
- return sanitizeDisplayText([...(status.progress?.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "");
98
- }
99
- function stalledDuration(status, now) {
100
- const lastEventAt = status.progress?.lastEventAt;
101
- if (status.state !== "running" || lastEventAt === undefined || !Number.isFinite(lastEventAt))
102
- return undefined;
103
- const duration = now - lastEventAt;
104
- return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
105
- }
106
- function accounting(status) {
68
+ /** Tokens and cost as the workflow header shows them. */
69
+ function usageStats(status) {
107
70
  const value = status.progress?.accounting;
108
- if (!value)
109
- return undefined;
110
- const total = value.input + value.output + value.cacheRead + value.cacheWrite;
111
- return `tokens=${String(total)} cost=${formatCost(value.cost) || "$0.00"}`;
71
+ return value ? [formatTokens(value.input + value.output), formatCost(value.cost)].filter(Boolean).join(" · ") : "";
72
+ }
73
+ function modelName(status) {
74
+ const model = status.progress?.state?.model ?? status.attemptDetails?.at(-1)?.setup.model;
75
+ return model ? `${model.model}${model.thinking ? `:${model.thinking}` : ""}` : "";
112
76
  }
113
77
  function timestamp(value) {
114
78
  if (value === undefined || !Number.isFinite(value) || value < 0)
@@ -129,15 +93,16 @@ function boundedValue(value) {
129
93
  }
130
94
  function formatSubagentProgress(status, args, theme, spinner, now, expanded) {
131
95
  const color = stateColor(status.state);
132
- const elapsed = runtime(status.startedAt, status.finishedAt, now);
96
+ const elapsed = status.startedAt === undefined ? "" : formatWorkflowRuntime((status.finishedAt ?? now) - status.startedAt);
133
97
  const metadata = requestMetadata(args, args.id === undefined);
98
+ const stats = usageStats(status);
134
99
  const lines = [
135
- `${theme.fg(color, stateGlyph(status.state, spinner))} ${theme.bold(theme.fg("accent", `Subagent: ${label({ ...args, id: status.id })}`))} ${theme.fg(color, `[${status.state}]`)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}${elapsed ? ` runtime=${elapsed}` : ""}`,
100
+ `${theme.fg(color, stateGlyph(status.state, "●"))} ${theme.bold(theme.fg("accent", `Subagent: ${label({ ...args, id: status.id })}`))} ${theme.fg(color, `[${status.state}]`)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}${stats ? ` ${stats}` : ""}${elapsed ? ` runtime=${elapsed}` : ""}`,
136
101
  ];
137
- const current = activity(status);
138
- const stalled = stalledDuration(status, now);
139
- if (current)
140
- lines.push(` ${theme.fg("accent", spinner)} ${theme.fg("dim", current)}${stalled === undefined ? "" : ` ${theme.fg("warning", `- stalled? ${formatStalledDuration(stalled)}`)}`}`);
102
+ const current = status.state === "running" ? agentActivityLabel(status.progress ?? {}) : undefined;
103
+ const stalled = stalledDuration({ ...status.progress, state: status.state }, now);
104
+ if (current || status.state === "running")
105
+ lines.push(` ${theme.fg("accent", spinner)}${current ? ` ${theme.fg("dim", current)}` : ""}${stalled === undefined ? "" : ` ${theme.fg("warning", `- stalled? ${formatStalledDuration(stalled)}`)}`}`);
141
106
  else if (stalled !== undefined)
142
107
  lines.push(` ${theme.fg("warning", `stalled? ${formatStalledDuration(stalled)}`)}`);
143
108
  if (status.error)
@@ -147,9 +112,6 @@ function formatSubagentProgress(status, args, theme, spinner, now, expanded) {
147
112
  const model = status.progress?.state?.model;
148
113
  if (model)
149
114
  lines.push(` ${theme.fg("dim", `model=${model.provider}/${model.model}${model.thinking ? `:${model.thinking}` : ""}`)}`);
150
- const usage = accounting(status);
151
- if (usage)
152
- lines.push(` ${theme.fg("dim", usage)}`);
153
115
  if (status.worktree)
154
116
  lines.push(` ${theme.fg("dim", `worktree=${status.worktree.path} branch=${status.worktree.branch}`)}`);
155
117
  }
@@ -221,7 +183,7 @@ export function subagentProgressBlock(status, args, theme, freezeAt) {
221
183
  const now = TERMINAL_STATES.has(current.state) ? frozenAt : Date.now();
222
184
  if (!TERMINAL_STATES.has(current.state))
223
185
  frozenAt = now;
224
- const frame = SPINNER[Math.floor(now / 80) % SPINNER.length] ?? "◇";
186
+ const frame = SPINNER[Math.floor(now / REPAINT_MS) % SPINNER.length] ?? "◇";
225
187
  return formatSubagentProgress(current, args, currentTheme, frame, now, expanded).split("\n").map((line) => truncateToWidth(line, Math.max(1, width), "…"));
226
188
  },
227
189
  invalidate() { },
@@ -237,7 +199,7 @@ export function renderSubagentResult(result, options, theme, context) {
237
199
  state.subagentStatus = state.subagentStatus?.id === incoming.id ? { ...state.subagentStatus, ...incoming } : incoming;
238
200
  const status = state.subagentStatus;
239
201
  if (status?.state === "running" && options.isPartial && !state.subagentSpinner) {
240
- state.subagentSpinner = setInterval(() => { context.invalidate(); }, 80);
202
+ state.subagentSpinner = setInterval(() => { context.invalidate(); }, REPAINT_MS);
241
203
  state.subagentSpinner.unref();
242
204
  }
243
205
  else if ((!options.isPartial || status?.state !== "running") && state.subagentSpinner) {
@@ -263,9 +225,75 @@ export function renderSubagentResult(result, options, theme, context) {
263
225
  component.setExpanded(options.expanded);
264
226
  return component;
265
227
  }
266
- export function createSubagentBackgroundWidget() {
228
+ /** One run as a workflow agent row: a quiet or stalled warning on the left, model and figures against the right rule. */
229
+ function widgetRow({ status, request }, now, paint) {
230
+ const lastEventAt = status.progress?.lastEventAt;
231
+ const silent = lastEventAt === undefined || !Number.isFinite(lastEventAt) || now - lastEventAt < QUIET_MS ? undefined : now - lastEventAt;
232
+ const role = silent !== undefined && silent >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? "error" : "warning";
233
+ const icon = silent === undefined ? mark(status.state, now, paint) : paint(role, "⚠");
234
+ const warning = silent === undefined ? "" : ` ${paint(role, `${role === "error" ? "stalled" : "quiet"} ${formatElapsed(silent)}`)}`;
235
+ const accounting = status.progress?.accounting;
236
+ const attempts = status.attempts ?? 1;
237
+ const right = [
238
+ modelName(status),
239
+ formatTokens(accounting === undefined ? undefined : accounting.input + accounting.output),
240
+ formatCost(accounting?.cost),
241
+ status.startedAt === undefined ? "" : formatElapsed(now - status.startedAt),
242
+ attempts > 1 ? `attempt ${String(attempts)}` : "",
243
+ ].filter(Boolean).join(" · ");
244
+ return { text: `${icon} ${sanitizeDisplayText(label({ ...request, id: status.id }))}${warning}`, right };
245
+ }
246
+ const RECEIPT_ENTRY_TYPE = "piewf-subagent-receipt";
247
+ function receiptFor(status, request) {
248
+ const accounting = status.progress?.accounting;
249
+ const model = modelName(status);
250
+ const role = roleName(request.role);
251
+ const tools = status.progress?.state?.tools ?? status.attemptDetails?.at(-1)?.setup.tools;
252
+ return {
253
+ id: status.id,
254
+ label: label({ ...request, id: status.id }),
255
+ state: status.state,
256
+ ...(model ? { model } : {}),
257
+ ...(role === undefined ? {} : { role }),
258
+ ...(tools === undefined ? {} : { tools: [...tools] }),
259
+ input: accounting?.input ?? 0,
260
+ output: accounting?.output ?? 0,
261
+ cacheRead: accounting?.cacheRead ?? 0,
262
+ costUsd: accounting?.cost ?? 0,
263
+ durationMs: status.startedAt === undefined ? 0 : Math.max(0, (status.finishedAt ?? Date.now()) - status.startedAt),
264
+ attempts: status.attempts ?? 1,
265
+ ...(status.error === undefined ? {} : { error: `${status.error.code}: ${status.error.message}` }),
266
+ };
267
+ }
268
+ /** The transcript line a finished background run leaves behind, in the shape of the workflow receipt. */
269
+ export function renderSubagentReceipt(data, expanded, theme) {
270
+ const glyph = data.state === "completed" ? theme.fg("success", "✓") : data.state === "failed" ? theme.fg("error", "✗") : theme.fg("muted", "·");
271
+ const headline = [formatTokens(data.input + data.output), formatCost(data.costUsd) || "$0.00", formatElapsed(data.durationMs), data.state].filter(Boolean).join(" · ");
272
+ const lines = [`${glyph} ${theme.bold(sanitizeDisplayText(data.label))} ${theme.fg("muted", headline)}`];
273
+ if (!expanded)
274
+ return lines;
275
+ const meta = [data.model ?? "", data.role ? `role ${data.role}` : "", data.attempts > 1 ? `${String(data.attempts)} attempts` : "", data.tools?.join(" ") ?? ""].filter(Boolean);
276
+ if (meta.length > 0)
277
+ lines.push(theme.fg("muted", ` ${meta.join(" · ")}`));
278
+ const split = [data.input ? `in ${formatTokens(data.input)}` : "", data.output ? `out ${formatTokens(data.output)}` : "", data.cacheRead ? `cache ${formatTokens(data.cacheRead)}` : ""].filter(Boolean);
279
+ if (split.length > 0)
280
+ lines.push(theme.fg("muted", ` ${split.join(" · ")}`));
281
+ if (data.error !== undefined)
282
+ lines.push(theme.fg("error", ` ${sanitizeDisplayText(data.error)}`));
283
+ lines.push(theme.fg("muted", ` run ${data.id}`));
284
+ return lines;
285
+ }
286
+ export function createSubagentBackgroundWidget(host = {}) {
267
287
  const key = "piewf-subagents-background";
268
288
  const runs = new Map();
289
+ const receipted = new Set();
290
+ host.registerEntryRenderer?.(RECEIPT_ENTRY_TYPE, (entry, options, theme) => {
291
+ if (!entry.data)
292
+ return undefined;
293
+ const lines = renderSubagentReceipt(entry.data, options.expanded, theme);
294
+ // Pi treats an over-wide line as fatal, and an error message has no bound.
295
+ return { render: (width) => lines.map((line) => truncateToWidth(line, Math.max(1, width), "…")), invalidate() { } };
296
+ });
269
297
  let context;
270
298
  let requestRender;
271
299
  let timer;
@@ -298,31 +326,20 @@ export function createSubagentBackgroundWidget() {
298
326
  return {
299
327
  render(width) {
300
328
  const now = Date.now();
301
- const frame = SPINNER[Math.floor(now / 80) % SPINNER.length] ?? "◇";
302
- const title = theme.bold(theme.fg("accent", `Subagents (${String(runs.size)} running)`));
303
- const blocks = [...runs.values()].map(({ status, request }) => formatSubagentProgress(status, request, theme, frame, now, false).split("\n"));
304
- const allRunsFit = blocks.reduce((rows, block) => rows + block.length, 1) <= MAX_BACKGROUND_WIDGET_ROWS;
305
- const runRowBudget = allRunsFit ? MAX_BACKGROUND_WIDGET_ROWS - 1 : MAX_BACKGROUND_WIDGET_ROWS - 2;
306
- const lines = [title];
307
- let runRows = 0;
308
- let displayedRuns = 0;
309
- for (const block of blocks) {
310
- if (runRows + block.length > runRowBudget)
311
- break;
312
- lines.push(...block);
313
- runRows += block.length;
314
- displayedRuns += 1;
315
- }
316
- const hiddenRuns = blocks.length - displayedRuns;
317
- if (hiddenRuns > 0)
318
- lines.push(`… ${String(hiddenRuns)} more`);
319
- return lines.map((line) => truncateToWidth(line, Math.max(1, width), "…"));
329
+ const paintRole = (role, text) => theme.fg(role, text);
330
+ // One row per run, so the default concurrency of eight fills the frame exactly.
331
+ // ponytail: no Alt+O scrolling as in the workflow widget; add it if runs routinely exceed the frame.
332
+ const budget = MAX_BACKGROUND_WIDGET_ROWS - 2;
333
+ const rows = [...runs.values()].map((run) => widgetRow(run, now, paintRole));
334
+ const body = rows.length <= budget ? rows : [...rows.slice(0, budget - 1), { text: theme.fg("muted", `… ${String(rows.length - budget + 1)} more`) }];
335
+ const title = runs.size === 1 ? "Subagent" : `Subagents · ${String(runs.size)} runs`;
336
+ return drawFrame(title, "", body, width, paintRole, (text) => theme.fg("accent", text)).map((line) => truncateToWidth(line, Math.max(1, width), ""));
320
337
  },
321
338
  invalidate() { },
322
339
  };
323
340
  }, { placement: "belowEditor" });
324
341
  showing = true;
325
- timer = setInterval(() => { requestRender?.(); }, 80);
342
+ timer = setInterval(() => { requestRender?.(); }, REPAINT_MS);
326
343
  timer.unref();
327
344
  }
328
345
  catch {
@@ -333,13 +350,23 @@ export function createSubagentBackgroundWidget() {
333
350
  start(next) {
334
351
  hide();
335
352
  runs.clear();
353
+ receipted.clear();
336
354
  context = next.mode === "tui" ? next : undefined;
337
355
  },
338
356
  update(status, request) {
339
357
  if (request.mode !== "background" || context === undefined)
340
358
  return;
341
- if (TERMINAL_STATES.has(status.state))
359
+ if (TERMINAL_STATES.has(status.state)) {
342
360
  runs.delete(status.id);
361
+ // A foreground run already has its tool block; a background one leaves a receipt, once, like a workflow run.
362
+ if (!receipted.has(status.id)) {
363
+ receipted.add(status.id);
364
+ try {
365
+ host.appendEntry?.(RECEIPT_ENTRY_TYPE, receiptFor(status, request));
366
+ }
367
+ catch { /* A receipt is display-only. */ }
368
+ }
369
+ }
343
370
  else
344
371
  runs.set(status.id, { status, request });
345
372
  paint();
@@ -353,7 +380,7 @@ export function createSubagentBackgroundWidget() {
353
380
  }
354
381
  export function renderSubagentInspectCall(args, theme) {
355
382
  const title = theme.fg("toolTitle", theme.bold("subagents_inspect"));
356
- return textBlock(args.id ? `${title} ${theme.fg("accent", args.id)}` : `${title} ${theme.fg("muted", "all")}`);
383
+ return textBlock(args.id ? `${title} ${theme.fg("accent", args.id)}` : `${title} ${theme.fg("muted", args.scope === "all" ? "all" : "session")}`);
357
384
  }
358
385
  export function renderSubagentInspectResult(result, options, theme, args) {
359
386
  const status = statusValue(result.details);