pi-agent-flow 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,5 @@
1
1
  <p align="center"><code>pi install npm:pi-agent-flow</code></p>
2
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/>
7
3
 
8
4
  ---
9
5
 
@@ -162,7 +158,6 @@ flow [explore] accomplished
162
158
  ## Docs
163
159
 
164
160
  - [**Pi Documentation**](https://pi.dev)
165
- - [**Contributing**](./docs/contributing.md) *(if available)*
166
161
  - [**License**](./LICENSE)
167
162
 
168
163
  This repository is licensed under the [MIT License](LICENSE).
package/index.ts CHANGED
@@ -284,10 +284,6 @@ function makeFlowDetailsFactory(projectFlowsDir: string | null) {
284
284
  });
285
285
  }
286
286
 
287
- function formatFlowNames(flows: FlowConfig[]): string {
288
- return flows.map((f) => `${f.name} (${f.source})`).join(", ") || "none";
289
- }
290
-
291
287
  function getFlowCycleViolations(
292
288
  requestedNames: Set<string>,
293
289
  ancestorFlowStack: string[],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render-utils.ts CHANGED
@@ -4,27 +4,6 @@
4
4
 
5
5
  import type { UsageStats } from "./types.js";
6
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:${model}`);
25
- return parts.join(" ");
26
- }
27
-
28
7
  /**
29
8
  * Format a token count to exactly 5 characters with leading spaces.
30
9
  * Shifts from k to M when value would exceed 5 chars.
@@ -58,30 +37,35 @@ export function formatFlowTypeName(type: string): string {
58
37
  return lower + ".".repeat(targetWidth - lower.length);
59
38
  }
60
39
 
61
- export function formatCompactStats(usage: Partial<UsageStats>, model?: string): string {
40
+ export function formatCompactStats(usage: Partial<UsageStats>, model?: string, maxWidth?: number): string {
62
41
  const parts: string[] = [];
63
42
  parts.push(`${formatFixedTokens(usage.input || 0)}↑`);
64
43
  parts.push(`${formatFixedTokens(usage.output || 0)}↓`);
65
44
  if (usage.cacheRead) parts.push(`cr:${formatFixedTokens(usage.cacheRead)}`);
66
45
  if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatFixedTokens(usage.contextTokens)}`);
