@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,871 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { type ExtensionUIContext, SettingsManager, truncateToVisualLines, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { listRunIds, RunStore, type AwaitingCheckpoint, type PersistedRun, type WorktreeReference } from "./persistence.js";
|
|
5
|
+
import { type WorkflowRegistryApi } from "./registry.js";
|
|
6
|
+
import { deepFreeze, errorText, object, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
7
|
+
import { saveModelAliases, resolveWorkflowSettings, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
|
|
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";
|
|
10
|
+
import { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded } from "./host-phases.js";
|
|
11
|
+
import { type WorkflowRecoveryContext, type createWorkflowRecovery } from "./host-recovery.js";
|
|
12
|
+
import { runDependencyIds } from "./retention.js";
|
|
13
|
+
import { failureDiagnosticsFrom, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback } from "./host-delivery.js";
|
|
14
|
+
import { type WorkflowRunRecord } from "./host-runtime.js";
|
|
15
|
+
import { getTrajectoryHost, type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
|
|
16
|
+
import { type AgentAttemptAction, type AgentAttemptActionContext, type AgentAttemptSummary, type AgentRecord, type HardTerminalRunState, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type PreparedAgentSession, type WorkflowAgentSession } from "./types.js";
|
|
17
|
+
|
|
18
|
+
type UiSelect = (title: string, options: string[]) => Promise<string | undefined>;
|
|
19
|
+
type UiInput = (title: string, placeholder?: string) => Promise<string | undefined>;
|
|
20
|
+
type UiSetStatus = (key: string, text?: string) => void;
|
|
21
|
+
type UiCustom = ExtensionUIContext["custom"];
|
|
22
|
+
type NavigatorStoredRun = { store: RunStore; loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }; resolvedAt: number | undefined };
|
|
23
|
+
type NavigatorRunDependencies = ReadonlyMap<string, readonly string[]>;
|
|
24
|
+
type ManualDeletionPlan = { ordered: readonly NavigatorStoredRun[]; skipped: readonly { entry: NavigatorStoredRun; dependentIds: readonly string[] }[] };
|
|
25
|
+
function dependencyClosure(runId: string, dependencies: NavigatorRunDependencies): ReadonlySet<string> {
|
|
26
|
+
const closure = new Set<string>();
|
|
27
|
+
const pending = [runId];
|
|
28
|
+
while (pending.length) {
|
|
29
|
+
const current = pending.pop();
|
|
30
|
+
if (current === undefined) continue;
|
|
31
|
+
for (const dependency of dependencies.get(current) ?? []) {
|
|
32
|
+
if (closure.has(dependency)) continue;
|
|
33
|
+
closure.add(dependency);
|
|
34
|
+
pending.push(dependency);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return closure;
|
|
38
|
+
}
|
|
39
|
+
async function navigatorRunDependencies(entries: readonly NavigatorStoredRun[]): Promise<NavigatorRunDependencies> {
|
|
40
|
+
const dependencies = new Map<string, readonly string[]>();
|
|
41
|
+
for (const entry of entries) dependencies.set(entry.loaded.run.id, runDependencyIds(entry.loaded.run, await entry.store.borrowedWorktrees()));
|
|
42
|
+
return dependencies;
|
|
43
|
+
}
|
|
44
|
+
function dependentRunId(targetRunId: string, entries: readonly NavigatorStoredRun[], dependencies: NavigatorRunDependencies): string | undefined {
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.store.runId !== targetRunId && dependencyClosure(entry.store.runId, dependencies).has(targetRunId)) return entry.store.runId;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export function manualDeletionPlan(entries: readonly NavigatorStoredRun[], state: "completed" | "failed", dependencies: NavigatorRunDependencies): ManualDeletionPlan {
|
|
51
|
+
const candidates = new Set(entries.filter(({ loaded }) => loaded.run.state === state).map(({ store }) => store.runId));
|
|
52
|
+
const protectedRuns = new Set<string>();
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (candidates.has(entry.store.runId)) continue;
|
|
55
|
+
for (const dependency of dependencyClosure(entry.store.runId, dependencies)) if (candidates.has(dependency)) protectedRuns.add(dependency);
|
|
56
|
+
}
|
|
57
|
+
const skipped = entries.filter(({ store }) => candidates.has(store.runId) && protectedRuns.has(store.runId)).map((entry) => ({ entry, dependentIds: entries.filter((candidate) => !candidates.has(candidate.store.runId) && dependencyClosure(candidate.store.runId, dependencies).has(entry.store.runId)).map(({ store }) => store.runId) }));
|
|
58
|
+
const remaining = new Set([...candidates].filter((runId) => !protectedRuns.has(runId)));
|
|
59
|
+
const ordered: NavigatorStoredRun[] = [];
|
|
60
|
+
while (remaining.size) {
|
|
61
|
+
const next = entries.find((entry) => remaining.has(entry.store.runId) && ![...remaining].some((childId) => (dependencies.get(childId) ?? []).includes(entry.store.runId)));
|
|
62
|
+
if (!next) throw new Error("Run dependency cycle prevents safe deletion");
|
|
63
|
+
ordered.push(next);
|
|
64
|
+
remaining.delete(next.store.runId);
|
|
65
|
+
}
|
|
66
|
+
return { ordered, skipped };
|
|
67
|
+
}
|
|
68
|
+
function formatSkippedDeletion(skipped: ManualDeletionPlan["skipped"]): string {
|
|
69
|
+
return skipped.map(({ entry, dependentIds }) => `${entry.store.runId} (needed by ${dependentIds.join(", ") || "a surviving workflow"})`).join(", ");
|
|
70
|
+
}
|
|
71
|
+
export type UiHostCapabilities = { select?: UiSelect; input?: UiInput; setStatus?: UiSetStatus; custom?: UiCustom };
|
|
72
|
+
type UiConfirm = (title: string, message: string) => Promise<boolean>;
|
|
73
|
+
type ReportBlocked = (active: boolean, label?: string) => void;
|
|
74
|
+
async function confirmWithBlocked(ui: { confirm: UiConfirm }, reportBlocked: ReportBlocked | undefined, title: string, message: string): Promise<boolean> {
|
|
75
|
+
reportBlocked?.(true, title);
|
|
76
|
+
try { return await ui.confirm(title, message); }
|
|
77
|
+
finally { reportBlocked?.(false); }
|
|
78
|
+
}
|
|
79
|
+
function isUiSelect(value: unknown): value is UiSelect { return typeof value === "function"; }
|
|
80
|
+
function isUiInput(value: unknown): value is UiInput { return typeof value === "function"; }
|
|
81
|
+
function isUiSetStatus(value: unknown): value is UiSetStatus { return typeof value === "function"; }
|
|
82
|
+
function isUiCustom(value: unknown): value is UiCustom { return typeof value === "function"; }
|
|
83
|
+
function isCheckpointDecision(value: unknown): value is "Approve" | "Reject" { return value === "Approve" || value === "Reject"; }
|
|
84
|
+
export function uiHostCapabilities(ui: unknown): UiHostCapabilities | undefined {
|
|
85
|
+
if (!object(ui)) return undefined;
|
|
86
|
+
const selectValue = ui.select;
|
|
87
|
+
const inputValue = ui.input;
|
|
88
|
+
const setStatusValue = ui.setStatus;
|
|
89
|
+
const customValue = ui.custom;
|
|
90
|
+
const select = isUiSelect(selectValue) ? selectValue.bind(ui) : undefined;
|
|
91
|
+
const input = isUiInput(inputValue) ? inputValue.bind(ui) : undefined;
|
|
92
|
+
const setStatus = isUiSetStatus(setStatusValue) ? setStatusValue.bind(ui) : undefined;
|
|
93
|
+
const custom = isUiCustom(customValue) ? customValue.bind(ui) : undefined;
|
|
94
|
+
return { ...(select ? { select } : {}), ...(input ? { input } : {}), ...(setStatus ? { setStatus } : {}), ...(custom ? { custom } : {}) };
|
|
95
|
+
}
|
|
96
|
+
function tuiRows(tui: unknown): number {
|
|
97
|
+
const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
|
|
98
|
+
return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
|
|
99
|
+
}
|
|
100
|
+
const WORKFLOW_PANEL_FOOTER_ROWS = 2;
|
|
101
|
+
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
102
|
+
const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
|
|
103
|
+
const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } } as const;
|
|
104
|
+
type WorkflowOverlayComponent = { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void; dispose?(): void };
|
|
105
|
+
function borderWorkflowOverlay(component: WorkflowOverlayComponent, theme: { fg(color: "border", text: string): string }): WorkflowOverlayComponent {
|
|
106
|
+
return { ...component, render(width: number) { const border = theme.fg("border", "─".repeat(Math.max(1, width))); return [border, ...component.render(width), border]; } };
|
|
107
|
+
}
|
|
108
|
+
type KeybindingsHostCapabilities = { getKeys?: (name: string) => readonly string[] };
|
|
109
|
+
type KeybindingGetKeys = NonNullable<KeybindingsHostCapabilities["getKeys"]>;
|
|
110
|
+
function isKeybindingGetKeys(value: unknown): value is KeybindingGetKeys { return typeof value === "function"; }
|
|
111
|
+
function keybindingKeys(keybindings: unknown, name: string): readonly string[] | undefined {
|
|
112
|
+
if (!object(keybindings)) return undefined;
|
|
113
|
+
const getKeys = keybindings.getKeys;
|
|
114
|
+
return isKeybindingGetKeys(getKeys) ? getKeys.call(keybindings, name) : undefined;
|
|
115
|
+
}
|
|
116
|
+
type WorkflowKeybindings = { matches(data: string, binding: string): boolean };
|
|
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
|
+
function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels: Readonly<Record<string, string>>): string {
|
|
120
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
121
|
+
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
122
|
+
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
123
|
+
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type WorkflowNavigatorDependencies = {
|
|
127
|
+
pi: Pick<ExtensionAPI, "registerCommand">;
|
|
128
|
+
home: string | undefined;
|
|
129
|
+
clipboard: (value: string) => Promise<void>;
|
|
130
|
+
extensionAgentDir: string;
|
|
131
|
+
runs: Map<string, WorkflowRunRecord>;
|
|
132
|
+
terminalRunStates: Map<string, HardTerminalRunState>;
|
|
133
|
+
hardTerminalRunStates: ReadonlySet<string>;
|
|
134
|
+
ensureSessionLease: (cwd: string, sessionId: string) => Promise<void>;
|
|
135
|
+
answerCheckpoint: (runId: string, name: string, approved: boolean, silent?: boolean) => Promise<boolean>;
|
|
136
|
+
recovery: ReturnType<typeof createWorkflowRecovery>;
|
|
137
|
+
stopWorkflowRun: (runId: string) => Promise<{ runId: string; state: string; stopped: boolean; reason?: "unknown_run" | "already_terminal" }>;
|
|
138
|
+
moveForegroundToBackground: (runId: string) => Promise<{ runId: string; state: "running"; detached: true }>;
|
|
139
|
+
isForegroundAttached: (runId: string) => boolean;
|
|
140
|
+
liveAgents: {
|
|
141
|
+
get(runId: string, agentId: string): Readonly<{ session?: WorkflowAgentSession; prepared?: Readonly<PreparedAgentSession>; handoff?: LiveSessionHandoff }> | undefined;
|
|
142
|
+
overlay(run: PersistedRun): PersistedRun;
|
|
143
|
+
};
|
|
144
|
+
registry: WorkflowRegistryApi;
|
|
145
|
+
projectTrusted: (context: unknown) => boolean;
|
|
146
|
+
resumeHostContext: (context: unknown) => WorkflowRecoveryContext;
|
|
147
|
+
resumeSelectedWorkflow: (runId: string, foreground: boolean, context: unknown, budgetPatch?: unknown) => Promise<{ workflowName: string; state: "running" | "completed" | "awaiting_approval"; attached: boolean; value?: JsonValue }>;
|
|
148
|
+
reportBlocked?: ReportBlocked;
|
|
149
|
+
setNavigatorOpen?: (open: boolean) => void;
|
|
150
|
+
trajectoryProvider: TrajectoryPublisherProvider;
|
|
151
|
+
coordinateRunMutation?: <T>(task: () => Promise<T>) => Promise<T>;
|
|
152
|
+
};
|
|
153
|
+
export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies): void {
|
|
154
|
+
const { pi, home, clipboard, extensionAgentDir, runs, terminalRunStates, hardTerminalRunStates, ensureSessionLease, answerCheckpoint, recovery, stopWorkflowRun, moveForegroundToBackground, isForegroundAttached, liveAgents, registry, projectTrusted, resumeHostContext, resumeSelectedWorkflow, reportBlocked, setNavigatorOpen, trajectoryProvider, coordinateRunMutation: coordinateRunMutationDependency } = deps;
|
|
155
|
+
const coordinateRunMutation = coordinateRunMutationDependency ?? (<T>(task: () => Promise<T>): Promise<T> => task());
|
|
156
|
+
const command = {
|
|
157
|
+
description: "Open the workflow picker; workflow actions are available contextually",
|
|
158
|
+
handler: async (args, ctx) => {
|
|
159
|
+
const command = args.trim();
|
|
160
|
+
if (command && command !== "trajectory") {
|
|
161
|
+
ctx.ui.notify("Workflow slash commands do not accept arguments. Open the workflow picker with /workflow; actions are available there or through workflow tools.", "warning");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (command === "trajectory") {
|
|
165
|
+
const trajectory = getTrajectoryHost();
|
|
166
|
+
if (!trajectory) {
|
|
167
|
+
ctx.ui.notify("Trajectory is disabled", "warning");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await trajectory.open(trajectoryProvider, ctx);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
174
|
+
const loadStores = async (): Promise<NavigatorStoredRun[]> => {
|
|
175
|
+
const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
|
|
176
|
+
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
177
|
+
try {
|
|
178
|
+
const loaded = await store.load();
|
|
179
|
+
const summary = await store.loadSummary().catch(() => undefined);
|
|
180
|
+
const terminalAt = summary?.terminalAt === undefined ? undefined : Date.parse(summary.terminalAt);
|
|
181
|
+
return { store, loaded: { ...loaded, run: liveAgents.overlay(loaded.run) }, resolvedAt: terminalAt !== undefined && Number.isFinite(terminalAt) ? terminalAt : undefined };
|
|
182
|
+
}
|
|
183
|
+
catch { if (!await store.isComplete()) await store.delete(true).catch(() => undefined); return undefined; }
|
|
184
|
+
}));
|
|
185
|
+
return entries.filter((entry): entry is { store: RunStore; loaded: { run: PersistedRun; snapshot: Readonly<LaunchSnapshot> }; resolvedAt: number | undefined } => entry !== undefined);
|
|
186
|
+
};
|
|
187
|
+
let stores = await loadStores();
|
|
188
|
+
const setWorkflowStatus = (text: string | undefined) => {
|
|
189
|
+
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
190
|
+
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
191
|
+
};
|
|
192
|
+
const runAction = async (actionCommand: string, status: (text: string | undefined) => void = setWorkflowStatus): Promise<"dashboard" | "picker" | "stopped"> => {
|
|
193
|
+
const [action, runId, ...rest] = actionCommand.split(/\s+/);
|
|
194
|
+
try {
|
|
195
|
+
const run = runId ? runs.get(runId) : undefined;
|
|
196
|
+
const storedEntry = runId ? stores.find(({ store }) => store.runId === runId) : undefined;
|
|
197
|
+
const stored = storedEntry ? { store: storedEntry.store, loaded: await storedEntry.store.load() } : undefined;
|
|
198
|
+
if (action === "background" && runId) {
|
|
199
|
+
const result = await moveForegroundToBackground(runId);
|
|
200
|
+
ctx.ui.notify(`Moved workflow ${result.runId} to background.`, "info");
|
|
201
|
+
return "dashboard";
|
|
202
|
+
}
|
|
203
|
+
if ((action === "approve" || action === "reject") && runId && rest.length) {
|
|
204
|
+
const accepted = await answerCheckpoint(runId, rest.join(" "), action === "approve", true);
|
|
205
|
+
ctx.ui.notify(accepted ? `${action === "approve" ? "Approved" : "Rejected"} checkpoint ${rest.join(" ")}.` : "Checkpoint is not awaiting a response.", accepted ? "info" : "warning");
|
|
206
|
+
return "dashboard";
|
|
207
|
+
}
|
|
208
|
+
if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
|
|
209
|
+
const result = await recovery.answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
|
|
210
|
+
ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
|
|
211
|
+
return "dashboard";
|
|
212
|
+
}
|
|
213
|
+
if (action === "delete" && stored) {
|
|
214
|
+
if (!hardTerminalRunStates.has(stored.loaded.run.state)) { ctx.ui.notify("Stop the workflow before deleting it.", "warning"); return "dashboard"; }
|
|
215
|
+
if (!await confirmWithBlocked(ctx.ui, reportBlocked, "Delete workflow?", `Delete ${stored.loaded.run.workflowName} (${stored.store.runId}) and all owned artifacts? This cannot be undone.`)) return "dashboard";
|
|
216
|
+
const deleted = await coordinateRunMutation(async () => {
|
|
217
|
+
const currentStores = await loadStores();
|
|
218
|
+
const current = currentStores.find(({ store }) => store.runId === stored.store.runId);
|
|
219
|
+
if (!current) { ctx.ui.notify(`Workflow ${stored.store.runId} is no longer available.`, "warning"); return false; }
|
|
220
|
+
if (!hardTerminalRunStates.has(current.loaded.run.state)) { ctx.ui.notify("Stop the workflow before deleting it.", "warning"); return false; }
|
|
221
|
+
const dependencies = await navigatorRunDependencies(currentStores);
|
|
222
|
+
const dependent = dependentRunId(current.store.runId, currentStores, dependencies);
|
|
223
|
+
if (dependent) {
|
|
224
|
+
const dependentEntry = currentStores.find(({ store }) => store.runId === dependent);
|
|
225
|
+
ctx.ui.notify(`Cannot delete workflow ${current.store.runId}: surviving workflow ${dependent}${dependentEntry ? ` (${dependentEntry.loaded.run.workflowName})` : ""} depends on it.`, "warning");
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
await current.store.delete(true);
|
|
229
|
+
runs.delete(current.store.runId);
|
|
230
|
+
terminalRunStates.delete(current.store.runId);
|
|
231
|
+
ctx.ui.notify(`Deleted workflow ${current.store.runId}.`, "info");
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
return deleted ? "picker" : "dashboard";
|
|
235
|
+
}
|
|
236
|
+
if (action === "pause" && run) {
|
|
237
|
+
await run.lifecycle.pause();
|
|
238
|
+
ctx.ui.notify(run.lifecycle.state === "pausing" ? `Pausing workflow ${run.store.runId}; it pauses once the active operation finishes.` : `Paused workflow ${run.store.runId}.`, "info");
|
|
239
|
+
return "dashboard";
|
|
240
|
+
}
|
|
241
|
+
if (action === "cancel-pause" && run?.lifecycle.state === "pausing") { await run.lifecycle.resume(); ctx.ui.notify(`Cancelled the pending pause of workflow ${run.store.runId}.`, "info"); return "dashboard"; }
|
|
242
|
+
if (action === "resume" && run) {
|
|
243
|
+
if (run.lifecycle.state === "budget_exhausted") {
|
|
244
|
+
const patch: unknown = rest.length ? JSON.parse(rest.join(" ")) as unknown : undefined;
|
|
245
|
+
const result = await recovery.resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
|
|
246
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
247
|
+
} else {
|
|
248
|
+
if (run.lifecycle.state === "interrupted") await recovery.coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), resumeHostContext(ctx), undefined, false);
|
|
249
|
+
else {
|
|
250
|
+
if (run.lifecycle.state === "paused") await recovery.refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
|
|
251
|
+
await run.lifecycle.resume();
|
|
252
|
+
}
|
|
253
|
+
ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
|
|
254
|
+
}
|
|
255
|
+
return "dashboard";
|
|
256
|
+
}
|
|
257
|
+
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
258
|
+
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}" );
|
|
259
|
+
if (input === undefined) return "dashboard";
|
|
260
|
+
const result = await recovery.resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
|
|
261
|
+
ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
|
|
262
|
+
return "dashboard";
|
|
263
|
+
}
|
|
264
|
+
if (action === "stop" && run) {
|
|
265
|
+
const workflowName = stored?.loaded.run.workflowName ?? run.metadata.name;
|
|
266
|
+
if (!await confirmWithBlocked(ctx.ui, reportBlocked, "Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`)) return "dashboard";
|
|
267
|
+
status(`Stopping workflow ${workflowName}...`);
|
|
268
|
+
await stopWorkflowRun(run.store.runId);
|
|
269
|
+
status(`Workflow ${run.store.runId} stopped.`);
|
|
270
|
+
ctx.ui.notify(`Stopped workflow ${run.store.runId}.`, "info"); return "stopped";
|
|
271
|
+
}
|
|
272
|
+
if (action && runId) ctx.ui.notify(`Cannot ${action} workflow ${runId}: the run is no longer available.`, "warning");
|
|
273
|
+
else ctx.ui.notify("Workflow action is no longer available.", "warning");
|
|
274
|
+
return "dashboard";
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const message = errorText(error);
|
|
277
|
+
if (action === "stop") status(`Could not stop workflow ${runId ?? ""}: ${message}`);
|
|
278
|
+
ctx.ui.notify(`Cannot ${action ?? "workflow action"}${runId ? ` for ${runId}` : ""}: ${message}`, "warning");
|
|
279
|
+
return "dashboard";
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const deleteBulkRuns = async (state: "completed" | "failed"): Promise<void> => {
|
|
283
|
+
const label = state === "failed" ? "failed" : "completed";
|
|
284
|
+
const title = state === "failed" ? "Delete failed runs?" : "Delete completed runs?";
|
|
285
|
+
const message = `Delete all ${label} workflow runs and their artifacts? This cannot be undone.`;
|
|
286
|
+
if (!await confirmWithBlocked(ctx.ui, reportBlocked, title, message)) return;
|
|
287
|
+
try {
|
|
288
|
+
await coordinateRunMutation(async () => {
|
|
289
|
+
const currentStores = await loadStores();
|
|
290
|
+
const dependencies = await navigatorRunDependencies(currentStores);
|
|
291
|
+
const plan = manualDeletionPlan(currentStores, state, dependencies);
|
|
292
|
+
for (const entry of plan.ordered) {
|
|
293
|
+
await entry.store.delete(true);
|
|
294
|
+
runs.delete(entry.store.runId);
|
|
295
|
+
terminalRunStates.delete(entry.store.runId);
|
|
296
|
+
}
|
|
297
|
+
if (plan.skipped.length) ctx.ui.notify(`Skipped ${label} runs required by surviving workflows: ${formatSkippedDeletion(plan.skipped)}.`, "warning");
|
|
298
|
+
if (plan.ordered.length) ctx.ui.notify(`Deleted ${String(plan.ordered.length)} ${label} workflow run(s).`, "info");
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
ctx.ui.notify(`Cannot delete ${label} runs: ${errorText(error)}`, "warning");
|
|
302
|
+
}
|
|
303
|
+
stores = await loadStores();
|
|
304
|
+
};
|
|
305
|
+
const manageAliases = async (): Promise<void> => {
|
|
306
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
307
|
+
let aliasSettingsPath = settingsPath;
|
|
308
|
+
const trustedProject = projectTrusted(ctx);
|
|
309
|
+
const modelRegistry = resumeHostContext(ctx).modelRegistry;
|
|
310
|
+
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
311
|
+
const selectTarget = async (aliases: Readonly<Record<string, string>>): Promise<string | undefined> => {
|
|
312
|
+
const models = available();
|
|
313
|
+
const choice = await ctx.ui.select("Model alias target", [...models, ...Object.keys(aliases).sort(), "Manual model ID", "Back"]);
|
|
314
|
+
if (!choice || choice === "Back") return undefined;
|
|
315
|
+
if (choice !== "Manual model ID") return choice;
|
|
316
|
+
return (await ctx.ui.input("Manual model ID", "provider/model[:thinking] or alias[:thinking]"))?.trim() || undefined;
|
|
317
|
+
};
|
|
318
|
+
const save = (aliases: Readonly<Record<string, string>>): boolean => {
|
|
319
|
+
try { saveModelAliases(aliasSettingsPath, aliases); ctx.ui.notify(`Saved model aliases to ${aliasSettingsPath}.`, "info"); return true; }
|
|
320
|
+
catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${errorText(error)}`, "error"); return false; }
|
|
321
|
+
};
|
|
322
|
+
for (;;) {
|
|
323
|
+
let aliases: Readonly<Record<string, string>>;
|
|
324
|
+
try { const resolution = resolveWorkflowSettings(ctx.cwd, trustedProject, settingsPath); aliases = resolution.effective.modelAliases ?? {}; aliasSettingsPath = resolution.sources.modelAliases; }
|
|
325
|
+
catch (error) { ctx.ui.notify(`${trustedProject ? workflowProjectSettingsPath(ctx.cwd) : settingsPath}: ${errorText(error)}`, "error"); return; }
|
|
326
|
+
const names = Object.keys(aliases).sort();
|
|
327
|
+
const listing = names.length ? names.map((name) => `${name} = ${aliases[name] ?? ""}`).join("\n") : "(none)";
|
|
328
|
+
const options = ["Add alias", ...names.map((name) => `Edit ${name}`), ...names.map((name) => `Delete ${name}`), "Back"];
|
|
329
|
+
const choice = await ctx.ui.select(`Model aliases\n${listing}`, options);
|
|
330
|
+
if (!choice || choice === "Back") return;
|
|
331
|
+
if (choice === "Add alias") {
|
|
332
|
+
const name = (await ctx.ui.input("Alias name", "reviewer-model"))?.trim();
|
|
333
|
+
if (!name) continue;
|
|
334
|
+
if (Object.prototype.hasOwnProperty.call(aliases, name)) { ctx.ui.notify(`Alias ${name} already exists; choose Edit ${name}.`, "warning"); continue; }
|
|
335
|
+
const target = await selectTarget(aliases);
|
|
336
|
+
if (!target) continue;
|
|
337
|
+
const next = { ...aliases, [name]: target };
|
|
338
|
+
try { validateModelAliases(next, aliasSettingsPath); } catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${errorText(error)}`, "error"); continue; }
|
|
339
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
340
|
+
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
341
|
+
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
342
|
+
if (!await confirmWithBlocked(ctx.ui, reportBlocked, "Save unknown model?", "Save this target for cross-machine portability?")) continue;
|
|
343
|
+
}
|
|
344
|
+
save(next);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const edit = /^Edit (.+)$/.exec(choice);
|
|
348
|
+
if (edit?.[1]) {
|
|
349
|
+
const target = await selectTarget(aliases);
|
|
350
|
+
if (!target) continue;
|
|
351
|
+
const next = { ...aliases, [edit[1]]: target };
|
|
352
|
+
try { validateModelAliases(next, aliasSettingsPath); } catch (error) { ctx.ui.notify(`${aliasSettingsPath}: ${errorText(error)}`, "error"); continue; }
|
|
353
|
+
const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
|
|
354
|
+
if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
|
|
355
|
+
ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
|
|
356
|
+
if (!await confirmWithBlocked(ctx.ui, reportBlocked, "Save unknown model?", "Save this target for cross-machine portability?")) continue;
|
|
357
|
+
}
|
|
358
|
+
save(next);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const deletion = /^Delete (.+)$/.exec(choice);
|
|
362
|
+
if (deletion?.[1] && await confirmWithBlocked(ctx.ui, reportBlocked, "Delete model alias?", `Delete ${deletion[1]}? Future workflow resumes using this alias may fail.`)) {
|
|
363
|
+
const next = Object.fromEntries(Object.entries(aliases).filter(([name]) => name !== deletion[1]));
|
|
364
|
+
save(next);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
if (!command) {
|
|
369
|
+
for (;;) {
|
|
370
|
+
if (!ctx.hasUI) {
|
|
371
|
+
if (!stores.length) { ctx.ui.notify("No workflow runs in this session. Mutations are available through workflow tools.", "info"); return; }
|
|
372
|
+
const sorted = navigatorAttentionSort(stores);
|
|
373
|
+
const details = await Promise.all(sorted.map(async ({ store, loaded }) => formatNavigatorRun(loaded, await store.awaitingCheckpoints(), await store.worktrees())));
|
|
374
|
+
ctx.ui.notify(`${details.join("\n\n")}\n\nMutations are available through workflow tools.`, "info"); return;
|
|
375
|
+
}
|
|
376
|
+
const sorted = navigatorAttentionSort(stores);
|
|
377
|
+
const labels = navigatorRunLabels(sorted);
|
|
378
|
+
const terminalStates = hardTerminalRunStates;
|
|
379
|
+
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
380
|
+
const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
|
|
381
|
+
const pickerOptions = [...labels, "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
|
|
382
|
+
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
383
|
+
if (!runChoice || runChoice === "Close") return;
|
|
384
|
+
if (runChoice === "Model aliases") { await manageAliases(); stores = await loadStores(); continue; }
|
|
385
|
+
if (runChoice === "Delete all completed") {
|
|
386
|
+
await deleteBulkRuns("completed");
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (runChoice === "Delete all failed") {
|
|
390
|
+
await deleteBulkRuns("failed");
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const runIndex = labels.indexOf(runChoice);
|
|
394
|
+
if (runIndex < 0) return;
|
|
395
|
+
const selected = sorted[runIndex];
|
|
396
|
+
if (!selected) return;
|
|
397
|
+
const { store } = selected;
|
|
398
|
+
const copyArtifact = async (value: string, artifact: string) => {
|
|
399
|
+
try {
|
|
400
|
+
await clipboard(value);
|
|
401
|
+
ctx.ui.notify(`Copied ${artifact}.`, "info");
|
|
402
|
+
} catch (error) {
|
|
403
|
+
ctx.ui.notify(`Failed to copy ${artifact}: ${errorText(error)}`, "error");
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const loadDashboard = async () => {
|
|
407
|
+
const loaded = await store.load();
|
|
408
|
+
const activeRun = runs.get(store.runId);
|
|
409
|
+
const liveRun = liveAgents.overlay({ ...loaded.run, ...(activeRun ? { usage: activeRun.budget.usage } : {}) });
|
|
410
|
+
const checkpoints = await store.awaitingCheckpoints();
|
|
411
|
+
const worktrees = await store.worktrees();
|
|
412
|
+
const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
|
|
413
|
+
const agentResults = new Map<string, JsonValue>();
|
|
414
|
+
for (const agent of liveRun.agents) {
|
|
415
|
+
if (agent.state !== "completed" || agent.parentId || !agent.resultPath) continue;
|
|
416
|
+
const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
|
|
417
|
+
if (operation) agentResults.set(agent.id, operation.value);
|
|
418
|
+
}
|
|
419
|
+
const actions = new Map<string, string>();
|
|
420
|
+
const copies = new Map<string, { value: string; artifact: string }>();
|
|
421
|
+
const reviews = new Map<string, AwaitingCheckpoint>();
|
|
422
|
+
const add = (label: string, value: string) => { actions.set(label, `${value} ${store.runId}`); };
|
|
423
|
+
const addCopy = (label: string, value: string, artifact: string) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
|
|
424
|
+
if (liveRun.state === "running") add("Pause", "pause");
|
|
425
|
+
if (liveRun.state === "pausing") add("Cancel pause", "cancel-pause");
|
|
426
|
+
if (["paused", "interrupted"].includes(liveRun.state)) add("Resume", "resume");
|
|
427
|
+
if (liveRun.state === "budget_exhausted") { actions.set("Resume unchanged", `resume ${store.runId}`); actions.set("Adjust budget", `adjust ${store.runId}`); }
|
|
428
|
+
for (const decision of await store.pendingWorkflowDecisions()) {
|
|
429
|
+
const id = decision.proposalId.slice(0, 8);
|
|
430
|
+
actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
|
|
431
|
+
actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
|
|
432
|
+
}
|
|
433
|
+
if (isForegroundAttached(store.runId)) actions.set("Move to background", `background ${store.runId}`);
|
|
434
|
+
if (!terminalStates.has(liveRun.state)) add("Stop", "stop");
|
|
435
|
+
for (const cp of checkpoints) {
|
|
436
|
+
if (ctx.mode === "tui") {
|
|
437
|
+
const label = `Review ${cp.name}`;
|
|
438
|
+
actions.set(label, "review");
|
|
439
|
+
reviews.set(label, cp);
|
|
440
|
+
} else {
|
|
441
|
+
actions.set(`Approve ${cp.name}`, `approve ${store.runId} ${cp.name}`);
|
|
442
|
+
actions.set(`Reject ${cp.name}`, `reject ${store.runId} ${cp.name}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (ctx.mode !== "tui") actions.set("Refresh", "refresh");
|
|
446
|
+
else actions.set("Open script in editor", "open-script");
|
|
447
|
+
if (ctx.mode !== "tui" && liveRun.agents.length) actions.set("Agents...", "agents");
|
|
448
|
+
if (terminalStates.has(liveRun.state)) add("Delete", "delete");
|
|
449
|
+
if (ctx.mode === "tui") {
|
|
450
|
+
addCopy("Copy run path", store.directory, "run path");
|
|
451
|
+
addCopy("Copy run ID", store.runId, "run ID");
|
|
452
|
+
}
|
|
453
|
+
return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
|
|
454
|
+
};
|
|
455
|
+
const agentWorktreeFor = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): WorktreeReference | undefined => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
|
|
456
|
+
const agentAttemptActionContext = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): AgentAttemptActionContext | undefined => {
|
|
457
|
+
const attempt = (agent.attemptDetails ?? []).reduce<AgentAttemptSummary | undefined>((latest, candidate) => !latest || candidate.attempt > latest.attempt ? candidate : latest, undefined);
|
|
458
|
+
if (!attempt) return undefined;
|
|
459
|
+
const liveAgent = liveAgents.get(dashboard.run.id, agent.id);
|
|
460
|
+
const liveCandidate = liveAgent?.session;
|
|
461
|
+
const live = liveCandidate && attempt.session && liveCandidate.reference.transport === attempt.session.transport && liveCandidate.reference.sessionId === attempt.session.sessionId ? liveCandidate : undefined;
|
|
462
|
+
const run = runs.get(dashboard.run.id);
|
|
463
|
+
const ui = { notify: (message: string, level: "info" | "warning" | "error" = "info") => { ctx.ui.notify(message, level); }, confirm: (title: string, message: string) => confirmWithBlocked(ctx.ui, reportBlocked, title, message), select: (title: string, options: readonly string[]) => { return ctx.ui.select(title, [...options]); }, input: (title: string, placeholder?: string) => ctx.ui.input(title, placeholder), setWorkingMessage: (message?: string) => { ctx.ui.setWorkingMessage(message); } };
|
|
464
|
+
const attemptSnapshot = deepFreeze(structuredClone(attempt));
|
|
465
|
+
const prepared = live ? liveAgent?.prepared : undefined;
|
|
466
|
+
const handoff = live ? liveAgent?.handoff : undefined;
|
|
467
|
+
return { run: deepFreeze(structuredClone(dashboard.run)), agent: deepFreeze(structuredClone(agent)), attempt: attemptSnapshot, ...(attemptSnapshot.session ? { session: attemptSnapshot.session } : {}), ...(live ? { liveSession: live } : {}), ...(prepared ? { prepared } : {}), ...(handoff ? { handoff } : {}), signal: run?.abortController.signal ?? new AbortController().signal, ui: Object.freeze(ui) };
|
|
468
|
+
};
|
|
469
|
+
const visibleAgentAttemptActions = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): readonly [string, AgentAttemptAction][] => {
|
|
470
|
+
const context = agentAttemptActionContext(dashboard, agent);
|
|
471
|
+
return context ? visibleRegisteredAgentAttemptActions(registry.agentAttemptActions(), context) : [];
|
|
472
|
+
};
|
|
473
|
+
const agentActionOptions = (dashboard: Awaited<ReturnType<typeof loadDashboard>>, agent: AgentRecord): string[] => {
|
|
474
|
+
const worktree = agentWorktreeFor(dashboard, agent);
|
|
475
|
+
return agentActionLabels({
|
|
476
|
+
extensionLabels: visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
|
|
477
|
+
hasWorktree: worktree !== undefined,
|
|
478
|
+
openPrompt: ctx.mode === "tui" && agent.prompt !== undefined,
|
|
479
|
+
openSystemPrompt: ctx.mode === "tui" && agent.systemPrompt !== undefined,
|
|
480
|
+
openResult: ctx.mode === "tui" && dashboard.agentResults.has(agent.id),
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, requestedAgentId?: string): Promise<void> => {
|
|
484
|
+
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
485
|
+
const title = (agent: AgentRecord): string => agentBreadcrumb(agent, byId, true);
|
|
486
|
+
const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
|
|
487
|
+
let selected: AgentRecord | undefined;
|
|
488
|
+
if (requestedAgentId) selected = dashboard.agents.find((agent) => agent.id === requestedAgentId);
|
|
489
|
+
else {
|
|
490
|
+
const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
|
|
491
|
+
const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
|
|
492
|
+
selected = selectedIndex >= 0 ? dashboard.agents[selectedIndex] : undefined;
|
|
493
|
+
}
|
|
494
|
+
if (!selected) return;
|
|
495
|
+
const worktree = agentWorktreeFor(dashboard, selected);
|
|
496
|
+
const actions = agentActionOptions(dashboard, selected);
|
|
497
|
+
for (;;) {
|
|
498
|
+
const action = await ctx.ui.select(title(selected), actions);
|
|
499
|
+
if (!action || action === "Back") return;
|
|
500
|
+
const extensionAction = visibleAgentAttemptActions(dashboard, selected).find(([, candidate]) => candidate.label === action);
|
|
501
|
+
if (extensionAction) {
|
|
502
|
+
const context = agentAttemptActionContext(dashboard, selected);
|
|
503
|
+
if (context) { try { await extensionAction[1].run(context); } catch (error) { ctx.ui.notify(`Agent attempt action failed: ${errorText(error)}`, "error"); } }
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (action === "Copy agent ID") { await copyArtifact(selected.id, "agent ID"); continue; }
|
|
507
|
+
if (action === "Copy branch" && worktree) { await copyArtifact(worktree.branch, "branch"); continue; }
|
|
508
|
+
if (action === "Copy worktree path" && worktree) { await copyArtifact(worktree.path, "worktree path"); continue; }
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const resumeDashboard = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>, action: string): Promise<void> => {
|
|
512
|
+
const ui = uiHostCapabilities(ctx.ui);
|
|
513
|
+
if (!ui?.select) return;
|
|
514
|
+
const mode = await ui.select(`Resume ${dashboard.run.workflowName}`, ["Foreground", "Background", "Cancel"]);
|
|
515
|
+
if (!mode || mode === "Cancel") return;
|
|
516
|
+
let budgetPatch: unknown;
|
|
517
|
+
if (dashboard.run.state === "budget_exhausted") {
|
|
518
|
+
if (action === "Adjust budget") {
|
|
519
|
+
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
520
|
+
if (input === undefined) return;
|
|
521
|
+
try { budgetPatch = JSON.parse(input); } catch (error) { ctx.ui.notify(`Cannot parse budget patch: ${errorText(error)}`, "warning"); return; }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
const result = await resumeSelectedWorkflow(store.runId, mode === "Foreground", ctx, budgetPatch);
|
|
526
|
+
if (result.state === "completed" && !result.attached) ctx.ui.notify(`Workflow ${result.workflowName} completed${result.value === undefined ? "." : `: ${JSON.stringify(result.value)}`}`, "info");
|
|
527
|
+
else if (result.state === "awaiting_approval") ctx.ui.notify(`Budget adjustment for ${result.workflowName} is awaiting approval.`, "warning");
|
|
528
|
+
else if (result.state === "running") ctx.ui.notify(`Resumed workflow ${result.workflowName} in ${mode.toLowerCase()}.`, "info");
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error && typeof error === "object" && "workflowResumeAttached" in error && error.workflowResumeAttached === true) return;
|
|
531
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
532
|
+
const message = diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(dashboard.run.workflowName, store.runId, store.directory, error);
|
|
533
|
+
ctx.ui.notify(`Cannot resume workflow ${store.runId}: ${message}`, "warning");
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
for (;;) {
|
|
537
|
+
let view = await loadDashboard();
|
|
538
|
+
const actionChoice = ctx.mode === "tui"
|
|
539
|
+
? await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
540
|
+
let dashboardOffset = 0;
|
|
541
|
+
let refreshing = false;
|
|
542
|
+
let disposed = false;
|
|
543
|
+
let detailsMode = false;
|
|
544
|
+
let actionMode = false;
|
|
545
|
+
let actionIndex = 0;
|
|
546
|
+
let stopRequested = false;
|
|
547
|
+
let stopStatus: string | undefined;
|
|
548
|
+
let selectionNeedsScroll = true;
|
|
549
|
+
let renderedWidth = 80;
|
|
550
|
+
let refreshGeneration = 0;
|
|
551
|
+
let frozenAt = Date.now();
|
|
552
|
+
let previousRunState = view.run.state;
|
|
553
|
+
let tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
554
|
+
let selectedNodeId = tree.nodes[0]?.id;
|
|
555
|
+
let expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
556
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
557
|
+
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
558
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
559
|
+
const progressNow = () => {
|
|
560
|
+
const now = Date.now();
|
|
561
|
+
if (hardTerminalRunStates.has(view.run.state)) {
|
|
562
|
+
if (!hardTerminalRunStates.has(previousRunState)) frozenAt = now;
|
|
563
|
+
previousRunState = view.run.state;
|
|
564
|
+
return frozenAt;
|
|
565
|
+
}
|
|
566
|
+
frozenAt = now;
|
|
567
|
+
previousRunState = view.run.state;
|
|
568
|
+
return now;
|
|
569
|
+
};
|
|
570
|
+
const selectedAgentRecord = (): AgentRecord | undefined => {
|
|
571
|
+
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
|
|
572
|
+
return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
|
|
573
|
+
};
|
|
574
|
+
const actionOptions = () => {
|
|
575
|
+
const agent = selectedAgentRecord();
|
|
576
|
+
return agent ? agentActionOptions(view, agent) : [...view.actions.keys(), "Back"];
|
|
577
|
+
};
|
|
578
|
+
let editorRunning = false;
|
|
579
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
580
|
+
const stopTimer = () => {
|
|
581
|
+
if (timer !== undefined) {
|
|
582
|
+
clearInterval(timer);
|
|
583
|
+
timer = undefined;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const openArtifact = async (artifact: Promise<WorkflowArtifact>, label: string): Promise<void> => {
|
|
587
|
+
if (editorRunning) return;
|
|
588
|
+
editorRunning = true;
|
|
589
|
+
try {
|
|
590
|
+
const command = SettingsManager.create(view.cwd, extensionAgentDir, { projectTrusted: projectTrusted(ctx) }).getExternalEditorCommand();
|
|
591
|
+
if (!command) { ctx.ui.notify(`Cannot open ${label}: no external editor is configured.`, "warning"); return; }
|
|
592
|
+
const exitCode = await openWorkflowArtifact(tui, command, await artifact);
|
|
593
|
+
if (exitCode !== 0) {
|
|
594
|
+
const detail = exitCode === null ? "could not be started" : `exited with code ${String(exitCode)}`;
|
|
595
|
+
ctx.ui.notify(`Cannot open ${label}: external editor ${detail}.`, "warning");
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
ctx.ui.notify(`Cannot open ${label}: ${errorText(error)}`, "warning");
|
|
599
|
+
} finally {
|
|
600
|
+
editorRunning = false;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const updateDashboard = async () => {
|
|
604
|
+
const generation = ++refreshGeneration;
|
|
605
|
+
const hadExpandableNodes = tree.nodes.some((node) => node.children.length > 0);
|
|
606
|
+
const next = await loadDashboard();
|
|
607
|
+
if (disposed || generation !== refreshGeneration) return;
|
|
608
|
+
const previousNodeId = selectedNodeId;
|
|
609
|
+
const previousExpanded = expandedNodeIds;
|
|
610
|
+
const selectedAction = actionMode ? actionOptions()[actionIndex] : undefined;
|
|
611
|
+
view = next;
|
|
612
|
+
if (hardTerminalRunStates.has(next.run.state) && next.run.agents.every((agent) => SETTLED_AGENT_STATES.has(agent.state))) stopTimer();
|
|
613
|
+
tree = buildWorkflowPhaseTree(view.phaseModel);
|
|
614
|
+
selectedNodeId = preserveWorkflowPhaseTreeSelection(tree, { nodeId: previousNodeId }).nodeId;
|
|
615
|
+
expandedNodeIds = new Set([...previousExpanded].filter((id) => tree.byId.has(id)));
|
|
616
|
+
if (!hadExpandableNodes && !expandedNodeIds.size && tree.nodes.some((node) => node.children.length > 0)) expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
617
|
+
const nextActions = actionOptions();
|
|
618
|
+
const preservedActionIndex = selectedAction ? nextActions.indexOf(selectedAction) : -1;
|
|
619
|
+
actionIndex = preservedActionIndex >= 0 ? preservedActionIndex : selectedAction ? nextActions.length - 1 : Math.min(actionIndex, Math.max(0, nextActions.length - 1));
|
|
620
|
+
selectionNeedsScroll = true;
|
|
621
|
+
tui.requestRender();
|
|
622
|
+
};
|
|
623
|
+
const requestStop = () => {
|
|
624
|
+
if (stopRequested) return;
|
|
625
|
+
stopRequested = true;
|
|
626
|
+
stopStatus = undefined;
|
|
627
|
+
setWorkflowStatus(undefined);
|
|
628
|
+
void runAction(`stop ${store.runId}`, (status) => {
|
|
629
|
+
stopStatus = status;
|
|
630
|
+
setWorkflowStatus(status);
|
|
631
|
+
if (!disposed) tui.requestRender();
|
|
632
|
+
}).then(async (outcome) => {
|
|
633
|
+
if (outcome === "stopped") { done("__stopped__"); return; }
|
|
634
|
+
await updateDashboard();
|
|
635
|
+
}).catch((error: unknown) => {
|
|
636
|
+
if (disposed) return;
|
|
637
|
+
stopStatus = `Could not stop workflow ${store.runId}: ${errorText(error)}`;
|
|
638
|
+
setWorkflowStatus(stopStatus);
|
|
639
|
+
tui.requestRender();
|
|
640
|
+
}).finally(() => {
|
|
641
|
+
stopRequested = false;
|
|
642
|
+
if (!disposed) tui.requestRender();
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
timer = setInterval(() => {
|
|
646
|
+
if (refreshing || stopRequested) return;
|
|
647
|
+
refreshing = true;
|
|
648
|
+
//NOTE: background dashboard refresh self-heals next tick; swallow so a transient render/read error surfaces as an unhandled rejection instead of refreshing state.
|
|
649
|
+
void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
|
|
650
|
+
}, 1000);
|
|
651
|
+
timer.unref();
|
|
652
|
+
if (hardTerminalRunStates.has(view.run.state) && view.run.agents.every((agent) => SETTLED_AGENT_STATES.has(agent.state))) stopTimer();
|
|
653
|
+
return {
|
|
654
|
+
render(width: number) {
|
|
655
|
+
renderedWidth = width;
|
|
656
|
+
const narrow = width < 80;
|
|
657
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
658
|
+
const agent = selectedAgentRecord();
|
|
659
|
+
const actions = actionMode ? { title: agent ? "Agent actions" : "Run actions", options: actionOptions(), index: actionIndex } : undefined;
|
|
660
|
+
const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { nodeId: selectedNodeId, expandedNodeIds: [...expandedNodeIds], ...(narrow && !detailsMode && !actionMode ? { treeOnly: true } : {}), ...(narrow && (detailsMode || actionMode) ? { detailsOnly: true } : {}), ...(actions ? { actions } : {}) }, styles, progressNow());
|
|
661
|
+
const statusLines = stopStatus ? truncateToVisualLines(styles.error(stopStatus), Number.MAX_SAFE_INTEGER, width, 0).visualLines.map((line) => line.trimEnd()) : [];
|
|
662
|
+
const content = [...statusLines, ...phaseLines];
|
|
663
|
+
const rows = terminalRows();
|
|
664
|
+
const hintRows = rows >= 3 ? 1 : 0;
|
|
665
|
+
const viewport = Math.max(1, rows - hintRows);
|
|
666
|
+
const maxOffset = Math.max(0, content.length - viewport);
|
|
667
|
+
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
668
|
+
if (actionMode) {
|
|
669
|
+
const label = actions?.options[actionIndex];
|
|
670
|
+
const actionRow = label ? content.findIndex((line) => line.includes(label)) : -1;
|
|
671
|
+
if (actionRow >= 0) {
|
|
672
|
+
if (actionRow < dashboardOffset) dashboardOffset = actionRow;
|
|
673
|
+
else if (actionRow >= dashboardOffset + viewport) dashboardOffset = actionRow - viewport + 1;
|
|
674
|
+
}
|
|
675
|
+
} else if (!detailsMode && selectionNeedsScroll) {
|
|
676
|
+
const selectedRow = content.findIndex((line) => line.startsWith("→"));
|
|
677
|
+
if (selectedRow >= 0) {
|
|
678
|
+
if (selectedRow < dashboardOffset) dashboardOffset = selectedRow;
|
|
679
|
+
else if (selectedRow >= dashboardOffset + viewport) dashboardOffset = selectedRow - viewport + 1;
|
|
680
|
+
}
|
|
681
|
+
selectionNeedsScroll = false;
|
|
682
|
+
}
|
|
683
|
+
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
684
|
+
const selectedNode = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
|
|
685
|
+
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] : [])];
|
|
688
|
+
},
|
|
689
|
+
invalidate() {},
|
|
690
|
+
handleInput(data: string) {
|
|
691
|
+
if (stopRequested || editorRunning) return;
|
|
692
|
+
const narrow = renderedWidth < 80;
|
|
693
|
+
if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
|
|
694
|
+
if (actionMode) {
|
|
695
|
+
const options = actionOptions();
|
|
696
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) { actionMode = false; dashboardOffset = 0; tui.requestRender(); return; }
|
|
697
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
698
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) actionIndex = (actionIndex + 1) % options.length;
|
|
699
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
700
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
701
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
702
|
+
const action = options[actionIndex];
|
|
703
|
+
const agent = selectedAgentRecord();
|
|
704
|
+
if (!action || action === "Back") { actionMode = false; dashboardOffset = 0; }
|
|
705
|
+
else if (agent) {
|
|
706
|
+
const worktree = agentWorktreeFor(view, agent);
|
|
707
|
+
if (action === "Open prompt in editor") {
|
|
708
|
+
if (agent.prompt !== undefined) void openArtifact(Promise.resolve(workflowPromptArtifact(agent.prompt)), "agent prompt");
|
|
709
|
+
}
|
|
710
|
+
else if (action === "Open system prompt in editor") {
|
|
711
|
+
if (agent.systemPrompt !== undefined) void openArtifact(Promise.resolve(workflowPromptArtifact(agent.systemPrompt)), "agent system prompt");
|
|
712
|
+
}
|
|
713
|
+
else if (action === "Open result in editor") {
|
|
714
|
+
const result = view.agentResults.get(agent.id);
|
|
715
|
+
if (result !== undefined) void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
|
|
716
|
+
}
|
|
717
|
+
else if (action === "Copy agent ID") void copyArtifact(agent.id, "agent ID");
|
|
718
|
+
else if (action === "Copy branch" && worktree) void copyArtifact(worktree.branch, "branch");
|
|
719
|
+
else if (action === "Copy worktree path" && worktree) void copyArtifact(worktree.path, "worktree path");
|
|
720
|
+
else {
|
|
721
|
+
const extensionAction = visibleAgentAttemptActions(view, agent).find(([, candidate]) => candidate.label === action);
|
|
722
|
+
const actionContext = extensionAction ? agentAttemptActionContext(view, agent) : undefined;
|
|
723
|
+
if (extensionAction && actionContext) {
|
|
724
|
+
actionMode = false;
|
|
725
|
+
void Promise.resolve(extensionAction[1].run(actionContext)).catch((error: unknown) => { ctx.ui.notify(`Agent attempt action failed: ${errorText(error)}`, "error"); }).finally(() => { void updateDashboard(); });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else if (action === "Open script in editor") void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
|
|
730
|
+
else if (action === "Stop") requestStop();
|
|
731
|
+
else done(action);
|
|
732
|
+
}
|
|
733
|
+
tui.requestRender();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
|
|
737
|
+
if (narrow && detailsMode) { detailsMode = false; selectionNeedsScroll = true; } else done("Back");
|
|
738
|
+
} else if (narrow && detailsMode) {
|
|
739
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
740
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
741
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
742
|
+
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
|
|
743
|
+
if (node?.kind === "agent") { actionMode = true; actionIndex = 0; dashboardOffset = 0; }
|
|
744
|
+
}
|
|
745
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
|
|
746
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
|
|
747
|
+
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
748
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
|
|
749
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
|
|
750
|
+
selectedNodeId = next.nodeId; expandedNodeIds = new Set(next.expandedNodeIds); selectionNeedsScroll = true;
|
|
751
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
|
|
752
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
|
|
753
|
+
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
754
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
|
|
755
|
+
const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
|
|
756
|
+
selectedNodeId = next.nodeId; selectionNeedsScroll = true;
|
|
757
|
+
} else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
|
|
758
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) dashboardOffset += Math.max(1, terminalRows() - 1);
|
|
759
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
760
|
+
const node = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
|
|
761
|
+
if (node?.kind === "workflow") {
|
|
762
|
+
actionMode = true;
|
|
763
|
+
actionIndex = 0;
|
|
764
|
+
dashboardOffset = 0;
|
|
765
|
+
} else if (node?.kind === "agent") {
|
|
766
|
+
if (narrow) detailsMode = true;
|
|
767
|
+
else {
|
|
768
|
+
actionMode = true;
|
|
769
|
+
actionIndex = 0;
|
|
770
|
+
dashboardOffset = 0;
|
|
771
|
+
}
|
|
772
|
+
} else if (node?.children.length) {
|
|
773
|
+
if (expandedNodeIds.has(node.id)) expandedNodeIds.delete(node.id); else expandedNodeIds.add(node.id);
|
|
774
|
+
selectionNeedsScroll = true;
|
|
775
|
+
} else if (narrow) detailsMode = true;
|
|
776
|
+
}
|
|
777
|
+
tui.requestRender();
|
|
778
|
+
},
|
|
779
|
+
dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); },
|
|
780
|
+
};
|
|
781
|
+
})
|
|
782
|
+
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
783
|
+
if (actionChoice === "__stopped__") return;
|
|
784
|
+
if (!actionChoice || actionChoice === "Back") { stores = await loadStores(); break; }
|
|
785
|
+
if (actionChoice === "Agents...") { await selectAgent(view); continue; }
|
|
786
|
+
if (actionChoice.startsWith("__workflow_agent__:")) { await selectAgent(view, actionChoice.slice("__workflow_agent__:".length)); continue; }
|
|
787
|
+
if (actionChoice === "Refresh") continue;
|
|
788
|
+
if (["Resume", "Resume unchanged", "Adjust budget"].includes(actionChoice)) { await resumeDashboard(view, actionChoice); continue; }
|
|
789
|
+
const copy = view.copies.get(actionChoice);
|
|
790
|
+
if (copy) { await copyArtifact(copy.value, copy.artifact); continue; }
|
|
791
|
+
if (actionChoice.startsWith("Review ")) {
|
|
792
|
+
const checkpoint = view.reviews.get(actionChoice);
|
|
793
|
+
if (!checkpoint) continue;
|
|
794
|
+
const decision = await ctx.ui.custom<"Approve" | "Reject" | undefined>((tui, theme, keybindings, done) => {
|
|
795
|
+
const options = ["Approve", "Reject", "Cancel"];
|
|
796
|
+
let selectedIndex = 0;
|
|
797
|
+
let offset = 0;
|
|
798
|
+
let renderedLines: string[] = [];
|
|
799
|
+
const layout = () => {
|
|
800
|
+
const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
801
|
+
const compactControls = rows < 4;
|
|
802
|
+
const titleRows = rows >= 5 ? 1 : 0;
|
|
803
|
+
const hintRows = rows >= 8 ? 1 : 0;
|
|
804
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
805
|
+
const controlRows = compactControls ? 1 : options.length;
|
|
806
|
+
const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
|
|
807
|
+
return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
|
|
808
|
+
};
|
|
809
|
+
const move = (delta: number) => {
|
|
810
|
+
const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
|
|
811
|
+
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
812
|
+
};
|
|
813
|
+
return borderWorkflowOverlay({
|
|
814
|
+
render(width: number) {
|
|
815
|
+
renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
|
|
816
|
+
const currentLayout = layout();
|
|
817
|
+
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
818
|
+
offset = Math.min(offset, maxOffset);
|
|
819
|
+
const keyLabels: Record<string, string> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
820
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
821
|
+
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
|
+
const controls = currentLayout.compactControls
|
|
823
|
+
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
|
824
|
+
: options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
|
|
825
|
+
return [
|
|
826
|
+
...(currentLayout.titleRows ? [theme.fg("accent", "Checkpoint review")] : []),
|
|
827
|
+
...renderedLines.slice(offset, offset + currentLayout.contentViewport),
|
|
828
|
+
...(currentLayout.separatorRows ? [""] : []),
|
|
829
|
+
...controls,
|
|
830
|
+
...(currentLayout.hintRows ? [hint] : []),
|
|
831
|
+
];
|
|
832
|
+
},
|
|
833
|
+
invalidate() {},
|
|
834
|
+
handleInput(data: string) {
|
|
835
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up")) selectedIndex = (selectedIndex + options.length - 1) % options.length;
|
|
836
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.down")) selectedIndex = (selectedIndex + 1) % options.length;
|
|
837
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp")) move(-layout().contentViewport);
|
|
838
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown")) move(layout().contentViewport);
|
|
839
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
|
|
840
|
+
const selected = options[selectedIndex];
|
|
841
|
+
done(isCheckpointDecision(selected) ? selected : undefined);
|
|
842
|
+
}
|
|
843
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) done(undefined);
|
|
844
|
+
tui.requestRender();
|
|
845
|
+
},
|
|
846
|
+
}, theme);
|
|
847
|
+
}, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS });
|
|
848
|
+
if (decision) {
|
|
849
|
+
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
850
|
+
if (!accepted) ctx.ui.notify("Checkpoint is not awaiting a response.", "warning");
|
|
851
|
+
}
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const actionCommand = view.actions.get(actionChoice);
|
|
855
|
+
if (!actionCommand) { ctx.ui.notify(`Cannot select workflow action: ${actionChoice}`, "warning"); continue; }
|
|
856
|
+
const outcome = await runAction(actionCommand);
|
|
857
|
+
if (outcome === "picker") { stores = await loadStores(); break; }
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
} satisfies Parameters<ExtensionAPI["registerCommand"]>[1];
|
|
863
|
+
pi.registerCommand("workflow", {
|
|
864
|
+
...command,
|
|
865
|
+
handler: async (args, ctx) => {
|
|
866
|
+
setNavigatorOpen?.(true);
|
|
867
|
+
try { await command.handler(args, ctx); }
|
|
868
|
+
finally { setNavigatorOpen?.(false); }
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
}
|