pi-subagents 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -19
  2. package/docs/agents.md +1 -0
  3. package/docs/configuration.md +28 -5
  4. package/docs/missions.md +4 -2
  5. package/docs/observability.md +26 -2
  6. package/package.json +1 -1
  7. package/src/agents/agents.ts +27 -12
  8. package/src/api/preflight.ts +1 -1
  9. package/src/extension/config.ts +20 -0
  10. package/src/extension/index.ts +18 -8
  11. package/src/extension/public-execution.ts +30 -4
  12. package/src/extension/schemas.ts +8 -7
  13. package/src/extension/tool-description.ts +10 -8
  14. package/src/inspectors/herdr/actions.ts +11 -2
  15. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  16. package/src/intercom/intercom-bridge.ts +3 -1
  17. package/src/missions/store.ts +8 -3
  18. package/src/runs/background/active-async-capacity.ts +82 -25
  19. package/src/runs/background/async-execution.ts +24 -36
  20. package/src/runs/background/async-job-tracker.ts +4 -0
  21. package/src/runs/background/async-resume.ts +11 -1
  22. package/src/runs/background/fleet-view.ts +18 -5
  23. package/src/runs/background/resume-guidance.ts +27 -7
  24. package/src/runs/background/retained-children.ts +14 -6
  25. package/src/runs/background/run-status.ts +96 -4
  26. package/src/runs/background/subagent-runner.ts +4 -1
  27. package/src/runs/foreground/prompt-audit.ts +4 -3
  28. package/src/runs/foreground/subagent-executor.ts +219 -20
  29. package/src/runs/shared/completion-guard.ts +17 -1
  30. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  31. package/src/shared/agent-stream-options.ts +5 -0
  32. package/src/shared/artifacts.ts +2 -6
  33. package/src/shared/display-text.ts +50 -0
  34. package/src/shared/node-executable.ts +21 -0
  35. package/src/shared/types.ts +23 -4
  36. package/src/tui/fleet-transcript.ts +1 -48
  37. package/src/tui/fleet.ts +3 -1
  38. package/src/tui/render.ts +86 -39
  39. package/src/watchdog/permission-arbiter.ts +2 -1
  40. package/src/watchdog/review.ts +4 -3
  41. package/src/workflows/chat-progress.ts +8 -2
  42. package/src/workflows/scripted-workflow.ts +46 -7
package/src/tui/render.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  type NestedRunSummary,
16
16
  type NestedStepSummary,
17
17
  type WorkflowNodeStatus,
18
+ type MainWindowRendererConfig,
18
19
  MAX_WIDGET_JOBS,
19
20
  POLL_INTERVAL_MS,
20
21
  WIDGET_KEY,
@@ -30,6 +31,38 @@ import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../
30
31
 
31
32
  type Theme = ExtensionContext["ui"]["theme"];
32
33
 
34
+ interface MainWindowRenderLayout {
35
+ horizontalSpacing: number;
36
+ compactResultMaxLines?: number;
37
+ }
38
+
39
+ function resolveMainWindowRenderLayout(config?: MainWindowRendererConfig): MainWindowRenderLayout {
40
+ return {
41
+ horizontalSpacing: config?.horizontalSpacing ?? 2,
42
+ ...(config?.compactResultMaxLines !== undefined ? { compactResultMaxLines: config.compactResultMaxLines } : {}),
43
+ };
44
+ }
45
+
46
+ function mainWindowIndent(layout: MainWindowRenderLayout, level: number): string {
47
+ return " ".repeat(Math.max(0, layout.horizontalSpacing * level));
48
+ }
49
+
50
+ function capCompactMainWindowResult(component: Component, layout: MainWindowRenderLayout, theme: Theme, enabled: boolean): Component {
51
+ const maxLines = layout.compactResultMaxLines;
52
+ if (!enabled || maxLines === undefined) return component;
53
+ const capped = new Container();
54
+ capped.render = (width: number): string[] => {
55
+ const lines = component.render(width);
56
+ if (lines.length <= maxLines) return lines;
57
+ const visibleRows = maxLines === 1 ? 1 : maxLines - 1;
58
+ const hiddenCount = lines.length - visibleRows;
59
+ const hint = theme.fg("accent", `… ${hiddenCount} rows hidden · ${liveDetailKeyText()} expands`);
60
+ if (maxLines === 1) return [truncLine(`${lines[0] ?? ""} ${hint}`, width)];
61
+ return [...lines.slice(0, visibleRows), truncLine(hint, width)];
62
+ };
63
+ return capped;
64
+ }
65
+
33
66
  function liveDetailKeyText(): string {
34
67
  return keyText("app.tools.expand");
35
68
  }
