@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-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 (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. package/trajectory/src/server.ts +112 -12
@@ -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`;
@@ -82,7 +82,7 @@ function formatLogTimestamp(timestamp) {
82
82
  return "--:--:--";
83
83
  return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
84
84
  }
85
- function workflowLogLines(run, styles, expanded, width, showHint = false) {
85
+ function workflowLogLines(run, styles, expanded, width, showHint = false, tail = 5) {
86
86
  const events = (run.events ?? []).filter((event) => event.type === "log");
87
87
  if (!events.length)
88
88
  return [];
@@ -94,7 +94,7 @@ function workflowLogLines(run, styles, expanded, width, showHint = false) {
94
94
  const chunks = truncateToVisualLines(message, Number.MAX_SAFE_INTEGER, Math.max(1, width - 11), 0).visualLines;
95
95
  return chunks.map((chunk, chunkIndex) => `${chunkIndex === 0 ? prefix : indent}${chunk}`);
96
96
  }));
97
- const visible = expanded ? visualLines : visualLines.slice(-5);
97
+ const visible = expanded ? visualLines : visualLines.slice(-tail);
98
98
  let hint = "";
99
99
  if (!expanded && showHint) {
100
100
  try {
@@ -104,7 +104,55 @@ function workflowLogLines(run, styles, expanded, width, showHint = false) {
104
104
  }
105
105
  return [expanded ? ` ${styles.muted("Logs")}` : ` ${styles.muted(`Logs${hint}`)}`, ...visible];
106
106
  }
107
- export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width, showHint = false) {
107
+ const ACTIVE_AGENT_STATES = new Set(["running", "waiting_for_child", "retrying", "paused"]);
108
+ const COMPACT_RECENT_SETTLED = 3;
109
+ const COMPACT_LOG_LINES = 3;
110
+ function expandHint() { try {
111
+ return ` (${keyHint("app.tools.expand", "to expand")})`;
112
+ }
113
+ catch {
114
+ return "";
115
+ } }
116
+ function agentEnd(agent) { return (agent.startedAt ?? 0) + (agent.durationMs ?? 0); }
117
+ /** One line for a finished phase: outcome counts, wall time across its agents, tokens, and cost. */
118
+ function phaseSummary(agents, styles) {
119
+ const count = (state) => agents.filter((agent) => agent.state === state).length;
120
+ const starts = agents.map((agent) => agent.startedAt).filter((value) => value !== undefined);
121
+ const span = starts.length ? Math.max(...agents.map(agentEnd)) - Math.min(...starts) : 0;
122
+ const accounting = sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()));
123
+ const failed = count("failed");
124
+ 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);
125
+ return styles.muted(parts.join(" · "));
126
+ }
127
+ /**
128
+ * Renders the run; with `maxLines`, a view taller than that limit switches to a compact one so the live block stays on screen.
129
+ * A block taller than the terminal pushes its changing header above the viewport, and pi-tui then clears and redraws the whole
130
+ * scrollback on every update, which snaps the reader back to the bottom.
131
+ */
132
+ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width, showHint = false, maxLines) {
133
+ const full = workflowProgressLines(run, spinner, styles, now, expanded, width, showHint);
134
+ if (expanded || maxLines === undefined || full.length <= maxLines)
135
+ return full.join("\n");
136
+ // Finished phases collapse to a summary line; the current one keeps failed, active, and the latest settled agents, in that priority.
137
+ const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
138
+ const currentStart = Math.min(run.agents.length, phases.at(-1)?.afterAgent ?? 0);
139
+ const current = run.agents.slice(currentStart);
140
+ 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);
141
+ const candidates = [...current.filter((agent) => agent.state === "failed"), ...current.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)), ...recent];
142
+ const render = (count) => workflowProgressLines(run, spinner, styles, now, false, width, showHint, new Set(candidates.slice(0, count)));
143
+ let low = 0;
144
+ let high = candidates.length;
145
+ while (low < high) {
146
+ const middle = Math.ceil((low + high) / 2);
147
+ if (render(middle).length <= maxLines)
148
+ low = middle;
149
+ else
150
+ high = middle - 1;
151
+ }
152
+ // NOTE: failures in finished phases always stay listed, so a run with more of them than maxLines still overflows.
153
+ return render(low).join("\n");
154
+ }
155
+ function workflowProgressLines(run, spinner, styles, now, expanded, width, showHint, shown) {
108
156
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
109
157
  const workflowIcon = runStateGlyph(run.state, spinner);
110
158
  const iconStyle = workflowIconStyle(run.state, styles);
@@ -120,14 +168,15 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
120
168
  if (shellActivity)
121
169
  lines.push(` ${shellActivity}`);
122
170
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
123
- const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
171
+ const position = new Map(run.agents.map((agent, index) => [agent, index]));
172
+ const renderAgents = (agents, nested) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
124
173
  const icon = agentStateGlyph(agent.state, spinner);
125
174
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
126
175
  const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
127
176
  const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
128
177
  const state = progressStyleForState(agent.state, styles);
129
178
  const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
130
- return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
179
+ return `${indent}#${String((position.get(agent) ?? 0) + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
131
180
  }, run.agents, (label) => styles.muted(label)).map((line) => nested ? ` ${line}` : line);
