@osolmaz/pi-workflows 0.15.3 → 0.16.0
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/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +43 -0
- package/dist/controllers/sqlite.js +137 -2
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +278 -183
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-view.d.ts +7 -2
- package/dist/extension/session-view.js +60 -52
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +14 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +11 -0
- package/dist/host/runner.js +473 -17
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +6 -3
- package/dist/host/state.js +52 -26
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +74 -61
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +44 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +213 -2
- package/src/extension/index.ts +349 -208
- package/src/extension/session-view.ts +73 -50
- package/src/extension/widget.ts +16 -8
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +648 -48
- package/src/host/state.ts +71 -43
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
package/src/viewer/tui.ts
CHANGED
|
@@ -1,160 +1,232 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
renderRunListLines,
|
|
6
|
-
type ViewportSize,
|
|
7
|
-
} from "./render.js";
|
|
8
|
-
import { watchStateDatabase } from "./watch.js";
|
|
1
|
+
import type { WorkflowClient } from "../client/client.js";
|
|
2
|
+
import { materializeRunView } from "../client/materialize.js";
|
|
3
|
+
import { sanitizeText } from "../render/ansi.js";
|
|
4
|
+
import type { JsonValue } from "../state/json.js";
|
|
9
5
|
|
|
10
6
|
const ALT_SCREEN_ON = "\u001b[?1049h\u001b[?25l";
|
|
11
7
|
const ALT_SCREEN_OFF = "\u001b[?25h\u001b[?1049l";
|
|
12
8
|
const CLEAR = "\u001b[2J\u001b[H";
|
|
13
9
|
|
|
14
|
-
type ViewerMode = { view: "list" } | { view: "detail"; runId: string };
|
|
15
|
-
|
|
16
10
|
export type ViewerOptions = {
|
|
17
|
-
|
|
11
|
+
client: WorkflowClient;
|
|
18
12
|
runId?: string | undefined;
|
|
19
|
-
/** Redraw interval for elapsed timers while a run is active. */
|
|
20
|
-
tickMs?: number;
|
|
21
13
|
};
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
width: process.stdout.columns ?? 80,
|
|
26
|
-
height: process.stdout.rows ?? 24,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Interactive live viewer. Watches the SQLite database and re-renders after
|
|
32
|
-
* committed run changes. Returns when the user quits.
|
|
33
|
-
*/
|
|
15
|
+
/** Interactive client view. All durable reads stay in the workflow host. */
|
|
34
16
|
export async function runViewer(options: ViewerOptions): Promise<void> {
|
|
35
|
-
|
|
36
|
-
let
|
|
37
|
-
let
|
|
38
|
-
let
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
let selectedStep: number | null = null;
|
|
42
|
-
let detailStepCount = 0;
|
|
43
|
-
|
|
44
|
-
if (options.runId) {
|
|
45
|
-
bundles = store.listRuns();
|
|
46
|
-
const match = bundles.find((bundle) => bundle.state.runId === options.runId);
|
|
47
|
-
if (!match) {
|
|
48
|
-
throw new Error(`Run not found: ${options.runId}`);
|
|
49
|
-
}
|
|
50
|
-
mode = { view: "detail", runId: match.runId };
|
|
51
|
-
}
|
|
17
|
+
let value: JsonValue = options.runId === undefined ? [] : null;
|
|
18
|
+
let selected = 0;
|
|
19
|
+
let scroll = 0;
|
|
20
|
+
let mode: "runs" | "run" = options.runId === undefined ? "runs" : "run";
|
|
21
|
+
let unsubscribe: (() => Promise<void>) | undefined;
|
|
22
|
+
let runMaterializationGeneration = 0;
|
|
52
23
|
|
|
53
|
-
const draw =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
24
|
+
const draw = () => {
|
|
25
|
+
const width = process.stdout.columns ?? 100;
|
|
26
|
+
const height = Math.max(1, (process.stdout.rows ?? 24) - 1);
|
|
27
|
+
const lines = renderClientView(
|
|
28
|
+
value,
|
|
29
|
+
width,
|
|
30
|
+
height,
|
|
31
|
+
mode === "runs" ? selected : undefined,
|
|
32
|
+
scroll,
|
|
33
|
+
);
|
|
34
|
+
process.stdout.write(`${CLEAR}${lines.join("\n")}`);
|
|
62
35
|
};
|
|
63
36
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
detailScroll = Math.min(detailScroll, maxDetailScroll(bundle, size, selectedStep));
|
|
75
|
-
return renderRunDetailLines(bundle, size, new Date(), detailScroll, selectedStep);
|
|
37
|
+
const watchRuns = async () => {
|
|
38
|
+
runMaterializationGeneration += 1;
|
|
39
|
+
await unsubscribe?.();
|
|
40
|
+
mode = "runs";
|
|
41
|
+
scroll = 0;
|
|
42
|
+
unsubscribe = await options.client.watchRuns((event) => {
|
|
43
|
+
value = event.payload;
|
|
44
|
+
selected = Math.min(selected, Math.max(0, arrayLength(value) - 1));
|
|
45
|
+
draw();
|
|
46
|
+
});
|
|
76
47
|
};
|
|
77
48
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
const watchRun = async (runId: string) => {
|
|
50
|
+
runMaterializationGeneration += 1;
|
|
51
|
+
await unsubscribe?.();
|
|
52
|
+
mode = "run";
|
|
53
|
+
scroll = 0;
|
|
54
|
+
unsubscribe = await options.client.watchRun(runId, (event) => {
|
|
55
|
+
const generation = ++runMaterializationGeneration;
|
|
56
|
+
value = event.payload;
|
|
57
|
+
draw();
|
|
58
|
+
void materializeRunView(options.client, event.payload)
|
|
59
|
+
.then((materialized) => {
|
|
60
|
+
if (generation !== runMaterializationGeneration || mode !== "run") return;
|
|
61
|
+
value = materialized;
|
|
62
|
+
draw();
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
65
|
+
// A newer revision event retries the complete view from its own stable snapshot.
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
85
69
|
|
|
70
|
+
process.stdout.write(ALT_SCREEN_ON);
|
|
86
71
|
const rawModeSupported = process.stdin.isTTY === true;
|
|
87
|
-
if (rawModeSupported) {
|
|
88
|
-
process.stdin.setRawMode(true);
|
|
89
|
-
}
|
|
90
|
-
process.stdin.resume();
|
|
91
|
-
|
|
92
72
|
try {
|
|
73
|
+
if (options.runId === undefined) await watchRuns();
|
|
74
|
+
else await watchRun(options.runId);
|
|
75
|
+
draw();
|
|
76
|
+
if (rawModeSupported) process.stdin.setRawMode(true);
|
|
77
|
+
process.stdin.resume();
|
|
93
78
|
await new Promise<void>((resolve) => {
|
|
94
79
|
const onKey = (data: Buffer) => {
|
|
95
80
|
const key = data.toString("utf8");
|
|
96
|
-
if (key === "
|
|
97
|
-
if (mode.view === "detail" && key === "q") {
|
|
98
|
-
mode = { view: "list" };
|
|
99
|
-
void draw();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
81
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
102
82
|
resolve();
|
|
103
83
|
return;
|
|
104
84
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const handleNavigationKey = (key: string) => {
|
|
109
|
-
if (mode.view !== "list") {
|
|
110
|
-
if (key === "r") {
|
|
111
|
-
void draw();
|
|
112
|
-
} else if (key === "\u001b[A" || key === "k") {
|
|
113
|
-
detailScroll = Math.max(0, detailScroll - 1);
|
|
114
|
-
void draw();
|
|
115
|
-
} else if (key === "\u001b[B" || key === "j") {
|
|
116
|
-
// Clamped against the content height in renderDetail.
|
|
117
|
-
detailScroll += 1;
|
|
118
|
-
void draw();
|
|
119
|
-
} else if (key === "\u001b[D" || key === "h") {
|
|
120
|
-
const current = selectedStep ?? detailStepCount - 1;
|
|
121
|
-
selectedStep = Math.max(0, current - 1);
|
|
122
|
-
void draw();
|
|
123
|
-
} else if (key === "\u001b[C" || key === "l") {
|
|
124
|
-
// renderDetail snaps back to live once this reaches the end.
|
|
125
|
-
selectedStep = selectedStep === null ? null : selectedStep + 1;
|
|
126
|
-
void draw();
|
|
127
|
-
}
|
|
85
|
+
if (key === "q") {
|
|
86
|
+
if (mode === "run" && options.runId === undefined) void watchRuns();
|
|
87
|
+
else resolve();
|
|
128
88
|
return;
|
|
129
89
|
}
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
90
|
+
if (mode === "run") {
|
|
91
|
+
const page = Math.max(1, (process.stdout.rows ?? 24) - 2);
|
|
92
|
+
if (key === "\u001b[A" || key === "k") {
|
|
93
|
+
scroll = Math.max(0, scroll - 1);
|
|
94
|
+
draw();
|
|
95
|
+
} else if (key === "\u001b[B" || key === "j") {
|
|
96
|
+
scroll += 1;
|
|
97
|
+
draw();
|
|
98
|
+
} else if (key === "\u001b[5~") {
|
|
99
|
+
scroll = Math.max(0, scroll - page);
|
|
100
|
+
draw();
|
|
101
|
+
} else if (key === "\u001b[6~") {
|
|
102
|
+
scroll += page;
|
|
103
|
+
draw();
|
|
104
|
+
}
|
|
105
|
+
} else if (mode === "runs") {
|
|
106
|
+
if (key === "\u001b[A" || key === "k") {
|
|
107
|
+
selected = Math.max(0, selected - 1);
|
|
108
|
+
draw();
|
|
109
|
+
} else if (key === "\u001b[B" || key === "j") {
|
|
110
|
+
selected = Math.min(Math.max(0, arrayLength(value) - 1), selected + 1);
|
|
111
|
+
draw();
|
|
112
|
+
} else if (key === "\r" || key === "\n") {
|
|
113
|
+
const runId = selectedRunId(value, selected);
|
|
114
|
+
if (runId !== undefined) void watchRun(runId);
|
|
143
115
|
}
|
|
144
116
|
}
|
|
145
117
|
};
|
|
146
|
-
|
|
147
118
|
process.stdin.on("data", onKey);
|
|
148
|
-
void draw();
|
|
149
119
|
});
|
|
150
120
|
} finally {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (rawModeSupported) {
|
|
155
|
-
process.stdin.setRawMode(false);
|
|
156
|
-
}
|
|
121
|
+
runMaterializationGeneration += 1;
|
|
122
|
+
await unsubscribe?.();
|
|
123
|
+
if (rawModeSupported) process.stdin.setRawMode(false);
|
|
157
124
|
process.stdin.pause();
|
|
158
125
|
process.stdout.write(ALT_SCREEN_OFF);
|
|
159
126
|
}
|
|
160
127
|
}
|
|
128
|
+
|
|
129
|
+
export function renderClientView(
|
|
130
|
+
value: JsonValue,
|
|
131
|
+
width: number,
|
|
132
|
+
height: number,
|
|
133
|
+
selected: number | undefined,
|
|
134
|
+
scroll: number,
|
|
135
|
+
): string[] {
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
if (value.length === 0) return ["No workflow runs found."];
|
|
138
|
+
const selectedIndex = selected ?? 0;
|
|
139
|
+
const start = Math.min(
|
|
140
|
+
Math.max(0, selectedIndex - height + 1),
|
|
141
|
+
Math.max(0, value.length - height),
|
|
142
|
+
);
|
|
143
|
+
return value.slice(start, start + height).map((item, offset) => {
|
|
144
|
+
const index = start + offset;
|
|
145
|
+
const prefix = index === selected ? "> " : " ";
|
|
146
|
+
return truncate(`${prefix}${summary(item)}`, width);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const lines = renderRun(value);
|
|
150
|
+
const start = Math.min(scroll, Math.max(0, lines.length - height));
|
|
151
|
+
return lines.slice(start, start + height).map((line) => truncate(line, width));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderRun(value: JsonValue): string[] {
|
|
155
|
+
if (!isRecord(value) || value.schema !== "pi-workflows.run-view.v1") {
|
|
156
|
+
return JSON.stringify(value, null, 2).split("\n");
|
|
157
|
+
}
|
|
158
|
+
const manifest = isRecord(value.manifest) ? value.manifest : {};
|
|
159
|
+
const state = isRecord(value.state) ? value.state : {};
|
|
160
|
+
const display = isRecord(value.display) ? value.display : {};
|
|
161
|
+
const workflowName =
|
|
162
|
+
typeof manifest.workflowName === "string"
|
|
163
|
+
? sanitizeText(manifest.workflowName)
|
|
164
|
+
: typeof state.workflowName === "string"
|
|
165
|
+
? sanitizeText(state.workflowName)
|
|
166
|
+
: "unknown";
|
|
167
|
+
const runId = typeof value.runId === "string" ? sanitizeText(value.runId) : "unknown";
|
|
168
|
+
const status = typeof display.status === "string" ? display.status : "unknown";
|
|
169
|
+
const lines = [
|
|
170
|
+
`workflow ${workflowName}`,
|
|
171
|
+
`${statusGlyph(status)} ${sanitizeText(status)} · run ${runId}`,
|
|
172
|
+
];
|
|
173
|
+
if (typeof display.reason === "string") lines.push(`reason: ${sanitizeText(display.reason)}`);
|
|
174
|
+
|
|
175
|
+
const steps = Array.isArray(state.steps) ? state.steps : [];
|
|
176
|
+
if (steps.length > 0) {
|
|
177
|
+
lines.push("", "steps");
|
|
178
|
+
for (const step of steps) {
|
|
179
|
+
if (!isRecord(step)) continue;
|
|
180
|
+
const nodeId = typeof step.nodeId === "string" ? sanitizeText(step.nodeId) : "unknown";
|
|
181
|
+
const outcome = typeof step.outcome === "string" ? step.outcome : "unknown";
|
|
182
|
+
lines.push(` ${stepGlyph(outcome)} ${nodeId} · ${sanitizeText(outcome)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (status === "completed" && Object.hasOwn(state, "finalOutput")) {
|
|
186
|
+
lines.push("", `output ${JSON.stringify(state.finalOutput)}`);
|
|
187
|
+
} else if (typeof state.error === "string") {
|
|
188
|
+
lines.push("", `error: ${sanitizeText(state.error)}`);
|
|
189
|
+
}
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function summary(value: JsonValue): string {
|
|
194
|
+
if (!isRecord(value)) return JSON.stringify(value);
|
|
195
|
+
const status = isRecord(value.display) ? value.display.status : undefined;
|
|
196
|
+
const name = typeof value.workflowName === "string" ? value.workflowName : "workflow";
|
|
197
|
+
const runId = typeof value.runId === "string" ? value.runId : "unknown";
|
|
198
|
+
return `${typeof status === "string" ? sanitizeText(status) : "unknown"} ${sanitizeText(name)} ${sanitizeText(runId)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function statusGlyph(status: string): string {
|
|
202
|
+
if (status === "completed") return "✓";
|
|
203
|
+
if (status === "failed" || status === "timed_out" || status === "cancelled") return "✗";
|
|
204
|
+
if (status === "paused") return "‖";
|
|
205
|
+
if (status === "waiting") return "○";
|
|
206
|
+
if (status === "queued") return "…";
|
|
207
|
+
if (status === "ambiguous") return "!";
|
|
208
|
+
return "●";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stepGlyph(outcome: string): string {
|
|
212
|
+
return outcome === "ok" ? "✓" : outcome === "failed" || outcome === "timed_out" ? "✗" : "○";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function selectedRunId(value: JsonValue, index: number): string | undefined {
|
|
216
|
+
if (!Array.isArray(value)) return undefined;
|
|
217
|
+
const item = value[index];
|
|
218
|
+
return isRecord(item) && typeof item.runId === "string" ? item.runId : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function arrayLength(value: JsonValue): number {
|
|
222
|
+
return Array.isArray(value) ? value.length : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function truncate(value: string, width: number): string {
|
|
226
|
+
if (value.length <= width) return value;
|
|
227
|
+
return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}…`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isRecord(value: unknown): value is Record<string, JsonValue> {
|
|
231
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
232
|
+
}
|