@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 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> # show a run's progress
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quintinshaw/pi-dynamic-workflows",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Claude-Code-style dynamic workflow orchestration for Pi.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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);