132
181
  const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
133
182
  if (scopedShells) {
@@ -141,10 +190,25 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
141
190
  }
142
191
  let renderedAgents = 0;
143
192
  let nested = false;
193
+ // 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.
194
+ const segment = (agents, closed) => {
195
+ if (!shown)
196
+ return renderAgents(agents, nested);
197
+ const listed = agents.filter((agent) => closed ? agent.state === "failed" : shown.has(agent));
198
+ const hidden = closed ? [] : agents.filter((agent) => !shown.has(agent));
199
+ 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]];
200
+ const summary = counts.filter(([, count]) => count > 0).map(([label, count]) => `+${String(count)} ${label}`).join(" · ");
201
+ return [...renderAgents(listed, nested), ...(summary ? [`${nested ? " " : " "}${styles.muted(`… ${summary}${showHint ? expandHint() : ""}`)}`] : [])];
202
+ };
144
203
  for (const [phaseIndex, phase] of phases.entries()) {
145
204
  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}]`)}`);
205
+ const before = run.agents.slice(renderedAgents, boundary);
206
+ if (shown && !nested && before.length)
207
+ lines.push(` ${phaseSummary(before, styles)}`);
208
+ lines.push(...segment(before, true));
209
+ const next = phases[phaseIndex + 1];
210
+ const phaseAgents = next ? run.agents.slice(boundary, Math.max(boundary, Math.min(run.agents.length, next.afterAgent))) : [];
211
+ lines.push(` ${styles.muted(`[Phase: ${phase.phase}]`)}${shown && next ? ` ${phaseSummary(phaseAgents, styles)}` : ""}`);
148
212
  const phaseShell = scopedShells ? shellActivityFor(run, phaseIndex) : undefined;
149
213
  if (phaseShell) {
150
214
  const rendered = formatShellActivity(phaseShell.active, phaseShell.startedAt, spinner, styles, now);
@@ -154,9 +218,9 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
154
218
  renderedAgents = boundary;
155
219
  nested = true;
156
220
  }
157
- lines.push(...renderAgents(run.agents.slice(renderedAgents), renderedAgents, nested));
158
- lines.push(...workflowLogLines(run, styles, expanded, width, showHint));
159
- return lines.join("\n");
221
+ lines.push(...segment(run.agents.slice(renderedAgents), false));
222
+ lines.push(...workflowLogLines(run, styles, expanded, width, showHint, shown ? COMPACT_LOG_LINES : 5));
223
+ return lines;
160
224
  }
161
225
  const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
162
226
  const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
@@ -418,7 +482,11 @@ export function workflowProgressBlock(run, theme, progress, refresh, invalidate,
418
482
  }
419
483
  previousState = displayed.state;