@@ -1555,7 +1588,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1555
1588
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1556
1589
  }
1557
1590
 
1558
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
1591
+ function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1559
1592
  const output = r.truncation?.text || getSingleResultOutput(r);
1560
1593
  const progress = r.progress || r.progressSummary;
1561
1594
  const isRunning = isResultRunning(r);
@@ -1566,40 +1599,43 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1566
1599
  ]);
1567
1600
  const c = new Container();
1568
1601
  const width = getTermWidth() - 4;
1602
+ const detailIndent = mainWindowIndent(layout, 1);
1603
+ const continuationIndent = mainWindowIndent(layout, 2) + (layout.horizontalSpacing > 0 ? " " : "");
1569
1604
  const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1570
1605
  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));
1571
1606
 
1572
1607
  if (isRunning && r.progress) {
1573
1608
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
1574
1609
  const activity = compactCurrentActivity(r.progress);
1575
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1610
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
1576
1611
  const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
1577
- if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
1612
+ if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", `${continuationIndent}${liveStatus}`), width), 0, 0));
1578
1613
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, progressSnapshotNow)) {
1579
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1614
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1580
1615
  }
1581
- c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1582
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1616
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1617
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1583
1618
  return c;
1584
1619
  }
1585
1620
 
1586
1621
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
1587
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1622
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1588
1623
  }
1589
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1624
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1590
1625
  const preview = firstOutputLine(output);
1591
1626
  if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
1592
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
1627
+ c.addChild(new Text(truncLine(theme.fg("dim", `${continuationIndent}${preview}`), width), 0, 0));
1593
1628
  }
1594
- if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
1595
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1596
- if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
1629
+ if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
1630
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1631
+ if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
1597
1632
  return c;
1598
1633
  }
1599
1634
 
1600
1635
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1601
1636
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1602
1637
  if (row.state === "complete") return theme.fg("success", "✓");
1638
+ if (row.state === "stopped") return theme.fg("warning", "■");
1603
1639
  return theme.fg("error", "✗");
1604
1640
  }
1605
1641
 
@@ -1607,6 +1643,7 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
1607
1643
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1608
1644
  if (row.state === "running") return theme.fg("accent", label);
1609
1645
  if (row.state === "complete") return theme.fg("success", label);
1646
+ if (row.state === "stopped") return theme.fg("warning", label);
1610
1647
  return theme.fg("error", label);
1611
1648
  }
1612
1649
 
@@ -1616,7 +1653,7 @@ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue:
1616
1653
  return "running";
1617
1654
  }
1618
1655
 
