pi-agent-flow 0.3.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
 
@@ -82,9 +78,9 @@ description: Discover files, trace code paths, map architecture
82
78
  tools: read, bash
83
79
  ---
84
80
 
85
- You are the explore flow — your mission is discovery. Stay focused on your intent at all times.
81
+ During this explore flow — your mission is discovery. Stay focused on your intent at all times.
86
82
 
87
- When your mission is accomplished, end your response with:
83
+ When accomplished, end your response with:
88
84
 
89
85
  flow [explore] accomplished
90
86
 
@@ -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).
@@ -5,7 +5,7 @@ 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. 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.
8
+ During this 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
@@ -18,7 +18,7 @@ Principles:
18
18
  - Design for 10x, build for 1x
19
19
  - No tech debt — do it right or don't
20
20
 
21
- When your mission is accomplished, end your response with:
21
+ When accomplished, end your response with:
22
22
 
23
23
  flow [architect] accomplished
24
24
 
@@ -6,14 +6,14 @@ inheritContext: false
6
6
  maxDepth: 0
7
7
  ---
8
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.
9
+ During this 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
11
  Approach:
12
12
  1. Diverge — explore many possibilities without judgment
13
13
  2. Evaluate — weigh trade-offs, risks, and feasibility
14
14
  3. Recommend — present the best options with reasoning
15
15
 
16
- When your mission is accomplished, end your response with:
16
+ When accomplished, end your response with:
17
17
 
18
18
  flow [brainstorm] accomplished
19
19
 
package/agents/code.md CHANGED
@@ -5,7 +5,7 @@ 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 and ship. 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.
8
+ During this code flow — your mission is to build and ship. 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
@@ -32,7 +32,7 @@ Shipping Guidelines:
32
32
 
33
33
  If you hit an unexpected error or need to trace execution, delegate to [debug] rather than guessing.
34
34
 
35
- When your mission is accomplished, end your response with:
35
+ When accomplished, end your response with:
36
36
 
37
37
  flow [code] accomplished
38
38
 
package/agents/debug.md CHANGED
@@ -5,7 +5,7 @@ tools: read, bash, find, grep, ls
5
5
  maxDepth: 0
6
6
  ---
7
7
 
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.
8
+ During this 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
@@ -18,7 +18,7 @@ Rules:
18
18
  - Read logs before reading code — symptoms point to cause.
19
19
  - Don't suggest fixes until root cause is confirmed.
20
20
 
21
- When your mission is accomplished, end your response with:
21
+ When accomplished, end your response with:
22
22
 
23
23
  flow [debug] accomplished
24
24
 
package/agents/explore.md CHANGED
@@ -5,7 +5,7 @@ tools: read, bash, find, grep, ls
5
5
  maxDepth: 0
6
6
  ---
7
7
 
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.
8
+ During this 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
11
  - Use grep/find/ls before reading entire files — be efficient.
@@ -13,7 +13,7 @@ Rules:
13
13
  - Show actual code/data, not excessive summaries.
14
14
  - If not found, say so immediately — don't guess.
15
15
 
16
- When your mission is accomplished, end your response with:
16
+ When accomplished, end your response with:
17
17
 
18
18
  flow [explore] accomplished
19
19
 
package/agents/review.md CHANGED
@@ -5,7 +5,7 @@ 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. 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.
8
+ During this 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
@@ -19,7 +19,7 @@ Rules:
19
19
  - Do not modify source files — report findings with severity only
20
20
  - If the audit reveals structural issues requiring redesign, recommend [architect] in [Next Steps]
21
21
 
22
- When your mission is accomplished, end your response with:
22
+ When accomplished, end your response with:
23
23
 
24
24
  flow [review] accomplished
25
25
 
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.3.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. */
@@ -92,14 +76,31 @@ export function visibleLength(text: string): number {
92
76
  return text.replace(ANSI_RE, "").length;
93
77
  }
94
78
 
79
+ /**
80
+ * Compute the remaining visible-character budget for a line,
81
+ * given the length of its prefix (indent + label + space).
82
+ * Respects `process.stdout.columns` with a floor of 40 and default of 80.
83
+ */
84
+ export function getTruncationBudget(prefixLength: number): number {
85
+ const cols = process.stdout.columns ?? 80;
86
+ const width = Math.max(cols, 40);
87
+ return Math.max(width - prefixLength, 1);
88
+ }
89
+
95
90
  /**
96
91
  * Truncate an ANSI-colored string to at most `max` visible characters,
97
- * preserving ANSI codes in the kept portions and appending a reset before
98
- * 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.
99
94
  */
