@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,12 +1,12 @@
1
1
  import { keyHint, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { budgetUsage } from "./budget.js";
4
4
  import { formatCost, formatTokens } from "./background-widget.js";
5
- import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting } from "./types.js";
5
+ import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting, zeroAccounting } from "./types.js";
6
6
  import { object, sanitizeDisplayText } from "./utils.js";
7
7
  import { WORKFLOW_PHASE_STATES, buildWorkflowPhaseModel, buildWorkflowPhaseTree, phaseAgentCounts, shellActivityFor, workflowPhaseTreeInitialExpanded, workflowPhaseTreePath, workflowPhaseTreeVisibleNodes, } from "./host-phases.js";
8
8
  export { SETTLED_AGENT_STATES } from "./types.js";
9
- const PLAIN_WORKFLOW_PROGRESS_STYLES = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
9
+ export const PLAIN_WORKFLOW_PROGRESS_STYLES = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
10
10
  function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
11
11
  function agentGroupLabel(agents) {
12
12
  const structural = agents[0]?.structuralPath ?? [];
@@ -42,7 +42,7 @@ function renderGroupedAgents(agents, render, allAgents = agents, groupLabel = (l
42
42
  }
43
43
  const RUN_STATE_GLYPH = { "not started": "○", queued: "○", pausing: "⏸", paused: "⏸", completed: "✓", failed: "✗", stopped: "✗", interrupted: "↯", budget_exhausted: "!", awaiting_input: "?" };
44
44
  const AGENT_STATE_GLYPH = { queued: "○", waiting_for_child: "…", paused: "⏸", retrying: "↻", completed: "✓", failed: "✗", cancelled: "✗" };
45
- function runStateGlyph(state, running) { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
45
+ export function runStateGlyph(state, running) { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
46
46
  function agentStateGlyph(state, running) { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
47
47
  const PROGRESS_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", running: "accent", paused: "warning", pausing: "warning", interrupted: "warning", retrying: "accent", budget_exhausted: "warning", awaiting_input: "warning" };
48
48
  const WORKFLOW_ICON_STYLE = { completed: "success", failed: "error", stopped: "error", interrupted: "warning", budget_exhausted: "warning", awaiting_input: "warning", paused: "warning", pausing: "warning", running: "accent" };
@@ -51,10 +51,10 @@ function styleForState(map, state, styles) {
51
51
  const key = map[state] ?? "muted";
52
52
  return (text) => styles[key](text);
53
53
  }
54
- function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
54
+ export function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
55
55
  function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
56
56
  function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
57
- function formatWorkflowRuntime(durationMs) {
57
+ export function formatWorkflowRuntime(durationMs) {
58
58
  const seconds = Math.max(0, Math.floor(durationMs / 1000));
59
59
  if (seconds < 60)
60
60
  return `${String(seconds)}s`;
@@ -74,6 +74,17 @@ function formatShellActivity(activeShells, startedAt, spinner, styles, now) {
74
74
  const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
75
75
  return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
76
76
  }
77
+ function formatToolActivity(run, spinner, styles, now) {
78
+ const calls = run.activeTools ?? [];
79
+ if (!calls.length)
80
+ return undefined;
81
+ const counts = new Map();
82
+ for (const { name } of calls)
83
+ counts.set(name, (counts.get(name) ?? 0) + 1);
84
+ const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
85
+ const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
86
+ return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
87
+ }
77
88
  function formatLogTimestamp(timestamp) {
78
89
  if (timestamp === undefined || !Number.isFinite(timestamp))
79
90
  return "--:--:--";
@@ -82,7 +93,7 @@ function formatLogTimestamp(timestamp) {
82
93
  return "--:--:--";
83
94
  return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
84
95
  }
85
- function workflowLogLines(run, styles, expanded, width, showHint = false) {
96
+ function workflowLogLines(run, styles, expanded, width, showHint = false, tail = 5) {
86
97
  const events = (run.events ?? []).filter((event) => event.type === "log");
87
98
  if (!events.length)
88
99
  return [];
@@ -94,7 +105,7 @@ function workflowLogLines(run, styles, expanded, width, showHint = false) {
94
105
  const chunks = truncateToVisualLines(message, Number.MAX_SAFE_INTEGER, Math.max(1, width - 11), 0).visualLines;
95
106
  return chunks.map((chunk, chunkIndex) => `${chunkIndex === 0 ? prefix : indent}${chunk}`);
96
107
  }));
97
- const visible = expanded ? visualLines : visualLines.slice(-5);
108
+ const visible = expanded ? visualLines : visualLines.slice(-tail);
98
109
  let hint = "";
99
110
  if (!expanded && showHint) {
100
111
  try {
@@ -104,7 +115,55 @@ function workflowLogLines(run, styles, expanded, width, showHint = false) {
104
115
  }
105
116
  return [expanded ? ` ${styles.muted("Logs")}` : ` ${styles.muted(`Logs${hint}`)}`, ...visible];
106
117
  }
107
- export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width, showHint = false) {
118
+ const ACTIVE_AGENT_STATES = new Set(["running", "waiting_for_child", "retrying", "paused"]);
119
+ const COMPACT_RECENT_SETTLED = 3;
120
+ const COMPACT_LOG_LINES = 3;
121
+ function expandHint() { try {
122
+ return ` (${keyHint("app.tools.expand", "to expand")})`;
123
+ }
124
+ catch {
125
+ return "";
126
+ } }
127
+ function agentEnd(agent) { return (agent.startedAt ?? 0) + (agent.durationMs ?? 0); }
128
+ /** One line for a finished phase: outcome counts, wall time across its agents, tokens, and cost. */
129
+ function phaseSummary(agents, styles) {
130
+ const count = (state) => agents.filter((agent) => agent.state === state).length;
131
+ const starts = agents.map((agent) => agent.startedAt).filter((value) => value !== undefined);
132
+ const span = starts.length ? Math.max(...agents.map(agentEnd)) - Math.min(...starts) : 0;
133
+ const accounting = sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()));
134
+ const failed = count("failed");
135
+ const parts = [`${String(count("completed"))} done`, ...(failed ? [styles.error(`${String(failed)} failed`)] : []), ...(count("cancelled") ? [`${String(count("cancelled"))} cancelled`] : []), ...(span > 0 ? [formatWorkflowRuntime(span)] : []), formatTokens(accounting.input + accounting.output), formatCost(accounting.cost)].filter(Boolean);
136
+ return styles.muted(parts.join(" · "));
137
+ }
138
+ /**
139
+ * Renders the run; with `maxLines`, a view taller than that limit switches to a compact one so the live block stays on screen.
140
+ * A block taller than the terminal pushes its changing header above the viewport, and pi-tui then clears and redraws the whole
141
+ * scrollback on every update, which snaps the reader back to the bottom.
142
+ */
143
+ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width, showHint = false, maxLines) {
144
+ const full = workflowProgressLines(run, spinner, styles, now, expanded, width, showHint);
145
+ if (expanded || maxLines === undefined || full.length <= maxLines)
146
+ return full.join("\n");
147
+ // Finished phases collapse to a summary line; the current one keeps failed, active, and the latest settled agents, in that priority.
148
+ const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
149
+ const currentStart = Math.min(run.agents.length, phases.at(-1)?.afterAgent ?? 0);
150
+ const current = run.agents.slice(currentStart);
151
+ const recent = current.filter((agent) => SETTLED_AGENT_STATES.has(agent.state) && agent.state !== "failed").sort((left, right) => agentEnd(right) - agentEnd(left)).slice(0, COMPACT_RECENT_SETTLED);
152
+ const candidates = [...current.filter((agent) => agent.state === "failed"), ...current.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)), ...recent];
153
+ const render = (count) => workflowProgressLines(run, spinner, styles, now, false, width, showHint, new Set(candidates.slice(0, count)));
154
+ let low = 0;
155
+ let high = candidates.length;
156
+ while (low < high) {
157
+ const middle = Math.ceil((low + high) / 2);
158
+ if (render(middle).length <= maxLines)
159
+ low = middle;
160
+ else
161
+ high = middle - 1;
162
+ }
163
+ // NOTE: failures in finished phases always stay listed, so a run with more of them than maxLines still overflows.
164
+ return render(low).join("\n");
165
+ }
166
+ function workflowProgressLines(run, spinner, styles, now, expanded, width, showHint, shown) {
108
167
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
109
168
  const workflowIcon = runStateGlyph(run.state, spinner);
110
169
  const iconStyle = workflowIconStyle(run.state, styles);
@@ -119,15 +178,20 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
119
178
  const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
120
179
  if (shellActivity)
121
180
  lines.push(` ${shellActivity}`);
181
+ const toolActivity = formatToolActivity(run, spinner, styles, now);
182
+ if (toolActivity)
183
+ lines.push(` ${toolActivity}`);
122
184
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
123
- const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
124
- const icon = agentStateGlyph(agent.state, spinner);
185
+ const position = new Map(run.agents.map((agent, index) => [agent, index]));
186
+ const renderAgents = (agents, nested) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
187
+ const icon = agentStateGlyph(agent.state, "●");
125
188
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
126
- const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
189
+ const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
190
+ const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
127
191
  const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
128
192
  const state = progressStyleForState(agent.state, styles);
129
193
  const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
130
- return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
194
+ return `${indent}#${String((position.get(agent) ?? 0) + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
131
195
  }, run.agents, (label) => styles.muted(label)).map((line) => nested ? ` ${line}` : line);
132
196
  const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
133
197
  if (scopedShells) {
@@ -141,10 +205,25 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
141
205
  }
142
206
  let renderedAgents = 0;
143
207
  let nested = false;
208
+ // In compact mode (`shown` set) earlier segments list only their failures, and the current one only the shown agents plus a count of the rest.
209
+ const segment = (agents, closed) => {
210
+ if (!shown)
211
+ return renderAgents(agents, nested);
212
+ const listed = agents.filter((agent) => closed ? agent.state === "failed" : shown.has(agent));
213
+ const hidden = closed ? [] : agents.filter((agent) => !shown.has(agent));
214
+ const counts = [["queued", hidden.filter((agent) => !SETTLED_AGENT_STATES.has(agent.state) && !ACTIVE_AGENT_STATES.has(agent.state)).length], ["running", hidden.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)).length], ["done", hidden.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length]];
215
+ const summary = counts.filter(([, count]) => count > 0).map(([label, count]) => `+${String(count)} ${label}`).join(" · ");
216
+ return [...renderAgents(listed, nested), ...(summary ? [`${nested ? " " : " "}${styles.muted(`… ${summary}${showHint ? expandHint() : ""}`)}`] : [])];
217
+ };
144
218
  for (const [phaseIndex, phase] of phases.entries()) {
145
219
  const boundary = Math.max(renderedAgents, Math.min(run.agents.length, phase.afterAgent));
146
- lines.push(...renderAgents(run.agents.slice(renderedAgents, boundary), renderedAgents, nested));
147
- lines.push(` ${styles.muted(`[Phase: ${phase.phase}]`)}`);
220
+ const before = run.agents.slice(renderedAgents, boundary);
221
+ if (shown && !nested && before.length)
222
+ lines.push(` ${phaseSummary(before, styles)}`);
223
+ lines.push(...segment(before, true));
224
+ const next = phases[phaseIndex + 1];
225
+ const phaseAgents = next ? run.agents.slice(boundary, Math.max(boundary, Math.min(run.agents.length, next.afterAgent))) : [];
226
+ lines.push(` ${styles.muted(`[Phase: ${phase.phase}]`)}${shown && next ? ` ${phaseSummary(phaseAgents, styles)}` : ""}`);
148
227
  const phaseShell = scopedShells ? shellActivityFor(run, phaseIndex) : undefined;
149
228
  if (phaseShell) {
150
229
  const rendered = formatShellActivity(phaseShell.active, phaseShell.startedAt, spinner, styles, now);
@@ -154,9 +233,9 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
154
233
  renderedAgents = boundary;
155
234
  nested = true;
156
235
  }
157
- lines.push(...renderAgents(run.agents.slice(renderedAgents), renderedAgents, nested));
158
- lines.push(...workflowLogLines(run, styles, expanded, width, showHint));
159
- return lines.join("\n");
236
+ lines.push(...segment(run.agents.slice(renderedAgents), false));
237
+ lines.push(...workflowLogLines(run, styles, expanded, width, showHint, shown ? COMPACT_LOG_LINES : 5));
238
+ return lines;
160
239
  }
161
240
  const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
162
241
  const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
@@ -418,14 +497,18 @@ export function workflowProgressBlock(run, theme, progress, refresh, invalidate,
418
497
  }
419
498
  previousState = displayed.state;
420
499
  const frame = displayed.state === "running" ? workflowSpinner[Math.floor(now / 80) % workflowSpinner.length] ?? "◇" : "◇";
421
- const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true);
500
+ // pi-tui only patches rows that are still on screen; leave room below the block for the editor and footer.
501
+ // A finished or frozen block no longer changes, so it keeps the full list.
502
+ const rows = freezeAt === undefined && !terminal ? process.stdout.rows : undefined;
503
+ const maxLines = rows ? Math.max(6, rows - 10) - (prefix ? prefix.split("\n").length + 1 : 0) : undefined;
504
+ const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true, maxLines);
422
505
  return truncateWorkflowProgress(prefix ? `${prefix}\n\n${progressText}` : progressText, width);
423
506
  },
424
507
  setExpanded(value) { expanded = value; },
425
508
  invalidate() {
426
509
  const displayed = currentRun();
427
510
  const now = Date.now();
428
- if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0))
511
+ if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length))
429
512
  return;
430
513
  if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
431
514
  return;
@@ -538,7 +621,7 @@ export function formatStalledDuration(durationMs) {
538
621
  const remainingMinutes = minutes % 60;
539
622
  return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
540
623
  }
541
- function stalledDuration(agent, now) {
624
+ export function stalledDuration(agent, now) {
542
625
  if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
543
626
  return undefined;
544
627
  const duration = now - agent.lastEventAt;
@@ -550,7 +633,7 @@ function elapsedDurationMs(agent, end) {
550
633
  return Math.max(0, agent.durationMs);
551
634
  return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
552
635
  }
553
- function agentActivityLabel(agent) {
636
+ export function agentActivityLabel(agent) {
554
637
  const activity = agent.activity;
555
638
  if (activity?.kind === "reasoning" || activity?.kind === "text") {
556
639
  const name = activity.kind === "reasoning" ? "reasoning" : "responding";
@@ -588,7 +671,7 @@ function formatAccounting(accounting) {
588
671
  const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
589
672
  return `${formatAccountingValue(total)} tok`;
590
673
  }
591
- function formatAgentError(error, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
674
+ export function formatAgentError(error, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
592
675
  return styles.error(`Error: ${error.code}: ${error.message}`);
593
676
  }
594
677
  export function formatAgentDetail(agent, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), options = {}) {
@@ -663,6 +746,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date
663
746
  const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
664
747
  if (shellActivity)
665
748
  lines.push(` ${shellActivity}`);
749
+ const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
750
+ if (toolActivity)
751
+ lines.push(` ${toolActivity}`);
666
752
  if (run.error)
667
753
  lines.push(`Error: ${run.error.code}: ${run.error.message}`);
668
754
  if (run.events?.length)
@@ -708,6 +794,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.no
708
794
  const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
709
795
  if (shellActivity)
710
796
  lines.push(` ${shellActivity}`);
797
+ const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
798
+ if (toolActivity)
799
+ lines.push(` ${toolActivity}`);
711
800
  if (run.error)
712
801
  lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
713
802
  if (run.events?.length)
@@ -751,11 +840,7 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
751
840
  const model = buildWorkflowPhaseModel(run, snapshot);
752
841
  const tree = buildWorkflowPhaseTree(model);
753
842
  const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
754
- const wrap = (text, limit = safeWidth) => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
755
- // ponytail: ANSI-only width, good enough for the ASCII labels the tree renders
756
- const ansiPattern = new RegExp(ANSI_SGR_SOURCE, "g");
757
- const visibleLength = (text) => text.replace(ansiPattern, "").length;
758
- const padTo = (text, limit) => `${text}${" ".repeat(Math.max(0, limit - visibleLength(text)))}`;
843
+ const wrap = (text) => wrapNavigatorLine(text, safeWidth);
759
844
  const phaseStyle = (state) => phaseStyleForState(state, styles);
760
845
  const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
761
846
  const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
@@ -833,35 +918,41 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
833
918
  const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
834
919
  if (shellActivity)
835
920
  lines.push(` ${shellActivity}`);
836
- const renderTree = (limit) => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
921
+ const toolActivity = formatToolActivity(run, "⠦", styles, now);
922
+ if (toolActivity)
923
+ lines.push(` ${toolActivity}`);
837
924
  const actionRows = () => {
838
925
  const actions = selection.actions;
839
926
  if (!actions)
840
927
  return [];
841
928
  return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
842
929
  };
843
- const detailRows = () => [...details(selectedNode), ...actionRows()];
844
- if (safeWidth >= 80) {
845
- const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
846
- const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
847
- const sidebar = renderTree(sidebarWidth).flatMap((line) => wrap(line, sidebarWidth));
848
- const detail = detailRows().flatMap((line) => wrap(line, detailWidth));
849
- const rows = Math.max(sidebar.length, detail.length);
850
- for (let index = 0; index < rows; index += 1)
851
- lines.push(`${padTo(sidebar[index] ?? "", sidebarWidth)} | ${detail[index] ?? ""}`);
852
- }
853
- else if (selection.detailsOnly) {
854
- lines.push(...detailRows().flatMap((line) => wrap(line)));
855
- }
856
- else {
857
- lines.push(...renderTree(safeWidth));
858
- if (!selection.treeOnly)
859
- lines.push("", ...detailRows().flatMap((line) => wrap(line)));
860
- }
930
+ const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
931
+ lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, selection));
861
932
  if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
862
933
  lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
863
934
  return lines.flatMap((line) => wrap(line));
864
935
  }
936
+ function wrapNavigatorLine(text, width) {
937
+ return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
938
+ }
939
+ /** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
940
+ export function formatNavigatorColumns(sidebar, detail, width, layout = {}) {
941
+ const safeWidth = Math.max(1, width);
942
+ if (safeWidth >= 80) {
943
+ const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
944
+ const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
945
+ const right = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
946
+ return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
947
+ const cell = left[index] ?? "";
948
+ return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
949
+ });
950
+ }
951
+ if (layout.detailsOnly)
952
+ return detail.flatMap((line) => wrapNavigatorLine(line, safeWidth));
953
+ const left = sidebar.flatMap((line) => wrapNavigatorLine(line, safeWidth));
954
+ return layout.treeOnly ? left : [...left, ...detail.flatMap((line) => wrapNavigatorLine(line, safeWidth))];
955
+ }
865
956
  export function formatCheckpointReview(checkpoint) {
866
957
  const context = JSON.stringify(checkpoint.context, null, 2);
867
958
  return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
@@ -2,7 +2,7 @@ import { Type } from "@earendil-works/pi-ai";
2
2
  import { copyToClipboard, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { type BackgroundWidgetAPI } from "./background-widget.js";
4
4
  import { type AgentTransport } from "./types.js";
5
- export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
5
+ export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
6
6
  export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
7
7
  export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
8
8
  export type { WorkflowPhaseAgentCounts, WorkflowPhaseModel, WorkflowPhaseSelection, WorkflowPhaseState, WorkflowPhaseTree, WorkflowPhaseTreeDirection, WorkflowPhaseTreeNode, WorkflowPhaseTreeNodeKind, WorkflowPhaseTreeSelection, WorkflowPhaseView, } from "./host-phases.js";
@@ -16,7 +16,7 @@ export declare function formatWorkflowPreview(args: {
16
16
  description?: unknown;
17
17
  }): string;
18
18
  export declare const WORKFLOW_TOOL_LABEL = "Workflow";
19
- export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
19
+ export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default. Scripts call this session's tools as `await tools.<name>(args)`, outside withWorktree: a tool resolves to its text unless its description gives a `tools.<name>(args)` result shape, and failures reject";
20
20
  export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Provide exactly one of script or scriptPath and a non-empty name. Registered catalog functions are available as globals inside the script; call them there, for example return await someFunction(args). Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Manage runs from the interactive /workflow picker; use workflow_status, workflow_resume, workflow_retry, workflow_stop, and workflow_respond for explicit tool controls. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately and delivers completion or failure as a follow-up. After failure follow-ups, especially CANCELLED or interrupted runs, call workflow_status({ runId }) before recovery or replacement work, then pass its state as expectedState to workflow_retry/workflow_resume so recovery cannot act on a state that changed. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, expectedState?, foreground? }) replays a failed run into a child; workflow_resume({ runId, expectedState?, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
21
21
  export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
22
22
  name: Type.TString;