1619
- function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, frame?: number): Component {
1656
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1620
1657
  const workflow = d.workflow;
1621
1658
  const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1622
1659
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
@@ -1626,28 +1663,29 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1626
1663
  const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
1627
1664
  const phase = rows.find((row) => row.state === "running" && row.phase)?.phase ?? [...rows].reverse().find((row) => row.phase)?.phase;
1628
1665
  const c = new Container();
1666
+ const rowIndent = mainWindowIndent(layout, 1);
1629
1667
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold("workflow"))} ${runId} ${theme.fg("dim", "·")} ${d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo"} ${theme.fg("dim", "·")} ${state}`, width), 0, 0));
1630
- c.addChild(new Text(truncLine(theme.fg("dim", ` Repo ${repoLabel}`), width), 0, 0));
1631
- if (phase) c.addChild(new Text(truncLine(theme.fg("dim", ` Phase ${phase}`), width), 0, 0));
1668
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Repo ${repoLabel}`), width), 0, 0));
1669
+ if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
1632
1670
  if (rows.length === 0) {
1633
- c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
1671
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ waiting for workflow child launches`), width), 0, 0));
1634
1672
  return c;
1635
1673
  }
1636
1674
  const visible = visibleWorkflowRows(rows);
1637
- if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", ` … ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
1675
+ if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}… ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
1638
1676
  for (const row of visible.rows) {
1639
1677
  const status = workflowRowStateLabel(row, theme);
1640
1678
  const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
1641
1679
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1642
1680
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1643
1681
  const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
1644
- c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1682
+ c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1645
1683
  }
1646
- if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
1684
+ if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
1647
1685
  return c;
1648
1686
  }
1649
1687
 
1650
- function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component {
1688
+ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1651
1689
  const hasRunning = detailsHaveRunningResult(d);
1652
1690
  const detached = d.results.some((r) => r.detached)
1653
1691
  || workflowGraphHasStatus(d, ["detached"]);
@@ -1687,6 +1725,9 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1687
1725
  const contextBadge = contextModeBadge(theme, d.context);
1688
1726
  const c = new Container();
1689
1727
  const width = getTermWidth() - 4;
1728
+ const rowIndent = mainWindowIndent(layout, 1);
1729
+ const detailIndent = mainWindowIndent(layout, 2);
1730
+ const continuationIndent = mainWindowIndent(layout, 3) + (layout.horizontalSpacing > 0 ? " " : "");
1690
1731
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1691
1732
 
1692
1733
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
@@ -1704,8 +1745,8 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1704
1745
  if (entry.kind === "placeholder") {
1705
1746
  const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
1706
1747
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1707
- c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1708
- if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
1748
+ c.addChild(new Text(truncLine(`${rowIndent}${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1749
+ if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", `${detailIndent}⎿ Error: ${entry.error}`), width), 0, 0));
1709
1750
  continue;
1710
1751
  }
1711
1752
  const i = entry.resultIndex;
@@ -1714,7 +1755,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1714
1755
  const agentName = entry.agentName;
1715
1756
  if (!r) {
1716
1757
  const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
1717
- c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1758
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1718
1759
  continue;
1719
1760
  }
1720
1761
  const output = getSingleResultOutput(r);
@@ -1730,28 +1771,28 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1730
1771
  const rowProgressModel = rProg && "status" in rProg ? rProg : undefined;
1731
1772
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1732
1773
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1733
- c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1774
+ c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
1734
1775
  if (rRunning && rProg && "status" in rProg) {
1735
1776
  const liveProgress = rProg as AgentProgress;
1736
1777
  const activity = compactCurrentActivity(liveProgress);
1737
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1778
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
1738
1779
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(liveProgress))) {
1739
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1780
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1740
1781
  }
1741
- c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1782
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1742
1783
  } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
1743
- c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1784
+ c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", `${detailIndent}⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1744
1785
  }
1745
1786
  if (!rRunning && !rPending) {
1746
1787
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
1747
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1788
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1748
1789
  }
1749
1790
  }
1750
1791
  const outputTarget = extractOutputTarget(r.task);
1751
- if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
1752
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1792
+ if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${outputTarget}`), width), 0, 0));
1793
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1753
1794
  }
1754
- if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
1795
+ if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
1755
1796
  return c;
1756
1797
  }
1757
1798
 
@@ -1802,22 +1843,28 @@ export function renderSubagentResult(
1802
1843
  options: { expanded: boolean },
1803
1844
  theme: Theme,
1804
1845
  frame?: number,
1846
+ rendererConfig?: MainWindowRendererConfig,
1805
1847
  ): Component {
1848
+ const layout = resolveMainWindowRenderLayout(rendererConfig);
1849
+ const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
1806
1850
  const d = result.details;
1807
- if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) return renderWorkflowChatProgress(d, result, theme, frame);
1851
+ if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) {
1852
+ return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
1853
+ }
1808
1854
  if (!d || !d.results.length) {
1809
1855
  const t = result.content[0];
1810
1856
  const text = t?.type === "text" ? t.text : "(no output)";
1811
1857
  const contextPrefix = contextModePrefix(theme, d?.context);
1812
1858
  const width = getTermWidth() - 4;
1813
- if (!text.includes("\n")) return new Text(truncLine(`${contextPrefix}${text}`, width), 0, 0);
1859
+ if (!text.includes("\n")) return compact(new Text(truncLine(`${contextPrefix}${text}`, width), 0, 0));
1814
1860
  if (d && !options.expanded && !result.isError) {
1815
1861
  const lines = text.split(/\r?\n/);
1816
1862
  const firstNonEmptyLine = lines.find((line) => line.trim())?.trim() || "(no output)";
1817
1863
  const c = new Container();
1864
+ const detailIndent = mainWindowIndent(layout, 1);
1818
1865
  c.addChild(new Text(truncLine(`${contextPrefix}${firstNonEmptyLine} · ${lines.length} lines`, width), 0, 0));
1819
- c.addChild(new Text(truncLine(theme.fg("accent", ` Press ${liveDetailKeyText()} for full output`), width), 0, 0));
1820
- return c;
1866
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}Press ${liveDetailKeyText()} for full output`), width), 0, 0));
1867
+ return compact(c);
1821
1868
  }
