@marcoscale98/pi-extensible-workflows 5.14.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.
- package/CHANGELOG.md +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { keyHint, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { budgetUsage } from "./budget.js";
|
|
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";
|
|
6
|
+
import { object, sanitizeDisplayText } from "./utils.js";
|
|
7
|
+
import { WORKFLOW_PHASE_STATES, buildWorkflowPhaseModel, buildWorkflowPhaseTree, phaseAgentCounts, shellActivityFor, workflowPhaseTreeInitialExpanded, workflowPhaseTreePath, workflowPhaseTreeVisibleNodes, } from "./host-phases.js";
|
|
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 };
|
|
10
|
+
function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
11
|
+
function agentGroupLabel(agents) {
|
|
12
|
+
const structural = agents[0]?.structuralPath ?? [];
|
|
13
|
+
const breadcrumbs = [...new Set(agents.map((agent) => agent.parentBreadcrumb).filter((value) => Boolean(value)))];
|
|
14
|
+
return [...(structural.length ? [structural.join(" > ")] : []), ...(breadcrumbs.length === 1 ? breadcrumbs : breadcrumbs.length ? [breadcrumbs.join(" | ")] : [])].join(" > ") || "Agents";
|
|
15
|
+
}
|
|
16
|
+
function agentGroups(agents, allAgents = agents) {
|
|
17
|
+
const byId = new Map(allAgents.map((agent) => [agent.id, agent]));
|
|
18
|
+
const groups = new Map();
|
|
19
|
+
for (const [index, agent] of agents.entries()) {
|
|
20
|
+
let depth = 0;
|
|
21
|
+
const seen = new Set([agent.id]);
|
|
22
|
+
for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) {
|
|
23
|
+
if (seen.has(parent))
|
|
24
|
+
break;
|
|
25
|
+
seen.add(parent);
|
|
26
|
+
depth += 1;
|
|
27
|
+
}
|
|
28
|
+
const key = agentGroupKey(agent);
|
|
29
|
+
const group = groups.get(key) ?? { agents: [] };
|
|
30
|
+
group.agents.push({ agent, index, depth });
|
|
31
|
+
groups.set(key, group);
|
|
32
|
+
}
|
|
33
|
+
return [...groups].map(([, group]) => ({ label: agentGroupLabel(group.agents.map(({ agent }) => agent)), entries: group.agents }));
|
|
34
|
+
}
|
|
35
|
+
function renderGroupedAgents(agents, render, allAgents = agents, groupLabel = (label) => label) {
|
|
36
|
+
const groups = agentGroups(agents, allAgents);
|
|
37
|
+
const grouped = groups.length > 1 || groups.some(({ label }) => label !== "Agents");
|
|
38
|
+
return groups.flatMap((group) => [
|
|
39
|
+
...(grouped ? [` ${groupLabel(group.label)}`] : []),
|
|
40
|
+
...group.entries.map((entry) => render(entry, grouped)),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
const RUN_STATE_GLYPH = { "not started": "○", queued: "○", pausing: "⏸", paused: "⏸", completed: "✓", failed: "✗", stopped: "✗", interrupted: "↯", budget_exhausted: "!", awaiting_input: "?" };
|
|
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] ?? "◆"; }
|
|
46
|
+
function agentStateGlyph(state, running) { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
|
|
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
|
+
const WORKFLOW_ICON_STYLE = { completed: "success", failed: "error", stopped: "error", interrupted: "warning", budget_exhausted: "warning", awaiting_input: "warning", paused: "warning", pausing: "warning", running: "accent" };
|
|
49
|
+
const PHASE_STATE_STYLE = { completed: "success", failed: "error", cancelled: "error", stopped: "error", running: "accent", paused: "warning", pausing: "warning", interrupted: "warning", budget_exhausted: "warning" };
|
|
50
|
+
function styleForState(map, state, styles) {
|
|
51
|
+
const key = map[state] ?? "muted";
|
|
52
|
+
return (text) => styles[key](text);
|
|
53
|
+
}
|
|
54
|
+
function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
55
|
+
function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
56
|
+
function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
57
|
+
function formatWorkflowRuntime(durationMs) {
|
|
58
|
+
const seconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
59
|
+
if (seconds < 60)
|
|
60
|
+
return `${String(seconds)}s`;
|
|
61
|
+
const minutes = Math.floor(seconds / 60);
|
|
62
|
+
const remainingSeconds = seconds % 60;
|
|
63
|
+
if (minutes < 60)
|
|
64
|
+
return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
|
|
65
|
+
const hours = Math.floor(minutes / 60);
|
|
66
|
+
const remainingMinutes = minutes % 60;
|
|
67
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
68
|
+
}
|
|
69
|
+
function formatShellActivity(activeShells, startedAt, spinner, styles, now) {
|
|
70
|
+
const count = activeShells ?? 0;
|
|
71
|
+
if (count <= 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
const started = startedAt !== undefined && Number.isFinite(startedAt) ? new Date(startedAt) : undefined;
|
|
74
|
+
const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
|
|
75
|
+
return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
|
|
76
|
+
}
|
|
77
|
+
function formatLogTimestamp(timestamp) {
|
|
78
|
+
if (timestamp === undefined || !Number.isFinite(timestamp))
|
|
79
|
+
return "--:--:--";
|
|
80
|
+
const date = new Date(timestamp);
|
|
81
|
+
if (Number.isNaN(date.getTime()))
|
|
82
|
+
return "--:--:--";
|
|
83
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
84
|
+
}
|
|
85
|
+
function workflowLogLines(run, styles, expanded, width, showHint = false) {
|
|
86
|
+
const events = (run.events ?? []).filter((event) => event.type === "log");
|
|
87
|
+
if (!events.length)
|
|
88
|
+
return [];
|
|
89
|
+
const indent = " ".repeat(11);
|
|
90
|
+
const visualLines = events.flatMap((event) => event.message.split("\n").flatMap((message, lineIndex) => {
|
|
91
|
+
const prefix = lineIndex === 0 ? ` ${styles.dim(formatLogTimestamp(event.timestamp))} ` : indent;
|
|
92
|
+
if (width === undefined)
|
|
93
|
+
return [`${prefix}${message}`];
|
|
94
|
+
const chunks = truncateToVisualLines(message, Number.MAX_SAFE_INTEGER, Math.max(1, width - 11), 0).visualLines;
|
|
95
|
+
return chunks.map((chunk, chunkIndex) => `${chunkIndex === 0 ? prefix : indent}${chunk}`);
|
|
96
|
+
}));
|
|
97
|
+
const visible = expanded ? visualLines : visualLines.slice(-5);
|
|
98
|
+
let hint = "";
|
|
99
|
+
if (!expanded && showHint) {
|
|
100
|
+
try {
|
|
101
|
+
hint = ` (${keyHint("app.tools.expand", "to expand")})`;
|
|
102
|
+
}
|
|
103
|
+
catch { /* Theme is unavailable in non-interactive render tests. */ }
|
|
104
|
+
}
|
|
105
|
+
return [expanded ? ` ${styles.muted("Logs")}` : ` ${styles.muted(`Logs${hint}`)}`, ...visible];
|
|
106
|
+
}
|
|
107
|
+
export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width, showHint = false) {
|
|
108
|
+
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
109
|
+
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
110
|
+
const iconStyle = workflowIconStyle(run.state, styles);
|
|
111
|
+
const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
|
|
112
|
+
const state = progressStyleForState(run.state, styles)(`[${run.state}]`);
|
|
113
|
+
const usageStats = run.usage ? [formatTokens(run.usage.tokens), formatCost(run.usage.costUsd)].filter(Boolean).join(" · ") : "";
|
|
114
|
+
const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
115
|
+
const lines = [`${iconStyle(workflowIcon)} ${header} ${state}${usageStats ? ` ${usageStats}` : ""}${runtime}`];
|
|
116
|
+
const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
|
|
117
|
+
lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
|
|
118
|
+
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
119
|
+
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
120
|
+
if (shellActivity)
|
|
121
|
+
lines.push(` ${shellActivity}`);
|
|
122
|
+
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);
|
|
125
|
+
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
126
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
127
|
+
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
128
|
+
const state = progressStyleForState(agent.state, styles);
|
|
129
|
+
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
130
|
+
return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
|
|
131
|
+
}, run.agents, (label) => styles.muted(label)).map((line) => nested ? ` ${line}` : line);
|
|
132
|
+
const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
|
|
133
|
+
if (scopedShells) {
|
|
134
|
+
const preflight = shellActivityFor(run, -1);
|
|
135
|
+
if (preflight) {
|
|
136
|
+
lines.push(` ${styles.muted("[Preflight]")}`);
|
|
137
|
+
const rendered = formatShellActivity(preflight.active, preflight.startedAt, spinner, styles, now);
|
|
138
|
+
if (rendered)
|
|
139
|
+
lines.push(` ${rendered}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let renderedAgents = 0;
|
|
143
|
+
let nested = false;
|
|
144
|
+
for (const [phaseIndex, phase] of phases.entries()) {
|
|
145
|
+
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}]`)}`);
|
|
148
|
+
const phaseShell = scopedShells ? shellActivityFor(run, phaseIndex) : undefined;
|
|
149
|
+
if (phaseShell) {
|
|
150
|
+
const rendered = formatShellActivity(phaseShell.active, phaseShell.startedAt, spinner, styles, now);
|
|
151
|
+
if (rendered)
|
|
152
|
+
lines.push(` ${rendered}`);
|
|
153
|
+
}
|
|
154
|
+
renderedAgents = boundary;
|
|
155
|
+
nested = true;
|
|
156
|
+
}
|
|
157
|
+
lines.push(...renderAgents(run.agents.slice(renderedAgents), renderedAgents, nested));
|
|
158
|
+
lines.push(...workflowLogLines(run, styles, expanded, width, showHint));
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
162
|
+
const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
|
|
163
|
+
export function textBlock(text) {
|
|
164
|
+
return {
|
|
165
|
+
render(width) {
|
|
166
|
+
return text.split("\n").map((line) => truncateToWidth(line, Math.max(1, width), "…"));
|
|
167
|
+
},
|
|
168
|
+
invalidate() { },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function styledTextBlock(text) {
|
|
172
|
+
return {
|
|
173
|
+
render(width) {
|
|
174
|
+
return truncateWorkflowProgress(text, width);
|
|
175
|
+
},
|
|
176
|
+
invalidate() { },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function workflowCatalogBlock(text, expanded) {
|
|
180
|
+
return {
|
|
181
|
+
render(width) {
|
|
182
|
+
const safeWidth = Math.max(1, width);
|
|
183
|
+
if (!expanded)
|
|
184
|
+
return truncateWorkflowProgress(text, safeWidth);
|
|
185
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines.map((line) => line.trimEnd());
|
|
186
|
+
},
|
|
187
|
+
invalidate() { },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function controlString(value) { return typeof value === "string" && value.trim() ? value : undefined; }
|
|
191
|
+
function controlValue(value) {
|
|
192
|
+
if (value === null)
|
|
193
|
+
return "removed";
|
|
194
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
195
|
+
return String(value);
|
|
196
|
+
const json = JSON.stringify(value);
|
|
197
|
+
return typeof json === "string" ? json : "unknown";
|
|
198
|
+
}
|
|
199
|
+
function controlTitle(name, theme) { return theme.fg("toolTitle", theme.bold(name)); }
|
|
200
|
+
function controlState(state, theme) {
|
|
201
|
+
const color = state === "completed" || state === "running" || state === "stopped" ? "success" : state === "failed" || state === "unknown" ? "error" : state === "budget_exhausted" || state === "awaiting_approval" ? "warning" : "accent";
|
|
202
|
+
return theme.fg(color, state);
|
|
203
|
+
}
|
|
204
|
+
function controlAction(action, theme) {
|
|
205
|
+
const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
|
|
206
|
+
return theme.fg(color, action);
|
|
207
|
+
}
|
|
208
|
+
function budgetPatchEntries(value) {
|
|
209
|
+
if (!object(value))
|
|
210
|
+
return value === undefined ? [] : [controlValue(value)];
|
|
211
|
+
return Object.entries(value).map(([dimension, limits]) => {
|
|
212
|
+
if (limits === null)
|
|
213
|
+
return `${dimension}=removed`;
|
|
214
|
+
if (!object(limits))
|
|
215
|
+
return `${dimension}=${controlValue(limits)}`;
|
|
216
|
+
const parts = ["soft", "hard"].filter((key) => Object.prototype.hasOwnProperty.call(limits, key)).map((key) => `${key}=${controlValue(limits[key])}`);
|
|
217
|
+
return `${dimension} ${parts.join(" ")}`;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function budgetPatchSummary(value) {
|
|
221
|
+
const entries = budgetPatchEntries(value);
|
|
222
|
+
return entries.length ? entries.join(", ") : "unchanged";
|
|
223
|
+
}
|
|
224
|
+
function budgetPatchDetails(value, theme) {
|
|
225
|
+
const entries = budgetPatchEntries(value);
|
|
226
|
+
return entries.length ? [theme.fg("accent", theme.bold("Budget patch")), ...entries.map((entry) => ` ${theme.fg("toolOutput", entry)}`)] : [];
|
|
227
|
+
}
|
|
228
|
+
export function workflowControlCall(name, args, theme) {
|
|
229
|
+
const runId = controlString(args.runId) ?? "(missing run ID)";
|
|
230
|
+
if (name === "workflow_respond") {
|
|
231
|
+
const proposalId = controlString(args.proposalId);
|
|
232
|
+
const target = proposalId ? `budget proposal ${proposalId}` : `checkpoint ${controlString(args.name) ?? "(missing name)"}`;
|
|
233
|
+
const decision = args.approved === true ? "approve" : "reject";
|
|
234
|
+
return [`${controlTitle(name, theme)} ${theme.fg("accent", runId)}`, `${theme.fg("muted", target)} · ${controlAction(decision, theme)}`].join("\n");
|
|
235
|
+
}
|
|
236
|
+
if (name === "workflow_status")
|
|
237
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)}`;
|
|
238
|
+
if (name === "workflow_resume")
|
|
239
|
+
return args.budget === undefined ? `${controlTitle(name, theme)} ${theme.fg("accent", runId)}` : [`${controlTitle(name, theme)} ${theme.fg("accent", runId)}`, `${theme.fg("muted", "Budget")} ${theme.fg("toolOutput", budgetPatchSummary(args.budget))}`].join("\n");
|
|
240
|
+
if (name === "workflow_retry")
|
|
241
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)} ${theme.fg("muted", "failed run")}`;
|
|
242
|
+
return `${controlTitle(name, theme)} ${theme.fg("accent", runId)}`;
|
|
243
|
+
}
|
|
244
|
+
export function workflowControlResult(name, args, result, expanded, theme, isError) {
|
|
245
|
+
if (isError) {
|
|
246
|
+
const text = result.content?.filter(({ type }) => type === "text").map(({ text }) => text ?? "").join("\n").trim();
|
|
247
|
+
return theme.fg("error", text || `The ${name} tool failed.`);
|
|
248
|
+
}
|
|
249
|
+
const value = catalogResultValue(result);
|
|
250
|
+
if (!object(value))
|
|
251
|
+
return theme.fg("error", `The ${name} tool returned an invalid result.`);
|
|
252
|
+
const runId = controlString(args.runId) ?? controlString(value.runId) ?? "(unknown)";
|
|
253
|
+
const title = controlTitle(name, theme);
|
|
254
|
+
if (name === "workflow_stop") {
|
|
255
|
+
const state = controlString(value.state) ?? "unknown";
|
|
256
|
+
const action = value.stopped === true ? "stopped" : value.reason === "already_terminal" ? "already terminal" : value.reason === "unknown_run" ? "run not found" : "no change";
|
|
257
|
+
if (!expanded)
|
|
258
|
+
return `${title}\nRun ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`;
|
|
259
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(controlString(value.reason) ? [`Reason: ${theme.fg("toolOutput", controlValue(value.reason))}`] : [])].join("\n");
|
|
260
|
+
}
|
|
261
|
+
if (name === "workflow_status") {
|
|
262
|
+
const state = controlString(value.state) ?? "unknown";
|
|
263
|
+
const workflowName = controlString(value.workflowName);
|
|
264
|
+
const agents = Array.isArray(value.agents) ? value.agents.length : 0;
|
|
265
|
+
if (!expanded)
|
|
266
|
+
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)}`, ...(workflowName ? [workflowName] : [])].join("\n");
|
|
267
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, ...(workflowName ? [`Workflow: ${theme.fg("toolOutput", workflowName)}`] : []), `State: ${controlState(state, theme)}`, `Agents: ${String(agents)}`].join("\n");
|
|
268
|
+
}
|
|
269
|
+
if (name === "workflow_retry") {
|
|
270
|
+
const childRunId = controlString(value.runId) ?? "(unknown)";
|
|
271
|
+
const state = controlString(value.state) ?? "unknown";
|
|
272
|
+
const action = state === "completed" ? "completed" : "started";
|
|
273
|
+
if (!expanded)
|
|
274
|
+
return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
|
|
275
|
+
return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
|
|
276
|
+
}
|
|
277
|
+
if (name === "workflow_resume") {
|
|
278
|
+
const state = controlString(value.state) ?? "unknown";
|
|
279
|
+
const proposalId = controlString(value.proposalId);
|
|
280
|
+
const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
|
|
281
|
+
if (!expanded)
|
|
282
|
+
return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
|
|
283
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal: ${theme.fg("accent", proposalId)}`] : []), ...budgetPatchDetails(args.budget, theme)].join("\n");
|
|
284
|
+
}
|
|
285
|
+
const proposalId = controlString(args.proposalId);
|
|
286
|
+
const checkpointName = controlString(args.name);
|
|
287
|
+
const target = proposalId ? `Budget proposal ${theme.fg("accent", proposalId)}` : `Checkpoint ${theme.fg("accent", checkpointName ?? "(missing)")}`;
|
|
288
|
+
const accepted = value.accepted === true;
|
|
289
|
+
const approved = value.approved === true;
|
|
290
|
+
const reason = controlString(value.reason);
|
|
291
|
+
const action = reason === "proposal_not_pending" ? "not pending" : reason === "checkpoint" && !accepted ? "not pending" : approved ? "approved" : "rejected";
|
|
292
|
+
const state = controlString(value.state);
|
|
293
|
+
if (!expanded)
|
|
294
|
+
return [title, target, `Run ${theme.fg("accent", runId)} · ${controlAction(action, theme)}${state ? ` · ${controlState(state, theme)}` : ""}`].join("\n");
|
|
295
|
+
return [title, `Run: ${theme.fg("accent", runId)}`, `Target: ${target}`, `Action: ${controlAction(action, theme)}`, ...(state ? [`State: ${controlState(state, theme)}`] : []), ...(reason ? [`Reason: ${theme.fg("toolOutput", reason)}`] : [])].join("\n");
|
|
296
|
+
}
|
|
297
|
+
function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
|
|
298
|
+
export function catalogResultValue(result) {
|
|
299
|
+
if (result.details !== undefined)
|
|
300
|
+
return result.details;
|
|
301
|
+
const text = result.content?.find((entry) => entry.type === "text")?.text;
|
|
302
|
+
if (!text)
|
|
303
|
+
return undefined;
|
|
304
|
+
try {
|
|
305
|
+
return JSON.parse(text);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return text;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function isCatalogIndex(value) {
|
|
312
|
+
return object(value) && Array.isArray(value.functions);
|
|
313
|
+
}
|
|
314
|
+
function isCatalogFunction(value) {
|
|
315
|
+
return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.input) && object(value.output);
|
|
316
|
+
}
|
|
317
|
+
function isWorkflowCatalogModelAlias(value) {
|
|
318
|
+
return object(value) && typeof value.name === "string" && (value.kind === "static" || value.kind === "dynamic") && typeof value.provenance === "string" && (value.version === undefined || typeof value.version === "string") && (value.headline === undefined || typeof value.headline === "string");
|
|
319
|
+
}
|
|
320
|
+
function isCatalogError(value) {
|
|
321
|
+
return object(value) && object(value.error) && typeof value.error.message === "string";
|
|
322
|
+
}
|
|
323
|
+
function catalogSectionTitle(label, count, theme) {
|
|
324
|
+
return theme.fg("accent", theme.bold(`${label} (${String(count)})`));
|
|
325
|
+
}
|
|
326
|
+
function catalogIndexEntries(entries, theme) {
|
|
327
|
+
const width = Math.max(0, ...entries.map((entry) => entry.name.length));
|
|
328
|
+
return entries.map((entry) => ` ${theme.fg("accent", entry.name.padEnd(width))} ${theme.fg("toolOutput", catalogText(entry.description))}`);
|
|
329
|
+
}
|
|
330
|
+
function formatCatalogIndex(catalog, theme) {
|
|
331
|
+
const aliases = Object.prototype.propertyIsEnumerable.call(catalog, "modelAliases") ? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" })) : catalog.modelAliasEntries ?? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" }));
|
|
332
|
+
const aliasWidth = Math.max(0, ...aliases.map(({ name }) => name.length));
|
|
333
|
+
const aliasLines = aliases.map(({ name, kind, provenance }) => ` ${theme.fg("accent", name.padEnd(aliasWidth))} ${theme.fg("toolOutput", `${kind} · ${provenance}`)}`);
|
|
334
|
+
return [
|
|
335
|
+
catalogSectionTitle("Functions", catalog.functions.length, theme),
|
|
336
|
+
...catalogIndexEntries(catalog.functions, theme),
|
|
337
|
+
"",
|
|
338
|
+
catalogSectionTitle("Model aliases", aliases.length, theme),
|
|
339
|
+
...aliasLines,
|
|
340
|
+
].join("\n");
|
|
341
|
+
}
|
|
342
|
+
function catalogSchemaLines(schema, theme) {
|
|
343
|
+
const json = JSON.stringify(schema, null, 2);
|
|
344
|
+
return json.split("\n").map((line) => ` ${theme.fg("toolOutput", line)}`);
|
|
345
|
+
}
|
|
346
|
+
function formatCatalogDetail(value, expanded, theme) {
|
|
347
|
+
if ("kind" in value)
|
|
348
|
+
return [theme.fg("accent", theme.bold("Model alias")), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", `${value.kind} · ${value.provenance}`)}`].join("\n");
|
|
349
|
+
const kind = "Function";
|
|
350
|
+
if (!expanded)
|
|
351
|
+
return [theme.fg("accent", theme.bold(kind)), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", catalogText(value.description))}`, ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)} ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", catalogText(value.headline))}`].join("\n");
|
|
352
|
+
const lines = [theme.fg("accent", theme.bold(`${kind}: ${value.name}`)), `${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.description)}`, "", theme.fg("accent", theme.bold("Extension")), ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)}`, ` ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", catalogText(value.headline))}`, "", theme.fg("accent", theme.bold("Schema")), theme.fg("muted", "Input schema"), ...catalogSchemaLines(value.input, theme), "", theme.fg("muted", "Output schema"), ...catalogSchemaLines(value.output, theme)];
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
export function formatWorkflowCatalog(value, expanded, theme) {
|
|
356
|
+
if (isCatalogIndex(value))
|
|
357
|
+
return formatCatalogIndex(value, theme);
|
|
358
|
+
if (isCatalogFunction(value))
|
|
359
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
360
|
+
if (isWorkflowCatalogModelAlias(value))
|
|
361
|
+
return formatCatalogDetail(value, expanded, theme);
|
|
362
|
+
if (isCatalogError(value))
|
|
363
|
+
return theme.fg("error", value.error.message);
|
|
364
|
+
return theme.fg("error", "The workflow catalog returned an invalid result.");
|
|
365
|
+
}
|
|
366
|
+
const ANSI_SGR_SOURCE = `${String.fromCharCode(27)}\\[[0-9;]*m`;
|
|
367
|
+
const ANSI_SGR = new RegExp(ANSI_SGR_SOURCE);
|
|
368
|
+
export function truncateWorkflowProgress(text, width) {
|
|
369
|
+
const safeWidth = Math.max(1, width);
|
|
370
|
+
return text.split("\n").flatMap((line) => {
|
|
371
|
+
if (!line)
|
|
372
|
+
return [""];
|
|
373
|
+
const visualLines = truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines;
|
|
374
|
+
if (visualLines.length <= 1)
|
|
375
|
+
return [visualLines[0]?.trimEnd() ?? ""];
|
|
376
|
+
if (safeWidth === 1)
|
|
377
|
+
return [ANSI_SGR.test(line) ? "…\u001b[0m" : "…"];
|
|
378
|
+
const prefix = (truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, safeWidth - 1, 0).visualLines[0] ?? "").trimEnd();
|
|
379
|
+
const truncated = `${prefix}…`;
|
|
380
|
+
return [ANSI_SGR.test(line) ? `${truncated}\u001b[0m` : truncated];
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
export function themeWorkflowProgressStyles(theme) {
|
|
384
|
+
return {
|
|
385
|
+
accent: (text) => theme.fg("accent", text),
|
|
386
|
+
success: (text) => theme.fg("success", text),
|
|
387
|
+
error: (text) => theme.fg("error", text),
|
|
388
|
+
warning: (text) => theme.fg("warning", text),
|
|
389
|
+
muted: (text) => theme.fg("muted", text),
|
|
390
|
+
dim: (text) => theme.fg("dim", text),
|
|
391
|
+
bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
export function workflowProgressBlock(run, theme, progress, refresh, invalidate, prefix, freezeAt) {
|
|
395
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
396
|
+
let expanded = false;
|
|
397
|
+
let frozenAt = freezeAt ?? Date.now();
|
|
398
|
+
let previousState = run.state;
|
|
399
|
+
const currentRun = () => {
|
|
400
|
+
const displayed = progress?.run ?? run;
|
|
401
|
+
if (!progress || displayed.state !== "running")
|
|
402
|
+
return displayed;
|
|
403
|
+
const durationMs = Math.max(displayed.usage?.durationMs ?? 0, progress.runtimeBaseMs + Date.now() - progress.runtimeStartedAt);
|
|
404
|
+
return { ...displayed, usage: { ...budgetUsage(displayed.usage), durationMs } };
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
render(width) {
|
|
408
|
+
const displayed = currentRun();
|
|
409
|
+
const terminal = HARD_TERMINAL_RUN_STATES.has(displayed.state);
|
|
410
|
+
let now = Date.now();
|
|
411
|
+
if (freezeAt !== undefined || terminal) {
|
|
412
|
+
if (previousState !== displayed.state && !HARD_TERMINAL_RUN_STATES.has(previousState))
|
|
413
|
+
frozenAt = now;
|
|
414
|
+
now = frozenAt;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
frozenAt = now;
|
|
418
|
+
}
|
|
419
|
+
previousState = displayed.state;
|
|
420
|
+
const frame = displayed.state === "running" ? workflowSpinner[Math.floor(now / 80) % workflowSpinner.length] ?? "◇" : "◇";
|
|
421
|
+
const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true);
|
|
422
|
+
return truncateWorkflowProgress(prefix ? `${prefix}\n\n${progressText}` : progressText, width);
|
|
423
|
+
},
|
|
424
|
+
setExpanded(value) { expanded = value; },
|
|
425
|
+
invalidate() {
|
|
426
|
+
const displayed = currentRun();
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0))
|
|
429
|
+
return;
|
|
430
|
+
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
|
|
431
|
+
return;
|
|
432
|
+
progress.lastRefreshAt = now;
|
|
433
|
+
const inputRun = progress.inputRun;
|
|
434
|
+
const pending = refresh().then((next) => {
|
|
435
|
+
if (next && progress.inputRun === inputRun) {
|
|
436
|
+
progress.run = next;
|
|
437
|
+
invalidate?.();
|
|
438
|
+
}
|
|
439
|
+
}).catch(() => undefined);
|
|
440
|
+
progress.refresh = pending;
|
|
441
|
+
void pending.finally(() => {
|
|
442
|
+
if (progress.refresh === pending)
|
|
443
|
+
delete progress.refresh;
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
export function formatBudgetStatus(run) {
|
|
449
|
+
const usage = budgetUsage(run.usage);
|
|
450
|
+
if (!run.budget || !Object.keys(run.budget).length)
|
|
451
|
+
return ["Budget: unlimited"];
|
|
452
|
+
const lines = [`Budget version ${String(run.budgetVersion ?? 1)}`];
|
|
453
|
+
for (const dimension of BUDGET_DIMENSIONS) {
|
|
454
|
+
const limits = run.budget[dimension];
|
|
455
|
+
if (!limits || (limits.soft === undefined && limits.hard === undefined))
|
|
456
|
+
continue;
|
|
457
|
+
const limit = limits.hard ?? limits.soft;
|
|
458
|
+
const percent = limit === undefined ? "" : ` ${limit === 0 ? "100.0" : ((usage[dimension] / limit) * 100).toFixed(1)}%`;
|
|
459
|
+
const state = (run.budgetEvents ?? []).filter((event) => event.dimensions.includes(dimension)).at(-1)?.type;
|
|
460
|
+
lines.push(` ${dimension}: ${String(usage[dimension])}${limits.soft !== undefined ? ` soft=${String(limits.soft)}` : ""}${limits.hard !== undefined ? ` hard=${String(limits.hard)}` : ""}${percent}${state ? ` state=${state}` : ""}`);
|
|
461
|
+
}
|
|
462
|
+
const events = run.budgetEvents ?? [];
|
|
463
|
+
if (events.length)
|
|
464
|
+
lines.push(` events: ${events.map((event) => `${event.type}@v${String(event.budgetVersion)}`).join(", ")}`);
|
|
465
|
+
return lines;
|
|
466
|
+
}
|
|
467
|
+
function formatCompactBudgetStatus(run) {
|
|
468
|
+
if (!Object.values(run.budget ?? {}).some((limits) => limits.soft !== undefined || limits.hard !== undefined))
|
|
469
|
+
return [];
|
|
470
|
+
return formatBudgetStatus(run);
|
|
471
|
+
}
|
|
472
|
+
const ATTENTION_ORDER = { awaiting_input: 0, budget_exhausted: 1, running: 2, pausing: 3, paused: 4, interrupted: 5, failed: 6, queued: 7, stopped: 8, completed: 9 };
|
|
473
|
+
export function navigatorAttentionSortByState(entries, stateOf, resolvedAtOf, stateOrder = ATTENTION_ORDER) {
|
|
474
|
+
return [...entries].sort((left, right) => {
|
|
475
|
+
const leftResolvedAt = resolvedAtOf(left);
|
|
476
|
+
const rightResolvedAt = resolvedAtOf(right);
|
|
477
|
+
if (leftResolvedAt !== undefined || rightResolvedAt !== undefined) {
|
|
478
|
+
if (leftResolvedAt === undefined)
|
|
479
|
+
return -1;
|
|
480
|
+
if (rightResolvedAt === undefined)
|
|
481
|
+
return 1;
|
|
482
|
+
if (leftResolvedAt !== rightResolvedAt)
|
|
483
|
+
return rightResolvedAt - leftResolvedAt;
|
|
484
|
+
}
|
|
485
|
+
return (stateOrder[stateOf(left)] ?? 9) - (stateOrder[stateOf(right)] ?? 9);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
export function navigatorAttentionSort(entries) {
|
|
489
|
+
return navigatorAttentionSortByState(entries, (entry) => entry.loaded.run.state, (entry) => entry.resolvedAt);
|
|
490
|
+
}
|
|
491
|
+
export function navigatorRunLabels(entries) {
|
|
492
|
+
const nameCount = new Map();
|
|
493
|
+
for (const { loaded: { run } } of entries)
|
|
494
|
+
nameCount.set(run.workflowName, (nameCount.get(run.workflowName) ?? 0) + 1);
|
|
495
|
+
return entries.map(({ store, loaded: { run } }) => {
|
|
496
|
+
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
497
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
498
|
+
const suffix = (nameCount.get(run.workflowName) ?? 0) > 1 ? ` ${store.runId.slice(0, 8)}` : "";
|
|
499
|
+
const cost = run.agents.reduce((sum, a) => sum + (a.accounting?.cost ?? 0), 0);
|
|
500
|
+
const costStr = formatCost(cost);
|
|
501
|
+
const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
502
|
+
return `${glyph} ${run.workflowName}${suffix} ${run.state} ${run.phase ?? ""} ${String(done)}/${String(run.agents.length)} agents${costStr ? ` ${costStr}` : ""}${runtime}`;
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
export function agentBreadcrumbParts(agent, byId, includeStructuralPath = false) {
|
|
506
|
+
const leaf = agent.label ?? agent.name;
|
|
507
|
+
const parts = includeStructuralPath && agent.structuralPath?.length ? [agent.structuralPath.join(" > ")] : [];
|
|
508
|
+
if (agent.parentBreadcrumb)
|
|
509
|
+
parts.push(agent.parentBreadcrumb);
|
|
510
|
+
const ancestors = [];
|
|
511
|
+
const seen = new Set([agent.id]);
|
|
512
|
+
for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
|
|
513
|
+
if (seen.has(parentId))
|
|
514
|
+
break;
|
|
515
|
+
seen.add(parentId);
|
|
516
|
+
const parent = byId.get(parentId);
|
|
517
|
+
if (!parent)
|
|
518
|
+
break;
|
|
519
|
+
ancestors.push(parent.label ?? parent.name);
|
|
520
|
+
}
|
|
521
|
+
parts.push(...ancestors.reverse(), leaf);
|
|
522
|
+
return parts;
|
|
523
|
+
}
|
|
524
|
+
export function agentBreadcrumb(agent, byId, includeStructuralPath = false) {
|
|
525
|
+
return agentBreadcrumbParts(agent, byId, includeStructuralPath).join(" > ");
|
|
526
|
+
}
|
|
527
|
+
function styledAgentBreadcrumb(agent, byId, styles) {
|
|
528
|
+
const parts = agentBreadcrumbParts(agent, byId);
|
|
529
|
+
if (parts.length <= 1)
|
|
530
|
+
return parts[0] ?? "";
|
|
531
|
+
return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
|
|
532
|
+
}
|
|
533
|
+
export function formatStalledDuration(durationMs) {
|
|
534
|
+
const minutes = Math.max(0, Math.floor(durationMs / 60_000));
|
|
535
|
+
if (minutes < 60)
|
|
536
|
+
return `${String(minutes)}m`;
|
|
537
|
+
const hours = Math.floor(minutes / 60);
|
|
538
|
+
const remainingMinutes = minutes % 60;
|
|
539
|
+
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
540
|
+
}
|
|
541
|
+
function stalledDuration(agent, now) {
|
|
542
|
+
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
|
|
543
|
+
return undefined;
|
|
544
|
+
const duration = now - agent.lastEventAt;
|
|
545
|
+
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
546
|
+
}
|
|
547
|
+
/** A recorded duration wins; otherwise the elapsed time since start up to `end`, when both are finite. */
|
|
548
|
+
function elapsedDurationMs(agent, end) {
|
|
549
|
+
if (agent.durationMs !== undefined && Number.isFinite(agent.durationMs))
|
|
550
|
+
return Math.max(0, agent.durationMs);
|
|
551
|
+
return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
|
|
552
|
+
}
|
|
553
|
+
function agentActivityLabel(agent) {
|
|
554
|
+
const activity = agent.activity;
|
|
555
|
+
if (activity?.kind === "reasoning" || activity?.kind === "text") {
|
|
556
|
+
const name = activity.kind === "reasoning" ? "reasoning" : "responding";
|
|
557
|
+
const text = sanitizeDisplayText(activity.text);
|
|
558
|
+
return text && text !== name ? `${name} · ${text}` : name;
|
|
559
|
+
}
|
|
560
|
+
if (activity?.kind === "tool")
|
|
561
|
+
return sanitizeDisplayText(activity.text);
|
|
562
|
+
return sanitizeDisplayText([...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "");
|
|
563
|
+
}
|
|
564
|
+
function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
565
|
+
const label = agentActivityLabel(agent);
|
|
566
|
+
const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
|
|
567
|
+
const stalled = stalledDuration(agent, now);
|
|
568
|
+
if (stalled === undefined)
|
|
569
|
+
return activity;
|
|
570
|
+
const warning = `stalled? ${formatStalledDuration(stalled)}`;
|
|
571
|
+
return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
|
|
572
|
+
}
|
|
573
|
+
function formatWorkflowAgentDetail(agent, now) {
|
|
574
|
+
const durationMs = elapsedDurationMs(agent, now);
|
|
575
|
+
const accounting = agent.accounting;
|
|
576
|
+
return [
|
|
577
|
+
`${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`,
|
|
578
|
+
accounting ? formatTokens(accounting.input + accounting.output) : "",
|
|
579
|
+
accounting ? formatCost(accounting.cost) : "",
|
|
580
|
+
durationMs === undefined ? "" : formatWorkflowRuntime(durationMs),
|
|
581
|
+
agent.attempts > 1 ? `attempt ${String(agent.attempts)}` : "",
|
|
582
|
+
].filter(Boolean).join(" · ");
|
|
583
|
+
}
|
|
584
|
+
function formatAccountingValue(value) {
|
|
585
|
+
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
|
|
586
|
+
}
|
|
587
|
+
function formatAccounting(accounting) {
|
|
588
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
589
|
+
return `${formatAccountingValue(total)} tok`;
|
|
590
|
+
}
|
|
591
|
+
function formatAgentError(error, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
|
|
592
|
+
return styles.error(`Error: ${error.code}: ${error.message}`);
|
|
593
|
+
}
|
|
594
|
+
export function formatAgentDetail(agent, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), options = {}) {
|
|
595
|
+
const duration = elapsedDurationMs(agent, agent.finishedAt ?? now);
|
|
596
|
+
const stalled = stalledDuration(agent, now);
|
|
597
|
+
const state = phaseStyleForState(agent.state, styles);
|
|
598
|
+
const model = agent.model === undefined ? undefined : `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
|
|
599
|
+
const tools = agent.tools?.join(", ") || "(none)";
|
|
600
|
+
const lines = [
|
|
601
|
+
...(agent.activity ? [`Activity: ${agentActivityLabel(agent) || agent.activity.kind}`] : []),
|
|
602
|
+
...(stalled === undefined ? [] : [styles.warning(`stalled? ${formatStalledDuration(stalled)}`)]),
|
|
603
|
+
`State: ${state(agent.state)}`,
|
|
604
|
+
...(agent.structuralPath?.length ? [`Structural path: ${agent.structuralPath.join(" > ")}`] : []),
|
|
605
|
+
...(model === undefined ? [] : [`Model: ${model}`]),
|
|
606
|
+
`Role: ${agent.role ?? "(none)"}`,
|
|
607
|
+
`Tools: ${tools}`,
|
|
608
|
+
...(agent.attempts !== undefined && Number.isFinite(agent.attempts) && agent.attempts > 1 ? [`Attempts: ${String(agent.attempts)}`] : []),
|
|
609
|
+
...(duration === undefined ? [] : [`Duration: ${formatWorkflowRuntime(duration)}`]),
|
|
610
|
+
...(agent.accounting ? formatAgentAccounting(agent.accounting) : []),
|
|
611
|
+
];
|
|
612
|
+
if (agent.error && options.includeError !== false)
|
|
613
|
+
lines.push(formatAgentError(agent.error, styles));
|
|
614
|
+
return lines;
|
|
615
|
+
}
|
|
616
|
+
export function agentActionLabels(options) {
|
|
617
|
+
return [
|
|
618
|
+
...options.extensionLabels,
|
|
619
|
+
...(options.hasWorktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
620
|
+
...(options.openPrompt ? ["Open prompt in editor"] : []),
|
|
621
|
+
...(options.openSystemPrompt ? ["Open system prompt in editor"] : []),
|
|
622
|
+
...(options.openResult ? ["Open result in editor"] : []),
|
|
623
|
+
...(options.standaloneState === "running" ? ["Steer", "Stop"] : []),
|
|
624
|
+
...(options.standaloneState === "failed" || options.standaloneState === "stopped" ? ["Retry"] : []),
|
|
625
|
+
"Copy agent ID",
|
|
626
|
+
"Back",
|
|
627
|
+
];
|
|
628
|
+
}
|
|
629
|
+
export function visibleAgentAttemptActions(actions, context) {
|
|
630
|
+
return Object.entries(actions).filter(([, action]) => {
|
|
631
|
+
try {
|
|
632
|
+
return action.visible(context);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
export function visibleStandaloneAgentAttemptActions(actions, context) {
|
|
640
|
+
return Object.entries(actions).filter(([, action]) => {
|
|
641
|
+
try {
|
|
642
|
+
return action.visibleStandalone?.(context) === true;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function formatAgentAccounting(accounting) {
|
|
650
|
+
const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
|
|
651
|
+
return [`Tokens: ∑${formatAccountingValue(total)} ↑${formatAccountingValue(accounting.input)} ↓${formatAccountingValue(accounting.output)} ⇢${formatAccountingValue(accounting.cacheRead)} ⇠${formatAccountingValue(accounting.cacheWrite)}`, `Cost: ${formatCost(accounting.cost) || "$0.00"}`];
|
|
652
|
+
}
|
|
653
|
+
export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
|
|
654
|
+
void worktrees;
|
|
655
|
+
const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
|
|
656
|
+
const totalAccounting = sumAccounting(run.agents.flatMap((a) => a.accounting ? [a.accounting] : []));
|
|
657
|
+
const hasAccounting = run.agents.some((a) => a.accounting);
|
|
658
|
+
const glyph = runStateGlyph(run.state, "⠦");
|
|
659
|
+
const header = `${glyph} ${run.workflowName}`;
|
|
660
|
+
const runtime = run.usage ? `runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
661
|
+
const meta = [run.state, run.phase ? `phase: ${run.phase}` : "", `${String(done)}/${String(run.agents.length)} agents`, runtime, hasAccounting ? formatAccounting(totalAccounting) : "", formatCost(totalAccounting.cost)].filter(Boolean).join(" · ");
|
|
662
|
+
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
663
|
+
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
664
|
+
if (shellActivity)
|
|
665
|
+
lines.push(` ${shellActivity}`);
|
|
666
|
+
if (run.error)
|
|
667
|
+
lines.push(`Error: ${run.error.code}: ${run.error.message}`);
|
|
668
|
+
if (run.events?.length)
|
|
669
|
+
lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
670
|
+
lines.push("");
|
|
671
|
+
const byId = new Map(run.agents.map((a) => [a.id, a]));
|
|
672
|
+
const render = ({ agent, depth }, grouped) => {
|
|
673
|
+
const icon = agentStateGlyph(agent.state, "⠦");
|
|
674
|
+
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
675
|
+
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
676
|
+
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
677
|
+
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
|
|
678
|
+
if (agent.state === "failed" && agent.attemptDetails?.length) {
|
|
679
|
+
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
680
|
+
if (last?.error)
|
|
681
|
+
result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
682
|
+
}
|
|
683
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
684
|
+
if (activity)
|
|
685
|
+
result.push(`${indent} ${activity}`);
|
|
686
|
+
return result.join("\n");
|
|
687
|
+
};
|
|
688
|
+
lines.push(...renderGroupedAgents(run.agents, render));
|
|
689
|
+
if (checkpoints.length) {
|
|
690
|
+
lines.push("");
|
|
691
|
+
for (const cp of checkpoints)
|
|
692
|
+
lines.push(`● checkpoint ${cp.name}: ${cp.prompt}`);
|
|
693
|
+
}
|
|
694
|
+
return lines.join("\n");
|
|
695
|
+
}
|
|
696
|
+
export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
|
|
697
|
+
const { run, snapshot } = loaded;
|
|
698
|
+
const lines = [
|
|
699
|
+
`Workflow: ${run.workflowName}`,
|
|
700
|
+
`Run: ${run.id}`,
|
|
701
|
+
`Status: ${run.state}`,
|
|
702
|
+
`Phase: ${run.phase ?? "(none)"}`,
|
|
703
|
+
`Launch cwd: ${run.cwd}`,
|
|
704
|
+
...formatCompactBudgetStatus(run),
|
|
705
|
+
`Launch models: ${snapshot.models.join(", ") || "(none)"}`,
|
|
706
|
+
`Settings: concurrency=${String(snapshot.settings.concurrency)}`,
|
|
707
|
+
];
|
|
708
|
+
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
709
|
+
if (shellActivity)
|
|
710
|
+
lines.push(` ${shellActivity}`);
|
|
711
|
+
if (run.error)
|
|
712
|
+
lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
713
|
+
if (run.events?.length)
|
|
714
|
+
lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
715
|
+
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
716
|
+
if (aliases && Object.keys(aliases).length)
|
|
717
|
+
lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
|
|
718
|
+
if (snapshot.settingsSources)
|
|
719
|
+
lines.push(`Settings sources: concurrency=${snapshot.settingsSources.concurrency}, modelAliases=${snapshot.settingsSources.modelAliases}, skills=${snapshot.settingsSources.skills ?? "(none)"}, extensions=${snapshot.settingsSources.extensions ?? "(none)"}, tools=${snapshot.settingsSources.tools ?? "(none)"}`);
|
|
720
|
+
lines.push("Agents / ownership:");
|
|
721
|
+
if (!run.agents.length)
|
|
722
|
+
lines.push(" (none)");
|
|
723
|
+
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
724
|
+
lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
|
|
725
|
+
const model = `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
|
|
726
|
+
const role = agent.role ? ` role=${agent.role}` : "";
|
|
727
|
+
const tools = ` tools=${agent.tools.join(",") || "(none)"}`;
|
|
728
|
+
const accounting = agent.accounting ? ` input=${String(agent.accounting.input)} output=${String(agent.accounting.output)} cache-read=${String(agent.accounting.cacheRead)} cache-write=${String(agent.accounting.cacheWrite)} cost=${formatCost(agent.accounting.cost) || "$0.00"}` : "";
|
|
729
|
+
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
730
|
+
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${role}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
|
|
731
|
+
for (const attempt of agent.attemptDetails ?? [])
|
|
732
|
+
result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
733
|
+
for (const call of agent.toolCalls ?? [])
|
|
734
|
+
result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
735
|
+
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
736
|
+
if (activity)
|
|
737
|
+
result.push(`${indent} ${activity}`);
|
|
738
|
+
return result.join("\n");
|
|
739
|
+
}));
|
|
740
|
+
lines.push("Checkpoints:");
|
|
741
|
+
if (!checkpoints.length)
|
|
742
|
+
lines.push(" (none)");
|
|
743
|
+
for (const checkpoint of checkpoints)
|
|
744
|
+
lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
|
|
745
|
+
lines.push(`Worktrees: ${String(worktrees.length)}`);
|
|
746
|
+
lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
|
|
747
|
+
return lines.join("\n");
|
|
748
|
+
}
|
|
749
|
+
export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
|
|
750
|
+
const safeWidth = Math.max(1, width);
|
|
751
|
+
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
752
|
+
const tree = buildWorkflowPhaseTree(model);
|
|
753
|
+
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)))}`;
|
|
759
|
+
const phaseStyle = (state) => phaseStyleForState(state, styles);
|
|
760
|
+
const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
|
|
761
|
+
const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
|
|
762
|
+
const selectedNode = (selection.nodeId ? tree.byId.get(selection.nodeId) : undefined) ?? selectedByAgent ?? (phase ? tree.byId.get(workflowPhaseTreePath("phase", phase.id, [])) : undefined) ?? tree.byId.get("workflow") ?? (model.currentPhaseId ? tree.byId.get(workflowPhaseTreePath("phase", model.currentPhaseId, [])) : undefined) ?? tree.nodes[0];
|
|
763
|
+
const selectedPhase = selectedNode?.phase ?? (selectedNode ? model.phases.find((candidate) => candidate.id === selectedNode.phaseId) : undefined);
|
|
764
|
+
const currentPhase = model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex];
|
|
765
|
+
const visibleNodes = workflowPhaseTreeVisibleNodes(tree, expanded);
|
|
766
|
+
const nodeAgents = (node) => {
|
|
767
|
+
const agents = [];
|
|
768
|
+
const visit = (id) => {
|
|
769
|
+
const child = tree.byId.get(id);
|
|
770
|
+
if (!child)
|
|
771
|
+
return;
|
|
772
|
+
if (child.agent)
|
|
773
|
+
agents.push(child.agent);
|
|
774
|
+
else
|
|
775
|
+
for (const childId of child.children)
|
|
776
|
+
visit(childId);
|
|
777
|
+
};
|
|
778
|
+
if (node.agent)
|
|
779
|
+
agents.push(node.agent);
|
|
780
|
+
else
|
|
781
|
+
for (const childId of node.children)
|
|
782
|
+
visit(childId);
|
|
783
|
+
return agents;
|
|
784
|
+
};
|
|
785
|
+
const nodeStatus = (node) => phaseStyle(node.state)(node.state);
|
|
786
|
+
const nodeIcon = (node) => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : node.kind === "shell" ? "◇" : " ";
|
|
787
|
+
const treeLine = (node) => {
|
|
788
|
+
const selected = node.id === selectedNode?.id;
|
|
789
|
+
const state = progressStyleForState(node.state, styles);
|
|
790
|
+
const glyph = node.kind === "agent" && node.agent ? agentStateGlyph(node.agent.state, "⠦") : runStateGlyph(node.state, "⠦");
|
|
791
|
+
return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(glyph)}`;
|
|
792
|
+
};
|
|
793
|
+
const details = (node) => {
|
|
794
|
+
if (!node)
|
|
795
|
+
return [styles.muted("No workflow node is selected"), ...(selection.actions ? [] : [styles.muted("enter run actions")])];
|
|
796
|
+
const agents = nodeAgents(node);
|
|
797
|
+
if (node.kind === "workflow") {
|
|
798
|
+
const counts = phaseAgentCounts(agents);
|
|
799
|
+
return [styles.bold(`Selected workflow: ${run.workflowName}`), `Run ID: ${run.id}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted("enter run actions")])];
|
|
800
|
+
}
|
|
801
|
+
if (node.kind === "phase") {
|
|
802
|
+
const selected = node.phase;
|
|
803
|
+
const counts = selected?.counts ?? phaseAgentCounts(agents);
|
|
804
|
+
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
805
|
+
return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
806
|
+
}
|
|
807
|
+
if (node.kind === "operation") {
|
|
808
|
+
const states = phaseAgentCounts(agents);
|
|
809
|
+
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
810
|
+
return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
811
|
+
}
|
|
812
|
+
if (node.kind === "shell") {
|
|
813
|
+
return [styles.bold("Selected shell"), formatShellActivity(node.shellActivity?.active, node.shellActivity?.startedAt, "⠦", styles, now) ?? "shell [running]"];
|
|
814
|
+
}
|
|
815
|
+
const agent = node.agent;
|
|
816
|
+
if (!agent)
|
|
817
|
+
return [styles.muted("Agent details are unavailable")];
|
|
818
|
+
const attemptError = agent.attemptDetails?.at(-1)?.error;
|
|
819
|
+
const detail = formatAgentDetail(attemptError === undefined ? agent : { ...agent, error: attemptError }, styles, now, { includeError: false });
|
|
820
|
+
const errorLine = attemptError === undefined ? undefined : formatAgentError(attemptError, styles);
|
|
821
|
+
return [...detail, ...(selection.actions ? [] : [styles.muted("enter agent actions")]), ...(errorLine === undefined ? [] : [errorLine])];
|
|
822
|
+
};
|
|
823
|
+
const statusSummary = WORKFLOW_PHASE_STATES.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
|
|
824
|
+
const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
|
|
825
|
+
if (run.error)
|
|
826
|
+
lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
|
|
827
|
+
const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
|
|
828
|
+
lines.push(`phase: ${run.phase ?? selectedPhase?.name ?? currentPhase?.name ?? "none"}`, `Run state: ${run.state}${runtime}`, `Phases: ${statusSummary}`);
|
|
829
|
+
for (const event of run.events?.filter((event) => event.type === "warning") ?? [])
|
|
830
|
+
lines.push(styles.warning(`Warning: ${event.message}`));
|
|
831
|
+
lines.push(...formatCompactBudgetStatus(run));
|
|
832
|
+
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
833
|
+
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
834
|
+
if (shellActivity)
|
|
835
|
+
lines.push(` ${shellActivity}`);
|
|
836
|
+
const renderTree = (limit) => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
|
|
837
|
+
const actionRows = () => {
|
|
838
|
+
const actions = selection.actions;
|
|
839
|
+
if (!actions)
|
|
840
|
+
return [];
|
|
841
|
+
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
842
|
+
};
|
|
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
|
+
}
|
|
861
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
|
|
862
|
+
lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
863
|
+
return lines.flatMap((line) => wrap(line));
|
|
864
|
+
}
|
|
865
|
+
export function formatCheckpointReview(checkpoint) {
|
|
866
|
+
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
867
|
+
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
868
|
+
}
|