@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.
- 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 +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -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 +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- 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-restore-session-delivery-controls-plan.md +139 -0
- 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 +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- 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 +56 -3
- 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 +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- 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
|
@@ -2,6 +2,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
export type ClaimedSessionDelivery = {
|
|
4
4
|
deliveryId: string;
|
|
5
|
+
claimExpiresAt: number;
|
|
6
|
+
isStillDeliverable: () => boolean | Promise<boolean>;
|
|
5
7
|
findSessionEntryId: (entries: readonly unknown[]) => string | undefined;
|
|
6
8
|
send: () => void;
|
|
7
9
|
settle: (sessionEntryId: string) => Promise<void>;
|
|
@@ -26,6 +28,7 @@ const SESSION_ENTRY_CONFIRMATION_MS = 10_000;
|
|
|
26
28
|
*/
|
|
27
29
|
export class SessionDeliveryCoordinator {
|
|
28
30
|
private readonly queued = new Map<string, QueuedSessionDelivery>();
|
|
31
|
+
private claimed: ClaimedSessionDelivery | undefined;
|
|
29
32
|
private synchronizing = false;
|
|
30
33
|
|
|
31
34
|
async synchronize(
|
|
@@ -36,6 +39,7 @@ export class SessionDeliveryCoordinator {
|
|
|
36
39
|
this.synchronizing = true;
|
|
37
40
|
try {
|
|
38
41
|
if (await this.settleQueued(ctx)) return;
|
|
42
|
+
if (await this.sendClaimed(ctx)) return;
|
|
39
43
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
40
44
|
|
|
41
45
|
for (const claim of claimers) {
|
|
@@ -44,32 +48,17 @@ export class SessionDeliveryCoordinator {
|
|
|
44
48
|
|
|
45
49
|
const existingEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
46
50
|
if (existingEntryId !== undefined) {
|
|
47
|
-
|
|
51
|
+
if (delivery.claimExpiresAt <= Date.now()) return;
|
|
52
|
+
this.rememberQueued(delivery);
|
|
53
|
+
await this.settleDelivery(ctx, delivery.deliveryId, existingEntryId);
|
|
48
54
|
return;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
//
|
|
52
|
-
// request is in flight,
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.queued.set(delivery.deliveryId, {
|
|
57
|
-
delivery,
|
|
58
|
-
queuedAt: Date.now(),
|
|
59
|
-
ambiguityReported: false,
|
|
60
|
-
});
|
|
61
|
-
try {
|
|
62
|
-
delivery.send();
|
|
63
|
-
} catch (error) {
|
|
64
|
-
this.queued.delete(delivery.deliveryId);
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const insertedEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
69
|
-
if (insertedEntryId !== undefined) {
|
|
70
|
-
this.queued.delete(delivery.deliveryId);
|
|
71
|
-
await delivery.settle(insertedEntryId);
|
|
72
|
-
}
|
|
57
|
+
// Remember the host claim before the final idle check. If Pi starts a
|
|
58
|
+
// turn while the claim request is in flight, a later poll can use this
|
|
59
|
+
// exact still-live claim instead of making a conflicting second claim.
|
|
60
|
+
this.claimed = delivery;
|
|
61
|
+
await this.sendClaimed(ctx);
|
|
73
62
|
return;
|
|
74
63
|
}
|
|
75
64
|
} finally {
|
|
@@ -78,9 +67,63 @@ export class SessionDeliveryCoordinator {
|
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
clear(): void {
|
|
70
|
+
this.claimed = undefined;
|
|
81
71
|
this.queued.clear();
|
|
82
72
|
}
|
|
83
73
|
|
|
74
|
+
private async sendClaimed(
|
|
75
|
+
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager" | "ui">,
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
const delivery = this.claimed;
|
|
78
|
+
if (delivery === undefined) return false;
|
|
79
|
+
if (delivery.claimExpiresAt <= Date.now()) {
|
|
80
|
+
this.claimed = undefined;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existingEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
85
|
+
if (existingEntryId !== undefined) {
|
|
86
|
+
this.rememberQueued(delivery);
|
|
87
|
+
this.claimed = undefined;
|
|
88
|
+
await this.settleDelivery(ctx, delivery.deliveryId, existingEntryId);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return true;
|
|
92
|
+
if (!(await delivery.isStillDeliverable())) {
|
|
93
|
+
this.claimed = undefined;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (delivery.claimExpiresAt <= Date.now()) {
|
|
97
|
+
this.claimed = undefined;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return true;
|
|
101
|
+
|
|
102
|
+
this.rememberQueued(delivery);
|
|
103
|
+
this.claimed = undefined;
|
|
104
|
+
try {
|
|
105
|
+
delivery.send();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.queued.delete(delivery.deliveryId);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const insertedEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
112
|
+
if (insertedEntryId !== undefined) {
|
|
113
|
+
await this.settleDelivery(ctx, delivery.deliveryId, insertedEntryId);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private rememberQueued(delivery: ClaimedSessionDelivery): void {
|
|
119
|
+
if (this.queued.has(delivery.deliveryId)) return;
|
|
120
|
+
this.queued.set(delivery.deliveryId, {
|
|
121
|
+
delivery,
|
|
122
|
+
queuedAt: Date.now(),
|
|
123
|
+
ambiguityReported: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
84
127
|
private async settleQueued(
|
|
85
128
|
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager" | "ui">,
|
|
86
129
|
): Promise<boolean> {
|
|
@@ -103,9 +146,29 @@ export class SessionDeliveryCoordinator {
|
|
|
103
146
|
}
|
|
104
147
|
return true;
|
|
105
148
|
}
|
|
106
|
-
this.
|
|
107
|
-
await queued.delivery.settle(sessionEntryId);
|
|
149
|
+
await this.settleDelivery(ctx, deliveryId, sessionEntryId);
|
|
108
150
|
}
|
|
109
151
|
return hadQueuedDelivery;
|
|
110
152
|
}
|
|
153
|
+
|
|
154
|
+
private async settleDelivery(
|
|
155
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
156
|
+
deliveryId: string,
|
|
157
|
+
sessionEntryId: string,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const queued = this.queued.get(deliveryId);
|
|
160
|
+
if (queued === undefined) return;
|
|
161
|
+
try {
|
|
162
|
+
await queued.delivery.settle(sessionEntryId);
|
|
163
|
+
this.queued.delete(deliveryId);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (!queued.ambiguityReported) {
|
|
166
|
+
queued.ambiguityReported = true;
|
|
167
|
+
ctx.ui.notify(
|
|
168
|
+
`Workflow session delivery ${deliveryId} is visible but its durable receipt is ambiguous: ${String(error)}. Do not retry it until recovery checks the session history.`,
|
|
169
|
+
"warning",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
111
174
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WorkflowSessionView } from "../client/view.js";
|
|
3
|
+
import type { WorkflowDefinitionSnapshot, WorkflowRunState } from "../workflows/types.js";
|
|
4
|
+
import { buildWidgetView } from "./widget.js";
|
|
5
|
+
|
|
6
|
+
const WIDGET_KEY = "pi-workflows";
|
|
7
|
+
const WIDGET_SCROLL_STEP = 3;
|
|
8
|
+
|
|
9
|
+
/** Client-backed projection of the host-owned run into the origin Pi session. */
|
|
10
|
+
export class SessionWorkflowView {
|
|
11
|
+
private session: WorkflowSessionView | null = null;
|
|
12
|
+
private scroll: number | null = null;
|
|
13
|
+
private shownScroll = 0;
|
|
14
|
+
private maxScroll = 0;
|
|
15
|
+
private stepCount = 0;
|
|
16
|
+
private visible = false;
|
|
17
|
+
private actionHint: string | undefined;
|
|
18
|
+
|
|
19
|
+
update(session: WorkflowSessionView, ctx: ExtensionContext): void {
|
|
20
|
+
const run = session.run;
|
|
21
|
+
if (
|
|
22
|
+
run === null ||
|
|
23
|
+
!isWorkflowRunState(run.state) ||
|
|
24
|
+
!isWorkflowDefinitionSnapshot(run.workflow)
|
|
25
|
+
) {
|
|
26
|
+
this.session = session;
|
|
27
|
+
this.clearWidget(ctx);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const previousRun = this.session?.run;
|
|
31
|
+
if (previousRun?.runId !== run.runId || this.stepCount !== run.state.steps.length) {
|
|
32
|
+
this.scroll = null;
|
|
33
|
+
this.stepCount = run.state.steps.length;
|
|
34
|
+
}
|
|
35
|
+
this.session = session;
|
|
36
|
+
this.render(ctx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setActionHint(hint: string | undefined, ctx: ExtensionContext): void {
|
|
40
|
+
this.actionHint = hint;
|
|
41
|
+
if (this.session?.run !== null) this.render(ctx);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
refresh(ctx: ExtensionContext): void {
|
|
45
|
+
if (this.session?.run === null || this.session === null) {
|
|
46
|
+
this.clearWidget(ctx);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.render(ctx);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
scrollUp(ctx: ExtensionContext): void {
|
|
53
|
+
this.scrollBy(ctx, -WIDGET_SCROLL_STEP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
scrollDown(ctx: ExtensionContext): void {
|
|
57
|
+
this.scrollBy(ctx, WIDGET_SCROLL_STEP);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear(ctx: ExtensionContext): void {
|
|
61
|
+
this.session = null;
|
|
62
|
+
this.scroll = null;
|
|
63
|
+
this.shownScroll = 0;
|
|
64
|
+
this.maxScroll = 0;
|
|
65
|
+
this.stepCount = 0;
|
|
66
|
+
this.clearWidget(ctx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private scrollBy(ctx: ExtensionContext, delta: number): void {
|
|
70
|
+
if (this.session?.run === null || this.session === null) return;
|
|
71
|
+
const current = this.scroll ?? this.shownScroll;
|
|
72
|
+
this.scroll = Math.max(0, Math.min(this.maxScroll, current + delta));
|
|
73
|
+
this.render(ctx);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private clearWidget(ctx: ExtensionContext): void {
|
|
77
|
+
if (!this.visible) return;
|
|
78
|
+
this.visible = false;
|
|
79
|
+
safelyUpdateUi(ctx, () => {
|
|
80
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
81
|
+
ctx.ui.setStatus(WIDGET_KEY, undefined);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private render(ctx: ExtensionContext): void {
|
|
86
|
+
const run = this.session?.run;
|
|
87
|
+
if (run === null || run === undefined) return;
|
|
88
|
+
if (!isWorkflowRunState(run.state) || !isWorkflowDefinitionSnapshot(run.workflow)) return;
|
|
89
|
+
const state = run.state;
|
|
90
|
+
const snapshot = run.workflow;
|
|
91
|
+
const render = (
|
|
92
|
+
width = Number.POSITIVE_INFINITY,
|
|
93
|
+
theme?: Parameters<typeof buildWidgetView>[6],
|
|
94
|
+
) => {
|
|
95
|
+
const view = buildWidgetView(
|
|
96
|
+
state,
|
|
97
|
+
snapshot,
|
|
98
|
+
new Date(),
|
|
99
|
+
this.scroll,
|
|
100
|
+
run.display.status === "paused",
|
|
101
|
+
width,
|
|
102
|
+
theme,
|
|
103
|
+
undefined,
|
|
104
|
+
this.actionHint,
|
|
105
|
+
run.display.status,
|
|
106
|
+
);
|
|
107
|
+
this.shownScroll = view.scroll;
|
|
108
|
+
this.maxScroll = view.maxScroll;
|
|
109
|
+
if (this.scroll !== null) this.scroll = view.scroll;
|
|
110
|
+
return view.lines;
|
|
111
|
+
};
|
|
112
|
+
safelyUpdateUi(ctx, () => {
|
|
113
|
+
if (ctx.mode === "tui") {
|
|
114
|
+
ctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({
|
|
115
|
+
render: (width) => render(width, theme),
|
|
116
|
+
invalidate() {},
|
|
117
|
+
}));
|
|
118
|
+
} else {
|
|
119
|
+
ctx.ui.setWidget(WIDGET_KEY, render());
|
|
120
|
+
}
|
|
121
|
+
ctx.ui.setStatus(WIDGET_KEY, `${state.workflowName} [${run.display.status}]`);
|
|
122
|
+
this.visible = true;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isWorkflowRunState(value: unknown): value is WorkflowRunState {
|
|
128
|
+
return (
|
|
129
|
+
typeof value === "object" &&
|
|
130
|
+
value !== null &&
|
|
131
|
+
!Array.isArray(value) &&
|
|
132
|
+
(value as { schema?: unknown }).schema === "pi-workflows.run-state.v1" &&
|
|
133
|
+
typeof (value as { workflowName?: unknown }).workflowName === "string" &&
|
|
134
|
+
Array.isArray((value as { steps?: unknown }).steps)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isWorkflowDefinitionSnapshot(value: unknown): value is WorkflowDefinitionSnapshot {
|
|
139
|
+
return (
|
|
140
|
+
typeof value === "object" &&
|
|
141
|
+
value !== null &&
|
|
142
|
+
!Array.isArray(value) &&
|
|
143
|
+
(value as { schema?: unknown }).schema === "pi-workflows.definition-snapshot.v1" &&
|
|
144
|
+
typeof (value as { nodes?: unknown }).nodes === "object"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function safelyUpdateUi(ctx: ExtensionContext, update: () => void): void {
|
|
149
|
+
try {
|
|
150
|
+
if (ctx.hasUI) update();
|
|
151
|
+
} catch {
|
|
152
|
+
// A session replacement can make a captured context stale between updates.
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/extension/widget.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { WorkflowDisplayStatus } from "../client/view.js";
|
|
3
4
|
import { formatDuration } from "../render/format.js";
|
|
4
5
|
import { nodeTypeGlyph } from "../render/node-type.js";
|
|
5
6
|
import {
|
|
@@ -14,17 +15,19 @@ import { sanitizeText } from "../workflows/text.js";
|
|
|
14
15
|
import type {
|
|
15
16
|
WorkflowDefinitionSnapshot,
|
|
16
17
|
WorkflowRunState,
|
|
17
|
-
WorkflowRunStatus,
|
|
18
18
|
WorkflowUpdateRecord,
|
|
19
19
|
} from "../workflows/types.js";
|
|
20
20
|
|
|
21
|
-
const STATUS_GLYPHS: Record<
|
|
21
|
+
const STATUS_GLYPHS: Record<WorkflowDisplayStatus, string> = {
|
|
22
|
+
queued: "…",
|
|
22
23
|
running: "◐",
|
|
23
|
-
waiting: "
|
|
24
|
+
waiting: "○",
|
|
25
|
+
paused: "⏸",
|
|
24
26
|
completed: "✓",
|
|
25
27
|
failed: "✗",
|
|
26
28
|
timed_out: "✗",
|
|
27
29
|
cancelled: "✗",
|
|
30
|
+
ambiguous: "!",
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -79,26 +82,28 @@ export function buildWidgetView(
|
|
|
79
82
|
theme?: WidgetTheme,
|
|
80
83
|
updateHistory?: WorkflowUpdateRecord[],
|
|
81
84
|
actionHint?: string,
|
|
85
|
+
displayStatus?: WorkflowDisplayStatus,
|
|
82
86
|
): WidgetView {
|
|
83
87
|
const availableWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : width;
|
|
84
88
|
if (availableWidth === 0) return { lines: [], scroll: 0, maxScroll: 0 };
|
|
85
89
|
|
|
86
90
|
// `held` covers pauses the state cannot see yet: an escape-interrupted
|
|
87
91
|
// step or a pause requested while the current node is still finishing.
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
92
|
+
const status = displayStatus ?? (held || state.paused === true ? "paused" : state.status);
|
|
93
|
+
const paused = status === "paused";
|
|
94
|
+
const glyph = STATUS_GLYPHS[status];
|
|
95
|
+
const statusText = status;
|
|
91
96
|
// Titles, status details, and errors can carry model- or shell-controlled
|
|
92
97
|
// text; never let escape sequences or newlines reach the terminal.
|
|
93
98
|
const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
|
|
94
|
-
const headerTone = statusTone(
|
|
99
|
+
const headerTone = statusTone(status);
|
|
95
100
|
const header = `${paint(theme, headerTone, glyph)} workflow ${sanitizeText(state.workflowName)}${title} ${paint(theme, headerTone, `[${statusText}]`)}`;
|
|
96
101
|
|
|
97
102
|
const footer: string[] = [];
|
|
98
103
|
if (state.error) {
|
|
99
104
|
footer.push(paint(theme, "error", ` error: ${truncate(sanitizeText(state.error), 120)}`));
|
|
100
105
|
}
|
|
101
|
-
if (
|
|
106
|
+
if (status === "waiting" && state.waitingOn) {
|
|
102
107
|
footer.push(
|
|
103
108
|
paint(theme, "warning", ` waiting on checkpoint: ${sanitizeText(state.waitingOn)}`),
|
|
104
109
|
);
|
|
@@ -398,7 +403,7 @@ function elapsedSince(startedAt: string | undefined, now: Date): string | null {
|
|
|
398
403
|
return formatDuration(Math.max(0, now.getTime() - started));
|
|
399
404
|
}
|
|
400
405
|
|
|
401
|
-
function statusTone(status:
|
|
406
|
+
function statusTone(status: string): ThemeColor {
|
|
402
407
|
switch (status) {
|
|
403
408
|
case "completed":
|
|
404
409
|
return "success";
|
|
@@ -407,8 +412,12 @@ function statusTone(status: WorkflowRunStatus): ThemeColor {
|
|
|
407
412
|
case "cancelled":
|
|
408
413
|
return "error";
|
|
409
414
|
case "waiting":
|
|
415
|
+
case "paused":
|
|
410
416
|
return "warning";
|
|
417
|
+
case "ambiguous":
|
|
418
|
+
return "error";
|
|
411
419
|
case "running":
|
|
420
|
+
default:
|
|
412
421
|
return "accent";
|
|
413
422
|
}
|
|
414
423
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { once } from "node:events";
|
|
3
|
+
import { NdjsonFrameDecoder } from "../client/protocol.js";
|
|
3
4
|
import { errorMessage } from "../workflows/errors.js";
|
|
4
5
|
import { HostProcessRegistry, type ProcessIdentity } from "./processes.js";
|
|
5
|
-
import { NdjsonFrameDecoder } from "./protocol.js";
|
|
6
6
|
import type { WorkerOutcome } from "./state.js";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_STARTUP_TIMEOUT_MS = 15_000;
|
|
@@ -3,6 +3,12 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
6
|
+
import { MAX_PROTOCOL_MESSAGE_BYTES } from "../client/protocol.js";
|
|
7
|
+
import type {
|
|
8
|
+
ResolvedControllerInitialization,
|
|
9
|
+
ResolvedSettingsChange,
|
|
10
|
+
ResolvedWorkflowLaunch,
|
|
11
|
+
} from "../client/resolver.js";
|
|
6
12
|
import { discoverControllers, loadControllerFile } from "../controllers/loader.js";
|
|
7
13
|
import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
8
14
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
@@ -12,32 +18,11 @@ import { resolveWorkflowRef } from "../workflows/loader.js";
|
|
|
12
18
|
import { applyWorkflowSettingsPatch } from "../workflows/settings.js";
|
|
13
19
|
import { createDefinitionSnapshot } from "../workflows/store.js";
|
|
14
20
|
import type { WorkflowDefinition, WorkflowSource } from "../workflows/types.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
workflowSourceRef: string;
|
|
21
|
-
workflowSource: JsonValue;
|
|
22
|
-
definitionDigest: string;
|
|
23
|
-
definitionSnapshot: JsonValue;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ResolvedControllerInitialization = {
|
|
27
|
-
schema: "pi-workflows.resolved-controller-initialization.v1";
|
|
28
|
-
controllerName: string;
|
|
29
|
-
controllerPath: string;
|
|
30
|
-
sourceHash: string;
|
|
31
|
-
initialStatus: JsonValue;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type ResolvedSettingsChange = {
|
|
35
|
-
schema: "pi-workflows.resolved-settings-change.v1";
|
|
36
|
-
definitionDigest: string;
|
|
37
|
-
patch: JsonValue;
|
|
38
|
-
settings: JsonValue;
|
|
39
|
-
paths: JsonValue;
|
|
40
|
-
};
|
|
21
|
+
export type {
|
|
22
|
+
ResolvedControllerInitialization,
|
|
23
|
+
ResolvedSettingsChange,
|
|
24
|
+
ResolvedWorkflowLaunch,
|
|
25
|
+
} from "../client/resolver.js";
|
|
41
26
|
|
|
42
27
|
type ResolverRequest = {
|
|
43
28
|
schema: "pi-workflows.resolve-request.v1";
|