1822
1869
  const c = new Container();
1823
1870
  const wrapped = wrapPlainText(`${contextPrefix}${text}`, width);
@@ -1830,8 +1877,8 @@ export function renderSubagentResult(
1830
1877
 
1831
1878
  if (d.mode === "single" && d.results.length === 1) {
1832
1879
  const r = d.results[0];
1833
- if (!r) return renderMultiCompact(d, theme, frame);
1834
- if (!expanded) return renderSingleCompact(d, r, theme, frame);
1880
+ if (!r) return compact(renderMultiCompact(d, theme, layout, frame));
1881
+ if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame));
1835
1882
  const isRunning = isResultRunning(r);
1836
1883
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1837
1884
  const output = r.truncation?.text || getSingleResultOutput(r);
@@ -1924,7 +1971,7 @@ export function renderSubagentResult(
1924
1971
  return c;
1925
1972
  }
1926
1973
 
1927
- if (!expanded) return renderMultiCompact(d, theme, frame);
1974
+ if (!expanded) return compact(renderMultiCompact(d, theme, layout, frame));
1928
1975
 
1929
1976
  const hasRunning = detailsHaveRunningResult(d);
1930
1977
  const detached = d.results.some((r) => r.detached)
@@ -3,6 +3,7 @@ import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-a
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
4
  import { Type, type Static } from "typebox";
5
5
  import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
6
+ import { agentStreamOptions } from "../shared/agent-stream-options.ts";
6
7
  import { decodeChildWatchdogConfig } from "./child-status.ts";
7
8
  import { childResolvedConfig } from "./register-child.ts";
8
9
  import { resolveWatchdogReviewModel } from "./review.ts";
@@ -112,7 +113,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
112
113
  tools: [tool],
113
114
  },
114
115
  convertToLlm,
115
- streamFunction: streamFn,
116
+ ...agentStreamOptions(streamFn),
116
117
  getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
117
118
  beforeToolCall: async ({ toolCall }) => toolCall.name === tool.name ? undefined : { block: true, reason: `Permission arbiter tool '${toolCall.name}' is not allowed.` },
