@quintinshaw/pi-dynamic-workflows 1.8.0 → 1.9.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
@@ -1,12 +1,35 @@
1
1
  # pi-dynamic-workflows
2
2
 
3
- > Claude-Code-style dynamic workflows for [Pi](https://github.com/earendil-works/pi).
3
+ [![npm](https://img.shields.io/npm/v/@quintinshaw/pi-dynamic-workflows?color=cb3837&logo=npm)](https://www.npmjs.com/package/@quintinshaw/pi-dynamic-workflows)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue)](#license)
5
+ [![for Pi](https://img.shields.io/badge/for-Pi-7c3aed)](https://github.com/earendil-works/pi)
6
+ [![tests](https://img.shields.io/badge/tests-43%20passing-success)](#development)
4
7
 
5
- A Pi extension that adds a `workflow` tool. Instead of one assistant doing everything sequentially, the model writes a small JavaScript script that fans out the work across many isolated subagents, then synthesizes the results.
8
+ > **Claude-Code-style dynamic workflows for [Pi](https://github.com/earendil-works/pi).** One assistant turn fans out into dozens of isolated subagents, cross-checks itself, and hands you a synthesized result.
6
9
 
7
- Great for codebase audits, multi-perspective review, large refactors, and fan-out research. Inspired by Anthropic's [dynamic workflows in Claude Code](https://claude.com/blog/introducing-dynamic-workflows-in-claude-code).
10
+ Instead of one model grinding through a task step by step, Pi writes a small JavaScript **orchestration script** that spawns many subagents in parallel, holds the intermediate results in script variables (not the chat context), and returns only the answer. You get the structure of a pipeline with the flexibility of plain code.
8
11
 
9
- Fork of [Michaelliv/pi-dynamic-workflows](https://github.com/Michaelliv/pi-dynamic-workflows), updated for `@earendil-works/*` packages with a subagent settings-inheritance fix.
12
+ Perfect for **codebase-wide audits, multi-perspective review, large refactors, and cross-checked research** anything where one context window isn't enough.
13
+
14
+ Inspired by Anthropic's [dynamic workflows in Claude Code](https://claude.com/blog/introducing-dynamic-workflows-in-claude-code).
15
+
16
+ ---
17
+
18
+ ## ✨ Highlights
19
+
20
+ - 🚀 **Fan-out orchestration** — `agent()`, `parallel()`, `pipeline()`, `phase()` in a sandboxed script. Up to 16 concurrent / 1000 total subagents.
21
+ - 🧭 **Interactive `/workflows` TUI** — drill through runs → phases → agents → agent detail with the keyboard, just like Claude Code. Pause, stop, and save runs without leaving the view.
22
+ - 📊 **Real token & cost accounting** — read straight from each subagent's session (input / output / cost), not estimated. Your `budget` gates on the real total.
23
+ - 🧠 **Real per-agent / per-phase model routing** — send the cheap work to a small model and the hard synthesis to a big one, resolved against your authenticated models.
24
+ - ⏯️ **Resume** — interrupted runs replay completed agents from a journal (no re-run, no tokens) and only run what's left or what you changed.
25
+ - 🌲 **Git worktree isolation** — `isolation: "worktree"` gives an agent its own branch so parallel agents can edit the same files without clobbering each other.
26
+ - 🔭 **Bundled `/deep-research`** — fans out **real** web searches, fetches sources, keeps only multi-source-supported claims, and writes a cited report. Plus `/adversarial-review` for skeptic-vetted findings.
27
+ - 🧩 **Saved & nested workflows** — turn any run into a `/<name>` slash command; compose saved workflows from inside other scripts.
28
+ - 🪟 **Background + live task panel** — run workflows in the background, watch a "Workflows running" panel under your input, and get the result delivered back into the chat when it finishes.
29
+
30
+ > **This is a heavily extended fork.** The [upstream project](https://github.com/Michaelliv/pi-dynamic-workflows) shipped the core script runtime; here, every advertised capability is actually **implemented, real-tested against the Pi SDK, and shipped** — see the [comparison](#whats-different-from-upstream) below.
31
+
32
+ ---
10
33
 
11
34
  ## Install
12
35
 
@@ -14,7 +37,7 @@ Fork of [Michaelliv/pi-dynamic-workflows](https://github.com/Michaelliv/pi-dynam
14
37
  pi install @quintinshaw/pi-dynamic-workflows
15
38
  ```
16
39
 
17
- Then `/reload` in Pi. The extension registers a `workflow` tool and activates it on session start.
40
+ Then `/reload` in Pi. The extension registers the `workflow` tool and the `/workflows`, `/deep-research`, and `/adversarial-review` commands.
18
41
 
19
42
  <details>
20
43
  <summary>From source (for development)</summary>
@@ -25,54 +48,68 @@ pi install /path/to/pi-dynamic-workflows
25
48
  ```
26
49
  </details>
27
50
 
28
- ## Usage
51
+ ## 30-second demo
29
52
 
30
- Ask Pi for a workflow in plain language:
53
+ Just ask for a workflow in plain language:
31
54
 
32
55
  ```text
33
- Run a workflow to inspect this repository and summarize the main modules.
56
+ Run a workflow to audit every route under src/routes/ for missing auth checks.
34
57
  ```
35
58
 
36
- The model writes a workflow script and calls the `workflow` tool. Live progress streams inline:
59
+ Pi writes the script and streams compact progress inline:
37
60
 
38
61
  ```text
39
- ◆ Workflow: inspect_project (3/3 done · 12,480 tokens)
62
+ ◆ Workflow: auth_audit (5/5 done · 48,210 tokens · $0.0131)
40
63
  ✓ Scan 1/1
41
- #1 ✓ repo inventory
42
- Analyze 2/2
43
- #2 ✓ source modules
44
- #3 ✓ final summary
64
+ #1 ✓ enumerate routes
65
+ Review 3/3
66
+ #2 ✓ routes/users.ts
67
+ #3 ✓ routes/admin.ts
68
+ #4 ✓ routes/billing.ts
69
+ ✓ Verify 1/1
70
+ #5 ✓ adversarial recheck
45
71
  ```
46
72
 
47
- Press `Esc` to cancel a running run; active subagents are aborted and surfaced as skipped.
73
+ Press `Esc` to cancel; active subagents are aborted and surfaced as skipped.
48
74
 
49
- ### Background runs & `/workflows`
75
+ ## What's different from upstream
50
76
 
51
- Ask for a background workflow (the model passes `background: true`) and it runs without blocking your session. Manage it with the `/workflows` command:
77
+ This fork turns the original's roadmap into working, tested features:
52
78
 
53
- ```text
54
- /workflows # list runs (default)
55
- /workflows status <id> # watch a running run live (status bar), prints result when done
56
- /workflows stop <id> # abort a running run
57
- /workflows pause <id> # pause a running run
58
- /workflows resume <id> # resume an interrupted run (replays cached results)
59
- /workflows rm <id> # remove a run from the list
60
- ```
79
+ | Capability | Upstream | This fork |
80
+ | --- | :---: | :---: |
81
+ | Core `agent`/`parallel`/`pipeline` runtime | | |
82
+ | Structured (JSON-Schema) subagent output | ✅ | ✅ |
83
+ | **Token & cost accounting** | estimate | ✅ real, from the SDK session |
84
+ | **Per-agent / per-phase model routing** | prose-only* | actually switches models |
85
+ | **`/workflows` command + interactive TUI** | | ✅ full keyboard navigator |
86
+ | **Resume an interrupted run** | — | ✅ journaled, replays the prefix |
87
+ | **Git worktree isolation** | — | ✅ real worktrees, auto-cleanup |
88
+ | **`/deep-research` with real web access** | — | ✅ live search + cross-checking |
89
+ | **Saved workflows as `/<name>`** | — | ✅ |
90
+ | **Nested `workflow()`** | — | ✅ shares the global caps |
91
+ | **Background runs + live task panel + result delivery** | — | ✅ |
92
+ | Test suite | minimal | ✅ 43 tests + real Pi end-to-end |
61
93
 
62
- ### Bundled workflows
94
+ <sub>*Upstream injected the requested model as a text line in the prompt; it never changed the subagent's actual model.</sub>
95
+
96
+ ## Commands
63
97
 
64
98
  ```text
65
- /deep-research <question> # web-researched, source-cross-checked report
66
- /adversarial-review <task> # findings cross-checked by skeptical reviewers
67
- ```
99
+ /workflows # open the interactive navigator (plain list in print mode)
100
+ /workflows status <id> # watch a running run live; prints the result when it finishes
101
+ /workflows save <name> # save the latest run's script as a reusable /<name> command
102
+ /workflows pause|resume|stop|rm <id>
68
103
 
69
- `/deep-research` fans out web searches across several angles, fetches the top sources with real `web_search` / `web_fetch` tools, keeps only claims supported by multiple sources, and writes a cited report.
104
+ /deep-research <question> # web-researched, source-cross-checked report
105
+ /adversarial-review <task> # findings cross-checked by skeptical reviewers
106
+ ```
70
107
 
71
- Save any run as a reusable command: `/workflows save <name>` writes the most recent run's script to `.pi/workflows/saved/<name>.json`, and it immediately becomes `/<name>` (arguments parsed as `key=value` + positionals into `args`).
108
+ In the **interactive navigator**: `↑/↓` (or `j/k`) select · `enter`/`→` open · `esc`/`←` back · `j/k` scroll detail · `p` pause/resume · `x` stop · `s` save · `q` quit.
72
109
 
73
- ## Workflow script shape
110
+ ## Writing a workflow
74
111
 
75
- A workflow is plain JavaScript. The first statement must export literal metadata:
112
+ A workflow is plain JavaScript whose first statement exports literal metadata:
76
113
 
77
114
  ```js
78
115
  export const meta = {
@@ -101,7 +138,7 @@ return { inventory, summary }
101
138
  | `workflow(name, args)` | Run a saved workflow inline and return its result (one level deep; shares the global caps). |
102
139
  | `log(message)` | Append a workflow-level log line. |
103
140
  | `args` | Optional JSON value passed via the tool's `args` parameter. |
104
- | `budget` | `{ total, spent(), remaining() }` token-budget tracker. |
141
+ | `budget` | `{ total, spent(), remaining() }` token-budget tracker (real tokens). |
105
142
  | `cwd`, `process.cwd()` | Working directory for subagents. |
106
143
 
107
144
  ### Agent options
@@ -110,16 +147,16 @@ return { inventory, summary }
110
147
  | --- | --- | --- |
111
148
  | `label` | string | Human-readable label for progress display |
112
149
  | `phase` | string | Override the current phase for this agent |
113
- | `schema` | object | JSON Schema for structured output |
150
+ | `schema` | object | JSON Schema the subagent returns a validated object |
114
151
  | `model` | string | Run this agent on a specific model — `provider/modelId` or a bare `modelId` |
115
152
  | `isolation` | `"worktree"` | Run this agent in its own throwaway git worktree (parallel edits without conflict) |
116
153
  | `timeoutMs` | number | Override the default 5-minute agent timeout |
117
154
 
118
- Models can also be set per phase via `meta.phases[].model`. Precedence is `opts.model` > phase model > session default; an unknown model logs a warning and falls back to the default.
155
+ Models can also be set per phase via `meta.phases[].model`. Precedence: `opts.model` > phase model > session default; an unknown model logs a warning and falls back.
119
156
 
120
157
  ### Structured output
121
158
 
122
- Pass a JSON Schema via `opts.schema` and the subagent returns a validated object:
159
+ Pass a JSON Schema and the subagent returns a validated object instead of prose:
123
160
 
124
161
  ```js
125
162
  const finding = await agent('Find security-sensitive files.', {
@@ -135,50 +172,38 @@ const finding = await agent('Find security-sensitive files.', {
135
172
  })
136
173
  ```
137
174
 
138
- Backed by a Pi `structured_output` tool with `terminate: true`, so the subagent ends on that call.
139
-
140
- ### Determinism rules
175
+ Backed by a Pi `structured_output` tool with `terminate: true`, so the subagent ends on that call — no wasted follow-up turn.
141
176
 
142
- Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`, `new Date()`, `Math.random()`, `require`/`import`/`fs`/network, and (inside `meta`) spreads, computed keys, template interpolation, and function calls. This keeps `meta` parseable and runs reproducible.
177
+ ### Determinism
143
178
 
144
- ## What works today
145
-
146
- - **Core runtime** — `agent` / `parallel` / `pipeline` / `phase` / `log` / `budget` in a sandboxed script
147
- - **Structured output** — JSON-Schema-validated subagent results
148
- - **Real token & cost accounting** — read from each subagent's SDK session (input / output / total / cost), with a character estimate only as fallback when a provider reports no usage; `budget` gates on the real total
149
- - **Real per-agent / per-phase model routing** — `opts.model` and `meta.phases[].model` actually select the model (resolved against your authed model registry), with graceful fallback
150
- - **`/workflows` command** — list, inspect, stop, pause, **resume**, and remove background runs; runs started with `background: true` are reachable from the command
151
- - **Bundled `/deep-research` & `/adversarial-review`** — `/deep-research` runs real web searches (via built-in `web_search` / `web_fetch` tools), extracts claims, cross-checks them across sources, and reports only what survived; `/adversarial-review` investigates a task then has independent skeptics try to refute each finding, keeping only those that clear an agreement threshold
152
- - **Saved workflows as `/<name>`** — save a run's script with `/workflows save <name>` and it becomes a reusable slash command; arguments are parsed (`key=value` and positionals) and passed through as `args`
153
- - **Nested `workflow()`** — call `await workflow('saved-name', args)` inside a script to run a saved workflow inline; nesting is one level deep and shares the parent's concurrency limiter, agent counter, and token budget so the global caps hold
154
- - **Resume** — each agent result is journaled by a deterministic call index; resuming replays the unchanged prefix from cache (no re-run, no tokens) and runs only new or edited calls live
155
- - **Worktree isolation** — `isolation: "worktree"` runs an agent in its own git worktree on a throwaway branch, so parallel agents can edit the same files without conflict; the worktree is torn down after (results are not auto-merged), and it falls back to a logged no-op outside a git repo
156
- - **Safety limits** — 1000-agent cap (`maxAgents`), per-agent timeout (`agentTimeoutMs`), recoverable-vs-fatal error classification
157
- - **Live progress + token/cost display**, `Esc` to abort
158
- - **Log persistence** to `.pi/workflows/runs/`
179
+ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`, `new Date()`, `Math.random()`, `require`/`import`/`fs`/network, and (inside `meta`) spreads, computed keys, template interpolation, and function calls. This keeps `meta` parseable and runs **reproducible** — which is what makes resume reliable.
159
180
 
160
181
  ## How it works
161
182
 
162
183
  ```text
163
184
  user prompt
164
- → Pi model writes a workflow script
165
- → workflow tool parses + runs it in a vm sandbox
166
- → script calls agent() / parallel() / pipeline()
185
+ → Pi writes a workflow script
186
+ the workflow tool parses + runs it in a vm sandbox
187
+ the script calls agent() / parallel() / pipeline()
167
188
  → each agent() spawns a fresh in-memory Pi subagent session
168
- → snapshots stream back as compact progress
169
- → final structured result returns to the parent assistant
189
+ results are journaled; snapshots stream back as compact progress
190
+ the final structured result returns to the parent assistant
170
191
  ```
171
192
 
172
- Subagents run in fresh in-memory Pi sessions with the standard coding tools (read, bash, edit, write, grep, find, ls), so they work exactly like a normal Pi turn.
193
+ Subagents run in fresh in-memory Pi sessions with the standard coding tools (read, bash, edit, write, grep, find, ls), so they work exactly like a normal Pi turn — and inherit your provider/model settings.
173
194
 
174
195
  ## Development
175
196
 
176
197
  ```bash
177
198
  npm install
178
- npm test # biome check + tsc + unit tests
199
+ npm test # biome check + tsc + 43 unit tests
179
200
  ```
180
201
 
181
- Parser unit tests live in `tests/workflow-parser.test.ts`.
202
+ Tests live in `tests/`. Each feature is also verified end-to-end against a real Pi subagent session before release.
203
+
204
+ ## Credits
205
+
206
+ Fork of [Michaelliv/pi-dynamic-workflows](https://github.com/Michaelliv/pi-dynamic-workflows), rebuilt on `@earendil-works/*` packages with the advertised feature set implemented and a subagent settings-inheritance fix. Inspired by [Claude Code dynamic workflows](https://claude.com/blog/introducing-dynamic-workflows-in-claude-code).
182
207
 
183
208
  ## License
184
209
 
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { createRunPersistence, generateRunId } from "./run-persistence.js";
20
20
  export { parseCommandArgs, registerAllSavedWorkflows, registerSavedWorkflow, } from "./saved-commands.js";
21
21
  export type { StructuredOutputCapture, StructuredOutputToolOptions } from "./structured-output.js";
22
22
  export { createStructuredOutputTool } from "./structured-output.js";
23
+ export { installResultDelivery, installTaskPanel, type TaskPanelOptions } from "./task-panel.js";
23
24
  export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
24
25
  export type { AgentOptions, JournalEntry, SharedRuntime, WorkflowMeta, WorkflowMetaPhase, WorkflowRunOptions, WorkflowRunResult, } from "./workflow.js";
25
26
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
@@ -30,5 +31,6 @@ export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
30
31
  export { createWorkflowStorage } from "./workflow-saved.js";
31
32
  export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
32
33
  export { createWorkflowTool } from "./workflow-tool.js";
34
+ export { keyToAction, type NavAction, NavigatorModel, NavigatorState, openWorkflowNavigator, renderNavigator, type ViewKind, } from "./workflow-ui.js";
33
35
  export type { Worktree } from "./worktree.js";
34
36
  export { createWorktree, removeWorktree } from "./worktree.js";
package/dist/index.js CHANGED
@@ -11,10 +11,12 @@ export { buildModelRoutingInstructions, parseModelRoutingFromMeta, resolveModelF
11
11
  export { createRunPersistence, generateRunId } from "./run-persistence.js";
12
12
  export { parseCommandArgs, registerAllSavedWorkflows, registerSavedWorkflow, } from "./saved-commands.js";
13
13
  export { createStructuredOutputTool } from "./structured-output.js";
14
+ export { installResultDelivery, installTaskPanel } from "./task-panel.js";
14
15
  export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
15
16
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
16
17
  export { registerWorkflowCommands } from "./workflow-commands.js";
17
18
  export { WorkflowManager } from "./workflow-manager.js";
18
19
  export { createWorkflowStorage } from "./workflow-saved.js";
19
20
  export { createWorkflowTool } from "./workflow-tool.js";
21
+ export { keyToAction, NavigatorModel, NavigatorState, openWorkflowNavigator, renderNavigator, } from "./workflow-ui.js";
20
22
  export { createWorktree, removeWorktree } from "./worktree.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Background-run UX, mirroring Claude Code:
3
+ * - A live task panel below the input lists in-progress runs while you keep working.
4
+ * Focus it (↓) and press enter to open the full navigator.
5
+ * - When a background run finishes, its result is delivered back into the
6
+ * conversation so the paused task continues with the outcome.
7
+ */
8
+ import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
9
+ import type { WorkflowManager } from "./workflow-manager.js";
10
+ import type { WorkflowStorage } from "./workflow-saved.js";
11
+ export interface TaskPanelOptions {
12
+ storage?: WorkflowStorage;
13
+ cwd?: string;
14
+ }
15
+ /**
16
+ * Deliver a background run's result into the conversation when it completes or
17
+ * fails. Set up once per extension; idempotent via an internal guard.
18
+ */
19
+ export declare function installResultDelivery(pi: ExtensionAPI, manager: WorkflowManager): void;
20
+ /**
21
+ * Install the live "workflows running" panel below the editor. Re-rendered on
22
+ * every manager event; focus + enter opens the navigator.
23
+ */
24
+ export declare function installTaskPanel(pi: ExtensionAPI, manager: WorkflowManager, ui: ExtensionUIContext, opts?: TaskPanelOptions): void;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Background-run UX, mirroring Claude Code:
3
+ * - A live task panel below the input lists in-progress runs while you keep working.
4
+ * Focus it (↓) and press enter to open the full navigator.
5
+ * - When a background run finishes, its result is delivered back into the
6
+ * conversation so the paused task continues with the outcome.
7
+ */
8
+ import { parseKey } from "@earendil-works/pi-tui";
9
+ import { openWorkflowNavigator } from "./workflow-ui.js";
10
+ const RUN_EVENTS = ["agentStart", "agentEnd", "phase", "log", "complete", "error", "stopped", "paused", "resumed"];
11
+ function deliverText(run) {
12
+ const r = run.result?.result;
13
+ const body = r && typeof r.report === "string" && r.report.trim() ? r.report : JSON.stringify(run.result?.result, null, 2);
14
+ const tokens = run.result?.tokenUsage ? ` · ${run.result.tokenUsage.total.toLocaleString()} tokens` : "";
15
+ const agents = run.result?.agentCount ?? run.snapshot.agentCount;
16
+ return `✓ Workflow "${run.snapshot.name}" finished (${agents} agents${tokens}).\n\n${body}`;
17
+ }
18
+ /**
19
+ * Deliver a background run's result into the conversation when it completes or
20
+ * fails. Set up once per extension; idempotent via an internal guard.
21
+ */
22
+ export function installResultDelivery(pi, manager) {
23
+ if (manager.__deliveryInstalled)
24
+ return;
25
+ manager.__deliveryInstalled = true;
26
+ manager.on("complete", ({ runId }) => {
27
+ const run = manager.getRun(runId);
28
+ if (run)
29
+ void pi.sendMessage({ customType: "workflow-result", content: deliverText(run), display: true });
30
+ });
31
+ manager.on("error", ({ runId, error }) => {
32
+ void pi.sendMessage({
33
+ customType: "workflow-result",
34
+ content: `✗ Workflow ${runId} failed: ${error?.message ?? "unknown error"}`,
35
+ display: true,
36
+ });
37
+ });
38
+ }
39
+ function renderPanel(manager, theme, focused) {
40
+ const active = manager.listRuns().filter((r) => r.status === "running" || r.status === "paused");
41
+ if (!active.length)
42
+ return [];
43
+ const rows = active.map((r) => {
44
+ const live = manager.getRun(r.runId);
45
+ const agents = live?.snapshot.agents ?? r.agents;
46
+ const done = agents.filter((a) => a.status === "done").length;
47
+ const icon = r.status === "paused" ? "⏸" : "◆";
48
+ const phase = live?.snapshot.currentPhase ? ` · ${live.snapshot.currentPhase}` : "";
49
+ return ` ${icon} ${r.workflowName} ${done}/${agents.length} agents${phase}`;
50
+ });
51
+ const hint = focused
52
+ ? theme.fg("accent", " enter: open · esc: back")
53
+ : theme.fg("dim", " ↓ then enter, or /workflows, to open");
54
+ return [theme.bold(`Workflows running (${active.length}):`), ...rows, hint];
55
+ }
56
+ /**
57
+ * Install the live "workflows running" panel below the editor. Re-rendered on
58
+ * every manager event; focus + enter opens the navigator.
59
+ */
60
+ export function installTaskPanel(pi, manager, ui, opts = {}) {
61
+ ui.setWidget("workflow-tasks", (tui, theme) => {
62
+ const onEvent = () => tui.requestRender();
63
+ for (const ev of RUN_EVENTS)
64
+ manager.on(ev, onEvent);
65
+ const comp = {
66
+ focused: false,
67
+ render: () => renderPanel(manager, theme, comp.focused ?? false),
68
+ handleInput: (data) => {
69
+ const key = parseKey(data);
70
+ if (key === "enter" || key === "return" || key === "right") {
71
+ void openWorkflowNavigator(pi, manager, ui, opts);
72
+ }
73
+ },
74
+ invalidate: () => { },
75
+ dispose: () => {
76
+ for (const ev of RUN_EVENTS)
77
+ manager.off(ev, onEvent);
78
+ },
79
+ };
80
+ return comp;
81
+ }, { placement: "belowEditor" });
82
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { recomputeWorkflowSnapshot, renderWorkflowText } from "./display.js";
6
6
  import { registerSavedWorkflow } from "./saved-commands.js";
7
+ import { openWorkflowNavigator } from "./workflow-ui.js";
7
8
  const STATUS_ICON = {
8
9
  pending: "·",
9
10
  running: "◆",
@@ -110,7 +111,18 @@ export function registerWorkflowCommands(pi, manager, opts = {}) {
110
111
  const id = parts[1];
111
112
  const print = (text) => pi.sendMessage({ customType: "workflows", content: text, display: true });
112
113
  switch (sub) {
114
+ case "ui":
113
115
  case "list": {
116
+ // Interactive navigator when a UI is available; plain text otherwise
117
+ // (print/RPC mode) or when the user explicitly asks for `list`.
118
+ if (sub !== "list" && ctx.hasUI) {
119
+ await openWorkflowNavigator(pi, manager, ctx.ui, { storage: opts.storage, cwd: opts.cwd });
120
+ return;
121
+ }
122
+ if (parts.length === 0 && ctx.hasUI) {
123
+ await openWorkflowNavigator(pi, manager, ctx.ui, { storage: opts.storage, cwd: opts.cwd });
124
+ return;
125
+ }
114
126
  const runs = manager.listRuns();
115
127
  if (!runs.length) {
116
128
  await print("No workflow runs yet. Start one with a background workflow (background: true).");
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Interactive `/workflows` navigator, modeled on Claude Code's view:
3
+ *
4
+ * runs ──enter──▶ phases ──enter──▶ agents ──enter──▶ agent detail
5
+ * ◀──esc─── ◀──esc──── ◀──esc────
6
+ *
7
+ * Keys: ↑/↓ (or j/k) select · enter/→ drill in · esc/← back (esc at top closes)
8
+ * p pause/resume · x stop · r restart · s save · q quit
9
+ *
10
+ * The state machine and line rendering are pure and unit-tested; the pi-tui
11
+ * Component shell (openWorkflowNavigator) wires them to live manager events.
12
+ */
13
+ import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
14
+ import type { WorkflowAgentSnapshot } from "./display.js";
15
+ import type { WorkflowManager } from "./workflow-manager.js";
16
+ import type { WorkflowStorage } from "./workflow-saved.js";
17
+ /** Minimal theme surface so rendering is testable without the real Theme class. */
18
+ export interface ThemeLike {
19
+ fg(color: string, text: string): string;
20
+ bold(text: string): string;
21
+ }
22
+ export type ViewKind = "runs" | "phases" | "agents" | "detail";
23
+ interface RunRow {
24
+ runId: string;
25
+ name: string;
26
+ status: string;
27
+ done: number;
28
+ total: number;
29
+ tokens: number;
30
+ }
31
+ interface PhaseRow {
32
+ title: string;
33
+ done: number;
34
+ total: number;
35
+ tokens: number;
36
+ }
37
+ interface AgentRow {
38
+ id: number;
39
+ label: string;
40
+ status: string;
41
+ phase?: string;
42
+ tokens?: number;
43
+ }
44
+ /** Reads run/phase/agent data from the manager, preferring live snapshots. */
45
+ export declare class NavigatorModel {
46
+ private readonly manager;
47
+ constructor(manager: Pick<WorkflowManager, "listRuns" | "getRun">);
48
+ private snapshot;
49
+ runs(): RunRow[];
50
+ runName(runId: string): string;
51
+ runStatus(runId: string): string;
52
+ phases(runId: string): PhaseRow[];
53
+ agents(runId: string, phase: string): AgentRow[];
54
+ agentDetail(runId: string, agentId: number): WorkflowAgentSnapshot | undefined;
55
+ }
56
+ /** Navigation state machine: a stack of (view, cursor) frames plus detail scroll. */
57
+ export declare class NavigatorState {
58
+ private stack;
59
+ scroll: number;
60
+ private top;
61
+ get kind(): ViewKind;
62
+ get cursor(): number;
63
+ get runId(): string | undefined;
64
+ get phase(): string | undefined;
65
+ get agentId(): number | undefined;
66
+ get depth(): number;
67
+ /** Clamp the cursor to [0, count). */
68
+ clamp(count: number): void;
69
+ move(delta: number, count: number): void;
70
+ /** Drill into the selected item. Returns true if the view changed. */
71
+ drill(model: NavigatorModel): boolean;
72
+ /** Pop one level. Returns false when already at the top (caller should close). */
73
+ back(): boolean;
74
+ /** The runId the current view acts on (for pause/stop/save). */
75
+ activeRunId(model: NavigatorModel): string | undefined;
76
+ }
77
+ /** Build the lines for the current view. Pure: depends only on state + model + theme. */
78
+ export declare function renderNavigator(state: NavigatorState, model: NavigatorModel, width: number, theme?: ThemeLike): string[];
79
+ /** What a key press should do. Pure mapping from a parsed key id to an action. */
80
+ export type NavAction = {
81
+ type: "move";
82
+ delta: number;
83
+ } | {
84
+ type: "drill";
85
+ } | {
86
+ type: "back";
87
+ } | {
88
+ type: "close";
89
+ } | {
90
+ type: "pause";
91
+ } | {
92
+ type: "stop";
93
+ } | {
94
+ type: "restart";
95
+ } | {
96
+ type: "save";
97
+ } | {
98
+ type: "none";
99
+ };
100
+ export declare function keyToAction(keyId: string | undefined, kind: ViewKind): NavAction;
101
+ export interface NavigatorOptions {
102
+ storage?: WorkflowStorage;
103
+ cwd?: string;
104
+ }
105
+ /**
106
+ * Open the interactive `/workflows` navigator as a focused overlay. Resolves when
107
+ * the user closes it (esc at the top level, or `q`).
108
+ */
109
+ export declare function openWorkflowNavigator(pi: ExtensionAPI, manager: WorkflowManager, ui: ExtensionUIContext, opts?: NavigatorOptions): Promise<void>;
110
+ export {};