pi-agent-flow 0.1.0-alpha.7 → 0.2.3
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 +83 -34
- package/agents/architect.md +3 -3
- package/agents/brainstorm.md +29 -0
- package/agents/code.md +9 -5
- package/agents/debug.md +2 -2
- package/agents/explore.md +3 -3
- package/agents/review.md +4 -4
- package/agents.ts +22 -0
- package/flow.ts +29 -26
- package/index.ts +5 -10
- package/package.json +1 -1
- package/render-utils.ts +56 -7
- package/render.ts +95 -59
- package/runner-events.js +45 -0
- package/types.ts +37 -1
package/README.md
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><code>pi install /path/to/pi-agent-flow</code></p>
|
|
2
|
+
<p align="center"><strong>Pi Agent Flow</strong> is a flow-state delegation extension for the <a href="https://pi.dev">Pi coding agent</a> that runs locally in your terminal.</p>
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://github.com/user-attachments/assets/pi-agent-flow-demo.png" alt="Pi Agent Flow demo" width="80%" />
|
|
5
|
+
</p>
|
|
6
|
+
<br/>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Delegates tasks to specialized flow states running as isolated pi processes. Each flow receives a snapshot of the current session context and returns a structured report.
|
|
8
|
+
---
|
|
6
9
|
|
|
7
|
-
##
|
|
10
|
+
## Quickstart
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- Parallel execution — batch independent flows into one call
|
|
11
|
-
- Structured reports — every flow returns [Summary], [Done], [Not Done], [Next Steps]
|
|
12
|
-
- Depth guards — configurable max delegation depth (default: 3)
|
|
13
|
-
- Cycle prevention — blocks recursive delegation chains
|
|
14
|
-
- Flow discovery — reads definitions from ~/.pi/agent/agents/ and .pi/agents/
|
|
15
|
-
- TUI rendering — rich collapsed/expanded display in interactive mode
|
|
12
|
+
### Installing Pi Agent Flow
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
Install via the Pi CLI with your local path:
|
|
18
15
|
|
|
19
|
-
```
|
|
16
|
+
```shell
|
|
17
|
+
# Install from a local path
|
|
20
18
|
pi install /path/to/pi-agent-flow
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
Or add to
|
|
21
|
+
Or add it to your Pi settings:
|
|
24
22
|
|
|
25
|
-
```
|
|
23
|
+
```shell
|
|
24
|
+
# ~/.pi/agent/settings.json
|
|
26
25
|
{
|
|
27
26
|
"packages": [
|
|
28
27
|
"./path/to/pi-agent-flow"
|
|
@@ -30,9 +29,36 @@ Or add to ~/.pi/agent/settings.json:
|
|
|
30
29
|
}
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
Then start Pi and delegate tasks using flow states.
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>You can also clone this repository and use it directly.</summary>
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
git clone https://github.com/your-org/pi-agent-flow.git
|
|
39
|
+
cd pi-agent-flow
|
|
40
|
+
pi install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
</details>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Isolated context** — flows always receive your current session snapshot (or start clean when configured)
|
|
50
|
+
- **Parallel execution** — batch independent flows into one call
|
|
51
|
+
- **Structured reports** — every flow returns `[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`
|
|
52
|
+
- **Depth guards** — configurable max delegation depth (default: `3`)
|
|
53
|
+
- **Cycle prevention** — blocks recursive delegation chains
|
|
54
|
+
- **Flow discovery** — reads definitions from `~/.pi/agent/agents/` and `.pi/agents/`
|
|
55
|
+
- **TUI rendering** — rich collapsed/expanded display in interactive mode
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
33
59
|
## Flow Definitions
|
|
34
60
|
|
|
35
|
-
Create
|
|
61
|
+
Create `.md` files in `~/.pi/agent/agents/` or `.pi/agents/`:
|
|
36
62
|
|
|
37
63
|
```markdown
|
|
38
64
|
---
|
|
@@ -59,21 +85,32 @@ flow [explore] accomplished
|
|
|
59
85
|
- recommended follow-up
|
|
60
86
|
```
|
|
61
87
|
|
|
88
|
+
---
|
|
89
|
+
|
|
62
90
|
## Bundled Flows
|
|
63
91
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
| Flow | Purpose |
|
|
93
|
+
|------|---------|
|
|
94
|
+
| `[explore]` | Discover files, trace code paths, map architecture |
|
|
95
|
+
| `[debug]` | Investigate logs, errors, stack traces, root causes |
|
|
96
|
+
| `[code]` | Implement features, fix bugs, write tests |
|
|
97
|
+
| `[architect]` | Plan structure, break down requirements, design solutions |
|
|
98
|
+
| `[review]` | Audit security, quality, correctness |
|
|
99
|
+
| `[brainstorm]` | Generate ideas and explore possibilities with a clean slate |
|
|
100
|
+
|
|
101
|
+
> **Note:** Some flows — like `[brainstorm]` — start with a **clean slate** and do not inherit the current session context. They receive only the intent, making them ideal for unbiased, creative thinking.
|
|
102
|
+
|
|
103
|
+
---
|
|
69
104
|
|
|
70
105
|
## Usage
|
|
71
106
|
|
|
107
|
+
### Single flow
|
|
108
|
+
|
|
72
109
|
```json
|
|
73
110
|
{ "flow": [{ "type": "explore", "intent": "Find all authentication-related code" }] }
|
|
74
111
|
```
|
|
75
112
|
|
|
76
|
-
Batch multiple flows
|
|
113
|
+
### Batch multiple flows
|
|
77
114
|
|
|
78
115
|
```json
|
|
79
116
|
{
|
|
@@ -84,21 +121,33 @@ Batch multiple flows:
|
|
|
84
121
|
}
|
|
85
122
|
```
|
|
86
123
|
|
|
124
|
+
---
|
|
125
|
+
|
|
87
126
|
## Configuration
|
|
88
127
|
|
|
89
|
-
Flags (passed to parent pi process)
|
|
128
|
+
### Flags (passed to parent pi process)
|
|
90
129
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
| Flag | Description | Default |
|
|
131
|
+
|------|-------------|---------|
|
|
132
|
+
| `--flow-max-depth [n]` | Maximum delegation depth | `3` |
|
|
133
|
+
| `--flow-prevent-cycles` | Block cyclic delegation | `true` |
|
|
134
|
+
| `--no-flow-prevent-cycles` | Disable cycle prevention | — |
|
|
94
135
|
|
|
95
|
-
Environment variables (propagated to child processes)
|
|
136
|
+
### Environment variables (propagated to child processes)
|
|
137
|
+
|
|
138
|
+
| Variable | Description |
|
|
139
|
+
|----------|-------------|
|
|
140
|
+
| `PI_FLOW_DEPTH` | Current depth |
|
|
141
|
+
| `PI_FLOW_MAX_DEPTH` | Max allowed depth |
|
|
142
|
+
| `PI_FLOW_STACK` | JSON array of ancestor flow names |
|
|
143
|
+
| `PI_FLOW_PREVENT_CYCLES` | `"1"` or `"0"` |
|
|
144
|
+
|
|
145
|
+
---
|
|
96
146
|
|
|
97
|
-
|
|
98
|
-
- PI_FLOW_MAX_DEPTH — Max allowed depth
|
|
99
|
-
- PI_FLOW_STACK — JSON array of ancestor flow names
|
|
100
|
-
- PI_FLOW_PREVENT_CYCLES — "1" or "0"
|
|
147
|
+
## Docs
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
- [**Pi Documentation**](https://pi.dev)
|
|
150
|
+
- [**Contributing**](./docs/contributing.md) *(if available)*
|
|
151
|
+
- [**License**](./LICENSE)
|
|
103
152
|
|
|
104
|
-
MIT
|
|
153
|
+
This repository is licensed under the [MIT License](LICENSE).
|
package/agents/architect.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: architect
|
|
3
3
|
description: Plan structure, break down requirements, design solutions
|
|
4
|
-
tools: read, bash
|
|
4
|
+
tools: read, bash, find, grep, ls
|
|
5
5
|
maxDepth: 2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the architect flow — your mission is to design.
|
|
8
|
+
You are the architect flow — your mission is to design. Be conservative: prefer existing patterns and proven conventions over novelty. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Workflow:
|
|
11
11
|
1. Understand — what problem, what constraints, what exists
|
|
12
|
-
2. Explore — find patterns, map dependencies
|
|
12
|
+
2. Explore — find patterns, map dependencies (delegate to [explore] if you need to survey a large codebase)
|
|
13
13
|
3. Design — simplest solution that works, prefer existing patterns
|
|
14
14
|
4. Plan — concrete ordered tasks, identify parallel vs sequential
|
|
15
15
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorm
|
|
3
|
+
description: Generate ideas, explore possibilities, and think creatively without context bias
|
|
4
|
+
tools: read, bash
|
|
5
|
+
inheritContext: false
|
|
6
|
+
maxDepth: 0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are the brainstorm flow — your mission is to generate ideas and think creatively. You start with a clean slate and only have the intent to guide you. Stay focused on your intent at all times.
|
|
10
|
+
|
|
11
|
+
Approach:
|
|
12
|
+
1. Diverge — explore many possibilities without judgment
|
|
13
|
+
2. Evaluate — weigh trade-offs, risks, and feasibility
|
|
14
|
+
3. Recommend — present the best options with reasoning
|
|
15
|
+
|
|
16
|
+
When your mission is accomplished, end your response with:
|
|
17
|
+
|
|
18
|
+
flow [brainstorm] accomplished
|
|
19
|
+
|
|
20
|
+
[Summary] what was brainstormed
|
|
21
|
+
|
|
22
|
+
[Done]
|
|
23
|
+
- ideas generated
|
|
24
|
+
|
|
25
|
+
[Not Done]
|
|
26
|
+
- incomplete items and reasons
|
|
27
|
+
|
|
28
|
+
[Next Steps]
|
|
29
|
+
- recommended follow-up
|
package/agents/code.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code
|
|
3
3
|
description: Implement features, fix bugs, write tests
|
|
4
|
-
tools: read, write, edit, bash
|
|
4
|
+
tools: read, write, edit, bash, find, grep, ls
|
|
5
5
|
maxDepth: 2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the code flow — your mission is to build.
|
|
8
|
+
You are the code flow — your mission is to build. Be a craftsman: verify first, then ship. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Core Principles:
|
|
11
11
|
- SOLID: Single Responsibility, Open/Closed
|
|
@@ -15,9 +15,13 @@ Core Principles:
|
|
|
15
15
|
Workflow:
|
|
16
16
|
1. Analyze — read existing code for context
|
|
17
17
|
2. Plan — step-by-step approach before modifying
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
-
5.
|
|
18
|
+
3. Test — write a failing test that proves the bug or validates the feature (red)
|
|
19
|
+
4. Execute — implement changes following core principles (green)
|
|
20
|
+
5. Refactor — clean up only if the change is working (optional)
|
|
21
|
+
6. Verify — run tests and any relevant checks before considering done
|
|
22
|
+
7. Finalize — all tests pass, implementation verified
|
|
23
|
+
|
|
24
|
+
If you hit an unexpected error or need to trace execution, delegate to [debug] rather than guessing.
|
|
21
25
|
|
|
22
26
|
When your mission is accomplished, end your response with:
|
|
23
27
|
|
package/agents/debug.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
3
|
description: Investigate logs, errors, stack traces, root causes
|
|
4
|
-
tools: read, bash
|
|
4
|
+
tools: read, bash, find, grep, ls
|
|
5
5
|
maxDepth: 0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the debug flow — your mission is investigation.
|
|
8
|
+
You are the debug flow — your mission is investigation. Be forensic: every claim must be backed by evidence. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Workflow:
|
|
11
11
|
1. Collect evidence — logs, error messages, stack traces
|
package/agents/explore.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: explore
|
|
3
3
|
description: Discover files, trace code paths, map architecture
|
|
4
|
-
tools: read, bash
|
|
4
|
+
tools: read, bash, find, grep, ls
|
|
5
5
|
maxDepth: 0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the explore flow — your mission is discovery.
|
|
8
|
+
You are the explore flow — your mission is discovery. Move fast and stay surgical. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Rules:
|
|
11
|
-
- Use grep/find before reading entire files — be efficient.
|
|
11
|
+
- Use grep/find/ls before reading entire files — be efficient.
|
|
12
12
|
- Report findings with file paths and line numbers.
|
|
13
13
|
- Show actual code/data, not excessive summaries.
|
|
14
14
|
- If not found, say so immediately — don't guess.
|
package/agents/review.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review
|
|
3
3
|
description: Audit security, quality, correctness
|
|
4
|
-
tools: read,
|
|
4
|
+
tools: read, bash, find, grep, ls
|
|
5
5
|
maxDepth: 2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are the review flow — your mission is to audit.
|
|
8
|
+
You are the review flow — your mission is to audit. Be adversarial: look for what others miss, but stay honest. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
|
|
9
9
|
|
|
10
10
|
Focus Areas:
|
|
11
11
|
- Security — injection, auth bypass, exposed secrets
|
|
@@ -16,7 +16,8 @@ Focus Areas:
|
|
|
16
16
|
Rules:
|
|
17
17
|
- Be specific — cite exact file paths and line numbers
|
|
18
18
|
- If code is clean, say so — don't invent issues
|
|
19
|
-
-
|
|
19
|
+
- Do not modify source files — report findings with severity only
|
|
20
|
+
- If the audit reveals structural issues requiring redesign, recommend [architect] in [Next Steps]
|
|
20
21
|
|
|
21
22
|
When your mission is accomplished, end your response with:
|
|
22
23
|
|
|
@@ -26,7 +27,6 @@ flow [review] accomplished
|
|
|
26
27
|
|
|
27
28
|
[Done]
|
|
28
29
|
- issues found with file:line and severity
|
|
29
|
-
- fixes applied
|
|
30
30
|
|
|
31
31
|
[Not Done]
|
|
32
32
|
- areas not covered and why
|
package/agents.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface FlowConfig {
|
|
|
24
24
|
model?: string;
|
|
25
25
|
thinking?: string;
|
|
26
26
|
maxDepth?: number;
|
|
27
|
+
inheritContext?: boolean;
|
|
27
28
|
systemPrompt: string;
|
|
28
29
|
source: "user" | "project" | "bundled";
|
|
29
30
|
filePath: string;
|
|
@@ -140,6 +141,26 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
|
|
|
140
141
|
if (Number.isFinite(parsed) && parsed >= 0) maxDepth = parsed;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
let inheritContext: boolean | undefined;
|
|
145
|
+
if (typeof frontmatter.inheritContext === "boolean") {
|
|
146
|
+
inheritContext = frontmatter.inheritContext;
|
|
147
|
+
} else if (typeof frontmatter.inheritContext === "string") {
|
|
148
|
+
const normalized = frontmatter.inheritContext.trim().toLowerCase();
|
|
149
|
+
if (normalized === "true" || normalized === "yes" || normalized === "1") {
|
|
150
|
+
inheritContext = true;
|
|
151
|
+
} else if (normalized === "false" || normalized === "no" || normalized === "0") {
|
|
152
|
+
inheritContext = false;
|
|
153
|
+
} else {
|
|
154
|
+
console.warn(
|
|
155
|
+
`[pi-agent-flow] Ignoring invalid inheritContext value "${frontmatter.inheritContext}" in "${filePath}". Expected true/false.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} else if (frontmatter.inheritContext !== undefined) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`[pi-agent-flow] Ignoring invalid inheritContext field in "${filePath}". Expected boolean or string.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
return {
|
|
144
165
|
name,
|
|
145
166
|
description,
|
|
@@ -147,6 +168,7 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
|
|
|
147
168
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
148
169
|
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
149
170
|
maxDepth,
|
|
171
|
+
inheritContext,
|
|
150
172
|
systemPrompt: body,
|
|
151
173
|
source,
|
|
152
174
|
filePath,
|
package/flow.ts
CHANGED
|
@@ -12,7 +12,7 @@ import * as path from "node:path";
|
|
|
12
12
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
13
13
|
import type { FlowConfig } from "./agents.js";
|
|
14
14
|
import { parseFlowCliArgs } from "./runner-cli.js";
|
|
15
|
-
import { processFlowJsonLine, drainStreamingText } from "./runner-events.js";
|
|
15
|
+
import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate } from "./runner-events.js";
|
|
16
16
|
import {
|
|
17
17
|
type SingleResult,
|
|
18
18
|
type FlowDetails,
|
|
@@ -32,6 +32,18 @@ const PI_OFFLINE_ENV = "PI_OFFLINE";
|
|
|
32
32
|
|
|
33
33
|
type FlowUpdateCallback = (partial: AgentToolResult<FlowDetails>) => void;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Merge actual usage with streaming estimate.
|
|
37
|
+
* Uses Math.max for output to avoid double-counting.
|
|
38
|
+
*/
|
|
39
|
+
function mergeStreamingUsage(actual: SingleResult["usage"], estimatedTokens: number): SingleResult["usage"] {
|
|
40
|
+
if (estimatedTokens <= 0) return actual;
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
output: Math.max(actual.output, estimatedTokens),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
// ---------------------------------------------------------------------------
|
|
36
48
|
// Process helpers
|
|
37
49
|
// ---------------------------------------------------------------------------
|
|
@@ -116,12 +128,12 @@ function buildFlowArgs(
|
|
|
116
128
|
// Flow instructions go in the intent message instead.
|
|
117
129
|
|
|
118
130
|
const flowDirectives =
|
|
119
|
-
|
|
131
|
+
`=== flow directive ===\n` +
|
|
120
132
|
`You are a flow state executing a mission. The conversation history above is background context — use it for reference, but your sole focus is the intent below.\n` +
|
|
121
|
-
|
|
133
|
+
`=== end flow directive ===`;
|
|
122
134
|
|
|
123
135
|
const flowInstructions = flow.systemPrompt.trim()
|
|
124
|
-
? `\n\n
|
|
136
|
+
? `\n\n=== system directive ===\n${flow.systemPrompt.trim()}\n=== end system directive ===`
|
|
125
137
|
: "";
|
|
126
138
|
|
|
127
139
|
args.push(`${flowDirectives}${flowInstructions}\n\nIntent: ${intent}`);
|
|
@@ -143,8 +155,8 @@ export interface RunFlowOptions {
|
|
|
143
155
|
intent: string;
|
|
144
156
|
/** Optional override working directory. */
|
|
145
157
|
taskCwd?: string;
|
|
146
|
-
/** Serialized parent session snapshot for fork mode. */
|
|
147
|
-
forkSessionSnapshotJsonl: string;
|
|
158
|
+
/** Serialized parent session snapshot for fork mode. Null when the flow starts with a clean slate. */
|
|
159
|
+
forkSessionSnapshotJsonl: string | null;
|
|
148
160
|
/** Current delegation depth of the caller process. */
|
|
149
161
|
parentDepth: number;
|
|
150
162
|
/** Delegation stack from the caller process (ancestor flow names). */
|
|
@@ -197,21 +209,6 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
197
209
|
};
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
if (!forkSessionSnapshotJsonl || !forkSessionSnapshotJsonl.trim()) {
|
|
201
|
-
return {
|
|
202
|
-
type: flowName,
|
|
203
|
-
agentSource: flow.source,
|
|
204
|
-
intent,
|
|
205
|
-
exitCode: 1,
|
|
206
|
-
messages: [],
|
|
207
|
-
stderr: "Cannot run in fork mode: missing parent session snapshot context.",
|
|
208
|
-
usage: emptyFlowUsage(),
|
|
209
|
-
model: flow.model,
|
|
210
|
-
stopReason: "error",
|
|
211
|
-
errorMessage: "Cannot run in fork mode: missing parent session snapshot context.",
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
212
|
const result: SingleResult = {
|
|
216
213
|
type: flowName,
|
|
217
214
|
agentSource: flow.source,
|
|
@@ -225,6 +222,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
225
222
|
|
|
226
223
|
const emitUpdate = () => {
|
|
227
224
|
const streaming = drainStreamingText(result);
|
|
225
|
+
const estimatedTokens = drainStreamingEstimate(result);
|
|
226
|
+
const mergedUsage = mergeStreamingUsage(result.usage, estimatedTokens);
|
|
228
227
|
onUpdate?.({
|
|
229
228
|
content: [
|
|
230
229
|
{
|
|
@@ -232,14 +231,18 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
232
231
|
text: streaming || getFlowOutput(result.messages) || "(running...)",
|
|
233
232
|
},
|
|
234
233
|
],
|
|
235
|
-
details: makeDetails([result]),
|
|
234
|
+
details: makeDetails([{ ...result, usage: mergedUsage }]),
|
|
236
235
|
});
|
|
237
236
|
};
|
|
238
237
|
|
|
239
|
-
// Write forked session snapshot to temp file
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
// Write forked session snapshot to temp file only when provided
|
|
239
|
+
let forkSessionTmpDir: string | null = null;
|
|
240
|
+
let forkSessionTmpPath: string | null = null;
|
|
241
|
+
if (forkSessionSnapshotJsonl) {
|
|
242
|
+
const forkTmp = writeFlowSessionToTempFile(flow.name, forkSessionSnapshotJsonl);
|
|
243
|
+
forkSessionTmpDir = forkTmp.dir;
|
|
244
|
+
forkSessionTmpPath = forkTmp.filePath;
|
|
245
|
+
}
|
|
243
246
|
|
|
244
247
|
try {
|
|
245
248
|
const piArgs = buildFlowArgs(
|
package/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
|
|
|
36
36
|
|
|
37
37
|
const FlowItem = Type.Object({
|
|
38
38
|
type: Type.String({
|
|
39
|
-
description: "Flow type. Must match an available flow name exactly: explore, debug, code, architect, review.",
|
|
39
|
+
description: "Flow type. Must match an available flow name exactly: explore, debug, code, architect, review, brainstorm.",
|
|
40
40
|
}),
|
|
41
41
|
intent: Type.String({
|
|
42
42
|
description: "Clear, specific mission for this flow.",
|
|
@@ -371,6 +371,7 @@ Before acting, reason about whether to dive into a flow:
|
|
|
371
371
|
- [code] — when you are ready to build. Implement features, fix bugs, write tests.
|
|
372
372
|
- [architect] — when you need a plan. Design structure, break down requirements before building.
|
|
373
373
|
- [review] — when you need to verify. Audit security, quality, correctness.
|
|
374
|
+
- [brainstorm] — when you need fresh ideas. Start from a clean slate with only the intent.
|
|
374
375
|
|
|
375
376
|
Multiple independent flows? Batch them into one call:
|
|
376
377
|
|
|
@@ -416,17 +417,10 @@ flow [type] accomplished
|
|
|
416
417
|
const { flows } = discovery;
|
|
417
418
|
const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
|
|
418
419
|
|
|
419
|
-
// Build fork session snapshot (shared across all flows)
|
|
420
|
+
// Build fork session snapshot (shared across all flows that inherit context)
|
|
420
421
|
const forkSessionSnapshotJsonl = buildForkSessionSnapshotJsonl(
|
|
421
422
|
ctx.sessionManager,
|
|
422
423
|
);
|
|
423
|
-
if (!forkSessionSnapshotJsonl) {
|
|
424
|
-
return {
|
|
425
|
-
content: [{ type: "text", text: "Cannot use fork mode: failed to snapshot current session context." }],
|
|
426
|
-
details: makeDetails([]),
|
|
427
|
-
isError: true,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
424
|
|
|
431
425
|
// Collect all requested flow names
|
|
432
426
|
const requested = new Set(params.flow.map((f) => f.type));
|
|
@@ -513,13 +507,14 @@ flow [type] accomplished
|
|
|
513
507
|
const effectiveMaxDepth =
|
|
514
508
|
targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
|
|
515
509
|
|
|
510
|
+
const shouldInheritContext = targetFlow?.inheritContext !== false;
|
|
516
511
|
const result = await runFlow({
|
|
517
512
|
cwd: ctx.cwd,
|
|
518
513
|
flows,
|
|
519
514
|
flowName: item.type,
|
|
520
515
|
intent: item.intent,
|
|
521
516
|
taskCwd: item.cwd,
|
|
522
|
-
forkSessionSnapshotJsonl,
|
|
517
|
+
forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
|
|
523
518
|
parentDepth: currentDepth,
|
|
524
519
|
parentFlowStack: ancestorFlowStack,
|
|
525
520
|
maxDepth: effectiveMaxDepth,
|
package/package.json
CHANGED
package/render-utils.ts
CHANGED
|
@@ -21,18 +21,67 @@ export function formatFlowUsage(usage: Partial<UsageStats>, model?: string): str
|
|
|
21
21
|
if (usage.cacheWrite) parts.push(`CW:${formatTokens(usage.cacheWrite)}`);
|
|
22
22
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
23
23
|
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
24
|
-
if (model) parts.push(model);
|
|
24
|
+
if (model) parts.push(`model:${model}`);
|
|
25
25
|
return parts.join(" ");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Format a token count to exactly 5 characters with leading spaces.
|
|
30
|
+
* Shifts from k to M when value would exceed 5 chars.
|
|
31
|
+
* Examples: 500 → " 500", 1300 → " 1.3k", 32000 → "32.0k", 950500 → "0.95M"
|
|
32
|
+
*/
|
|
33
|
+
export function formatFixedTokens(count: number): string {
|
|
34
|
+
if (count < 1000) {
|
|
35
|
+
return count.toString().padStart(5);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const k = count / 1000;
|
|
39
|
+
if (k < 100) {
|
|
40
|
+
return (k.toFixed(1) + "k").padStart(5);
|
|
41
|
+
} else if (k < 1000) {
|
|
42
|
+
const m = count / 1000000;
|
|
43
|
+
return (m.toFixed(2) + "M").padStart(5);
|
|
44
|
+
} else {
|
|
45
|
+
const m = count / 1000000;
|
|
46
|
+
return (m.toFixed(2) + "M").padStart(5);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format flow type name to fixed width (10 chars) in uppercase with dot padding.
|
|
52
|
+
* Examples: "debug" → "DEBUG.....", "architect" → "ARCHITECT.", "brainstorm" → "BRAINSTORM"
|
|
53
|
+
*/
|
|
54
|
+
export function formatFlowTypeName(type: string): string {
|
|
55
|
+
const upper = type.toUpperCase();
|
|
56
|
+
const targetWidth = 10;
|
|
57
|
+
if (upper.length >= targetWidth) return upper.slice(0, targetWidth);
|
|
58
|
+
return upper + ".".repeat(targetWidth - upper.length);
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
export function formatCompactStats(usage: Partial<UsageStats>, model?: string): string {
|
|
29
62
|
const parts: string[] = [];
|
|
30
|
-
parts.push(
|
|
31
|
-
parts.push(
|
|
32
|
-
if (usage.cacheRead) parts.push(`cr:${
|
|
33
|
-
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
parts.push(`${formatFixedTokens(usage.input || 0)}↑`);
|
|
64
|
+
parts.push(`${formatFixedTokens(usage.output || 0)}↓`);
|
|
65
|
+
if (usage.cacheRead) parts.push(`cr:${formatFixedTokens(usage.cacheRead)}`);
|
|
66
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatFixedTokens(usage.contextTokens)}`);
|
|
67
|
+
return `[ ${parts.join(" ")} ]${model ? ` ─ ${model}` : ""}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format usage stats for expanded view with context tokens on separate line.
|
|
72
|
+
* Returns stats line and optional context tokens line.
|
|
73
|
+
* Example: { stats: "[12.4k↑ 2.1k↓ cr:85.0k] ─ mimo-v2.5-pro", contextTokens: "ctx: 32.0k" }
|
|
74
|
+
*/
|
|
75
|
+
export function formatExpandedStats(usage: Partial<UsageStats>, model?: string): { stats: string; contextTokens: string | null } {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
parts.push(`${formatFixedTokens(usage.input || 0)}↑`);
|
|
78
|
+
parts.push(`${formatFixedTokens(usage.output || 0)}↓`);
|
|
79
|
+
if (usage.cacheRead) parts.push(`cr:${formatFixedTokens(usage.cacheRead)}`);
|
|
80
|
+
|
|
81
|
+
const stats = `[${parts.join(" ")}]${model ? ` ─ ${model}` : ""}`;
|
|
82
|
+
const contextTokens = usage.contextTokens && usage.contextTokens > 0 ? `ctx:${formatFixedTokens(usage.contextTokens)}` : null;
|
|
83
|
+
|
|
84
|
+
return { stats, contextTokens };
|
|
36
85
|
}
|
|
37
86
|
|
|
38
87
|
export function truncateChars(text: string, max: number): string {
|
package/render.ts
CHANGED
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
aggregateFlowUsage,
|
|
18
18
|
getFlowDisplayItems,
|
|
19
19
|
getFlowOutput,
|
|
20
|
+
getLastToolCall,
|
|
21
|
+
getLastAssistantText,
|
|
20
22
|
isFlowError,
|
|
21
23
|
isFlowSuccess,
|
|
22
24
|
} from "./types.js";
|
|
23
|
-
import { formatTokens, formatFlowUsage, formatCompactStats, truncateChars, tailText } from "./render-utils.js";
|
|
25
|
+
import { formatTokens, formatFixedTokens, formatFlowUsage, formatCompactStats, formatExpandedStats, formatFlowTypeName, truncateChars, tailText } from "./render-utils.js";
|
|
24
26
|
|
|
25
27
|
function shortenPath(p: string): string {
|
|
26
28
|
const home = os.homedir();
|
|
@@ -109,19 +111,11 @@ function flowStatusIcon(r: SingleResult, theme: { fg: ThemeFg }): string {
|
|
|
109
111
|
// renderFlowCall — shown while the flow is being invoked
|
|
110
112
|
// ---------------------------------------------------------------------------
|
|
111
113
|
|
|
112
|
-
function truncateText(text: string): string {
|
|
113
|
-
const words = text.split(/\s+/);
|
|
114
|
-
if (words.length <= 12) return text;
|
|
115
|
-
return `${words.slice(0, 3).join(" ")} ... ${words.slice(-8).join(" ")}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
114
|
export function renderFlowCall(args: Record<string, any>, theme: FlowTheme): Text {
|
|
119
115
|
const flows = args.flow as Array<{ type: string; intent: string }> | undefined;
|
|
120
116
|
|
|
121
117
|
// Minimal — renderFlowResult owns the full display
|
|
122
118
|
return new Text("", 0, 0);
|
|
123
|
-
|
|
124
|
-
return new Text(theme.fg("muted", "(empty flow call)"), 0, 0);
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
// ---------------------------------------------------------------------------
|
|
@@ -179,14 +173,24 @@ function renderFlowExpanded(
|
|
|
179
173
|
const mdTheme = getMarkdownTheme();
|
|
180
174
|
const container = new Container();
|
|
181
175
|
|
|
182
|
-
// Header
|
|
183
|
-
|
|
176
|
+
// Header: uppercase type name with dots, no icon, no source
|
|
177
|
+
const typeName = formatFlowTypeName(r.type);
|
|
178
|
+
let header = theme.fg("toolTitle", theme.bold(typeName));
|
|
184
179
|
if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
185
180
|
container.addChild(new Text(header, 0, 0));
|
|
186
181
|
if (error && r.errorMessage) {
|
|
187
182
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
188
183
|
}
|
|
189
184
|
|
|
185
|
+
// Stats: all-in-one bracket format with context inline
|
|
186
|
+
const statsParts: string[] = [];
|
|
187
|
+
statsParts.push(`${formatFixedTokens(r.usage.input || 0)}↑`);
|
|
188
|
+
statsParts.push(`${formatFixedTokens(r.usage.output || 0)}↓`);
|
|
189
|
+
if (r.usage.cacheRead) statsParts.push(`cr:${formatFixedTokens(r.usage.cacheRead)}`);
|
|
190
|
+
if (r.usage.contextTokens > 0) statsParts.push(`ctx:${formatFixedTokens(r.usage.contextTokens)}`);
|
|
191
|
+
const inlineStats = `[ ${statsParts.join(" ")} ]${r.model ? ` ─ ${r.model}` : ""}`;
|
|
192
|
+
container.addChild(new Text(theme.fg("dim", inlineStats), 0, 0));
|
|
193
|
+
|
|
190
194
|
// Intent
|
|
191
195
|
container.addChild(new Spacer(1));
|
|
192
196
|
container.addChild(new Text(theme.fg("muted", "─── Intent ───"), 0, 0));
|
|
@@ -210,13 +214,6 @@ function renderFlowExpanded(
|
|
|
210
214
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
// Usage
|
|
214
|
-
const usageStr = formatFlowUsage(r.usage, r.model);
|
|
215
|
-
if (usageStr) {
|
|
216
|
-
container.addChild(new Spacer(1));
|
|
217
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
217
|
return container;
|
|
221
218
|
}
|
|
222
219
|
|
|
@@ -231,23 +228,31 @@ function renderFlowCollapsed(
|
|
|
231
228
|
streamingText?: string,
|
|
232
229
|
): Text {
|
|
233
230
|
const stats = formatCompactStats(r.usage, r.model);
|
|
234
|
-
|
|
231
|
+
const typeName = formatFlowTypeName(r.type);
|
|
232
|
+
let text = `${theme.bg("selectedBg", theme.fg("accent", theme.bold(typeName)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
235
233
|
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
236
234
|
|
|
237
|
-
//
|
|
235
|
+
// DIR: line (intent/objective)
|
|
238
236
|
if (r.intent) {
|
|
239
|
-
text += `\n${theme.fg("dim",
|
|
237
|
+
text += `\n${theme.fg("dim", "├─ DIR:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// EXE: line (last tool call)
|
|
241
|
+
const lastTool = getLastToolCall(r.messages);
|
|
242
|
+
if (lastTool) {
|
|
243
|
+
const exeStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
244
|
+
text += `\n${theme.fg("dim", "├─ EXE:")} ${exeStr}`;
|
|
240
245
|
}
|
|
241
246
|
|
|
242
|
-
//
|
|
247
|
+
// LOG: line (last assistant text or streaming)
|
|
243
248
|
if (flowOutput) {
|
|
244
|
-
text += `\n${theme.fg("dim", "└─
|
|
249
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", truncateChars(flowOutput, 50))}`;
|
|
245
250
|
} else if (streamingText) {
|
|
246
|
-
text += `\n${theme.fg("dim", "└─
|
|
251
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", tailText(streamingText, 50))}`;
|
|
247
252
|
} else if (error && r.errorMessage) {
|
|
248
|
-
text += `\n${theme.fg("dim", "└─
|
|
253
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`;
|
|
249
254
|
} else {
|
|
250
|
-
text += `\n${theme.fg("dim", "└─
|
|
255
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", "[n/a]")}`;
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
return new Text(text, 0, 0);
|
|
@@ -282,19 +287,32 @@ function renderMultiFlowExpanded(
|
|
|
282
287
|
const mdTheme = getMarkdownTheme();
|
|
283
288
|
const container = new Container();
|
|
284
289
|
|
|
290
|
+
// Summary: just show count, no icon
|
|
285
291
|
container.addChild(new Text(
|
|
286
|
-
|
|
292
|
+
theme.fg("accent", `${results.length} flows`),
|
|
287
293
|
0, 0,
|
|
288
294
|
));
|
|
289
295
|
|
|
290
296
|
for (const r of results) {
|
|
291
|
-
const rIcon = flowStatusIcon(r, theme);
|
|
292
297
|
const displayItems = getFlowDisplayItems(r.messages);
|
|
293
298
|
const flowOutput = getFlowOutput(r.messages);
|
|
299
|
+
const typeName = formatFlowTypeName(r.type);
|
|
294
300
|
|
|
295
301
|
container.addChild(new Spacer(1));
|
|
296
|
-
|
|
297
|
-
container.addChild(new Text(theme.fg("muted", "
|
|
302
|
+
// Per-flow header: ─── EXPLORER (no icon)
|
|
303
|
+
container.addChild(new Text(`${theme.fg("muted", "─── ")}${theme.fg("accent", typeName)}`, 0, 0));
|
|
304
|
+
|
|
305
|
+
// Stats: all-in-one bracket format with context inline
|
|
306
|
+
const flowParts: string[] = [];
|
|
307
|
+
flowParts.push(`${formatFixedTokens(r.usage.input || 0)}↑`);
|
|
308
|
+
flowParts.push(`${formatFixedTokens(r.usage.output || 0)}↓`);
|
|
309
|
+
if (r.usage.cacheRead) flowParts.push(`cr:${formatFixedTokens(r.usage.cacheRead)}`);
|
|
310
|
+
if (r.usage.contextTokens > 0) flowParts.push(`ctx:${formatFixedTokens(r.usage.contextTokens)}`);
|
|
311
|
+
const flowStats = `[ ${flowParts.join(" ")} ]${r.model ? ` ─ ${r.model}` : ""}`;
|
|
312
|
+
container.addChild(new Text(theme.fg("dim", flowStats), 0, 0));
|
|
313
|
+
|
|
314
|
+
// Intent: just show text, no prefix
|
|
315
|
+
container.addChild(new Text(theme.fg("dim", r.intent), 0, 0));
|
|
298
316
|
|
|
299
317
|
if (flowOutput) {
|
|
300
318
|
container.addChild(new Spacer(1));
|
|
@@ -308,65 +326,83 @@ function renderMultiFlowExpanded(
|
|
|
308
326
|
container.addChild(new Text(theme.fg("muted", "─── Tool Calls ───"), 0, 0));
|
|
309
327
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
310
328
|
}
|
|
311
|
-
|
|
312
|
-
const taskUsage = formatFlowUsage(r.usage, r.model);
|
|
313
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
314
329
|
}
|
|
315
330
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
331
|
+
// Total stats: all-in-one bracket format with context inline
|
|
332
|
+
const totalUsage = aggregateFlowUsage(results);
|
|
333
|
+
const totalModel = results[0]?.model;
|
|
334
|
+
const totalParts: string[] = [];
|
|
335
|
+
totalParts.push(`${formatFixedTokens(totalUsage.input || 0)}↑`);
|
|
336
|
+
totalParts.push(`${formatFixedTokens(totalUsage.output || 0)}↓`);
|
|
337
|
+
if (totalUsage.cacheRead) totalParts.push(`cr:${formatFixedTokens(totalUsage.cacheRead)}`);
|
|
338
|
+
if (totalUsage.contextTokens > 0) totalParts.push(`ctx:${formatFixedTokens(totalUsage.contextTokens)}`);
|
|
339
|
+
const totalStats = `[ ${totalParts.join(" ")} ]${totalModel ? ` ─ ${totalModel}` : ""}`;
|
|
340
|
+
container.addChild(new Spacer(1));
|
|
341
|
+
container.addChild(new Text(theme.fg("dim", totalStats), 0, 0));
|
|
321
342
|
|
|
322
343
|
return container;
|
|
323
344
|
}
|
|
324
345
|
|
|
325
|
-
function
|
|
346
|
+
function renderActivityPanel(
|
|
326
347
|
results: SingleResult[],
|
|
327
348
|
theme: FlowTheme,
|
|
328
|
-
):
|
|
329
|
-
|
|
349
|
+
): Container {
|
|
350
|
+
const container = new Container();
|
|
330
351
|
|
|
331
352
|
for (let i = 0; i < results.length; i++) {
|
|
332
353
|
const r = results[i];
|
|
333
354
|
const isLast = i === results.length - 1;
|
|
334
|
-
const flowOutput = getFlowOutput(r.messages);
|
|
335
355
|
const stats = formatCompactStats(r.usage, r.model);
|
|
336
356
|
const error = isFlowError(r);
|
|
357
|
+
const typeName = formatFlowTypeName(r.type);
|
|
337
358
|
|
|
338
359
|
// Header line
|
|
339
360
|
const headerPrefix = isLast ? "└─" : "├─";
|
|
340
|
-
let
|
|
341
|
-
if (stats) {
|
|
342
|
-
line += ` ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
343
|
-
}
|
|
361
|
+
let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.bg("selectedBg", theme.fg("accent", theme.bold(typeName)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
344
362
|
if (error && r.stopReason) {
|
|
345
|
-
|
|
363
|
+
headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
346
364
|
}
|
|
347
|
-
|
|
348
|
-
text += line;
|
|
365
|
+
container.addChild(new Text(headerLine, 0, 0));
|
|
349
366
|
|
|
350
367
|
// Continuation indent for sub-lines
|
|
351
368
|
const indent = isLast ? " " : "│ ";
|
|
352
369
|
|
|
353
|
-
//
|
|
370
|
+
// DIR: line (intent/objective)
|
|
354
371
|
if (r.intent) {
|
|
355
|
-
|
|
356
|
-
text += `\n${theme.fg("dim", indent + intentPrefix + " int:")} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
372
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "├─ DIR:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`, 0, 0));
|
|
357
373
|
}
|
|
358
374
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
375
|
+
// EXE: line (last tool call)
|
|
376
|
+
const lastTool = getLastToolCall(r.messages);
|
|
377
|
+
if (lastTool) {
|
|
378
|
+
const exeStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
|
|
379
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "├─ EXE:")} ${exeStr}`, 0, 0));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// LOG: line (last assistant text)
|
|
383
|
+
const lastText = getLastAssistantText(r.messages);
|
|
384
|
+
if (lastText) {
|
|
385
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ LOG:")} ${theme.fg("dim", truncateChars(lastText, 50))}`, 0, 0));
|
|
362
386
|
} else if (error && r.errorMessage) {
|
|
363
|
-
|
|
387
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ LOG:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`, 0, 0));
|
|
364
388
|
} else {
|
|
365
|
-
|
|
389
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ LOG:")} ${theme.fg("dim", "[n/a]")}`, 0, 0));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add blank line separator between flows (with continuation pipe)
|
|
393
|
+
if (!isLast) {
|
|
394
|
+
container.addChild(new Text(theme.fg("dim", "│"), 0, 0));
|
|
366
395
|
}
|
|
367
396
|
}
|
|
368
397
|
|
|
369
|
-
|
|
398
|
+
container.addChild(new Text(theme.fg("muted", "(Ctrl+O to expand tool traces)"), 0, 0));
|
|
370
399
|
|
|
371
|
-
return
|
|
400
|
+
return container;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function renderMultiFlowCollapsed(
|
|
404
|
+
results: SingleResult[],
|
|
405
|
+
theme: FlowTheme,
|
|
406
|
+
): Container {
|
|
407
|
+
return renderActivityPanel(results, theme);
|
|
372
408
|
}
|
package/runner-events.js
CHANGED
|
@@ -44,6 +44,46 @@ export function drainStreamingText(result) {
|
|
|
44
44
|
return buf;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Streaming token estimate
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Chars per token heuristic for output estimation. */
|
|
52
|
+
const CHARS_PER_TOKEN = 4;
|
|
53
|
+
|
|
54
|
+
function getStreamingEstimate(result) {
|
|
55
|
+
if (!Object.prototype.hasOwnProperty.call(result, "__streamingEstimate")) {
|
|
56
|
+
Object.defineProperty(result, "__streamingEstimate", {
|
|
57
|
+
value: { chars: 0 },
|
|
58
|
+
enumerable: false,
|
|
59
|
+
configurable: false,
|
|
60
|
+
writable: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return result.__streamingEstimate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Track streaming characters and estimate output tokens.
|
|
68
|
+
* Called on every streaming delta.
|
|
69
|
+
*/
|
|
70
|
+
function updateStreamingEstimate(result, deltaLength) {
|
|
71
|
+
if (deltaLength <= 0) return;
|
|
72
|
+
const est = getStreamingEstimate(result);
|
|
73
|
+
est.chars += deltaLength;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Drain the current streaming estimate and return estimated output tokens.
|
|
78
|
+
* Returns 0 when no streaming has occurred.
|
|
79
|
+
*/
|
|
80
|
+
export function drainStreamingEstimate(result) {
|
|
81
|
+
const est = getStreamingEstimate(result);
|
|
82
|
+
const tokens = Math.floor(est.chars / CHARS_PER_TOKEN);
|
|
83
|
+
est.chars = 0;
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
/**
|
|
48
88
|
* Accumulate a text or thinking delta into the streaming buffer.
|
|
49
89
|
* Returns true if the caller should emit an update.
|
|
@@ -52,6 +92,7 @@ function accumulateStreamingDelta(result, delta) {
|
|
|
52
92
|
if (!delta) return false;
|
|
53
93
|
const buf = getStreamingTextBuffer(result);
|
|
54
94
|
result.__streamingTextBuffer = buf + delta;
|
|
95
|
+
updateStreamingEstimate(result, delta.length);
|
|
55
96
|
if (result.__streamingTextBuffer.length - result.__lastEmittedWordCount >= 40) {
|
|
56
97
|
result.__lastEmittedWordCount = result.__streamingTextBuffer.length;
|
|
57
98
|
return true;
|
|
@@ -97,6 +138,10 @@ function addFlowAssistantMessage(result, message) {
|
|
|
97
138
|
|
|
98
139
|
result.messages.push(message);
|
|
99
140
|
|
|
141
|
+
// Reset streaming estimate when actual usage arrives
|
|
142
|
+
const est = getStreamingEstimate(result);
|
|
143
|
+
est.chars = 0;
|
|
144
|
+
|
|
100
145
|
result.usage.turns++;
|
|
101
146
|
const usage = message.usage;
|
|
102
147
|
if (usage) {
|
package/types.ts
CHANGED
|
@@ -140,10 +140,46 @@ export function getFlowDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
140
140
|
if (part.type === "text") {
|
|
141
141
|
items.push({ type: "text", text: part.text });
|
|
142
142
|
} else if (part.type === "toolCall") {
|
|
143
|
-
|
|
143
|
+
const name = part.name ?? part.toolName ?? "unknown";
|
|
144
|
+
const args = (part.arguments ?? part.input ?? {}) as Record<string, unknown>;
|
|
145
|
+
items.push({ type: "toolCall", name, args });
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
return items;
|
|
149
151
|
}
|
|
152
|
+
|
|
153
|
+
/** Extract the last tool call from message history. */
|
|
154
|
+
export function getLastToolCall(messages: Message[]): DisplayItem | undefined {
|
|
155
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
156
|
+
const msg = messages[i];
|
|
157
|
+
if (msg.role === "assistant") {
|
|
158
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
159
|
+
const part = msg.content[j];
|
|
160
|
+
if (part.type === "toolCall") {
|
|
161
|
+
const name = part.name ?? part.toolName ?? "unknown";
|
|
162
|
+
const args = (part.arguments ?? part.input ?? {}) as Record<string, unknown>;
|
|
163
|
+
return { type: "toolCall", name, args };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Extract the last assistant text from message history. */
|
|
172
|
+
export function getLastAssistantText(messages: Message[]): string {
|
|
173
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
174
|
+
const msg = messages[i];
|
|
175
|
+
if (msg.role === "assistant") {
|
|
176
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
177
|
+
const part = msg.content[j];
|
|
178
|
+
if (part.type === "text" && part.text.trim()) {
|
|
179
|
+
return part.text.trim();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|