67
- return `[ ${parts.join(" ")} ]${model ? ` ─ ${model}` : ""}`;
68
- }
69
46
 
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)}`);
47
+ let result = `[ ${parts.join(" ")} ]${model ? ` ─ ${model}` : ""}`;
48
+
49
+ if (maxWidth && visibleLength(result) > maxWidth) {
50
+ // Drop model first
51
+ let narrow = `[ ${parts.join(" ")} ]`;
52
+ if (visibleLength(narrow) <= maxWidth) return narrow;
53
+
54
+ // Drop context tokens
55
+ const narrowParts = parts.slice();
56
+ const ctxIndex = narrowParts.findIndex((p) => p.startsWith("ctx:"));
57
+ if (ctxIndex !== -1) narrowParts.splice(ctxIndex, 1);
58
+ narrow = `[ ${narrowParts.join(" ")} ]`;
59
+ if (visibleLength(narrow) <= maxWidth) return narrow;
80
60
 
81
- const stats = `[${parts.join(" ")}]${model ? ` ─ ${model}` : ""}`;
82
- const contextTokens = usage.contextTokens && usage.contextTokens > 0 ? `ctx:${formatFixedTokens(usage.contextTokens)}` : null;
61
+ // Bare minimum (just input/output)
62
+ narrow = `[ ${parts[0]} ${parts[1]} ]`;
63
+ if (visibleLength(narrow) <= maxWidth) return narrow;
83
64
 
84
- return { stats, contextTokens };
65
+ return truncateChars(result, maxWidth);
66
+ }
67
+
68
+ return result;
85
69
  }
86
70
 
87
71
  /** Regex matching ANSI escape sequences. */
@@ -105,12 +89,18 @@ export function getTruncationBudget(prefixLength: number): number {
105
89
 
106
90
  /**
107
91
  * Truncate an ANSI-colored string to at most `max` visible characters,
108
- * preserving ANSI codes in the kept portions and appending a reset before
109
- * the ellipsis so colors don't bleed.
92
+ * preserving ANSI codes in the kept portions. Does not inject reset codes
93
+ * the caller is responsible for closing any open styles.
110
94
  */
111
95
  function truncateAnsi(text: string, max: number): string {
112
96
  if (visibleLength(text) <= max) return text;
113
97
 
98
+ if (max < 6) {
99
+ // Not enough room for " ... " — just truncate without ellipsis
100
+ const { raw } = takeVisible(text, max);
101
+ return raw;
102
+ }
103
+
114
104
  const head = Math.ceil(max * 0.6);
115
105
  const tail = max - head - 5; // 5 = " ... ".length
116
106
 
package/render.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  isFlowError,
23
23
  isFlowSuccess,
24
24
  } from "./types.js";
25
- import { formatTokens, formatFixedTokens, formatFlowUsage, formatCompactStats, formatExpandedStats, formatFlowTypeName, truncateChars, tailText, getTruncationBudget } from "./render-utils.js";
25
+ import { formatFixedTokens, formatCompactStats, formatFlowTypeName, truncateChars, tailText, getTruncationBudget } from "./render-utils.js";
26
26
 
27
27
  function shortenPath(p: string): string {
28
28
  const home = os.homedir();
@@ -217,8 +217,6 @@ function renderFlowExpanded(
217
217
  return container;
218
218
  }
219
219
 
220
-
221
-
222
220
  function renderFlowCollapsed(
223
221
  r: SingleResult,
224
222
  icon: string,
@@ -226,36 +224,39 @@ function renderFlowCollapsed(
226
224
  flowOutput: string,
227
225
  theme: FlowTheme,
228
226
  streamingText?: string,
229
- ): Text {
230
- const stats = formatCompactStats(r.usage, r.model);
227
+ ): Container {
228
+ const container = new Container();
229
+ const maxWidth = process.stdout.columns ?? 80;
230
+ const stats = formatCompactStats(r.usage, r.model, maxWidth);
231
231
  const typeName = formatFlowTypeName(r.type);
232
- let text = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
233
- if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
232
+ let header = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
233
+ if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
234
+ container.addChild(new Text(truncateChars(header, maxWidth), 0, 0));
234
235
 
235
236
  // dir: line (intent/objective)
236
237
  if (r.intent) {
237
- text += `\n${theme.fg("dim", "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, getTruncationBudget(8)))}`;
238
+ container.addChild(new Text(`${theme.fg("dim", "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, getTruncationBudget(8)))}`, 0, 0));
238
239
  }
239
240
 
240
241
  // exe: line (last tool call)
241
242
  const lastTool = getLastToolCall(r.messages);
242
243
  if (lastTool) {
243
244
  const exeStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
244
- text += `\n${theme.fg("dim", "├─ exe:")} ${truncateChars(exeStr, getTruncationBudget(8))}`;
245
+ container.addChild(new Text(`${theme.fg("dim", "├─ exe:")} ${truncateChars(exeStr, getTruncationBudget(8))}`, 0, 0));
245
246
  }
246
247
 
247
248
  // log: line (last assistant text or streaming)
248
249
  if (flowOutput) {
249
- text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", truncateChars(flowOutput, getTruncationBudget(8)))}`;
250
+ container.addChild(new Text(`${theme.fg("dim", "└─ log:")} ${theme.fg("dim", truncateChars(flowOutput, getTruncationBudget(8)))}`, 0, 0));
250
251
  } else if (streamingText) {
251
- text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", tailText(streamingText, getTruncationBudget(8)))}`;
252
+ container.addChild(new Text(`${theme.fg("dim", "└─ log:")} ${theme.fg("dim", tailText(streamingText, getTruncationBudget(8)))}`, 0, 0));
252
253
  } else if (error && r.errorMessage) {
253
- text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, getTruncationBudget(8)))}`;
254
+ container.addChild(new Text(`${theme.fg("dim", "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, getTruncationBudget(8)))}`, 0, 0));
254
255
  } else {
255
- text += `\n${theme.fg("dim", "└─ log:")} ${theme.fg("dim", "[n/a]")}`;
256
+ container.addChild(new Text(`${theme.fg("dim", "└─ log:")} ${theme.fg("dim", "[n/a]")}`, 0, 0));
256
257
  }
257
258
 
258
- return new Text(text, 0, 0);
259
+ return container;
259
260
  }
260
261
 
261
262
  // ---------------------------------------------------------------------------
@@ -348,11 +349,12 @@ function renderActivityPanel(
348
349
  theme: FlowTheme,
349
350
  ): Container {
350
351
  const container = new Container();
352
+ const maxWidth = process.stdout.columns ?? 80;
351
353
 
352
354
  for (let i = 0; i < results.length; i++) {
353
355
  const r = results[i];
354
356
  const isLast = i === results.length - 1;
355
- const stats = formatCompactStats(r.usage, r.model);
357
+ const stats = formatCompactStats(r.usage, r.model, maxWidth);
356
358
  const error = isFlowError(r);
357
359
  const typeName = formatFlowTypeName(r.type);
358
360
 
@@ -362,7 +364,7 @@ function renderActivityPanel(
362
364
  if (error && r.stopReason) {
363
365
  headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
364
366
  }
365
- container.addChild(new Text(headerLine, 0, 0));
367
+ container.addChild(new Text(truncateChars(headerLine, maxWidth), 0, 0));
366
368
 
367
369
  // Continuation indent for sub-lines
368
370
  const indent = isLast ? " " : "│ ";
package/runner-events.js CHANGED
@@ -174,7 +174,7 @@ function addFlowAssistantMessages(result, messages) {
174
174
  return changed;
175
175
  }
176
176
 
177
- export function processFlowEvent(event, result) {
177
+ function processFlowEvent(event, result) {
178
178
  if (!event || typeof event !== "object") return false;
179
179
 
180
180
  switch (event.type) {