@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 +39 -15
- package/package.json +1 -1
- package/tools/actions/output.ts +74 -6
- package/tools/index.ts +14 -3
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
|
|
59
|
+
const allProcs: ProcessInfo[] = [
|
|
60
|
+
...aliveish,
|
|
61
|
+
...finished.sort((a, b) => (b.endTime ?? 0) - (a.endTime ?? 0)),
|
|
62
|
+
];
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
91
|
+
parts.push(formatted);
|
|
92
|
+
currentLen += needed;
|
|
93
|
+
includedCount++;
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parts.push(theme.fg("dim", `+${hiddenCount} more`));
|
|
96
|
+
if (parts.length === 0) {
|
|
97
|
+
return [];
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
|
|
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
|
|
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
package/tools/actions/output.ts
CHANGED
|
@@ -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("\
|
|
57
|
-
outputParts.push(...output.stdout.
|
|
62
|
+
outputParts.push("\nstdout:");
|
|
63
|
+
outputParts.push(...output.stdout.map(stripAnsi));
|
|
58
64
|
}
|
|
59
65
|
if (output.stderr.length > 0) {
|
|
60
|
-
outputParts.push("\
|
|
61
|
-
outputParts.push(...output.stderr.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|