@osolmaz/pi-workflows 0.15.2 → 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.
Files changed (124) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
package/src/viewer/tui.ts CHANGED
@@ -1,160 +1,232 @@
1
- import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
2
- import {
3
- maxDetailScroll,
4
- renderRunDetailLines,
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
- databasePath: string;
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
- function viewportSize(): ViewportSize {
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
- const store = new WorkflowRunStore(options.databasePath, { readOnly: true });
36
- let mode: ViewerMode = { view: "list" };
37
- let bundles: LoadedWorkflowRun[] = [];
38
- let selectedIndex = 0;
39
- let detailScroll = 0;
40
- /** Replay position; null follows the latest step live. */
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 = async () => {
54
- bundles = store.listRuns();
55
- selectedIndex = Math.min(selectedIndex, Math.max(0, bundles.length - 1));
56
- const size = viewportSize();
57
- const lines =
58
- mode.view === "list"
59
- ? renderRunListLines(bundles, selectedIndex, size)
60
- : await renderDetail(mode.runId, size);
61
- process.stdout.write(CLEAR + lines.join("\n"));
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 renderDetail = async (runId: string, size: ViewportSize): Promise<string[]> => {
65
- const bundle = store.readRun(runId, { includeTrace: true });
66
- if (!bundle) {
67
- return ["SQLite run state disappeared. Press q to go back."];
68
- }
69
- detailStepCount = bundle.state.steps.length;
70
- if (selectedStep !== null && selectedStep >= detailStepCount - 1) {
71
- // Scrubbed to (or past) the end: snap back to following live updates.
72
- selectedStep = null;
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
- process.stdout.write(ALT_SCREEN_ON);
79
- const stopWatching = watchStateDatabase(options.databasePath, () => {
80
- void draw();
81
- });
82
- const ticker = setInterval(() => {
83
- void draw();
84
- }, options.tickMs ?? 1_000);
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 === "q" || key === "\u0003" || key === "\u001b") {
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
- handleNavigationKey(key);
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 (key === "\u001b[A" || key === "k") {
131
- selectedIndex = Math.max(0, selectedIndex - 1);
132
- void draw();
133
- } else if (key === "\u001b[B" || key === "j") {
134
- selectedIndex = Math.min(Math.max(0, bundles.length - 1), selectedIndex + 1);
135
- void draw();
136
- } else if (key === "\r" || key === "\n") {
137
- const selected = bundles[selectedIndex];
138
- if (selected) {
139
- mode = { view: "detail", runId: selected.runId };
140
- detailScroll = 0;
141
- selectedStep = null;
142
- void draw();
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
- clearInterval(ticker);
152
- stopWatching();
153
- store.close();
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
+ }