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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "0.1.0-alpha.3",
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"
@@ -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: { fg: ThemeFg; bold: (s: string) => string }): Text {
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: { fg: ThemeFg; bold: (s: string) => string },
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: { fg: ThemeFg; bold: (s: string) => string },
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: { fg: ThemeFg; bold: (s: string) => string },
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: { fg: ThemeFg; bold: (s: string) => string },
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("muted", `${prefix}─ int:`)} ${theme.fg("dim", truncateChars(r.intent, 40))}`;
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("muted", "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
246
+ text += `\n${theme.fg("dim", "└─ msg:")} ${renderFlowReport(truncateChars(flowOutput, 25), theme)}`;
245
247
  } else if (streamingText) {
246
- text += `\n${theme.fg("muted", "└─ msg:")} ${theme.fg("dim", tailText(streamingText, 40))}`;
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("muted", "└─ msg:")} ${theme.fg("error", truncateChars(r.errorMessage, 25))}`;
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: { fg: ThemeFg; bold: (s: string) => string },
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: { fg: ThemeFg; bold: (s: string) => string },
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: { fg: ThemeFg; bold: (s: string) => string },
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 (const r of results) {
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 usageStr = formatFlowUsage(r.usage, r.model);
334
- text += `\n${theme.fg("accent", `flow [${r.type}]`)}`;
335
- if (usageStr) text += ` ${theme.fg("dim", usageStr)}`;
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(truncateText(flowOutput), theme)}`;
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