pi-agent-flow 0.1.0-alpha.3 → 0.1.0-alpha.5
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/package.json +2 -1
- package/render-utils.ts +53 -0
- package/render.ts +46 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-flow",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
4
|
"description": "Flow-state delegation extension for Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"runner-cli.js",
|
|
12
12
|
"runner-events.js",
|
|
13
13
|
"render.ts",
|
|
14
|
+
"render-utils.ts",
|
|
14
15
|
"types.ts",
|
|
15
16
|
"agents/",
|
|
16
17
|
"README.md"
|
package/render-utils.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for rendering — extracted for testability.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { UsageStats } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function formatTokens(count: number): string {
|
|
8
|
+
if (count < 1000) return count.toString();
|
|
9
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
10
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
11
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatFlowUsage(usage: Partial<UsageStats>, model?: string): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
if (usage.toolCalls && usage.toolCalls > 0) parts.push(`${usage.toolCalls} calls`);
|
|
17
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
18
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
19
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
20
|
+
if (usage.cacheRead) parts.push(`CR:${formatTokens(usage.cacheRead)}`);
|
|
21
|
+
if (usage.cacheWrite) parts.push(`CW:${formatTokens(usage.cacheWrite)}`);
|
|
22
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
23
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
24
|
+
if (model) parts.push(model);
|
|
25
|
+
return parts.join(" ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatCompactStats(usage: Partial<UsageStats>, model?: string): string {
|
|
29
|
+
const io: string[] = [];
|
|
30
|
+
if (usage.input) io.push(`↑${formatTokens(usage.input)}`);
|
|
31
|
+
if (usage.output) io.push(`↓${formatTokens(usage.output)}`);
|
|
32
|
+
const meta: string[] = [];
|
|
33
|
+
if (usage.cacheRead) meta.push(`CR:${formatTokens(usage.cacheRead)}`);
|
|
34
|
+
if (usage.contextTokens && usage.contextTokens > 0) meta.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
35
|
+
if (model) meta.push(model);
|
|
36
|
+
const parts: string[] = [];
|
|
37
|
+
if (io.length) parts.push(io.join(" "));
|
|
38
|
+
if (meta.length) parts.push(meta.join(" "));
|
|
39
|
+
return parts.join(" │ ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function truncateChars(text: string, max: number): string {
|
|
43
|
+
if (text.length <= max) return text;
|
|
44
|
+
const head = Math.ceil(max * 0.6);
|
|
45
|
+
const tail = max - head - 3;
|
|
46
|
+
return text.slice(0, head) + " ... " + text.slice(-tail);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function tailText(text: string, max: number): string {
|
|
50
|
+
const flat = text.replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
|
|
51
|
+
if (flat.length <= max) return flat;
|
|
52
|
+
return flat.slice(-max);
|
|
53
|
+
}
|
package/render.ts
CHANGED
|
@@ -28,6 +28,8 @@ function shortenPath(p: string): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
type ThemeFg = (color: string, text: string) => string;
|
|
31
|
+
type ThemeBg = (color: string, text: string) => string;
|
|
32
|
+
type FlowTheme = { fg: ThemeFg; bold: (s: string) => string; bg: ThemeBg };
|
|
31
33
|
|
|
32
34
|
function formatFlowToolCall(toolName: string, args: Record<string, unknown>, fg: ThemeFg): string {
|
|
33
35
|
const pathArg = (args.file_path || args.path || "...") as string;
|
|
@@ -113,7 +115,7 @@ function truncateText(text: string): string {
|
|
|
113
115
|
return `${words.slice(0, 3).join(" ")} ... ${words.slice(-8).join(" ")}`;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
export function renderFlowCall(args: Record<string, any>, theme:
|
|
118
|
+
export function renderFlowCall(args: Record<string, any>, theme: FlowTheme): Text {
|
|
117
119
|
const flows = args.flow as Array<{ type: string; intent: string }> | undefined;
|
|
118
120
|
|
|
119
121
|
// Minimal — renderFlowResult owns the full display
|
|
@@ -129,7 +131,7 @@ export function renderFlowCall(args: Record<string, any>, theme: { fg: ThemeFg;
|
|
|
129
131
|
export function renderFlowResult(
|
|
130
132
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
131
133
|
expanded: boolean,
|
|
132
|
-
theme:
|
|
134
|
+
theme: FlowTheme,
|
|
133
135
|
): Container | Text {
|
|
134
136
|
const details = result.details as FlowDetails | undefined;
|
|
135
137
|
const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
|
|
@@ -152,7 +154,7 @@ export function renderFlowResult(
|
|
|
152
154
|
function renderSingleFlowResult(
|
|
153
155
|
r: SingleResult,
|
|
154
156
|
expanded: boolean,
|
|
155
|
-
theme:
|
|
157
|
+
theme: FlowTheme,
|
|
156
158
|
streamingText?: string,
|
|
157
159
|
): Container | Text {
|
|
158
160
|
const error = isFlowError(r);
|
|
@@ -172,7 +174,7 @@ function renderFlowExpanded(
|
|
|
172
174
|
error: boolean,
|
|
173
175
|
displayItems: DisplayItem[],
|
|
174
176
|
flowOutput: string,
|
|
175
|
-
theme:
|
|
177
|
+
theme: FlowTheme,
|
|
176
178
|
): Container {
|
|
177
179
|
const mdTheme = getMarkdownTheme();
|
|
178
180
|
const container = new Container();
|
|
@@ -225,27 +227,27 @@ function renderFlowCollapsed(
|
|
|
225
227
|
icon: string,
|
|
226
228
|
error: boolean,
|
|
227
229
|
flowOutput: string,
|
|
228
|
-
theme:
|
|
230
|
+
theme: FlowTheme,
|
|
229
231
|
streamingText?: string,
|
|
230
232
|
): Text {
|
|
231
233
|
const stats = formatCompactStats(r.usage, r.model);
|
|
232
|
-
let text = `${theme.fg("accent", r.type)} ─ ${theme.fg("dim", stats)}`;
|
|
234
|
+
let text = `${theme.bg("selectedBg", theme.fg("accent", theme.bold(r.type)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
233
235
|
if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
234
236
|
|
|
235
237
|
// Intent line
|
|
236
238
|
const hasOutput = !!(flowOutput || streamingText || (error && r.errorMessage));
|
|
237
239
|
if (r.intent) {
|
|
238
240
|
const prefix = hasOutput ? "├" : "└";
|
|
239
|
-
text += `\n${theme.fg("
|
|
241
|
+
text += `\n${theme.fg("dim", `${prefix}─ int:`)} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
// Output line
|
|
243
245
|
if (flowOutput) {
|
|
244
|
-
text += `\n${theme.fg("
|
|
246
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
|
|
245
247
|
} else if (streamingText) {
|
|
246
|
-
text += `\n${theme.fg("
|
|
248
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${theme.fg("dim", tailText(streamingText, 40))}`;
|
|
247
249
|
} else if (error && r.errorMessage) {
|
|
248
|
-
text += `\n${theme.fg("
|
|
250
|
+
text += `\n${theme.fg("dim", "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
return new Text(text, 0, 0);
|
|
@@ -258,7 +260,7 @@ function renderFlowCollapsed(
|
|
|
258
260
|
function renderMultiFlowResult(
|
|
259
261
|
details: FlowDetails,
|
|
260
262
|
expanded: boolean,
|
|
261
|
-
theme:
|
|
263
|
+
theme: FlowTheme,
|
|
262
264
|
): Container | Text {
|
|
263
265
|
const results = details.results;
|
|
264
266
|
const successCount = results.filter((r) => isFlowSuccess(r)).length;
|
|
@@ -275,7 +277,7 @@ function renderMultiFlowExpanded(
|
|
|
275
277
|
results: SingleResult[],
|
|
276
278
|
successCount: number,
|
|
277
279
|
icon: string,
|
|
278
|
-
theme:
|
|
280
|
+
theme: FlowTheme,
|
|
279
281
|
): Container {
|
|
280
282
|
const mdTheme = getMarkdownTheme();
|
|
281
283
|
const container = new Container();
|
|
@@ -324,17 +326,43 @@ function renderMultiFlowCollapsed(
|
|
|
324
326
|
results: SingleResult[],
|
|
325
327
|
successCount: number,
|
|
326
328
|
icon: string,
|
|
327
|
-
theme:
|
|
329
|
+
theme: FlowTheme,
|
|
328
330
|
): Text {
|
|
329
331
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold("flow "))}${theme.fg("accent", `${successCount}/${results.length} flows`)}`;
|
|
330
332
|
|
|
331
|
-
for (
|
|
333
|
+
for (let i = 0; i < results.length; i++) {
|
|
334
|
+
const r = results[i];
|
|
335
|
+
const isLast = i === results.length - 1;
|
|
332
336
|
const flowOutput = getFlowOutput(r.messages);
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
const stats = formatCompactStats(r.usage, r.model);
|
|
338
|
+
const error = isFlowError(r);
|
|
339
|
+
|
|
340
|
+
// Header line
|
|
341
|
+
const headerPrefix = isLast ? "└─" : "├─";
|
|
342
|
+
let line = `\n${theme.fg("dim", headerPrefix)} ${theme.bg("selectedBg", theme.fg("accent", theme.bold(r.type)))}`;
|
|
343
|
+
if (stats) {
|
|
344
|
+
line += ` ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
|
|
345
|
+
}
|
|
346
|
+
if (error && r.stopReason) {
|
|
347
|
+
line += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
348
|
+
}
|
|
349
|
+
text += line;
|
|
350
|
+
|
|
351
|
+
// Continuation indent for sub-lines
|
|
352
|
+
const indent = isLast ? " " : "│ ";
|
|
353
|
+
|
|
354
|
+
// Intent line
|
|
355
|
+
const hasOutput = !!(flowOutput || (error && r.errorMessage));
|
|
356
|
+
if (r.intent) {
|
|
357
|
+
const intentPrefix = hasOutput ? "├─" : "└─";
|
|
358
|
+
text += `\n${theme.fg("dim", indent + intentPrefix + " int:")} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Output line
|
|
336
362
|
if (flowOutput) {
|
|
337
|
-
text += `\n${renderFlowReport(
|
|
363
|
+
text += `\n${theme.fg("dim", indent + "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
|
|
364
|
+
} else if (error && r.errorMessage) {
|
|
365
|
+
text += `\n${theme.fg("dim", indent + "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
|
|
338
366
|
}
|
|
339
367
|
}
|
|
340
368
|
|