100
95
  function truncateAnsi(text: string, max: number): string {
101
96
  if (visibleLength(text) <= max) return text;
102
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
+
103
104
  const head = Math.ceil(max * 0.6);
104
105
  const tail = max - head - 5; // 5 = " ... ".length
105
106
 
@@ -115,9 +116,12 @@ function truncateAnsi(text: string, max: number): string {
115
116
  if (src[i] === "\x1b" && src[i + 1] === "[") {
116
117
  const end = src.indexOf("m", i + 2);
117
118
  if (end !== -1) {
118
- raw += src.slice(i, end + 1);
119
- i = end + 1;
120
- continue;
119
+ const seq = src.slice(i, end + 1);
120
+ if (/^\x1b\[[0-9;]*m$/.test(seq)) {
121
+ raw += seq;
122
+ i = end + 1;
123
+ continue;
124
+ }
121
125
  }
122
126
  }
123
127
  raw += src[i];
@@ -139,9 +143,12 @@ function truncateAnsi(text: string, max: number): string {
139
143
  if (src[i] === "m") {
140
144
  const escStart = src.lastIndexOf("\x1b[", i);
141
145
  if (escStart !== -1 && escStart < i) {
142
- // This is an ANSI sequence — don't count, skip past it
143
- i = escStart - 1;
144
- continue;
146
+ const seq = src.slice(escStart, i + 1);
147
+ if (/^\x1b\[[0-9;]*m$/.test(seq)) {
148
+ // This is an ANSI sequence — don't count, skip past it
149
+ i = escStart - 1;
150
+ continue;
151
+ }
145
152
  }
146
153
  }
147
154
  visible++;
@@ -173,8 +180,11 @@ export function tailText(text: string, max: number): string {
173
180
  if (flat[i] === "m") {
174
181
  const escStart = flat.lastIndexOf("\x1b[", i);
175
182
  if (escStart !== -1 && escStart < i) {
176
- i = escStart - 1;
177
- continue;
183
+ const seq = flat.slice(escStart, i + 1);
184
+ if (/^\x1b\[[0-9;]*m$/.test(seq)) {
185
+ i = escStart - 1;
186
+ continue;
187
+ }
178
188
  }
179
189
  }
180
190
  visible++;
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 } 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, 50))}`;
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, 50)}`;
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, 50))}`;
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, 50))}`;
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, 50))}`;
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,29 +364,29 @@ 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 ? " " : "│ ";
369
371
 
370
372
  // dir: line (intent/objective)
371
373
  if (r.intent) {
372
- container.addChild(new Text(`${theme.fg("dim", indent + "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, 50))}`, 0, 0));
374
+ container.addChild(new Text(`${theme.fg("dim", indent + "├─ dir:")} ${theme.fg("dim", truncateChars(r.intent, getTruncationBudget(11)))}`, 0, 0));
373
375
  }
374
376
 
375
377
  // exe: line (last tool call)
376
378
  const lastTool = getLastToolCall(r.messages);
377
379
  if (lastTool) {
378
380
  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));
381
+ container.addChild(new Text(`${theme.fg("dim", indent + "├─ exe:")} ${truncateChars(exeStr, getTruncationBudget(11))}`, 0, 0));
380
382
  }
381
383
 
382
384
  // log: line (last assistant text)
383
385
  const lastText = getLastAssistantText(r.messages);
384
386
  if (lastText) {
385
- container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("dim", truncateChars(lastText, 50))}`, 0, 0));
387
+ container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("dim", truncateChars(lastText, getTruncationBudget(11)))}`, 0, 0));
386
388
  } else if (error && r.errorMessage) {
387
- container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, 50))}`, 0, 0));
389
+ container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("error", truncateChars(r.errorMessage, getTruncationBudget(11)))}`, 0, 0));
388
390
  } else {
389
391
  container.addChild(new Text(`${theme.fg("dim", indent + "└─ log:")} ${theme.fg("dim", "[n/a]")}`, 0, 0));
390
392
  }
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) {