118
119
  toolExecution: "sequential",
@@ -1,9 +1,10 @@
1
1
  import { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { createReadOnlyTools, convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
- import type { Model } from "@earendil-works/pi-ai";
4
+ import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
5
5
  import { Type, type Static } from "typebox";
6
6
  import { resolveModelCandidate } from "../runs/shared/model-fallback.ts";
7
+ import { agentStreamOptions } from "../shared/agent-stream-options.ts";
7
8
  import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
8
9
  import type { WatchdogReviewFunction, WatchdogReviewRequest } from "./runtime.ts";
9
10
  import {
@@ -36,7 +37,7 @@ type RegistryModel = Model<any>;
36
37
 
37
38
  interface WatchdogReviewAuth {
38
39
  apiKey?: string;
39
- headers?: Record<string, string>;
40
+ headers?: ProviderHeaders;
40
41
  env?: Record<string, string>;
41
42
  }
42
43
 
@@ -280,7 +281,7 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
280
281
  tools,
281
282
  },
282
283
  convertToLlm,
283
- streamFunction: streamFn,
284
+ ...agentStreamOptions(streamFn),
284
285
  getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
285
286
  beforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)
286
287
  ? undefined
@@ -96,7 +96,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
96
96
 
97
97
  export interface WorkflowChatProgressRow {
98
98
  key: string;
99
- state: "running" | "complete" | "failed";
99
+ state: "running" | "complete" | "failed" | "stopped";
100
100
  label?: string;
101
101
  phase?: string;
102
102
  runId?: string;
@@ -123,7 +123,13 @@ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workfl
123
123
  continue;
124
124
  }
125
125
  const next: WorkflowChatProgressRow = existing ?? { key: entry.key, state: "running" };
126
- next.state = entry.state === "completed" ? "complete" : entry.state === "failed" ? "failed" : "running";
126
+ next.state = entry.state === "completed"
127
+ ? "complete"
128
+ : entry.state === "failed"
129
+ ? "failed"
130
+ : entry.state === "stopped"
131
+ ? "stopped"
132
+ : "running";
127
133
  const label = cleanLabel(entry.label);
128
134
  const phase = cleanLabel(entry.phase);
129
135
  if (label) next.label = label;
@@ -332,7 +332,7 @@ export interface WorkflowScriptChildResult {
332
332
  export interface WorkflowScriptTraceEntry {
333
333
  operation: "run" | "status";
334
334
  key: string;
335
- state: "started" | "completed" | "failed" | "reused";
335
+ state: "started" | "completed" | "failed" | "stopped" | "reused";
336
336
  /** Canonical child agent name when resolved launch or result data is available. */
337
337
  agent?: string;
338
338
  runId?: string;
@@ -492,6 +492,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
492
492
  const children = new Map<string, WorkflowScriptChildResult>();
493
493
  const childOrder: string[] = [];
494
494
  const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
495
+ const stoppedLaunches = new Set<string>();
495
496
  const batchAdmissions = new Map<string, Promise<void>>();
496
497
  const observedRunCalls = new Set<number>();
497
498
  const childController = new AbortController();
@@ -519,7 +520,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
519
520
  else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
520
521
  else resolve({ value: outcome.value, ...partial() });
521
522
  };
522
- const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
523
+ const onAbort = () => {
524
+ const signalReason = options.signal?.reason;
525
+ const error = signalReason instanceof Error
526
+ ? signalReason
527
+ : typeof signalReason === "string"
528
+ ? new Error(signalReason)
529
+ : new Error("Workflow script aborted.");
530
+ for (const key of launches.keys()) {
531
+ if (children.has(key)) continue;
532
+ stoppedLaunches.add(key);
533
+ const started = trace.findLast((entry) => entry.operation === "run" && entry.key === key && entry.state === "started");
534
+ trace.push({
535
+ operation: "run",
536
+ key,
537
+ state: "stopped",
538
+ ...(started?.agent ? { agent: started.agent } : {}),
539
+ ...(started?.phase ? { phase: started.phase } : {}),
540
+ ...(started?.label ? { label: started.label } : {}),
541
+ error: error.message,
542
+ });
543
+ }
544
+ traceChanged();
545
+ finish({ error });
546
+ };
523
547
  const timer = options.timeoutMs === undefined
524
548
  ? undefined
525
549
  : setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
@@ -531,6 +555,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
531
555
  if (!settled && code !== 0) finish({ error: new Error(`Workflow worker exited with code ${code}.`) });
532
556
  });
