pi-agent-flow 0.2.4 → 0.2.6

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.2.4",
3
+ "version": "0.2.6",
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
@@ -84,15 +84,100 @@ export function formatExpandedStats(usage: Partial<UsageStats>, model?: string):
84
84
  return { stats, contextTokens };
85
85
  }
86
86
 
87
- export function truncateChars(text: string, max: number): string {
88
- if (text.length <= max) return text;
87
+ /** Regex matching ANSI escape sequences. */
88
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
89
+
90
+ /** Return the visible (ANSI-stripped) character count. */
91
+ export function visibleLength(text: string): number {
92
+ return text.replace(ANSI_RE, "").length;
93
+ }
94
+
95
+ /**
96
+ * 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.
99
+ */
100
+ function truncateAnsi(text: string, max: number): string {
101
+ if (visibleLength(text) <= max) return text;
102
+
89
103
  const head = Math.ceil(max * 0.6);
90
- const tail = max - head - 3;
91
- return text.slice(0, head) + " ... " + text.slice(-tail);
104
+ const tail = max - head - 3; // 3 = " ...".length
105
+
106
+ // Walk through the string, collecting raw chars until we've consumed
107
+ // `count` visible characters. ANSI sequences are copied through without
108
+ // counting toward the limit.
109
+ function takeVisible(src: string, count: number): { raw: string; consumed: number } {
110
+ let raw = "";
111
+ let visible = 0;
112
+ let i = 0;
113
+ while (i < src.length && visible < count) {
114
+ // Check for ANSI escape sequence at current position
115
+ if (src[i] === "\x1b" && src[i + 1] === "[") {
116
+ const end = src.indexOf("m", i + 2);
117
+ if (end !== -1) {
118
+ raw += src.slice(i, end + 1);
119
+ i = end + 1;
120
+ continue;
121
+ }
122
+ }
123
+ raw += src[i];
124
+ visible++;
125
+ i++;
126
+ }
127
+ return { raw, consumed: visible };
128
+ }
129
+
130
+ // Take head from the start
131
+ const headResult = takeVisible(text, head);
132
+
133
+ // Take tail from the end (walk backwards)
134
+ function takeVisibleFromEnd(src: string, count: number): string {
135
+ let visible = 0;
136
+ let i = src.length - 1;
137
+ while (i >= 0 && visible < count) {
138
+ // Check if current char is end of an ANSI sequence
139
+ if (src[i] === "m") {
140
+ const escStart = src.lastIndexOf("\x1b[", i);
141
+ if (escStart !== -1 && escStart < i) {
142
+ // This is an ANSI sequence — don't count, skip past it
143
+ i = escStart - 1;
144
+ continue;
145
+ }
146
+ }
147
+ visible++;
148
+ i--;
149
+ }
150
+ // Extract the raw substring from (i+1) to end, including any trailing ANSI
151
+ return src.slice(i + 1);
152
+ }
153
+
154
+ const tailRaw = takeVisibleFromEnd(text, tail);
155
+
156
+ return headResult.raw + "\x1b[0m ... " + tailRaw;
157
+ }
158
+
159
+ export function truncateChars(text: string, max: number): string {
160
+ if (visibleLength(text) <= max) return text;
161
+ return truncateAnsi(text, max);
92
162
  }
93
163
 
94
164
  export function tailText(text: string, max: number): string {
95
165
  const flat = text.replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
96
- if (flat.length <= max) return flat;
97
- return flat.slice(-max);
166
+ if (visibleLength(flat) <= max) return flat;
167
+
168
+ // Take last `max` visible characters from the end
169
+ let visible = 0;
170
+ let i = flat.length - 1;
171
+ while (i >= 0 && visible < max) {
172
+ if (flat[i] === "m") {
173
+ const escStart = flat.lastIndexOf("\x1b[", i);
174
+ if (escStart !== -1 && escStart < i) {
175
+ i = escStart - 1;
176
+ continue;
177
+ }
178
+ }
179
+ visible++;
180
+ i--;
181
+ }
182
+ return flat.slice(i + 1);
98
183
  }
package/render.ts CHANGED
@@ -229,7 +229,7 @@ function renderFlowCollapsed(
229
229
  ): Text {
230
230
  const stats = formatCompactStats(r.usage, r.model);
231
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)}`;
232
+ let text = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
233
233
  if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
234
234
 
235
235
  // DIR: line (intent/objective)
@@ -358,7 +358,7 @@ function renderActivityPanel(
358
358
 
359
359
  // Header line
360
360
  const headerPrefix = isLast ? "└─" : "├─";
361
- let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.bg("selectedBg", theme.fg("accent", theme.bold(typeName)))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
361
+ let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", "─")} ${theme.fg("dim", stats)}`;
362
362
  if (error && r.stopReason) {
363
363
  headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
364
364
  }