pi-agent-flow 0.1.0-alpha.8 → 0.2.4
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/agents/architect.md +3 -3
- package/agents/brainstorm.md +5 -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/flow.ts +19 -5
- package/package.json +1 -1
- package/render-utils.ts +55 -6
- package/render.ts +95 -51
- package/runner-events.js +45 -0
- package/types.ts +34 -0
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
|
|
package/agents/brainstorm.md
CHANGED
|
@@ -8,6 +8,11 @@ maxDepth: 0
|
|
|
8
8
|
|
|
9
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
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
|
+
|
|
11
16
|
When your mission is accomplished, end your response with:
|
|
12
17
|
|
|
13
18
|
flow [brainstorm] accomplished
|
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/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}`);
|
|
@@ -210,6 +222,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
210
222
|
|
|
211
223
|
const emitUpdate = () => {
|
|
212
224
|
const streaming = drainStreamingText(result);
|
|
225
|
+
const estimatedTokens = drainStreamingEstimate(result);
|
|
226
|
+
const mergedUsage = mergeStreamingUsage(result.usage, estimatedTokens);
|
|
213
227
|
onUpdate?.({
|
|
214
228
|
content: [
|
|
215
229
|
{
|
|
@@ -217,7 +231,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
|
|
|
217
231
|
text: streaming || getFlowOutput(result.messages) || "(running...)",
|
|
218
232
|
},
|
|
219
233
|
],
|
|
220
|
-
details: makeDetails([result]),
|
|
234
|
+
details: makeDetails([{ ...result, usage: mergedUsage }]),
|
|
221
235
|
});
|
|
222
236
|
};
|
|
223
237
|
|
package/package.json
CHANGED
package/render-utils.ts
CHANGED
|
@@ -25,14 +25,63 @@ export function formatFlowUsage(usage: Partial<UsageStats>, model?: string): str
|
|
|
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();
|
|
@@ -171,14 +173,24 @@ function renderFlowExpanded(
|
|
|
171
173
|
const mdTheme = getMarkdownTheme();
|
|
172
174
|
const container = new Container();
|
|
173
175
|
|
|
174
|
-
// Header
|
|
175
|
-
|
|
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));
|
|
176
179
|
if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
177
180
|
container.addChild(new Text(header, 0, 0));
|
|
178
181
|
if (error && r.errorMessage) {
|
|
179
182
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
180
183
|
}
|
|
181
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
|
+
|
|
182
194
|
// Intent
|
|
183
195
|
container.addChild(new Spacer(1));
|
|
184
196
|
container.addChild(new Text(theme.fg("muted", "─── Intent ───"), 0, 0));
|
|
@@ -202,13 +214,6 @@ function renderFlowExpanded(
|
|
|
202
214
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
203
215
|
}
|
|
204
216
|
|
|
205
|
-
// Usage
|
|
206
|
-
const usageStr = formatFlowUsage(r.usage, r.model);
|
|
207
|
-
if (usageStr) {
|
|
208
|
-
container.addChild(new Spacer(1));
|
|
209
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
217
|
return container;
|
|
213
218
|
}
|
|
214
219
|
|
|
@@ -223,23 +228,31 @@ function renderFlowCollapsed(
|
|
|
223
228
|
streamingText?: string,
|
|
224
229
|
): Text {
|
|
225
230
|
const stats = formatCompactStats(r.usage, r.model);
|
|
226
|
-
|
|
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)}`;
|
|
227
233
|
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
228
234
|
|
|
229
|
-
//
|
|
235
|
+
// DIR: line (intent/objective)
|
|
230
236
|
if (r.intent) {
|
|
231
|
-
text += `\n${theme.fg("dim",
|
|
237
|
+
text += `\n${theme.fg("dim", "├─ DIR:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`;
|
|
232
238
|
}
|
|
233
239
|
|
|
234
|
-
//
|
|
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:")} ${truncateChars(exeStr, 50)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// LOG: line (last assistant text or streaming)
|
|
235
248
|
if (flowOutput) {
|
|
236
|
-
text += `\n${theme.fg("dim", "└─
|
|
249
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", truncateChars(flowOutput, 50))}`;
|
|
237
250
|
} else if (streamingText) {
|
|
238
|
-
text += `\n${theme.fg("dim", "└─
|
|
251
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", tailText(streamingText, 50))}`;
|
|
239
252
|
} else if (error && r.errorMessage) {
|
|
240
|
-
text += `\n${theme.fg("dim", "└─
|
|
253
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`;
|
|
241
254
|
} else {
|
|
242
|
-
text += `\n${theme.fg("dim", "└─
|
|
255
|
+
text += `\n${theme.fg("dim", "└─ LOG:")} ${theme.fg("dim", "[n/a]")}`;
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
return new Text(text, 0, 0);
|
|
@@ -274,19 +287,32 @@ function renderMultiFlowExpanded(
|
|
|
274
287
|
const mdTheme = getMarkdownTheme();
|
|
275
288
|
const container = new Container();
|
|
276
289
|
|
|
290
|
+
// Summary: just show count, no icon
|
|
277
291
|
container.addChild(new Text(
|
|
278
|
-
|
|
292
|
+
theme.fg("accent", `${results.length} flows`),
|
|
279
293
|
0, 0,
|
|
280
294
|
));
|
|
281
295
|
|
|
282
296
|
for (const r of results) {
|
|
283
|
-
const rIcon = flowStatusIcon(r, theme);
|
|
284
297
|
const displayItems = getFlowDisplayItems(r.messages);
|
|
285
298
|
const flowOutput = getFlowOutput(r.messages);
|
|
299
|
+
const typeName = formatFlowTypeName(r.type);
|
|
286
300
|
|
|
287
301
|
container.addChild(new Spacer(1));
|
|
288
|
-
|
|
289
|
-
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));
|
|
290
316
|
|
|
291
317
|
if (flowOutput) {
|
|
292
318
|
container.addChild(new Spacer(1));
|
|
@@ -300,65 +326,83 @@ function renderMultiFlowExpanded(
|
|
|
300
326
|
container.addChild(new Text(theme.fg("muted", "─── Tool Calls ───"), 0, 0));
|
|
301
327
|
container.addChild(new Text(toolTraces, 0, 0));
|
|
302
328
|
}
|
|
303
|
-
|
|
304
|
-
const taskUsage = formatFlowUsage(r.usage, r.model);
|
|
305
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
306
329
|
}
|
|
307
330
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
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));
|
|
313
342
|
|
|
314
343
|
return container;
|
|
315
344
|
}
|
|
316
345
|
|
|
317
|
-
function
|
|
346
|
+
function renderActivityPanel(
|
|
318
347
|
results: SingleResult[],
|
|
319
348
|
theme: FlowTheme,
|
|
320
|
-
):
|
|
321
|
-
|
|
349
|
+
): Container {
|
|
350
|
+
const container = new Container();
|
|
322
351
|
|
|
323
352
|
for (let i = 0; i < results.length; i++) {
|
|
324
353
|
const r = results[i];
|
|
325
354
|
const isLast = i === results.length - 1;
|
|
326
|
-
const flowOutput = getFlowOutput(r.messages);
|
|
327
355
|
const stats = formatCompactStats(r.usage, r.model);
|
|
328
356
|
const error = isFlowError(r);
|
|
357
|
+
const typeName = formatFlowTypeName(r.type);
|
|
329
358
|
|
|
330
359
|
// Header line
|
|
331
360
|
const headerPrefix = isLast ? "└─" : "├─";
|
|
332
|
-
let
|
|
333
|
-
if (stats) {
|
|
334
|
-
line += ` ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
335
|
-
}
|
|
361
|
+
let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.bg("selectedBg", theme.fg("accent", theme.bold(typeName)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
336
362
|
if (error && r.stopReason) {
|
|
337
|
-
|
|
363
|
+
headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
338
364
|
}
|
|
339
|
-
|
|
340
|
-
text += line;
|
|
365
|
+
container.addChild(new Text(headerLine, 0, 0));
|
|
341
366
|
|
|
342
367
|
// Continuation indent for sub-lines
|
|
343
368
|
const indent = isLast ? " " : "│ ";
|
|
344
369
|
|
|
345
|
-
//
|
|
370
|
+
// DIR: line (intent/objective)
|
|
346
371
|
if (r.intent) {
|
|
347
|
-
|
|
348
|
-
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));
|
|
349
373
|
}
|
|
350
374
|
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
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:")} ${truncateChars(exeStr, 50)}`, 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));
|
|
354
386
|
} else if (error && r.errorMessage) {
|
|
355
|
-
|
|
387
|
+
container.addChild(new Text(`${theme.fg("dim", indent + "└─ LOG:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`, 0, 0));
|
|
356
388
|
} else {
|
|
357
|
-
|
|
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));
|
|
358
395
|
}
|
|
359
396
|
}
|
|
360
397
|
|
|
361
|
-
|
|
398
|
+
container.addChild(new Text(theme.fg("muted", "(Ctrl+O to expand tool traces)"), 0, 0));
|
|
362
399
|
|
|
363
|
-
return
|
|
400
|
+
return container;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function renderMultiFlowCollapsed(
|
|
404
|
+
results: SingleResult[],
|
|
405
|
+
theme: FlowTheme,
|
|
406
|
+
): Container {
|
|
407
|
+
return renderActivityPanel(results, theme);
|
|
364
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
|
@@ -149,3 +149,37 @@ export function getFlowDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
149
149
|
}
|
|
150
150
|
return items;
|
|
151
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
|
+
}
|