420
484
  const frame = displayed.state === "running" ? workflowSpinner[Math.floor(now / 80) % workflowSpinner.length] ?? "◇" : "◇";
421
- const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true);
485
+ // pi-tui only patches rows that are still on screen; leave room below the block for the editor and footer.
486
+ // A finished or frozen block no longer changes, so it keeps the full list.
487
+ const rows = freezeAt === undefined && !terminal ? process.stdout.rows : undefined;
488
+ const maxLines = rows ? Math.max(6, rows - 10) - (prefix ? prefix.split("\n").length + 1 : 0) : undefined;
489
+ const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true, maxLines);
422
490
  return truncateWorkflowProgress(prefix ? `${prefix}\n\n${progressText}` : progressText, width);
423
491
  },
424
492
  setExpanded(value) { expanded = value; },
@@ -588,7 +656,7 @@ function formatAccounting(accounting) {
588
656
  const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
589
657
  return `${formatAccountingValue(total)} tok`;
590
658
  }
591
- function formatAgentError(error, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
659
+ export function formatAgentError(error, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
592
660
  return styles.error(`Error: ${error.code}: ${error.message}`);
593
661
  }
594
662
  export function formatAgentDetail(agent, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), options = {}) {
@@ -751,11 +819,7 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
751
819
  const model = buildWorkflowPhaseModel(run, snapshot);
752
820
  const tree = buildWorkflowPhaseTree(model);
753
821
  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)))}`;
822
+ const wrap = (text) => wrapNavigatorLine(text, safeWidth);
759
823
  const phaseStyle = (state) => phaseStyleForState(state, styles);
760
824
  const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
761
825
  const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
@@ -833,35 +897,38 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
833
897
  const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
834
898
  if (shellActivity)
835
899
  lines.push(` ${shellActivity}`);
836
- const renderTree = (limit) => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
837
900
  const actionRows = () => {
838
901
  const actions = selection.actions;
839
902
  if (!actions)
840
903
  return [];
841
904
  return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
842
905
  };
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
- }
906
+ const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
907
+ lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, selection));
861
908
  if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
862
909
  lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
863
910
  return lines.flatMap((line) => wrap(line));
864
911
  }
912
+ function wrapNavigatorLine(text, width) {
913
+ return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
914
+ }
915
+ /** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
916
+ export function formatNavigatorColumns(sidebar, detail, width, layout = {}) {
917
+ const safeWidth = Math.max(1, width);
918
+ if (safeWidth >= 80) {
919
+ const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
920
+ const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
921
+ const right = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
922
+ return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
923
+ const cell = left[index] ?? "";
924
+ return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
925
+ });
926
+ }
927
+ if (layout.detailsOnly)
928
+ return detail.flatMap((line) => wrapNavigatorLine(line, safeWidth));
929
+ const left = sidebar.flatMap((line) => wrapNavigatorLine(line, safeWidth));
930
+ return layout.treeOnly ? left : [...left, ...detail.flatMap((line) => wrapNavigatorLine(line, safeWidth))];
931
+ }
865
932
  export function formatCheckpointReview(checkpoint) {
866
933
  const context = JSON.stringify(checkpoint.context, null, 2);
867
934
  return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
package/dist/src/host.js CHANGED
@@ -13,7 +13,8 @@ import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunId
13
13
  import { retainTerminalRuns } from "./retention.js";
14
14
  import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
15
15
  import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
16
- import { loadAgentDefinitions, loadProjectAgentDefinitions, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
16
+ import { loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
17
+ import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
17
18
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry } from "./registry.js";
18
19
  import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
19
20
  import backgroundWidget from "./background-widget.js";
@@ -22,7 +23,7 @@ import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTra
22
23
  import { getTrajectoryHost } from "./trajectory-host-handle.js";
23
24
  import { getSubagentManager } from "./subagent-manager-handle.js";
24
25
  import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf } from "./types.js";
25
- import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, formatWorkflowProgress, } from "./host-view.js";
26
+ import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, } from "./host-view.js";
26
27
  import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix, } from "./host-delivery.js";
27
28
  export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
28
29
  export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
@@ -119,7 +120,9 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
119
120
  export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
120
121
  export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
121
122
  function workflowToolUpdate(run) {
122
- return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
123
+ //NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
124
+ const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
125
+ return { content: [{ type: "text", text: `Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done) [${run.state}]` }], details: { runId: run.id, run } };
123
126
  }
124
127
  function agentWithProgress(agent, progress) {
125
128
  const next = { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls };
@@ -143,6 +146,7 @@ function deliver(pi, content) {
143
146
  pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
144
147
  }
145
148
  const WORKFLOW_WARNING_ENTRY = "workflow-warning";
149
+ const PROGRESS_COALESCE_MS = 100;
146
150
  function deliverWarning(pi, content) {
147
151
  pi.appendEntry(WORKFLOW_WARNING_ENTRY, { message: content });
148
152
  }
@@ -406,7 +410,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
406
410
  const trusted = projectTrusted(context);
407
411
  const settings = resolveWorkflowSettings(cwd, trusted, workflowSettingsPath(extensionAgentDir)).effective.extensionSettings?.trajectory;
408
412
  const port = settings?.port;
409
- const themes = settings?.themes ?? false;
410
413
  const loadRuns = createTrajectoryRunLoader(cwd, sessionId, home, overlayLiveRun);
411
414
  const loadSubagents = createTrajectorySubagentLoader(cwd, sessionId, extensionAgentDir, overlayLiveSubagent);
412
415
  const loadRunsMetadata = createTrajectoryRunMetadataLoader(cwd, sessionId, home, overlayLiveRun);
@@ -415,7 +418,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
415
418
  const live = liveSubagents.get(subagentId);
416
419
  return live?.status.sessionId === sessionId ? live.status.attemptDetails?.at(-1)?.session : undefined;
417
420
  });
418
- return { cwd, sessionId, ...(port === undefined ? {} : { port }), themes, loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request) => trajectoryAction(request, context) };
421
+ return { cwd, sessionId, ...(port === undefined ? {} : { port }), loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request) => trajectoryAction(request, context) };
419
422
  };
420
423
  const withLiveActivities = (run) => liveAgents.overlay(run);
421
424
  /** Overlays the in-memory lifecycle state and budget usage of an active run onto its persisted record. */
@@ -619,25 +622,54 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
619
622
  const run = runs.get(runId);
620
623
  if (!run)
621
624
  throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
625
+ const applyProgress = async (progress) => {
626
+ let runState;
627
+ if (progress.persist) {
628
+ runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
629
+ }
630
+ else {
631
+ const loaded = await run.store.loadStatus();
632
+ if (!loaded.agents.some((agent) => agent.id === id))
633
+ return;
634
+ runState = { ...loaded, ...run.budget.snapshot(), agents: loaded.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
635
+ }
636
+ if (!runState.agents.some((agent) => agent.id === id))
637
+ return;
638
+ liveAgents.setActivity(runId, id, progress.activity);
639
+ liveAgents.setEventTime(runId, id, progress.lastEventAt);
640
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
641
+ };
642
+ // Display-only progress bursts coalesce to one trailing apply per window; persisted progress always applies immediately.
643
+ let deferred;
644
+ let deferredTimer;
645
+ let appliedAt = 0;
646
+ const cancelDeferredProgress = () => {
647
+ if (deferredTimer)
648
+ clearTimeout(deferredTimer);
649
+ deferredTimer = undefined;
650
+ deferred = undefined;
651
+ };
652
+ const onProgress = async (progress) => {
653
+ const now = Date.now();
654
+ if (progress.persist || (!deferredTimer && now - appliedAt >= PROGRESS_COALESCE_MS)) {
655
+ cancelDeferredProgress();
656
+ appliedAt = now;
657
+ await applyProgress(progress);
658
+ return;
659
+ }
660
+ deferred = progress;
661
+ deferredTimer ??= setTimeout(() => {
662
+ deferredTimer = undefined;
663
+ const next = deferred;
664
+ deferred = undefined;
665
+ if (!next)
666
+ return;
667
+ appliedAt = Date.now();
668
+ void applyProgress(next).catch(() => undefined);
669
+ }, Math.max(0, PROGRESS_COALESCE_MS - (now - appliedAt))).unref();
670
+ };
622
671
  try {
623
672
  const budget = run.budget.forAgent(id);
624
- const onProgress = async (progress) => {
625
- let runState;
626
- if (progress.persist) {
627
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
628
- }
629
- else {
630
- const loaded = await run.store.load();
631
- if (!loaded.run.agents.some((agent) => agent.id === id))
632
- return;
633
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
634
- }
635
- if (!runState.agents.some((agent) => agent.id === id))
636
- return;
637
- liveAgents.setActivity(runId, id, progress.activity);
638
- liveAgents.setEventTime(runId, id, progress.lastEventAt);
639
- run.update?.(workflowToolUpdate(withLiveActivities(runState)));
640
- };
641
673
  const onAttempt = async (attempt) => {
642
674
  liveAgents.setSession(runId, id, attempt.liveSession);
643
675
  liveAgents.setHandoff(runId, id, attempt);
@@ -654,6 +686,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
654
686
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
655
687
  };
656
688
  const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.role ? { role: options.role } : {}), ...(options.contextFiles ? { contextFiles: options.contextFiles } : {}), tools: options.tools, ...(options.skills ? { skills: options.skills } : {}), ...(options.extensions ? { extensions: options.extensions } : {}), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.sessionPath ? { sessionPath: options.sessionPath } : {}), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, skills, extensions) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}), ...(skills !== undefined ? { skills } : {}), ...(extensions !== undefined ? { extensions } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
689
+ cancelDeferredProgress();
657
690
  const before = (await run.store.load()).run;
658
691
  await persistAgentAttempts(run.store, id, result.attempts);
659
692
  const completed = (await run.store.load()).run;
@@ -665,6 +698,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
665
698
  return result.value;
666
699
  }
667
700
  catch (error) {
701
+ cancelDeferredProgress();
668
702
  liveAgents.setSession(runId, id);
669
703
  const attempts = getAgentAttempts(error);
670
704
  if (attempts?.length) {
@@ -1484,8 +1518,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1484
1518
  state.workflowProgressComponent = workflowProgressBlock(currentProgress?.run ?? incoming, theme, currentProgress, async () => {
1485
1519
  const active = runs.get(incoming.id);
1486
1520
  const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
1487
- const loaded = await store.load();
1488
- return withLiveActivities(loaded.run);
1521
+ return withLiveActivities(await store.loadStatus());
1489
1522
  }, () => { if (state.workflowProgress === currentProgress)
1490
1523
  requestRender(); }, undefined, state.workflowProgressFrozenAt);
1491
1524
  }
@@ -6,12 +6,14 @@ export * from "./utils.js";
6
6
  export { canonicalPath, sameFilesystemPath } from "./paths.js";
7
7
  export * from "./budget.js";
8
8
  export * from "./validation.js";
9
+ export * from "./roles.js";
9
10
  export * from "./registry.js";
10
11
  export * from "./execution.js";
11
12
  export * from "./host.js";
12
13
  export * from "./workflow-artifacts.js";
13
14
  export { formatCost } from "./background-widget.js";
14
- export { agentActionLabels, formatAgentDetail, navigatorAttentionSortByState, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
15
+ export { agentActionLabels, formatAgentDetail, formatAgentError, formatNavigatorColumns, formatWorkflowRuntime, navigatorAttentionSortByState, PLAIN_WORKFLOW_PROGRESS_STYLES, progressStyleForState, runStateGlyph, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
16
+ export { createInlineConfirm, workflowKeyLabel, workflowKeyMatches } from "./host-navigator.js";
15
17
  export type { AgentDetailPresentation, WorkflowProgressStyles } from "./host-view.js";
16
18
  export { default } from "./host.js";
17
19
  export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";