@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
package/src/host-delivery.ts
CHANGED
|
@@ -102,6 +102,7 @@ const WORKFLOW_ERROR_PROSE: Record<WorkflowErrorCode, (detail: string) => string
|
|
|
102
102
|
RUN_NOT_FOUND: (detail) => /^Unknown workflow run\b/.test(detail) ? "The workflow run was not found." : `The workflow run was not found: ${detail}.`,
|
|
103
103
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
104
104
|
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
105
|
+
TOOL_FAILED: (detail) => `The workflow tool call failed: ${detail}.`,
|
|
105
106
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
106
107
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
107
108
|
AGENT_RESULT_COLLECTED: (detail) => `The nested agent result was already collected: ${detail}.`,
|
package/src/host-navigator.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type WorkflowRegistryApi } from "./registry.js";
|
|
|
6
6
|
import { deepFreeze, errorText, object, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
7
7
|
import { saveModelAliases, resolveWorkflowSettings, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
|
|
8
8
|
import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
9
|
-
import { agentActionLabels, agentBreadcrumb, formatCheckpointReview, formatNavigatorRun, formatWorkflowPhaseDashboard, navigatorAttentionSort, navigatorRunLabels, SETTLED_AGENT_STATES, themeWorkflowProgressStyles, visibleAgentAttemptActions as visibleRegisteredAgentAttemptActions } from "./host-view.js";
|
|
9
|
+
import { agentActionLabels, agentBreadcrumb, formatCheckpointReview, formatNavigatorRun, formatWorkflowPhaseDashboard, navigatorAttentionSort, navigatorRunLabels, SETTLED_AGENT_STATES, themeWorkflowProgressStyles, visibleAgentAttemptActions as visibleRegisteredAgentAttemptActions, type WorkflowProgressStyles } from "./host-view.js";
|
|
10
10
|
import { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded } from "./host-phases.js";
|
|
11
11
|
import { type WorkflowRecoveryContext, type createWorkflowRecovery } from "./host-recovery.js";
|
|
12
12
|
import { runDependencyIds } from "./retention.js";
|
|
@@ -115,14 +115,52 @@ function keybindingKeys(keybindings: unknown, name: string): readonly string[] |
|
|
|
115
115
|
}
|
|
116
116
|
type WorkflowKeybindings = { matches(data: string, binding: string): boolean };
|
|
117
117
|
const WORKFLOW_VIM_KEYS: Readonly<Record<string, string>> = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
118
|
-
function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
119
|
-
|
|
118
|
+
export function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
119
|
+
const WORKFLOW_KEY_LABELS: Readonly<Record<string, string>> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
120
|
+
export function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels: Readonly<Record<string, string>> = WORKFLOW_KEY_LABELS): string {
|
|
120
121
|
const keys = keybindingKeys(keybindings, binding);
|
|
121
122
|
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
122
123
|
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
123
124
|
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* A yes/no question drawn inside a navigator dashboard. Pi's confirm dialog replaces a custom component and afterwards
|
|
129
|
+
* restores the editor rather than the component, which leaves the dashboard's promise pending and the session waiting on it.
|
|
130
|
+
*/
|
|
131
|
+
export function createInlineConfirm(requestRender: () => void) {
|
|
132
|
+
let pending: { readonly title: string; readonly message: string; yes: boolean; readonly resolve: (value: boolean) => void } | undefined;
|
|
133
|
+
const answer = (value: boolean): void => {
|
|
134
|
+
const current = pending;
|
|
135
|
+
pending = undefined;
|
|
136
|
+
current?.resolve(value);
|
|
137
|
+
requestRender();
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
ask(title: string, message: string): Promise<boolean> {
|
|
141
|
+
answer(false);
|
|
142
|
+
return new Promise<boolean>((resolve) => { pending = { title, message, yes: true, resolve }; requestRender(); });
|
|
143
|
+
},
|
|
144
|
+
active(): boolean { return pending !== undefined; },
|
|
145
|
+
rows(width: number, styles: WorkflowProgressStyles): string[] {
|
|
146
|
+
if (!pending) return [];
|
|
147
|
+
const yes = pending.yes;
|
|
148
|
+
const message = truncateToVisualLines(pending.message, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
149
|
+
return [styles.bold(pending.title), ...message, ...["Yes", "No"].map((option) => (option === "Yes") === yes ? `→ ${styles.accent(option)}` : ` ${option}`)];
|
|
150
|
+
},
|
|
151
|
+
hint(keybindings: unknown): string {
|
|
152
|
+
return `${workflowKeyLabel(keybindings, "tui.select.up", "↑")}/${workflowKeyLabel(keybindings, "tui.select.down", "↓")} select · ${workflowKeyLabel(keybindings, "tui.select.confirm", "enter")} confirm · ${workflowKeyLabel(keybindings, "tui.select.cancel", "esc")} cancel`;
|
|
153
|
+
},
|
|
154
|
+
handleInput(keybindings: WorkflowKeybindings, data: string): void {
|
|
155
|
+
if (!pending) return;
|
|
156
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up") || workflowKeyMatches(keybindings, data, "tui.select.down")) { pending.yes = !pending.yes; requestRender(); }
|
|
157
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) answer(pending.yes);
|
|
158
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) answer(false);
|
|
159
|
+
},
|
|
160
|
+
cancel(): void { answer(false); },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
export type WorkflowNavigatorDependencies = {
|
|
127
165
|
pi: Pick<ExtensionAPI, "registerCommand">;
|
|
128
166
|
home: string | undefined;
|
|
@@ -189,7 +227,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
189
227
|
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
190
228
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
191
229
|
};
|
|
192
|
-
const runAction = async (actionCommand: string, status: (text: string | undefined) => void = setWorkflowStatus): Promise<"dashboard" | "picker" | "stopped"> => {
|
|
230
|
+
const runAction = async (actionCommand: string, status: (text: string | undefined) => void = setWorkflowStatus, confirm: (title: string, message: string) => Promise<boolean> = (title, message) => confirmWithBlocked(ctx.ui, reportBlocked, title, message)): Promise<"dashboard" | "picker" | "stopped"> => {
|
|
193
231
|
const [action, runId, ...rest] = actionCommand.split(/\s+/);
|
|
194
232
|
try {
|
|
195
233
|
const run = runId ? runs.get(runId) : undefined;
|
|
@@ -263,7 +301,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
263
301
|
}
|
|
264
302
|
if (action === "stop" && run) {
|
|
265
303
|
const workflowName = stored?.loaded.run.workflowName ?? run.metadata.name;
|
|
266
|
-
if (!await
|
|
304
|
+
if (!await confirm("Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`)) return "dashboard";
|
|
267
305
|
status(`Stopping workflow ${workflowName}...`);
|
|
268
306
|
await stopWorkflowRun(run.store.runId);
|
|
269
307
|
status(`Workflow ${run.store.runId} stopped.`);
|
|
@@ -554,8 +592,13 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
554
592
|
let selectedNodeId = tree.nodes[0]?.id;
|
|
555
593
|
let expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
556
594
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
557
|
-
const
|
|
558
|
-
const
|
|
595
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback);
|
|
596
|
+
const inlineConfirm = createInlineConfirm(() => { if (!disposed) tui.requestRender(); });
|
|
597
|
+
const confirmInDashboard = async (title: string, message: string): Promise<boolean> => {
|
|
598
|
+
reportBlocked?.(true, title);
|
|
599
|
+
try { return await inlineConfirm.ask(title, message); }
|
|
600
|
+
finally { reportBlocked?.(false); }
|
|
601
|
+
};
|
|
559
602
|
const progressNow = () => {
|
|
560
603
|
const now = Date.now();
|
|
561
604
|
if (hardTerminalRunStates.has(view.run.state)) {
|
|
@@ -629,7 +672,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
629
672
|
stopStatus = status;
|
|
630
673
|
setWorkflowStatus(status);
|
|
631
674
|
if (!disposed) tui.requestRender();
|
|
632
|
-
}).then(async (outcome) => {
|
|
675
|
+
}, confirmInDashboard).then(async (outcome) => {
|
|
633
676
|
if (outcome === "stopped") { done("__stopped__"); return; }
|
|
634
677
|
await updateDashboard();
|
|
635
678
|
}).catch((error: unknown) => {
|
|
@@ -662,7 +705,8 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
662
705
|
const content = [...statusLines, ...phaseLines];
|
|
663
706
|
const rows = terminalRows();
|
|
664
707
|
const hintRows = rows >= 3 ? 1 : 0;
|
|
665
|
-
const
|
|
708
|
+
const confirmRows = inlineConfirm.rows(width, styles);
|
|
709
|
+
const viewport = Math.max(1, rows - hintRows - confirmRows.length);
|
|
666
710
|
const maxOffset = Math.max(0, content.length - viewport);
|
|
667
711
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
668
712
|
if (actionMode) {
|
|
@@ -683,11 +727,12 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
683
727
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
684
728
|
const selectedNode = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
|
|
685
729
|
const enterAction = selectedNode?.kind === "workflow" ? "run actions" : selectedNode?.kind === "agent" ? "agent actions" : selectedNode?.children.length ? "expand/collapse" : narrow ? "inspect" : "focus details";
|
|
686
|
-
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} ${enterAction} · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
687
|
-
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
730
|
+
const hint = truncateToVisualLines(theme.fg("dim", inlineConfirm.active() ? inlineConfirm.hint(keybindings) : actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} ${enterAction} · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
731
|
+
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...confirmRows, ...(hintRows ? [hint] : [])];
|
|
688
732
|
},
|
|
689
733
|
invalidate() {},
|
|
690
734
|
handleInput(data: string) {
|
|
735
|
+
if (inlineConfirm.active()) { inlineConfirm.handleInput(keybindings, data); return; }
|
|
691
736
|
if (stopRequested || editorRunning) return;
|
|
692
737
|
const narrow = renderedWidth < 80;
|
|
693
738
|
if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
|
|
@@ -776,7 +821,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
776
821
|
}
|
|
777
822
|
tui.requestRender();
|
|
778
823
|
},
|
|
779
|
-
dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); },
|
|
824
|
+
dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); inlineConfirm.cancel(); },
|
|
780
825
|
};
|
|
781
826
|
})
|
|
782
827
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
@@ -816,8 +861,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
816
861
|
const currentLayout = layout();
|
|
817
862
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
818
863
|
offset = Math.min(offset, maxOffset);
|
|
819
|
-
const
|
|
820
|
-
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
864
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback);
|
|
821
865
|
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
822
866
|
const controls = currentLayout.compactControls
|
|
823
867
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
package/src/host-recovery.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
5
5
|
import { listRunIds, RunStore, structuralPath as operationPath, type PersistedRun } from "./persistence.js";
|
|
6
6
|
import { budgetUsage, budgetRelaxed, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
7
7
|
import { aliasDrift, createLaunchSnapshot, errorCode, errorText, jsonValue, object } from "./utils.js";
|
|
8
|
-
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
8
|
+
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
9
9
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
10
10
|
import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
|
|
11
11
|
import { runWorkflow } from "./execution.js";
|
|
@@ -13,7 +13,7 @@ import { createWorkflowFailureDiagnostics, formatWorkflowFailureDelivery, format
|
|
|
13
13
|
|
|
14
14
|
/** The subset of Pi's model registry the host relies on; every member is optional because older or headless hosts expose fewer of them. */
|
|
15
15
|
export type ModelRegistryCapability = { getAll?: () => readonly Model<Api>[]; getAvailable?: () => readonly Model<Api>[]; find?: (provider: string, model: string) => Model<Api> | undefined; refresh?: () => Promise<void>; getError?: () => string | undefined };
|
|
16
|
-
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
16
|
+
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
17
17
|
export type WorkflowRecoveryDependencies = {
|
|
18
18
|
pi: Pick<ExtensionAPI, "getThinkingLevel">;
|
|
19
19
|
home: string | undefined;
|
|
@@ -32,6 +32,7 @@ export type WorkflowRecoveryDependencies = {
|
|
|
32
32
|
resumeRoles: (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => { definitions: Readonly<Record<string, AgentDefinition>>; capture: (role: string, model: ModelSpec) => Promise<void> };
|
|
33
33
|
workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
|
|
34
34
|
shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
|
|
35
|
+
toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
|
|
35
36
|
resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
|
|
36
37
|
checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, headless?: boolean) => (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => Promise<boolean>;
|
|
37
38
|
phaseBridge: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => (phase: string) => Promise<void>;
|
|
@@ -74,7 +75,7 @@ function assertExpectedWorkflowState(expectedState: string | undefined, actualSt
|
|
|
74
75
|
export function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
75
76
|
|
|
76
77
|
export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
77
|
-
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
78
|
+
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
78
79
|
const coordinateRunMutation = deps.coordinateRunMutation ?? (<T>(task: () => Promise<T>): Promise<T> => task());
|
|
79
80
|
type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun; completion?: CompletionDeliveryResult };
|
|
80
81
|
const budgetDecisionDelivery = (metadata: WorkflowMetadata, request: BudgetApprovalRequest) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
|
|
@@ -111,7 +112,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
111
112
|
return { hasUI: host?.hasUI === true, ui };
|
|
112
113
|
};
|
|
113
114
|
type ColdResumeResult = { value: JsonValue; resultPath: string; resultBytes: number; completion: CompletionDeliveryResult };
|
|
114
|
-
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
115
|
+
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
115
116
|
const loaded = await run.store.load();
|
|
116
117
|
const foreground = modeOverride ?? (loaded.run.delivery?.mode === "foreground" || (loaded.run.delivery?.mode === "background" && loaded.run.delivery.toolCallId !== undefined) || (loaded.run.delivery === undefined && loaded.snapshot.launchMode === "foreground"));
|
|
117
118
|
if (loaded.run.activeShells !== undefined || loaded.run.activeShellStartedAt !== undefined || loaded.run.activeShellsByPhase !== undefined) await persistRunState(run.store, run.metadata, withoutActiveShells);
|
|
@@ -137,7 +138,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
137
138
|
run.executor.setRunContext(runContext);
|
|
138
139
|
await scheduler.cancelRun(run.store.runId);
|
|
139
140
|
await run.lifecycle.resume();
|
|
140
|
-
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
141
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ tool: (identifier, args, signal, identity) => toolForRun(run.store, run.lifecycle, context?.toolContext, identifier, args, signal, identity), shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
141
142
|
run.execution = execution;
|
|
142
143
|
const completion = execution.result.then(async (value) => {
|
|
143
144
|
await scheduler.flush(run.store.runId);
|
|
@@ -314,7 +315,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
314
315
|
await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
|
|
315
316
|
const { hasUI, ui } = recoveryUi(context);
|
|
316
317
|
const recoveryContext = resumeHostContext(context);
|
|
317
|
-
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
318
|
+
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, toolContext: recoveryContext.toolContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
318
319
|
const completion = runs.get(prepared.childRun.store.runId)?.completion;
|
|
319
320
|
if (completion) {
|
|
320
321
|
childStarted = true;
|
package/src/host-view.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { keyHint, truncateToVisualLines, type Theme } 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 { type AwaitingCheckpoint, type PersistedRun, type RunStore, type WorktreeReference } from "./persistence.js";
|
|
4
4
|
import { budgetUsage } from "./budget.js";
|
|
5
5
|
import { formatCost, formatTokens } from "./background-widget.js";
|
|
6
|
-
import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting, type AgentAccounting, type AgentAttemptAction, type AgentAttemptActionContext, type AgentRecord, type LaunchSnapshot, type StandaloneAgentAttemptActionContext, type WorkflowCatalogFunction, type WorkflowCatalogIndex } from "./types.js";
|
|
6
|
+
import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting, zeroAccounting, type AgentAccounting, type AgentAttemptAction, type AgentAttemptActionContext, type AgentRecord, type LaunchSnapshot, type StandaloneAgentAttemptActionContext, type WorkflowCatalogFunction, type WorkflowCatalogIndex } from "./types.js";
|
|
7
7
|
import { object, sanitizeDisplayText } from "./utils.js";
|
|
8
8
|
import {
|
|
9
9
|
WORKFLOW_PHASE_STATES,
|
|
@@ -28,7 +28,7 @@ export interface WorkflowProgressStyles {
|
|
|
28
28
|
dim(text: string): string;
|
|
29
29
|
bold(text: string): string;
|
|
30
30
|
}
|
|
31
|
-
const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
31
|
+
export const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
32
32
|
type AgentGroup = { label: string; entries: readonly { agent: AgentRecord; index: number; depth: number }[] };
|
|
33
33
|
function agentGroupKey(agent: AgentRecord): string { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
34
34
|
function agentGroupLabel(agents: readonly AgentRecord[]): string {
|
|
@@ -60,7 +60,7 @@ function renderGroupedAgents(agents: readonly AgentRecord[], render: (entry: { a
|
|
|
60
60
|
}
|
|
61
61
|
const RUN_STATE_GLYPH: Record<string, string> = { "not started": "○", queued: "○", pausing: "⏸", paused: "⏸", completed: "✓", failed: "✗", stopped: "✗", interrupted: "↯", budget_exhausted: "!", awaiting_input: "?" };
|
|
62
62
|
const AGENT_STATE_GLYPH: Record<string, string> = { queued: "○", waiting_for_child: "…", paused: "⏸", retrying: "↻", completed: "✓", failed: "✗", cancelled: "✗" };
|
|
63
|
-
function runStateGlyph(state: string, running: string): string { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
63
|
+
export function runStateGlyph(state: string, running: string): string { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
64
64
|
function agentStateGlyph(state: string, running: string): string { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
|
|
65
65
|
type ProgressStyleKey = "success" | "error" | "warning" | "accent" | "muted";
|
|
66
66
|
const PROGRESS_STATE_STYLE: Record<string, ProgressStyleKey> = { completed: "success", failed: "error", cancelled: "error", running: "accent", paused: "warning", pausing: "warning", interrupted: "warning", retrying: "accent", budget_exhausted: "warning", awaiting_input: "warning" };
|
|
@@ -70,10 +70,10 @@ function styleForState(map: Record<string, ProgressStyleKey>, state: string, sty
|
|
|
70
70
|
const key = map[state] ?? "muted";
|
|
71
71
|
return (text) => styles[key](text);
|
|
72
72
|
}
|
|
73
|
-
function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
73
|
+
export function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
74
74
|
function workflowIconStyle(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
75
75
|
function phaseStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
76
|
-
function formatWorkflowRuntime(durationMs: number): string {
|
|
76
|
+
export function formatWorkflowRuntime(durationMs: number): string {
|
|
77
77
|
const seconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
78
78
|
if (seconds < 60) return `${String(seconds)}s`;
|
|
79
79
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -90,13 +90,22 @@ function formatShellActivity(activeShells: number | undefined, startedAt: number
|
|
|
90
90
|
const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
|
|
91
91
|
return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
|
|
92
92
|
}
|
|
93
|
+
function formatToolActivity(run: Pick<PersistedRun, "activeTools">, spinner: string, styles: WorkflowProgressStyles, now: number): string | undefined {
|
|
94
|
+
const calls = run.activeTools ?? [];
|
|
95
|
+
if (!calls.length) return undefined;
|
|
96
|
+
const counts = new Map<string, number>();
|
|
97
|
+
for (const { name } of calls) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
98
|
+
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
|
|
99
|
+
const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
|
|
100
|
+
return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
|
|
101
|
+
}
|
|
93
102
|
function formatLogTimestamp(timestamp: number | undefined): string {
|
|
94
103
|
if (timestamp === undefined || !Number.isFinite(timestamp)) return "--:--:--";
|
|
95
104
|
const date = new Date(timestamp);
|
|
96
105
|
if (Number.isNaN(date.getTime())) return "--:--:--";
|
|
97
106
|
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
98
107
|
}
|
|
99
|
-
function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, expanded: boolean, width?: number, showHint = false): string[] {
|
|
108
|
+
function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, expanded: boolean, width?: number, showHint = false, tail = 5): string[] {
|
|
100
109
|
const events = (run.events ?? []).filter((event) => event.type === "log");
|
|
101
110
|
if (!events.length) return [];
|
|
102
111
|
const indent = " ".repeat(11);
|
|
@@ -106,12 +115,48 @@ function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, exp
|
|
|
106
115
|
const chunks = truncateToVisualLines(message, Number.MAX_SAFE_INTEGER, Math.max(1, width - 11), 0).visualLines;
|
|
107
116
|
return chunks.map((chunk, chunkIndex) => `${chunkIndex === 0 ? prefix : indent}${chunk}`);
|
|
108
117
|
}));
|
|
109
|
-
const visible = expanded ? visualLines : visualLines.slice(-
|
|
118
|
+
const visible = expanded ? visualLines : visualLines.slice(-tail);
|
|
110
119
|
let hint = "";
|
|
111
120
|
if (!expanded && showHint) { try { hint = ` (${keyHint("app.tools.expand", "to expand")})`; } catch { /* Theme is unavailable in non-interactive render tests. */ } }
|
|
112
121
|
return [expanded ? ` ${styles.muted("Logs")}` : ` ${styles.muted(`Logs${hint}`)}`, ...visible];
|
|
113
122
|
}
|
|
114
|
-
|
|
123
|
+
const ACTIVE_AGENT_STATES: ReadonlySet<string> = new Set(["running", "waiting_for_child", "retrying", "paused"]);
|
|
124
|
+
const COMPACT_RECENT_SETTLED = 3;
|
|
125
|
+
const COMPACT_LOG_LINES = 3;
|
|
126
|
+
function expandHint(): string { try { return ` (${keyHint("app.tools.expand", "to expand")})`; } catch { return ""; } }
|
|
127
|
+
function agentEnd(agent: AgentRecord): number { return (agent.startedAt ?? 0) + (agent.durationMs ?? 0); }
|
|
128
|
+
/** One line for a finished phase: outcome counts, wall time across its agents, tokens, and cost. */
|
|
129
|
+
function phaseSummary(agents: readonly AgentRecord[], styles: WorkflowProgressStyles): string {
|
|
130
|
+
const count = (state: string) => agents.filter((agent) => agent.state === state).length;
|
|
131
|
+
const starts = agents.map((agent) => agent.startedAt).filter((value): value is number => value !== undefined);
|
|
132
|
+
const span = starts.length ? Math.max(...agents.map(agentEnd)) - Math.min(...starts) : 0;
|
|
133
|
+
const accounting = sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()));
|
|
134
|
+
const failed = count("failed");
|
|
135
|
+
const parts = [`${String(count("completed"))} done`, ...(failed ? [styles.error(`${String(failed)} failed`)] : []), ...(count("cancelled") ? [`${String(count("cancelled"))} cancelled`] : []), ...(span > 0 ? [formatWorkflowRuntime(span)] : []), formatTokens(accounting.input + accounting.output), formatCost(accounting.cost)].filter(Boolean);
|
|
136
|
+
return styles.muted(parts.join(" · "));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Renders the run; with `maxLines`, a view taller than that limit switches to a compact one so the live block stays on screen.
|
|
140
|
+
* A block taller than the terminal pushes its changing header above the viewport, and pi-tui then clears and redraws the whole
|
|
141
|
+
* scrollback on every update, which snaps the reader back to the bottom.
|
|
142
|
+
*/
|
|
143
|
+
export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width?: number, showHint = false, maxLines?: number): string {
|
|
144
|
+
const full = workflowProgressLines(run, spinner, styles, now, expanded, width, showHint);
|
|
145
|
+
if (expanded || maxLines === undefined || full.length <= maxLines) return full.join("\n");
|
|
146
|
+
// Finished phases collapse to a summary line; the current one keeps failed, active, and the latest settled agents, in that priority.
|
|
147
|
+
const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
|
|
148
|
+
const currentStart = Math.min(run.agents.length, phases.at(-1)?.afterAgent ?? 0);
|
|
149
|
+
const current = run.agents.slice(currentStart);
|
|
150
|
+
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);
|
|
151
|
+
const candidates = [...current.filter((agent) => agent.state === "failed"), ...current.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)), ...recent];
|
|
152
|
+
const render = (count: number) => workflowProgressLines(run, spinner, styles, now, false, width, showHint, new Set(candidates.slice(0, count)));
|
|
153
|
+
let low = 0;
|
|
154
|
+
let high = candidates.length;
|
|
155
|
+
while (low < high) { const middle = Math.ceil((low + high) / 2); if (render(middle).length <= maxLines) low = middle; else high = middle - 1; }
|
|
156
|
+
// NOTE: failures in finished phases always stay listed, so a run with more of them than maxLines still overflows.
|
|
157
|
+
return render(low).join("\n");
|
|
158
|
+
}
|
|
159
|
+
function workflowProgressLines(run: PersistedRun, spinner: string, styles: WorkflowProgressStyles, now: number, expanded: boolean, width: number | undefined, showHint: boolean, shown?: ReadonlySet<AgentRecord>): string[] {
|
|
115
160
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
116
161
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
117
162
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
@@ -125,15 +170,19 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
125
170
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
126
171
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
127
172
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
173
|
+
const toolActivity = formatToolActivity(run, spinner, styles, now);
|
|
174
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
128
175
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
129
|
-
const
|
|
130
|
-
|
|
176
|
+
const position = new Map(run.agents.map((agent, index) => [agent, index]));
|
|
177
|
+
const renderAgents = (agents: readonly AgentRecord[], nested: boolean) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
|
|
178
|
+
const icon = agentStateGlyph(agent.state, "●");
|
|
131
179
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
132
|
-
const
|
|
180
|
+
const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
|
|
181
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
|
|
133
182
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
134
183
|
const state = progressStyleForState(agent.state, styles);
|
|
135
184
|
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
136
|
-
return `${indent}#${String(
|
|
185
|
+
return `${indent}#${String((position.get(agent) ?? 0) + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
|
|
137
186
|
}, run.agents, (label) => styles.muted(label)).map((line) => nested ? ` ${line}` : line);
|
|
138
187
|
const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
|
|
139
188
|
if (scopedShells) {
|
|
@@ -146,10 +195,23 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
146
195
|
}
|
|
147
196
|
let renderedAgents = 0;
|
|
148
197
|
let nested = false;
|
|
198
|
+
// 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.
|
|
199
|
+
const segment = (agents: readonly AgentRecord[], closed: boolean) => {
|
|
200
|
+
if (!shown) return renderAgents(agents, nested);
|
|
201
|
+
const listed = agents.filter((agent) => closed ? agent.state === "failed" : shown.has(agent));
|
|
202
|
+
const hidden = closed ? [] : agents.filter((agent) => !shown.has(agent));
|
|
203
|
+
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]] as const;
|
|
204
|
+
const summary = counts.filter(([, count]) => count > 0).map(([label, count]) => `+${String(count)} ${label}`).join(" · ");
|
|
205
|
+
return [...renderAgents(listed, nested), ...(summary ? [`${nested ? " " : " "}${styles.muted(`… ${summary}${showHint ? expandHint() : ""}`)}`] : [])];
|
|
206
|
+
};
|
|
149
207
|
for (const [phaseIndex, phase] of phases.entries()) {
|
|
150
208
|
const boundary = Math.max(renderedAgents, Math.min(run.agents.length, phase.afterAgent));
|
|
151
|
-
|
|
152
|
-
lines.push(` ${
|
|
209
|
+
const before = run.agents.slice(renderedAgents, boundary);
|
|
210
|
+
if (shown && !nested && before.length) lines.push(` ${phaseSummary(before, styles)}`);
|
|
211
|
+
lines.push(...segment(before, true));
|
|
212
|
+
const next = phases[phaseIndex + 1];
|
|
213
|
+
const phaseAgents = next ? run.agents.slice(boundary, Math.max(boundary, Math.min(run.agents.length, next.afterAgent))) : [];
|
|
214
|
+
lines.push(` ${styles.muted(`[Phase: ${phase.phase}]`)}${shown && next ? ` ${phaseSummary(phaseAgents, styles)}` : ""}`);
|
|
153
215
|
const phaseShell = scopedShells ? shellActivityFor(run, phaseIndex) : undefined;
|
|
154
216
|
if (phaseShell) {
|
|
155
217
|
const rendered = formatShellActivity(phaseShell.active, phaseShell.startedAt, spinner, styles, now);
|
|
@@ -158,9 +220,9 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
158
220
|
renderedAgents = boundary;
|
|
159
221
|
nested = true;
|
|
160
222
|
}
|
|
161
|
-
lines.push(...
|
|
162
|
-
lines.push(...workflowLogLines(run, styles, expanded, width, showHint));
|
|
163
|
-
return lines
|
|
223
|
+
lines.push(...segment(run.agents.slice(renderedAgents), false));
|
|
224
|
+
lines.push(...workflowLogLines(run, styles, expanded, width, showHint, shown ? COMPACT_LOG_LINES : 5));
|
|
225
|
+
return lines;
|
|
164
226
|
}
|
|
165
227
|
|
|
166
228
|
const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -408,14 +470,18 @@ export function workflowProgressBlock(run: PersistedRun, theme: Theme, progress?
|
|
|
408
470
|
}
|
|
409
471
|
previousState = displayed.state;
|
|
410
472
|
const frame = displayed.state === "running" ? workflowSpinner[Math.floor(now / 80) % workflowSpinner.length] ?? "◇" : "◇";
|
|
411
|
-
|
|
473
|
+
// pi-tui only patches rows that are still on screen; leave room below the block for the editor and footer.
|
|
474
|
+
// A finished or frozen block no longer changes, so it keeps the full list.
|
|
475
|
+
const rows = freezeAt === undefined && !terminal ? process.stdout.rows : undefined;
|
|
476
|
+
const maxLines = rows ? Math.max(6, rows - 10) - (prefix ? prefix.split("\n").length + 1 : 0) : undefined;
|
|
477
|
+
const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true, maxLines);
|
|
412
478
|
return truncateWorkflowProgress(prefix ? `${prefix}\n\n${progressText}` : progressText, width);
|
|
413
479
|
},
|
|
414
480
|
setExpanded(value: boolean) { expanded = value; },
|
|
415
481
|
invalidate() {
|
|
416
482
|
const displayed = currentRun();
|
|
417
483
|
const now = Date.now();
|
|
418
|
-
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0)) return;
|
|
484
|
+
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length)) return;
|
|
419
485
|
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS) return;
|
|
420
486
|
progress.lastRefreshAt = now;
|
|
421
487
|
const inputRun = progress.inputRun;
|
|
@@ -519,7 +585,7 @@ export function formatStalledDuration(durationMs: number): string {
|
|
|
519
585
|
const remainingMinutes = minutes % 60;
|
|
520
586
|
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
521
587
|
}
|
|
522
|
-
function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
588
|
+
export function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
523
589
|
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt)) return undefined;
|
|
524
590
|
const duration = now - agent.lastEventAt;
|
|
525
591
|
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
@@ -529,7 +595,7 @@ function elapsedDurationMs(agent: Pick<AgentDetailPresentation, "durationMs" | "
|
|
|
529
595
|
if (agent.durationMs !== undefined && Number.isFinite(agent.durationMs)) return Math.max(0, agent.durationMs);
|
|
530
596
|
return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
|
|
531
597
|
}
|
|
532
|
-
function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
598
|
+
export function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
533
599
|
const activity = agent.activity;
|
|
534
600
|
if (activity?.kind === "reasoning" || activity?.kind === "text") {
|
|
535
601
|
const name = activity.kind === "reasoning" ? "reasoning" : "responding";
|
|
@@ -582,7 +648,7 @@ export interface AgentDetailPresentation {
|
|
|
582
648
|
readonly accounting?: AgentAccounting;
|
|
583
649
|
readonly error?: { readonly code: string; readonly message: string };
|
|
584
650
|
}
|
|
585
|
-
function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
651
|
+
export function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
586
652
|
return styles.error(`Error: ${error.code}: ${error.message}`);
|
|
587
653
|
}
|
|
588
654
|
|
|
@@ -661,6 +727,8 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
661
727
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
662
728
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
663
729
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
730
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
731
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
664
732
|
if (run.error) lines.push(`Error: ${run.error.code}: ${run.error.message}`);
|
|
665
733
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
666
734
|
lines.push("");
|
|
@@ -698,6 +766,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
698
766
|
];
|
|
699
767
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
700
768
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
769
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
770
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
701
771
|
if (run.error) lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
702
772
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
703
773
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
@@ -731,11 +801,7 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
731
801
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
732
802
|
const tree = buildWorkflowPhaseTree(model);
|
|
733
803
|
const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
|
|
734
|
-
const wrap = (text: string
|
|
735
|
-
// ponytail: ANSI-only width, good enough for the ASCII labels the tree renders
|
|
736
|
-
const ansiPattern = new RegExp(ANSI_SGR_SOURCE, "g");
|
|
737
|
-
const visibleLength = (text: string): number => text.replace(ansiPattern, "").length;
|
|
738
|
-
const padTo = (text: string, limit: number): string => `${text}${" ".repeat(Math.max(0, limit - visibleLength(text)))}`;
|
|
804
|
+
const wrap = (text: string): string[] => wrapNavigatorLine(text, safeWidth);
|
|
739
805
|
const phaseStyle = (state: string): ((text: string) => string) => phaseStyleForState(state, styles);
|
|
740
806
|
const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
|
|
741
807
|
const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
|
|
@@ -799,28 +865,36 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
799
865
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
800
866
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
801
867
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
802
|
-
const
|
|
868
|
+
const toolActivity = formatToolActivity(run, "⠦", styles, now);
|
|
869
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
803
870
|
const actionRows = (): string[] => {
|
|
804
871
|
const actions = selection.actions;
|
|
805
872
|
if (!actions) return [];
|
|
806
873
|
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
807
874
|
};
|
|
808
|
-
const
|
|
875
|
+
const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
|
|
876
|
+
lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, selection));
|
|
877
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned")) lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
878
|
+
return lines.flatMap((line) => wrap(line));
|
|
879
|
+
}
|
|
880
|
+
function wrapNavigatorLine(text: string, width: number): string[] {
|
|
881
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
882
|
+
}
|
|
883
|
+
/** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
|
|
884
|
+
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined }> = {}): string[] {
|
|
885
|
+
const safeWidth = Math.max(1, width);
|
|
809
886
|
if (safeWidth >= 80) {
|
|
810
887
|
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
lines.push(...detailRows().flatMap((line) => wrap(line)));
|
|
818
|
-
} else {
|
|
819
|
-
lines.push(...renderTree(safeWidth));
|
|
820
|
-
if (!selection.treeOnly) lines.push("", ...detailRows().flatMap((line) => wrap(line)));
|
|
888
|
+
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
|
|
889
|
+
const right = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
|
|
890
|
+
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
|
|
891
|
+
const cell = left[index] ?? "";
|
|
892
|
+
return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
|
|
893
|
+
});
|
|
821
894
|
}
|
|
822
|
-
if (
|
|
823
|
-
|
|
895
|
+
if (layout.detailsOnly) return detail.flatMap((line) => wrapNavigatorLine(line, safeWidth));
|
|
896
|
+
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, safeWidth));
|
|
897
|
+
return layout.treeOnly ? left : [...left, ...detail.flatMap((line) => wrapNavigatorLine(line, safeWidth))];
|
|
824
898
|
}
|
|
825
899
|
export function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string {
|
|
826
900
|
const context = JSON.stringify(checkpoint.context, null, 2);
|