533
557
  worker.on("message", (message: Record<string, unknown>) => {
558
+ if (settled) return;
534
559
  if (message.type === "emit") {
535
560
  try {
536
561
  assertWorkflowJsonValue(message.value, "emit");
@@ -617,7 +642,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
617
642
  const target = known?.runId ?? keyOrRunId;
618
643
  trace.push({ operation: "status", key: keyOrRunId, state: "started", ...(known?.runId ? { runId: known.runId } : {}) });
619
644
  traceChanged();
645
+ if (settled) return;
620
646
  respond(options.status(target, childController.signal).then((result) => {
647
+ if (settled) return result;
621
648
  trace.push({ operation: "status", key: keyOrRunId, state: result.ok ? "completed" : "failed", ...(result.runId ? { runId: result.runId } : {}), ...(!result.ok ? { error: result.output } : {}) });
622
649
  traceChanged();
623
650
  if (!result.ok) throw new Error(`Status '${keyOrRunId}' failed: ${result.output}`);
@@ -680,9 +707,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
680
707
  }
681
708
 
682
709
  const startedAt = Date.now();
683
- childOrder.push(key);
684
- trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
685
- traceChanged();
686
710
  const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
687
711
  ? { id: message.args.batch.id, calls: message.args.batch.calls.filter((call): call is { key: string; params: Record<string, unknown> } => isRecord(call) && typeof call.key === "string" && isRecord(call.params)) }
688
712
  : undefined;
@@ -694,11 +718,22 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
694
718
  seenKeys.add(call.key);
695
719
  return true;
696
720
  });
697
- admission = Promise.resolve().then(() => options.admit?.(calls));
721
+ admission = Promise.resolve().then(() => {
722
+ if (settled) return;
723
+ return options.admit?.(calls);
724
+ });
698
725
  if (batch) batchAdmissions.set(batch.id, admission);
699
726
  }
700
- const promise = admission.then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal, { admitted: true })).then((result) => {
727
+ const promise = admission.then(() => {
728
+ if (settled || stoppedLaunches.has(key)) {
729
+ const reason = childController.signal.reason;
730
+ const text = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.";
731
+ return { key, ok: false, output: text, error: text, artifactPaths: [] };
732
+ }
733
+ return options.launch(key, { ...params, async: params.async ?? false }, childController.signal, { admitted: true });
734
+ }).then((result) => {
701
735
  const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
736
+ if (stoppedLaunches.has(key)) return normalized;
702
737
  children.set(key, normalized);
703
738
  trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
704
739
  traceChanged();
@@ -706,12 +741,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
706
741
  }, (error: unknown) => {
707
742
  const text = error instanceof Error ? error.message : String(error);
708
743
  const failure: WorkflowScriptChildResult = { key, ok: false, output: text, error: text, artifactPaths: [] };
744
+ if (stoppedLaunches.has(key)) return failure;
709
745
  children.set(key, failure);
710
746
  trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
711
747
  traceChanged();
712
748
  return failure;
713
749
  });
714
750
  launches.set(key, { fingerprint, promise, observed: callObserved });
751
+ childOrder.push(key);
752
+ trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
753
+ traceChanged();
715
754
  respond(deliver(promise));
716
755
  });
717
756