@quintinshaw/pi-dynamic-workflows 1.9.0 → 1.9.2

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,36 @@
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
+ - 🪟 **Non-blocking by default + live task panel** — workflows run in the background: the turn ends immediately so you can keep chatting or start other tasks, a "Workflows running" panel tracks them under your input, and when one finishes its result is delivered back and the conversation **auto-continues** (queued politely after whatever you're doing, never interrupting).
29
+ - 🌈 **Workflows mode in the input box** — type `workflow`/`workflows` and the word turns into a flowing rainbow, arming a forced workflow for that message. One Backspace right after the word disarms it (turns plain white) without deleting it.
30
+
31
+ > **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.
32
+
33
+ ---
10
34
 
11
35
  ## Install
12
36
 
@@ -14,7 +38,7 @@ Fork of [Michaelliv/pi-dynamic-workflows](https://github.com/Michaelliv/pi-dynam
14
38
  pi install @quintinshaw/pi-dynamic-workflows
15
39
  ```
16
40
 
17
- Then `/reload` in Pi. The extension registers a `workflow` tool and activates it on session start.
41
+ Then `/reload` in Pi. The extension registers the `workflow` tool and the `/workflows`, `/deep-research`, and `/adversarial-review` commands.
18
42
 
19
43
  <details>
20
44
  <summary>From source (for development)</summary>
@@ -25,55 +49,72 @@ pi install /path/to/pi-dynamic-workflows
25
49
  ```
26
50
  </details>
27
51
 
28
- ## Usage
52
+ ## 30-second demo
29
53
 
30
- Ask Pi for a workflow in plain language:
54
+ Just ask for a workflow in plain language:
31
55
 
32
56
  ```text
33
- Run a workflow to inspect this repository and summarize the main modules.
57
+ Run a workflow to audit every route under src/routes/ for missing auth checks.
34
58
  ```
35
59
 
36
- The model writes a workflow script and calls the `workflow` tool. Live progress streams inline:
60
+ Pi writes the script and runs it in the background your turn ends right away, and a compact progress view streams in the "Workflows running" task panel while you keep working:
37
61
 
38
62
  ```text
39
- ◆ Workflow: inspect_project (3/3 done · 12,480 tokens)
63
+ ◆ Workflow: auth_audit (5/5 done · 48,210 tokens · $0.0131)
40
64
  ✓ Scan 1/1
41
- #1 ✓ repo inventory
42
- Analyze 2/2
43
- #2 ✓ source modules
44
- #3 ✓ final summary
65
+ #1 ✓ enumerate routes
66
+ Review 3/3
67
+ #2 ✓ routes/users.ts
68
+ #3 ✓ routes/admin.ts
69
+ #4 ✓ routes/billing.ts
70
+ ✓ Verify 1/1
71
+ #5 ✓ adversarial recheck
45
72
  ```
46
73
 
47
- Press `Esc` to cancel a running run; active subagents are aborted and surfaced as skipped.
74
+ When it finishes, the result is delivered back into the conversation and the turn auto-continues — queued after whatever you're doing so it never interrupts. (Need the result inline in the same turn instead? The model can pass `background: false` to block.)
48
75
 
49
- ### Background runs & `/workflows`
76
+ ## What's different from upstream
50
77
 
51
- Ask for a background workflow (the model passes `background: true`) and it runs without blocking your session. Manage it with the `/workflows` command:
78
+ This fork turns the original's roadmap into working, tested features:
52
79
 
53
- ```text
54
- /workflows # open the interactive navigator (plain list in print mode)
55
- /workflows list # force the plain-text list
56
- /workflows status <id> # watch a running run live (status bar), prints result when done
57
- /workflows stop <id> # abort a running run
58
- /workflows pause <id> # pause a running run
59
- /workflows resume <id> # resume an interrupted run (replays cached results)
60
- /workflows rm <id> # remove a run from the list
61
- ```
80
+ | Capability | Upstream | This fork |
81
+ | --- | :---: | :---: |
82
+ | Core `agent`/`parallel`/`pipeline` runtime | ✅ | ✅ |
83
+ | Structured (JSON-Schema) subagent output | | |
84
+ | **Token & cost accounting** | estimate | ✅ real, from the SDK session |
85
+ | **Per-agent / per-phase model routing** | prose-only* | ✅ actually switches models |
86
+ | **`/workflows` command + interactive TUI** | | full keyboard navigator |
87
+ | **Resume an interrupted run** | — | ✅ journaled, replays the prefix |
88
+ | **Git worktree isolation** | — | ✅ real worktrees, auto-cleanup |
89
+ | **`/deep-research` with real web access** | — | ✅ live search + cross-checking |
90
+ | **Saved workflows as `/<name>`** | — | ✅ |
91
+ | **Nested `workflow()`** | — | ✅ shares the global caps |
92
+ | **Non-blocking background runs + live task panel + auto-continue delivery** | — | ✅ |
93
+ | Test suite | minimal | ✅ 43 tests + real Pi end-to-end |
62
94
 
63
- ### Bundled workflows
95
+ <sub>*Upstream injected the requested model as a text line in the prompt; it never changed the subagent's actual model.</sub>
96
+
97
+ ## Commands
64
98
 
65
99
  ```text
66
- /deep-research <question> # web-researched, source-cross-checked report
67
- /adversarial-review <task> # findings cross-checked by skeptical reviewers
100
+ /workflows # open the interactive navigator (plain list in print mode)
101
+ /workflows status <id> # watch a running run live; prints the result when it finishes
102
+ /workflows save <name> # save the latest run's script as a reusable /<name> command
103
+ /workflows pause|resume|stop|rm <id>
104
+
105
+ /deep-research <question> # web-researched, source-cross-checked report
106
+ /adversarial-review <task> # findings cross-checked by skeptical reviewers
68
107
  ```
69
108
 
70
- `/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.
109
+ In the **interactive navigator**: `↑/↓` (or `j/k`) select · `enter`/`→` open · `esc`/`←` back · `j/k` scroll detail · `p` pause · `x` stop · `r` restart (re-runs the whole workflow as a fresh background run) · `s` save · `q` quit. The agents list and each agent's detail show **which model it ran on**.
71
110
 
72
- 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`).
111
+ ### Workflows mode (input box)
73
112
 
74
- ## Workflow script shape
113
+ As you type, the words `workflow`/`workflows` light up as a **flowing rainbow** — a signal that submitting this message will deliberately run a workflow (the message is rewritten to ask Pi to orchestrate subagents rather than answer directly). Changed your mind? Press **Backspace** once right after the word: it turns plain white (disarmed) without being deleted. Type a fresh trigger word to re-arm. Slash commands like `/workflows` are left alone (never highlighted). Everything else about the editor — history, autocomplete, paste, multiline — is unchanged.
75
114
 
76
- A workflow is plain JavaScript. The first statement must export literal metadata:
115
+ ## Writing a workflow
116
+
117
+ A workflow is plain JavaScript whose first statement exports literal metadata:
77
118
 
78
119
  ```js
79
120
  export const meta = {
@@ -102,7 +143,7 @@ return { inventory, summary }
102
143
  | `workflow(name, args)` | Run a saved workflow inline and return its result (one level deep; shares the global caps). |
103
144
  | `log(message)` | Append a workflow-level log line. |
104
145
  | `args` | Optional JSON value passed via the tool's `args` parameter. |
105
- | `budget` | `{ total, spent(), remaining() }` token-budget tracker. |
146
+ | `budget` | `{ total, spent(), remaining() }` token-budget tracker (real tokens). |
106
147
  | `cwd`, `process.cwd()` | Working directory for subagents. |
107
148
 
108
149
  ### Agent options
@@ -111,16 +152,18 @@ return { inventory, summary }
111
152
  | --- | --- | --- |
112
153
  | `label` | string | Human-readable label for progress display |
113
154
  | `phase` | string | Override the current phase for this agent |
114
- | `schema` | object | JSON Schema for structured output |
155
+ | `schema` | object | JSON Schema the subagent returns a validated object |
115
156
  | `model` | string | Run this agent on a specific model — `provider/modelId` or a bare `modelId` |
116
157
  | `isolation` | `"worktree"` | Run this agent in its own throwaway git worktree (parallel edits without conflict) |
117
158
  | `timeoutMs` | number | Override the default 5-minute agent timeout |
118
159
 
119
- 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.
160
+ 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. The model each agent ran on is recorded and shown in the `/workflows` navigator.
161
+
162
+ **Model routing is decided by the assistant, not hardcoded.** When it writes a workflow, Pi is given the routing policy and the list of your currently authenticated models, and picks each agent's `model` accordingly: a lighter same-family model (one tier below your main model — e.g. Claude→Haiku, GPT→a mini) for exploration/search/gathering agents, and your main model for analysis/judgment/decision agents. If you name a specific model, that wins.
120
163
 
121
164
  ### Structured output
122
165
 
123
- Pass a JSON Schema via `opts.schema` and the subagent returns a validated object:
166
+ Pass a JSON Schema and the subagent returns a validated object instead of prose:
124
167
 
125
168
  ```js
126
169
  const finding = await agent('Find security-sensitive files.', {
@@ -136,51 +179,38 @@ const finding = await agent('Find security-sensitive files.', {
136
179
  })
137
180
  ```
138
181
 
139
- Backed by a Pi `structured_output` tool with `terminate: true`, so the subagent ends on that call.
182
+ Backed by a Pi `structured_output` tool with `terminate: true`, so the subagent ends on that call — no wasted follow-up turn.
140
183
 
141
- ### Determinism rules
184
+ ### Determinism
142
185
 
143
- 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.
144
-
145
- ## What works today
146
-
147
- - **Core runtime** — `agent` / `parallel` / `pipeline` / `phase` / `log` / `budget` in a sandboxed script
148
- - **Structured output** — JSON-Schema-validated subagent results
149
- - **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
150
- - **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
151
- - **`/workflows` interactive navigator** — `/workflows` opens a focused TUI you drill through with the keyboard (runs → phases → agents → agent detail): `↑/↓` (or `j/k`) select, `enter`/`→` open, `esc`/`←` back, `j/k` scroll detail, `p` pause/resume, `x` stop, `s` save, `q` quit. In print/RPC mode it falls back to plain text
152
- - **Live task panel + background delivery** — while a `background: true` run is going, a "Workflows running" panel sits below the input (focus it and press `enter` to open the navigator); when the run finishes, its result is delivered back into the conversation so your paused task continues
153
- - **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
154
- - **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`
155
- - **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
156
- - **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
157
- - **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
158
- - **Safety limits** — 1000-agent cap (`maxAgents`), per-agent timeout (`agentTimeoutMs`), recoverable-vs-fatal error classification
159
- - **Live progress + token/cost display**, `Esc` to abort
160
- - **Log persistence** to `.pi/workflows/runs/`
186
+ 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.
161
187
 
162
188
  ## How it works
163
189
 
164
190
  ```text
165
191
  user prompt
166
- → Pi model writes a workflow script
167
- → workflow tool parses + runs it in a vm sandbox
168
- → script calls agent() / parallel() / pipeline()
192
+ → Pi writes a workflow script
193
+ the workflow tool parses + runs it in a vm sandbox
194
+ the script calls agent() / parallel() / pipeline()
169
195
  → each agent() spawns a fresh in-memory Pi subagent session
170
- → snapshots stream back as compact progress
171
- → final structured result returns to the parent assistant
196
+ results are journaled; snapshots stream back as compact progress
197
+ the final structured result returns to the parent assistant
172
198
  ```
173
199
 
174
- 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.
200
+ 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.
175
201
 
176
202
  ## Development
177
203
 
178
204
  ```bash
179
205
  npm install
180
- npm test # biome check + tsc + unit tests
206
+ npm test # biome check + tsc + 43 unit tests
181
207
  ```
182
208
 
183
- Parser unit tests live in `tests/workflow-parser.test.ts`.
209
+ Tests live in `tests/`. Each feature is also verified end-to-end against a real Pi subagent session before release.
210
+
211
+ ## Credits
212
+
213
+ 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).
184
214
 
185
215
  ## License
186
216
 
package/dist/agent.d.ts CHANGED
@@ -9,6 +9,12 @@ export interface WorkflowAgentOptions {
9
9
  /** Extra system guidance prepended to every subagent task. */
10
10
  instructions?: string;
11
11
  }
12
+ /**
13
+ * List the user's currently available models (those with auth configured) as
14
+ * `provider/modelId` specs. Used to tell the workflow author which models it may
15
+ * route agents to. Best-effort: returns [] if the registry can't be built.
16
+ */
17
+ export declare function listAvailableModelSpecs(): string[];
12
18
  /** Real token/cost usage for a single subagent run, read from the SDK session. */
13
19
  export interface AgentUsage {
14
20
  input: number;
package/dist/agent.js CHANGED
@@ -1,6 +1,22 @@
1
1
  import { join } from "node:path";
2
2
  import { AuthStorage, createAgentSession, createCodingTools, getAgentDir, ModelRegistry, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
3
3
  import { createStructuredOutputTool } from "./structured-output.js";
4
+ /**
5
+ * List the user's currently available models (those with auth configured) as
6
+ * `provider/modelId` specs. Used to tell the workflow author which models it may
7
+ * route agents to. Best-effort: returns [] if the registry can't be built.
8
+ */
9
+ export function listAvailableModelSpecs() {
10
+ try {
11
+ const dir = getAgentDir();
12
+ const auth = AuthStorage.create(join(dir, "auth.json"));
13
+ const registry = ModelRegistry.create(auth, join(dir, "models.json"));
14
+ return registry.getAvailable().map((m) => `${m.provider}/${m.id}`);
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
4
20
  export class WorkflowAgent {
5
21
  cwd;
6
22
  baseTools;
package/dist/display.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface WorkflowAgentSnapshot {
11
11
  error?: string;
12
12
  /** Tokens used by this agent. */
13
13
  tokens?: number;
14
+ /** The model this agent ran on (provider/id), when known. */
15
+ model?: string;
14
16
  }
15
17
  export interface WorkflowSnapshot {
16
18
  name: string;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { AdversarialReviewConfig } from "./adversarial-review.js";
2
2
  export { generateAdversarialReviewWorkflow, generateMultiPerspectiveWorkflow } from "./adversarial-review.js";
3
3
  export type { AgentRunOptions, AgentRunResult, WorkflowAgentOptions } from "./agent.js";
4
- export { WorkflowAgent } from "./agent.js";
4
+ export { listAvailableModelSpecs, WorkflowAgent } from "./agent.js";
5
5
  export type { AutoWorkflowConfig } from "./auto-workflow.js";
6
6
  export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
7
7
  export { registerBuiltinWorkflows } from "./builtin-commands.js";
@@ -25,12 +25,13 @@ export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-t
25
25
  export type { AgentOptions, JournalEntry, SharedRuntime, WorkflowMeta, WorkflowMetaPhase, WorkflowRunOptions, WorkflowRunResult, } from "./workflow.js";
26
26
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
27
27
  export { registerWorkflowCommands } from "./workflow-commands.js";
28
+ export { buildForcedWorkflowPrompt, colorizeWorkflow, endsWithTrigger, hasTrigger, installWorkflowEditor, RAINBOW, tokenizeAnsi, WorkflowEditor, type WorkflowModeState, } from "./workflow-editor.js";
28
29
  export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
29
30
  export { WorkflowManager } from "./workflow-manager.js";
30
31
  export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
31
32
  export { createWorkflowStorage } from "./workflow-saved.js";
32
33
  export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
33
- export { createWorkflowTool } from "./workflow-tool.js";
34
+ export { backgroundStartedText, createWorkflowTool } from "./workflow-tool.js";
34
35
  export { keyToAction, type NavAction, NavigatorModel, NavigatorState, openWorkflowNavigator, renderNavigator, type ViewKind, } from "./workflow-ui.js";
35
36
  export type { Worktree } from "./worktree.js";
36
37
  export { createWorktree, removeWorktree } from "./worktree.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { generateAdversarialReviewWorkflow, generateMultiPerspectiveWorkflow } from "./adversarial-review.js";
2
- export { WorkflowAgent } from "./agent.js";
2
+ export { listAvailableModelSpecs, WorkflowAgent } from "./agent.js";
3
3
  export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
4
4
  export { registerBuiltinWorkflows } from "./builtin-commands.js";
5
5
  export * from "./config.js";
@@ -15,8 +15,9 @@ export { installResultDelivery, installTaskPanel } from "./task-panel.js";
15
15
  export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
16
16
  export { parseWorkflowScript, runWorkflow } from "./workflow.js";
17
17
  export { registerWorkflowCommands } from "./workflow-commands.js";
18
+ export { buildForcedWorkflowPrompt, colorizeWorkflow, endsWithTrigger, hasTrigger, installWorkflowEditor, RAINBOW, tokenizeAnsi, WorkflowEditor, } from "./workflow-editor.js";
18
19
  export { WorkflowManager } from "./workflow-manager.js";
19
20
  export { createWorkflowStorage } from "./workflow-saved.js";
20
- export { createWorkflowTool } from "./workflow-tool.js";
21
+ export { backgroundStartedText, createWorkflowTool } from "./workflow-tool.js";
21
22
  export { keyToAction, NavigatorModel, NavigatorState, openWorkflowNavigator, renderNavigator, } from "./workflow-ui.js";
22
23
  export { createWorktree, removeWorktree } from "./worktree.js";
@@ -12,6 +12,8 @@ export interface PersistedAgentState {
12
12
  error?: string;
13
13
  startedAt?: string;
14
14
  endedAt?: string;
15
+ /** The model this agent ran on (provider/id), when known. */
16
+ model?: string;
15
17
  }
16
18
  export interface PersistedRunState {
17
19
  runId: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Background-run UX, mirroring Claude Code:
3
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.
4
+ * It is informational; run /workflows to open the full navigator.
5
5
  * - When a background run finishes, its result is delivered back into the
6
6
  * conversation so the paused task continues with the outcome.
7
7
  */
@@ -13,12 +13,21 @@ export interface TaskPanelOptions {
13
13
  cwd?: string;
14
14
  }
15
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.
16
+ * When a background run finishes (or fails), deliver its result back into the
17
+ * conversation AND continue the turn so the assistant can act on it — without
18
+ * blocking the user meanwhile:
19
+ *
20
+ * - `triggerTurn: true` starts a fresh turn when the agent is idle, feeding the
21
+ * result to the model so the paused conversation continues.
22
+ * - `deliverAs: "followUp"` means that if the user is busy in another turn, the
23
+ * result is queued and picked up after that turn finishes — never interrupting.
24
+ *
25
+ * Set up once per extension; idempotent via an internal guard.
18
26
  */
19
27
  export declare function installResultDelivery(pi: ExtensionAPI, manager: WorkflowManager): void;
20
28
  /**
21
29
  * Install the live "workflows running" panel below the editor. Re-rendered on
22
- * every manager event; focus + enter opens the navigator.
30
+ * every manager event. Informational only the user opens the navigator with
31
+ * /workflows. (`_pi`/`_opts` are kept for signature stability.)
23
32
  */
24
- export declare function installTaskPanel(pi: ExtensionAPI, manager: WorkflowManager, ui: ExtensionUIContext, opts?: TaskPanelOptions): void;
33
+ export declare function installTaskPanel(_pi: ExtensionAPI, manager: WorkflowManager, ui: ExtensionUIContext, _opts?: TaskPanelOptions): void;
@@ -1,42 +1,56 @@
1
1
  /**
2
2
  * Background-run UX, mirroring Claude Code:
3
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.
4
+ * It is informational; run /workflows to open the full navigator.
5
5
  * - When a background run finishes, its result is delivered back into the
6
6
  * conversation so the paused task continues with the outcome.
7
7
  */
8
- import { parseKey } from "@earendil-works/pi-tui";
9
- import { openWorkflowNavigator } from "./workflow-ui.js";
10
8
  const RUN_EVENTS = ["agentStart", "agentEnd", "phase", "log", "complete", "error", "stopped", "paused", "resumed"];
11
9
  function deliverText(run) {
12
10
  const r = run.result?.result;
13
11
  const body = r && typeof r.report === "string" && r.report.trim() ? r.report : JSON.stringify(run.result?.result, null, 2);
14
12
  const tokens = run.result?.tokenUsage ? ` · ${run.result.tokenUsage.total.toLocaleString()} tokens` : "";
15
13
  const agents = run.result?.agentCount ?? run.snapshot.agentCount;
16
- return `✓ Workflow "${run.snapshot.name}" finished (${agents} agents${tokens}).\n\n${body}`;
14
+ return [
15
+ `✓ Background workflow "${run.snapshot.name}" finished (${agents} agents${tokens}).`,
16
+ "Continue helping the user based on this result.",
17
+ "",
18
+ body,
19
+ ].join("\n");
17
20
  }
18
21
  /**
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.
22
+ * When a background run finishes (or fails), deliver its result back into the
23
+ * conversation AND continue the turn so the assistant can act on it — without
24
+ * blocking the user meanwhile:
25
+ *
26
+ * - `triggerTurn: true` starts a fresh turn when the agent is idle, feeding the
27
+ * result to the model so the paused conversation continues.
28
+ * - `deliverAs: "followUp"` means that if the user is busy in another turn, the
29
+ * result is queued and picked up after that turn finishes — never interrupting.
30
+ *
31
+ * Set up once per extension; idempotent via an internal guard.
21
32
  */
22
33
  export function installResultDelivery(pi, manager) {
23
34
  if (manager.__deliveryInstalled)
24
35
  return;
25
36
  manager.__deliveryInstalled = true;
37
+ const deliver = (content) => {
38
+ void pi.sendMessage({ customType: "workflow-result", content, display: true }, { triggerTurn: true, deliverAs: "followUp" });
39
+ };
26
40
  manager.on("complete", ({ runId }) => {
27
41
  const run = manager.getRun(runId);
28
- if (run)
29
- void pi.sendMessage({ customType: "workflow-result", content: deliverText(run), display: true });
42
+ // Only background/resumed runs are delivered: a foreground (sync) run already
43
+ // returns its result inline as the tool result, so re-delivering would dup it.
44
+ if (run?.background)
45
+ deliver(deliverText(run));
30
46
  });
31
47
  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
- });
48
+ if (!manager.getRun(runId)?.background)
49
+ return;
50
+ deliver(`✗ Background workflow ${runId} failed: ${error?.message ?? "unknown error"}`);
37
51
  });
38
52
  }
39
- function renderPanel(manager, theme, focused) {
53
+ function renderPanel(manager, theme) {
40
54
  const active = manager.listRuns().filter((r) => r.status === "running" || r.status === "paused");
41
55
  if (!active.length)
42
56
  return [];
@@ -48,29 +62,23 @@ function renderPanel(manager, theme, focused) {
48
62
  const phase = live?.snapshot.currentPhase ? ` · ${live.snapshot.currentPhase}` : "";
49
63
  return ` ${icon} ${r.workflowName} ${done}/${agents.length} agents${phase}`;
50
64
  });
51
- const hint = focused
52
- ? theme.fg("accent", " enter: open · esc: back")
53
- : theme.fg("dim", " ↓ then enter, or /workflows, to open");
65
+ const hint = theme.fg("dim", " run /workflows to open");
54
66
  return [theme.bold(`Workflows running (${active.length}):`), ...rows, hint];
55
67
  }
56
68
  /**
57
69
  * Install the live "workflows running" panel below the editor. Re-rendered on
58
- * every manager event; focus + enter opens the navigator.
70
+ * every manager event. Informational only the user opens the navigator with
71
+ * /workflows. (`_pi`/`_opts` are kept for signature stability.)
59
72
  */
60
- export function installTaskPanel(pi, manager, ui, opts = {}) {
73
+ export function installTaskPanel(_pi, manager, ui, _opts = {}) {
61
74
  ui.setWidget("workflow-tasks", (tui, theme) => {
62
75
  const onEvent = () => tui.requestRender();
63
76
  for (const ev of RUN_EVENTS)
64
77
  manager.on(ev, onEvent);
78
+ // Purely informational: it lists running runs and re-renders on events. To
79
+ // open the navigator, the user runs /workflows (the panel takes no input).
65
80
  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
- },
81
+ render: () => renderPanel(manager, theme),
74
82
  invalidate: () => { },
75
83
  dispose: () => {
76
84
  for (const ev of RUN_EVENTS)
@@ -0,0 +1,74 @@
1
+ /**
2
+ * "Workflows mode" input affordance, à la a smart input box:
3
+ *
4
+ * - While the editor text contains the word `workflow`/`workflows`, those letters
5
+ * render as a flowing rainbow, signalling that submitting will engage a workflow.
6
+ * - Pressing Backspace immediately after such a word toggles the highlight OFF
7
+ * (the word stays, but turns plain white) — a non-destructive "don't run a
8
+ * workflow after all". Re-typing a fresh trigger word turns it back on.
9
+ * - When the highlight is ON at submit time, the user's message is transformed to
10
+ * instruct Pi to actually run the workflow tool.
11
+ *
12
+ * Implementation: we replace the core editor with a thin subclass of the exported
13
+ * `CustomEditor` (which itself extends pi-tui's `Editor`), overriding only
14
+ * `render()` (to colorize) and `handleInput()` (for the Backspace toggle). All
15
+ * other editor behavior — history, autocomplete, paste, undo, multiline — is
16
+ * inherited untouched.
17
+ */
18
+ import { CustomEditor, type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
19
+ import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
20
+ /** 256-color ring cycling through the spectrum — shifted by a tick to "flow". */
21
+ export declare const RAINBOW: number[];
22
+ export declare function hasTrigger(text: string): boolean;
23
+ export declare function endsWithTrigger(textBeforeCursor: string): boolean;
24
+ /** Shared, mutable view of whether "workflows mode" is currently armed. */
25
+ export interface WorkflowModeState {
26
+ active: boolean;
27
+ }
28
+ interface AnsiToken {
29
+ esc?: string;
30
+ ch?: string;
31
+ }
32
+ /**
33
+ * Split a rendered line into ANSI-escape tokens (passed through verbatim) and
34
+ * single visible-character tokens. Handles CSI sequences (`\x1b[…m`, e.g. the
35
+ * cursor's inverse-video) and APC/OSC string sequences (e.g. the zero-width
36
+ * `CURSOR_MARKER` = `\x1b_pi:c\x07`) so colorization never corrupts them.
37
+ */
38
+ export declare function tokenizeAnsi(line: string): AnsiToken[];
39
+ /**
40
+ * Colorize every `workflow`/`workflows` occurrence in a rendered line with a
41
+ * flowing rainbow, leaving all ANSI escapes (cursor, markers) intact. Returns the
42
+ * line unchanged when it contains no trigger.
43
+ */
44
+ export declare function colorizeWorkflow(line: string, tick: number, palette?: number[]): string;
45
+ /**
46
+ * Editor that paints the trigger words and owns the on/off toggle. Reads/writes
47
+ * `state.active` so the extension's `input` handler can decide whether to force a
48
+ * workflow at submit time.
49
+ */
50
+ export declare class WorkflowEditor extends CustomEditor {
51
+ private readonly modeState;
52
+ private tick;
53
+ private timer?;
54
+ /** Toggled off by Backspace-after-word; re-armed when a fresh trigger appears. */
55
+ private disabled;
56
+ private wasTriggered;
57
+ constructor(tui: TUI, theme: EditorTheme, keybindings: ConstructorParameters<typeof CustomEditor>[2], modeState: WorkflowModeState);
58
+ /** Highlighted/armed: a trigger is present and the user hasn't toggled it off. */
59
+ isActive(): boolean;
60
+ handleInput(data: string): void;
61
+ render(width: number): string[];
62
+ /** Absolute text before the cursor, used to detect "right after the word". */
63
+ private cursorAfterTrigger;
64
+ private syncState;
65
+ private reconcileAnimation;
66
+ }
67
+ /** The directive appended to a submitted message when workflows mode is armed. */
68
+ export declare function buildForcedWorkflowPrompt(text: string): string;
69
+ /**
70
+ * Install the workflows-mode editor and the submit-time forcing hook.
71
+ * Call once with the UI context (e.g. in `session_start`).
72
+ */
73
+ export declare function installWorkflowEditor(pi: ExtensionAPI, ui: ExtensionUIContext): WorkflowModeState;
74
+ export {};