@aliou/pi-processes 0.2.1 → 0.2.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/hooks/widget.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@mariozechner/pi-coding-agent";
5
+ import { visibleWidth } from "@mariozechner/pi-tui";
5
6
  import type { ProcessInfo } from "../constants";
6
7
  import type { ProcessManager } from "../manager";
7
8
 
@@ -36,6 +37,7 @@ function formatProcessStatus(
36
37
  function renderWidget(
37
38
  processes: ProcessInfo[],
38
39
  theme: ExtensionContext["ui"]["theme"],
40
+ maxWidth?: number,
39
41
  ): string[] {
40
42
  if (processes.length === 0) {
41
43
  return [];
@@ -54,27 +56,48 @@ function renderWidget(
54
56
  p.status !== "terminate_timeout",
55
57
  );
56
58
 
57
- const parts: string[] = [];
59
+ const allProcs: ProcessInfo[] = [
60
+ ...aliveish,
61
+ ...finished.sort((a, b) => (b.endTime ?? 0) - (a.endTime ?? 0)),
62
+ ];
58
63
 
59
- for (const proc of aliveish) {
60
- parts.push(formatProcessStatus(proc, theme));
61
- }
64
+ const prefix = theme.fg("dim", "processes: ");
65
+ const prefixLen = visibleWidth(prefix);
66
+ const separator = theme.fg("dim", " | ");
67
+ const separatorLen = visibleWidth(separator);
68
+ const effectiveMax = maxWidth ?? 200;
62
69
 
63
- const recentFinished = finished
64
- .sort((a, b) => (b.endTime ?? 0) - (a.endTime ?? 0))
65
- .slice(0, 3);
70
+ const parts: string[] = [];
71
+ let currentLen = prefixLen;
72
+ let includedCount = 0;
73
+
74
+ for (const proc of allProcs) {
75
+ const formatted = formatProcessStatus(proc, theme);
76
+ const formattedLen = visibleWidth(formatted);
77
+
78
+ // Check if adding this part would exceed the width
79
+ const needed =
80
+ includedCount > 0 ? separatorLen + formattedLen : formattedLen;
81
+
82
+ if (currentLen + needed > effectiveMax && includedCount > 0) {
83
+ // Show how many are hidden
84
+ const remaining = allProcs.length - includedCount;
85
+ if (remaining > 0) {
86
+ parts.push(theme.fg("dim", `+${remaining} more`));
87
+ }
88
+ break;
89
+ }
66
90
 
67
- for (const proc of recentFinished) {
68
- parts.push(formatProcessStatus(proc, theme));
91
+ parts.push(formatted);
92
+ currentLen += needed;
93
+ includedCount++;
69
94
  }
70
95
 
71
- const hiddenCount = finished.length - recentFinished.length;
72
- if (hiddenCount > 0) {
73
- parts.push(theme.fg("dim", `+${hiddenCount} more`));
96
+ if (parts.length === 0) {
97
+ return [];
74
98
  }
75
99
 
76
- const prefix = theme.fg("dim", "processes: ");
77
- return [prefix + parts.join(theme.fg("dim", " | "))];
100
+ return [prefix + parts.join(separator)];
78
101
  }
79
102
 
80
103
  export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
@@ -84,7 +107,8 @@ export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
84
107
  if (!latestContext?.hasUI) return;
85
108
 
86
109
  const processes = manager.list();
87
- const lines = renderWidget(processes, latestContext.ui.theme);
110
+ const maxWidth = process.stdout.columns || 120;
111
+ const lines = renderWidget(processes, latestContext.ui.theme, maxWidth);
88
112
 
89
113
  if (lines.length === 0) {
90
114
  latestContext.ui.setWidget(WIDGET_ID, undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-processes",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "keywords": [
@@ -1,6 +1,9 @@
1
1
  import type { ExecuteResult } from "../../constants";
2
2
  import type { ProcessManager } from "../../manager";
3
- import { formatStatus } from "../../utils";
3
+ import { formatStatus, stripAnsi } from "../../utils";
4
+
5
+ const MAX_LINES = 200;
6
+ const MAX_BYTES = 50 * 1024; // 50KB
4
7
 
5
8
  interface OutputParams {
6
9
  id?: string;
@@ -47,22 +50,28 @@ export function executeOutput(
47
50
  };
48
51
  }
49
52
 
53
+ const logFiles = manager.getLogFiles(proc.id);
50
54
  const stdoutLines = output.stdout.length;
51
55
  const stderrLines = output.stderr.length;
52
56
  const message = `"${proc.name}" (${proc.id}) [${formatStatus(proc)}]: ${stdoutLines} stdout lines, ${stderrLines} stderr lines`;
53
57
 
58
+ // Build the full text content (ANSI-stripped), then truncate from the tail
59
+ // like bash does, so the agent sees the most recent output.
54
60
  const outputParts: string[] = [message];
55
61
  if (output.stdout.length > 0) {
56
- outputParts.push("\n--- stdout (last 100 lines) ---");
57
- outputParts.push(...output.stdout.slice(-100));
62
+ outputParts.push("\nstdout:");
63
+ outputParts.push(...output.stdout.map(stripAnsi));
58
64
  }
59
65
  if (output.stderr.length > 0) {
60
- outputParts.push("\n--- stderr (last 100 lines) ---");
61
- outputParts.push(...output.stderr.slice(-100));
66
+ outputParts.push("\nstderr:");
67
+ outputParts.push(...output.stderr.map(stripAnsi));
62
68
  }
63
69
 
70
+ const fullText = outputParts.join("\n");
71
+ const contentText = truncateTail(fullText, logFiles);
72
+
64
73
  return {
65
- content: [{ type: "text", text: outputParts.join("\n") }],
74
+ content: [{ type: "text", text: contentText }],
66
75
  details: {
67
76
  action: "output",
68
77
  success: true,
@@ -71,3 +80,62 @@ export function executeOutput(
71
80
  },
72
81
  };
73
82
  }
83
+
84
+ /**
85
+ * Truncate text from the tail (keep last N lines / MAX_BYTES), matching
86
+ * the behaviour of pi's built-in bash tool. When truncated, appends a
87
+ * notice pointing the agent to the full log files.
88
+ */
89
+ function truncateTail(
90
+ text: string,
91
+ logFiles: { stdoutFile: string; stderrFile: string } | null,
92
+ ): string {
93
+ const totalBytes = Buffer.byteLength(text, "utf-8");
94
+ const lines = text.split("\n");
95
+ const totalLines = lines.length;
96
+
97
+ if (totalLines <= MAX_LINES && totalBytes <= MAX_BYTES) {
98
+ return text;
99
+ }
100
+
101
+ // Work backwards, collecting lines that fit
102
+ const kept: string[] = [];
103
+ let keptBytes = 0;
104
+ let hitBytes = false;
105
+
106
+ for (let i = lines.length - 1; i >= 0 && kept.length < MAX_LINES; i--) {
107
+ const line = lines[i] ?? "";
108
+ const lineBytes =
109
+ Buffer.byteLength(line, "utf-8") + (kept.length > 0 ? 1 : 0);
110
+
111
+ if (keptBytes + lineBytes > MAX_BYTES) {
112
+ hitBytes = true;
113
+ break;
114
+ }
115
+
116
+ kept.unshift(line);
117
+ keptBytes += lineBytes;
118
+ }
119
+
120
+ let result = kept.join("\n");
121
+
122
+ // Append a notice so the agent knows output was truncated
123
+ const shownLines = kept.length;
124
+ const startLine = totalLines - shownLines + 1;
125
+ const sizeNote = hitBytes ? ` (${formatSize(MAX_BYTES)} limit)` : "";
126
+ result += `\n\n[Showing lines ${startLine}-${totalLines} of ${totalLines}${sizeNote}.`;
127
+
128
+ if (logFiles) {
129
+ result += ` Full logs: ${logFiles.stdoutFile} , ${logFiles.stderrFile}`;
130
+ }
131
+
132
+ result += "]";
133
+
134
+ return result;
135
+ }
136
+
137
+ function formatSize(bytes: number): string {
138
+ if (bytes < 1024) return `${bytes}B`;
139
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
140
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
141
+ }
package/tools/index.ts CHANGED
@@ -9,7 +9,7 @@ import { Text } from "@mariozechner/pi-tui";
9
9
  import { type Static, Type } from "@sinclair/typebox";
10
10
  import type { ProcessesDetails } from "../constants";
11
11
  import type { ProcessManager } from "../manager";
12
- import { formatRuntime, truncateCmd } from "../utils";
12
+ import { formatRuntime, hasAnsi, stripAnsi, truncateCmd } from "../utils";
13
13
  import { executeAction } from "./actions";
14
14
 
15
15
  const ProcessesParams = Type.Object({
@@ -144,12 +144,15 @@ Note: User always sees notifications in UI. Notification preferences only contro
144
144
  const lines: string[] = [];
145
145
  lines.push(theme.fg("muted", details.message));
146
146
 
147
+ let hadAnsi = false;
148
+
147
149
  if (details.output.stdout.length > 0) {
148
150
  lines.push("");
149
151
  lines.push(theme.fg("accent", "stdout:"));
150
152
  const stdoutLines = details.output.stdout.slice(-20);
151
153
  for (const line of stdoutLines) {
152
- lines.push(line);
154
+ if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
155
+ lines.push(stripAnsi(line));
153
156
  }
154
157
  if (details.output.stdout.length > 20) {
155
158
  lines.push(
@@ -166,7 +169,8 @@ Note: User always sees notifications in UI. Notification preferences only contro
166
169
  lines.push(theme.fg("warning", "stderr:"));
167
170
  const stderrLines = details.output.stderr.slice(-10);
168
171
  for (const line of stderrLines) {
169
- lines.push(theme.fg("warning", line));
172
+ if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
173
+ lines.push(theme.fg("warning", stripAnsi(line)));
170
174
  }
171
175
  if (details.output.stderr.length > 10) {
172
176
  lines.push(
@@ -178,6 +182,13 @@ Note: User always sees notifications in UI. Notification preferences only contro
178
182
  }
179
183
  }
180
184
 
185
+ if (hadAnsi) {
186
+ lines.push("");
187
+ lines.push(
188
+ theme.fg("muted", "ANSI escape codes were stripped from output"),
189
+ );
190
+ }
191
+
181
192
  return new Text(lines.join("\n"), 0, 0);
182
193
  }
183
194