@quintinshaw/pi-dynamic-workflows 1.7.0 → 1.7.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/README.md +1 -1
- package/dist/workflow-commands.js +68 -3
- package/package.json +1 -1
- package/src/workflow-commands.ts +64 -3
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Ask for a background workflow (the model passes `background: true`) and it runs
|
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
54
|
/workflows # list runs (default)
|
|
55
|
-
/workflows status <id> #
|
|
55
|
+
/workflows status <id> # watch a running run live (status bar), prints result when done
|
|
56
56
|
/workflows stop <id> # abort a running run
|
|
57
57
|
/workflows pause <id> # pause a running run
|
|
58
58
|
/workflows resume <id> # resume an interrupted run (replays cached results)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `/workflows` slash command: list, inspect, and control background workflow runs.
|
|
3
3
|
* Shares the extension's single WorkflowManager so background runs are reachable.
|
|
4
4
|
*/
|
|
5
|
-
import { renderWorkflowText } from "./display.js";
|
|
5
|
+
import { recomputeWorkflowSnapshot, renderWorkflowText } from "./display.js";
|
|
6
6
|
import { registerSavedWorkflow } from "./saved-commands.js";
|
|
7
7
|
const STATUS_ICON = {
|
|
8
8
|
pending: "·",
|
|
@@ -12,7 +12,7 @@ const STATUS_ICON = {
|
|
|
12
12
|
failed: "✗",
|
|
13
13
|
aborted: "⊘",
|
|
14
14
|
};
|
|
15
|
-
const USAGE = "Usage: /workflows [list] | status <id> | stop <id> | pause <id> | resume <id> | rm <id> | save <name> [runId]";
|
|
15
|
+
const USAGE = "Usage: /workflows [list] | status <id> | watch <id> | stop <id> | pause <id> | resume <id> | rm <id> | save <name> [runId]";
|
|
16
16
|
function summarizeRun(run) {
|
|
17
17
|
const icon = STATUS_ICON[run.status] ?? "?";
|
|
18
18
|
const done = run.agents.filter((a) => a.status === "done").length;
|
|
@@ -20,6 +20,64 @@ function summarizeRun(run) {
|
|
|
20
20
|
const tokens = run.tokenUsage ? ` · ${run.tokenUsage.total.toLocaleString()} tok` : "";
|
|
21
21
|
return `${icon} ${run.runId} ${run.workflowName} [${run.status}] ${done}/${total} agents${tokens}`;
|
|
22
22
|
}
|
|
23
|
+
function oneLineProgress(snapshot) {
|
|
24
|
+
const total = snapshot.agents.length;
|
|
25
|
+
const done = snapshot.agents.filter((a) => a.status === "done").length;
|
|
26
|
+
const running = snapshot.agents.filter((a) => a.status === "running").length;
|
|
27
|
+
const errs = snapshot.agents.filter((a) => a.status === "error").length;
|
|
28
|
+
const phase = snapshot.currentPhase ? ` · ${snapshot.currentPhase}` : "";
|
|
29
|
+
return `◆ ${snapshot.name}: ${done}/${total} done${running ? `, ${running} running` : ""}${errs ? `, ${errs} err` : ""}${phase}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to a running run's events and stream live progress to the status bar,
|
|
33
|
+
* printing the final snapshot when it finishes. Non-blocking: returns true if the
|
|
34
|
+
* run was active and is now being watched, false otherwise. Listeners clean up on
|
|
35
|
+
* completion so nothing leaks.
|
|
36
|
+
*/
|
|
37
|
+
function watchRun(manager, pi, ctx, id) {
|
|
38
|
+
const active = manager.getRun(id);
|
|
39
|
+
if (!active || active.status !== "running")
|
|
40
|
+
return false;
|
|
41
|
+
const key = `wf:${id}`;
|
|
42
|
+
const update = () => {
|
|
43
|
+
const run = manager.getRun(id);
|
|
44
|
+
if (run)
|
|
45
|
+
ctx.ui.setStatus(key, oneLineProgress(run.snapshot));
|
|
46
|
+
};
|
|
47
|
+
const onEvent = (e) => {
|
|
48
|
+
if (!e || e.runId === id)
|
|
49
|
+
update();
|
|
50
|
+
};
|
|
51
|
+
let settled = false;
|
|
52
|
+
const progressEvents = ["agentStart", "agentEnd", "phase", "log"];
|
|
53
|
+
const finalEvents = ["complete", "error", "stopped", "paused"];
|
|
54
|
+
const finish = (e) => {
|
|
55
|
+
if (e && e.runId !== id)
|
|
56
|
+
return;
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
for (const ev of progressEvents)
|
|
61
|
+
manager.off(ev, onEvent);
|
|
62
|
+
for (const ev of finalEvents)
|
|
63
|
+
manager.off(ev, finish);
|
|
64
|
+
ctx.ui.setStatus(key, undefined);
|
|
65
|
+
const run = manager.getRun(id);
|
|
66
|
+
if (run) {
|
|
67
|
+
void pi.sendMessage({
|
|
68
|
+
customType: "workflows",
|
|
69
|
+
content: renderWorkflowText(recomputeWorkflowSnapshot(run.snapshot), true),
|
|
70
|
+
display: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (const ev of progressEvents)
|
|
75
|
+
manager.on(ev, onEvent);
|
|
76
|
+
for (const ev of finalEvents)
|
|
77
|
+
manager.on(ev, finish);
|
|
78
|
+
update();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
23
81
|
function renderPersistedStatus(run) {
|
|
24
82
|
const lines = [`${STATUS_ICON[run.status] ?? "?"} ${run.workflowName} (${run.runId}) — ${run.status}`];
|
|
25
83
|
if (run.currentPhase)
|
|
@@ -61,14 +119,21 @@ export function registerWorkflowCommands(pi, manager, opts = {}) {
|
|
|
61
119
|
await print(["Workflow runs:", ...runs.map(summarizeRun), "", USAGE].join("\n"));
|
|
62
120
|
return;
|
|
63
121
|
}
|
|
122
|
+
case "watch":
|
|
64
123
|
case "status": {
|
|
65
124
|
if (!id) {
|
|
66
125
|
ctx.ui.notify(USAGE, "warning");
|
|
67
126
|
return;
|
|
68
127
|
}
|
|
128
|
+
// A running run streams live progress to the status bar and prints the
|
|
129
|
+
// final snapshot when it finishes — no need to re-run the command.
|
|
130
|
+
if (watchRun(manager, pi, ctx, id)) {
|
|
131
|
+
ctx.ui.notify(`Watching ${id} — live progress in the status bar; result prints when it finishes.`, "info");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
69
134
|
const live = manager.getSnapshot(id);
|
|
70
135
|
if (live) {
|
|
71
|
-
await print(renderWorkflowText(live, false));
|
|
136
|
+
await print(renderWorkflowText(recomputeWorkflowSnapshot(live), false));
|
|
72
137
|
return;
|
|
73
138
|
}
|
|
74
139
|
const run = manager.listRuns().find((r) => r.runId === id);
|
package/package.json
CHANGED
package/src/workflow-commands.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { renderWorkflowText } from "./display.js";
|
|
7
|
+
import { recomputeWorkflowSnapshot, renderWorkflowText, type WorkflowSnapshot } from "./display.js";
|
|
8
8
|
import type { PersistedRunState } from "./run-persistence.js";
|
|
9
9
|
import { registerSavedWorkflow } from "./saved-commands.js";
|
|
10
10
|
import type { WorkflowManager } from "./workflow-manager.js";
|
|
@@ -20,7 +20,7 @@ const STATUS_ICON: Record<string, string> = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const USAGE =
|
|
23
|
-
"Usage: /workflows [list] | status <id> | stop <id> | pause <id> | resume <id> | rm <id> | save <name> [runId]";
|
|
23
|
+
"Usage: /workflows [list] | status <id> | watch <id> | stop <id> | pause <id> | resume <id> | rm <id> | save <name> [runId]";
|
|
24
24
|
|
|
25
25
|
function summarizeRun(run: PersistedRunState): string {
|
|
26
26
|
const icon = STATUS_ICON[run.status] ?? "?";
|
|
@@ -30,6 +30,60 @@ function summarizeRun(run: PersistedRunState): string {
|
|
|
30
30
|
return `${icon} ${run.runId} ${run.workflowName} [${run.status}] ${done}/${total} agents${tokens}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function oneLineProgress(snapshot: WorkflowSnapshot): string {
|
|
34
|
+
const total = snapshot.agents.length;
|
|
35
|
+
const done = snapshot.agents.filter((a) => a.status === "done").length;
|
|
36
|
+
const running = snapshot.agents.filter((a) => a.status === "running").length;
|
|
37
|
+
const errs = snapshot.agents.filter((a) => a.status === "error").length;
|
|
38
|
+
const phase = snapshot.currentPhase ? ` · ${snapshot.currentPhase}` : "";
|
|
39
|
+
return `◆ ${snapshot.name}: ${done}/${total} done${running ? `, ${running} running` : ""}${
|
|
40
|
+
errs ? `, ${errs} err` : ""
|
|
41
|
+
}${phase}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to a running run's events and stream live progress to the status bar,
|
|
46
|
+
* printing the final snapshot when it finishes. Non-blocking: returns true if the
|
|
47
|
+
* run was active and is now being watched, false otherwise. Listeners clean up on
|
|
48
|
+
* completion so nothing leaks.
|
|
49
|
+
*/
|
|
50
|
+
function watchRun(manager: WorkflowManager, pi: ExtensionAPI, ctx: ExtensionCommandContext, id: string): boolean {
|
|
51
|
+
const active = manager.getRun(id);
|
|
52
|
+
if (!active || active.status !== "running") return false;
|
|
53
|
+
|
|
54
|
+
const key = `wf:${id}`;
|
|
55
|
+
const update = () => {
|
|
56
|
+
const run = manager.getRun(id);
|
|
57
|
+
if (run) ctx.ui.setStatus(key, oneLineProgress(run.snapshot));
|
|
58
|
+
};
|
|
59
|
+
const onEvent = (e: { runId?: string }) => {
|
|
60
|
+
if (!e || e.runId === id) update();
|
|
61
|
+
};
|
|
62
|
+
let settled = false;
|
|
63
|
+
const progressEvents = ["agentStart", "agentEnd", "phase", "log"];
|
|
64
|
+
const finalEvents = ["complete", "error", "stopped", "paused"];
|
|
65
|
+
const finish = (e: { runId?: string }) => {
|
|
66
|
+
if (e && e.runId !== id) return;
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
for (const ev of progressEvents) manager.off(ev, onEvent);
|
|
70
|
+
for (const ev of finalEvents) manager.off(ev, finish);
|
|
71
|
+
ctx.ui.setStatus(key, undefined);
|
|
72
|
+
const run = manager.getRun(id);
|
|
73
|
+
if (run) {
|
|
74
|
+
void pi.sendMessage({
|
|
75
|
+
customType: "workflows",
|
|
76
|
+
content: renderWorkflowText(recomputeWorkflowSnapshot(run.snapshot), true),
|
|
77
|
+
display: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const ev of progressEvents) manager.on(ev, onEvent);
|
|
82
|
+
for (const ev of finalEvents) manager.on(ev, finish);
|
|
83
|
+
update();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
33
87
|
function renderPersistedStatus(run: PersistedRunState): string {
|
|
34
88
|
const lines = [`${STATUS_ICON[run.status] ?? "?"} ${run.workflowName} (${run.runId}) — ${run.status}`];
|
|
35
89
|
if (run.currentPhase) lines.push(` phase: ${run.currentPhase}`);
|
|
@@ -81,14 +135,21 @@ export function registerWorkflowCommands(
|
|
|
81
135
|
await print(["Workflow runs:", ...runs.map(summarizeRun), "", USAGE].join("\n"));
|
|
82
136
|
return;
|
|
83
137
|
}
|
|
138
|
+
case "watch":
|
|
84
139
|
case "status": {
|
|
85
140
|
if (!id) {
|
|
86
141
|
ctx.ui.notify(USAGE, "warning");
|
|
87
142
|
return;
|
|
88
143
|
}
|
|
144
|
+
// A running run streams live progress to the status bar and prints the
|
|
145
|
+
// final snapshot when it finishes — no need to re-run the command.
|
|
146
|
+
if (watchRun(manager, pi, ctx, id)) {
|
|
147
|
+
ctx.ui.notify(`Watching ${id} — live progress in the status bar; result prints when it finishes.`, "info");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
89
150
|
const live = manager.getSnapshot(id);
|
|
90
151
|
if (live) {
|
|
91
|
-
await print(renderWorkflowText(live, false));
|
|
152
|
+
await print(renderWorkflowText(recomputeWorkflowSnapshot(live), false));
|
|
92
153
|
return;
|
|
93
154
|
}
|
|
94
155
|
const run = manager.listRuns().find((r) => r.runId === id);
|