@aliou/pi-processes 0.1.0 → 0.2.0
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 +27 -0
- package/commands/index.ts +3 -465
- package/hooks/cleanup.ts +3 -1
- package/hooks/process-end.ts +9 -21
- package/hooks/widget.ts +33 -62
- package/index.ts +8 -0
- package/manager.ts +254 -181
- package/package.json +6 -2
- package/tools/actions/clear.ts +20 -0
- package/tools/actions/index.ts +49 -0
- package/tools/actions/kill.ts +76 -0
- package/tools/actions/list.ts +37 -0
- package/tools/actions/logs.ts +59 -0
- package/tools/actions/output.ts +73 -0
- package/tools/actions/start.ts +55 -0
- package/tools/index.ts +29 -312
- package/constants.ts +0 -2
package/README.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Manage background processes from Pi. Start long-running commands (dev servers, build watchers, log tailers) without blocking the conversation.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install via npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:@aliou/pi-processes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via the pi-extensions package:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install git:github.com/aliou/pi-extensions
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or selectively in your `settings.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"packages": [
|
|
24
|
+
{
|
|
25
|
+
"source": "git:github.com/aliou/pi-extensions",
|
|
26
|
+
"extensions": ["extensions/processes"]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
5
32
|
## Features
|
|
6
33
|
|
|
7
34
|
- **Tool**: `processes` with actions: `start`, `list`, `output`, `logs`, `kill`, `clear`
|
package/commands/index.ts
CHANGED
|
@@ -1,449 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
// Max visible processes in the list (scrollable if more)
|
|
6
|
-
const MAX_VISIBLE_PROCESSES = 8;
|
|
7
|
-
// Max log lines shown
|
|
8
|
-
const MAX_LOG_LINES = 12;
|
|
9
|
-
|
|
10
|
-
function formatRuntime(startTime: number, endTime: number | null): string {
|
|
11
|
-
const end = endTime ?? Date.now();
|
|
12
|
-
const ms = end - startTime;
|
|
13
|
-
const seconds = Math.floor(ms / 1000);
|
|
14
|
-
const minutes = Math.floor(seconds / 60);
|
|
15
|
-
const hours = Math.floor(minutes / 60);
|
|
16
|
-
|
|
17
|
-
if (hours > 0) {
|
|
18
|
-
return `${hours}h ${minutes % 60}m`;
|
|
19
|
-
}
|
|
20
|
-
if (minutes > 0) {
|
|
21
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
22
|
-
}
|
|
23
|
-
return `${seconds}s`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatBytes(bytes: number): string {
|
|
27
|
-
if (bytes >= 1024 * 1024) {
|
|
28
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
29
|
-
}
|
|
30
|
-
if (bytes >= 1024) {
|
|
31
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
32
|
-
}
|
|
33
|
-
return `${bytes}B`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function truncate(str: string, maxLen: number): string {
|
|
37
|
-
if (maxLen <= 3) return str.slice(0, maxLen);
|
|
38
|
-
if (str.length <= maxLen) return str;
|
|
39
|
-
return `${str.slice(0, maxLen - 3)}...`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
class ProcessesComponent implements Component {
|
|
43
|
-
private tui: { requestRender: () => void };
|
|
44
|
-
private theme: Theme;
|
|
45
|
-
private onClose: () => void;
|
|
46
|
-
private manager: ProcessManager;
|
|
47
|
-
|
|
48
|
-
private selectedIndex = 0;
|
|
49
|
-
private processScrollOffset = 0;
|
|
50
|
-
private logScrollOffset = 0;
|
|
51
|
-
private scrollInfo = { above: 0, below: 0 };
|
|
52
|
-
private cachedLines: string[] = [];
|
|
53
|
-
private cachedWidth = 0;
|
|
54
|
-
private refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
55
|
-
|
|
56
|
-
constructor(
|
|
57
|
-
tui: { requestRender: () => void },
|
|
58
|
-
theme: Theme,
|
|
59
|
-
onClose: () => void,
|
|
60
|
-
manager: ProcessManager,
|
|
61
|
-
) {
|
|
62
|
-
this.tui = tui;
|
|
63
|
-
this.theme = theme;
|
|
64
|
-
this.onClose = onClose;
|
|
65
|
-
this.manager = manager;
|
|
66
|
-
|
|
67
|
-
this.refreshInterval = setInterval(() => {
|
|
68
|
-
this.invalidate();
|
|
69
|
-
this.tui.requestRender();
|
|
70
|
-
}, 1000);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
handleInput(data: string): boolean {
|
|
74
|
-
const processes = this.manager.list();
|
|
75
|
-
|
|
76
|
-
// Navigation
|
|
77
|
-
if (matchesKey(data, "down") || data === "j") {
|
|
78
|
-
if (processes.length > 0) {
|
|
79
|
-
this.selectedIndex = Math.min(
|
|
80
|
-
this.selectedIndex + 1,
|
|
81
|
-
processes.length - 1,
|
|
82
|
-
);
|
|
83
|
-
this.logScrollOffset = 0;
|
|
84
|
-
this.ensureProcessVisible(processes.length);
|
|
85
|
-
this.invalidate();
|
|
86
|
-
this.tui.requestRender();
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (matchesKey(data, "up") || data === "k") {
|
|
92
|
-
if (processes.length > 0) {
|
|
93
|
-
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
94
|
-
this.logScrollOffset = 0;
|
|
95
|
-
this.ensureProcessVisible(processes.length);
|
|
96
|
-
this.invalidate();
|
|
97
|
-
this.tui.requestRender();
|
|
98
|
-
}
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Scroll logs
|
|
103
|
-
if (data === "J") {
|
|
104
|
-
this.logScrollOffset = Math.max(0, this.logScrollOffset - 5);
|
|
105
|
-
this.invalidate();
|
|
106
|
-
this.tui.requestRender();
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (data === "K") {
|
|
111
|
-
this.logScrollOffset += 5;
|
|
112
|
-
this.invalidate();
|
|
113
|
-
this.tui.requestRender();
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Kill selected process
|
|
118
|
-
if (data === "x" || data === "X") {
|
|
119
|
-
if (processes.length > 0 && this.selectedIndex < processes.length) {
|
|
120
|
-
const proc = processes[this.selectedIndex];
|
|
121
|
-
if (proc && proc.status === "running") {
|
|
122
|
-
this.manager.kill(proc.id);
|
|
123
|
-
this.invalidate();
|
|
124
|
-
this.tui.requestRender();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Clear finished processes
|
|
131
|
-
if (data === "c" || data === "C") {
|
|
132
|
-
const cleared = this.manager.clearFinished();
|
|
133
|
-
if (cleared > 0) {
|
|
134
|
-
// Adjust selection if needed
|
|
135
|
-
const remaining = this.manager.list();
|
|
136
|
-
if (this.selectedIndex >= remaining.length) {
|
|
137
|
-
this.selectedIndex = Math.max(0, remaining.length - 1);
|
|
138
|
-
}
|
|
139
|
-
this.ensureProcessVisible(remaining.length);
|
|
140
|
-
this.invalidate();
|
|
141
|
-
this.tui.requestRender();
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Close
|
|
147
|
-
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
148
|
-
if (this.refreshInterval) {
|
|
149
|
-
clearInterval(this.refreshInterval);
|
|
150
|
-
this.refreshInterval = null;
|
|
151
|
-
}
|
|
152
|
-
this.onClose();
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private ensureProcessVisible(totalProcesses: number): void {
|
|
160
|
-
const visibleCount = Math.min(MAX_VISIBLE_PROCESSES, totalProcesses);
|
|
161
|
-
if (this.selectedIndex < this.processScrollOffset) {
|
|
162
|
-
this.processScrollOffset = this.selectedIndex;
|
|
163
|
-
} else if (this.selectedIndex >= this.processScrollOffset + visibleCount) {
|
|
164
|
-
this.processScrollOffset = this.selectedIndex - visibleCount + 1;
|
|
165
|
-
}
|
|
166
|
-
this.processScrollOffset = Math.max(
|
|
167
|
-
0,
|
|
168
|
-
Math.min(this.processScrollOffset, totalProcesses - visibleCount),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
invalidate(): void {
|
|
173
|
-
this.cachedWidth = 0;
|
|
174
|
-
this.cachedLines = [];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
render(width: number): string[] {
|
|
178
|
-
if (width === this.cachedWidth && this.cachedLines.length > 0) {
|
|
179
|
-
return this.cachedLines;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const theme = this.theme;
|
|
183
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
184
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
185
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
186
|
-
const bold = (s: string) => theme.bold(s);
|
|
187
|
-
const border = (s: string) => theme.fg("dim", s);
|
|
188
|
-
|
|
189
|
-
const lines: string[] = [];
|
|
190
|
-
const processes = this.manager.list();
|
|
191
|
-
const innerWidth = width - 2; // 1 char padding each side
|
|
192
|
-
|
|
193
|
-
// Helper to pad line
|
|
194
|
-
const padLine = (content: string): string => {
|
|
195
|
-
const len = visibleWidth(content);
|
|
196
|
-
return ` ${content}${" ".repeat(Math.max(0, innerWidth - len))} `;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Top border with title
|
|
200
|
-
const title = " Background Processes ";
|
|
201
|
-
const titleLen = title.length;
|
|
202
|
-
const borderLen = Math.max(0, width - titleLen);
|
|
203
|
-
const leftBorder = Math.floor(borderLen / 2);
|
|
204
|
-
const rightBorder = borderLen - leftBorder;
|
|
205
|
-
lines.push(
|
|
206
|
-
border("─".repeat(leftBorder)) +
|
|
207
|
-
accent(bold(title)) +
|
|
208
|
-
border("─".repeat(rightBorder)),
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
if (processes.length === 0) {
|
|
212
|
-
lines.push(padLine(""));
|
|
213
|
-
lines.push(padLine(dim("No background processes")));
|
|
214
|
-
lines.push(padLine(dim("Use the processes tool to start commands")));
|
|
215
|
-
lines.push(padLine(""));
|
|
216
|
-
} else {
|
|
217
|
-
// Calculate column widths
|
|
218
|
-
const prefixWidth = 2; // "> " or " "
|
|
219
|
-
const idWidth = 9;
|
|
220
|
-
const nameWidth = 15;
|
|
221
|
-
const statusWidth = 14;
|
|
222
|
-
const timeWidth = 8;
|
|
223
|
-
const sizeWidth = 8;
|
|
224
|
-
const cmdWidth = Math.max(
|
|
225
|
-
20,
|
|
226
|
-
innerWidth -
|
|
227
|
-
prefixWidth -
|
|
228
|
-
idWidth -
|
|
229
|
-
nameWidth -
|
|
230
|
-
statusWidth -
|
|
231
|
-
timeWidth -
|
|
232
|
-
sizeWidth,
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// Header with scroll indicator if needed
|
|
236
|
-
const hasProcessScroll = processes.length > MAX_VISIBLE_PROCESSES;
|
|
237
|
-
const headerSuffix = hasProcessScroll
|
|
238
|
-
? dim(
|
|
239
|
-
` [${this.processScrollOffset + 1}-${Math.min(this.processScrollOffset + MAX_VISIBLE_PROCESSES, processes.length)}/${processes.length}]`,
|
|
240
|
-
)
|
|
241
|
-
: "";
|
|
242
|
-
|
|
243
|
-
lines.push(padLine(""));
|
|
244
|
-
const header =
|
|
245
|
-
" " +
|
|
246
|
-
dim("ID".padEnd(idWidth)) +
|
|
247
|
-
dim("Name".padEnd(nameWidth)) +
|
|
248
|
-
dim("Command".padEnd(cmdWidth)) +
|
|
249
|
-
dim("Status".padEnd(statusWidth)) +
|
|
250
|
-
dim("Time".padEnd(timeWidth)) +
|
|
251
|
-
dim("Size".padStart(sizeWidth)) +
|
|
252
|
-
headerSuffix;
|
|
253
|
-
lines.push(padLine(header));
|
|
254
|
-
lines.push(border("─".repeat(width)));
|
|
255
|
-
|
|
256
|
-
// Process rows (limited to MAX_VISIBLE_PROCESSES)
|
|
257
|
-
const visibleProcessCount = Math.min(
|
|
258
|
-
MAX_VISIBLE_PROCESSES,
|
|
259
|
-
processes.length,
|
|
260
|
-
);
|
|
261
|
-
const startIdx = this.processScrollOffset;
|
|
262
|
-
const endIdx = startIdx + visibleProcessCount;
|
|
263
|
-
|
|
264
|
-
for (let i = startIdx; i < endIdx; i++) {
|
|
265
|
-
const proc = processes[i];
|
|
266
|
-
if (!proc) continue;
|
|
267
|
-
const isSelected = i === this.selectedIndex;
|
|
268
|
-
const sizes = this.manager.getFileSize(proc.id);
|
|
269
|
-
const totalSize = sizes ? sizes.stdout + sizes.stderr : 0;
|
|
270
|
-
|
|
271
|
-
const statusText = this.formatStatus(proc);
|
|
272
|
-
const statusPadding =
|
|
273
|
-
statusWidth + (statusText.length - visibleWidth(statusText));
|
|
274
|
-
|
|
275
|
-
const row =
|
|
276
|
-
(isSelected
|
|
277
|
-
? accent(proc.id.padEnd(idWidth))
|
|
278
|
-
: proc.id.padEnd(idWidth)) +
|
|
279
|
-
truncate(proc.name, nameWidth - 1).padEnd(nameWidth) +
|
|
280
|
-
truncate(proc.command, cmdWidth - 1).padEnd(cmdWidth) +
|
|
281
|
-
statusText.padEnd(statusPadding) +
|
|
282
|
-
formatRuntime(proc.startTime, proc.endTime).padEnd(timeWidth) +
|
|
283
|
-
formatBytes(totalSize).padStart(sizeWidth);
|
|
284
|
-
|
|
285
|
-
if (isSelected) {
|
|
286
|
-
lines.push(padLine(`${accent(">")} ${row}`));
|
|
287
|
-
} else {
|
|
288
|
-
lines.push(padLine(` ${row}`));
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Pad process list to fixed height
|
|
293
|
-
for (let i = visibleProcessCount; i < MAX_VISIBLE_PROCESSES; i++) {
|
|
294
|
-
lines.push(padLine(""));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Output section for selected process
|
|
298
|
-
if (this.selectedIndex < processes.length) {
|
|
299
|
-
const selected = processes[this.selectedIndex];
|
|
300
|
-
if (!selected) {
|
|
301
|
-
this.cachedLines = lines;
|
|
302
|
-
this.cachedWidth = width;
|
|
303
|
-
return this.cachedLines;
|
|
304
|
-
}
|
|
305
|
-
const output = this.manager.getOutput(selected.id, 200);
|
|
306
|
-
const sizes = this.manager.getFileSize(selected.id);
|
|
307
|
-
|
|
308
|
-
lines.push(border("─".repeat(width)));
|
|
309
|
-
|
|
310
|
-
// Output header with size info
|
|
311
|
-
const logTitle = `Output: ${accent(selected.name)} ${dim(`(${selected.id})`)}`;
|
|
312
|
-
const sizeInfo = sizes
|
|
313
|
-
? dim(
|
|
314
|
-
` stdout: ${formatBytes(sizes.stdout)}, stderr: ${formatBytes(sizes.stderr)}`,
|
|
315
|
-
)
|
|
316
|
-
: "";
|
|
317
|
-
lines.push(padLine(logTitle + sizeInfo));
|
|
318
|
-
lines.push(padLine(""));
|
|
319
|
-
|
|
320
|
-
let renderedLines = 0;
|
|
321
|
-
|
|
322
|
-
if (output) {
|
|
323
|
-
const logLines: { type: "stdout" | "stderr"; text: string }[] = [];
|
|
324
|
-
for (const line of output.stdout) {
|
|
325
|
-
logLines.push({ type: "stdout", text: line });
|
|
326
|
-
}
|
|
327
|
-
for (const line of output.stderr) {
|
|
328
|
-
logLines.push({ type: "stderr", text: line });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (logLines.length === 0) {
|
|
332
|
-
lines.push(padLine(dim("(no output yet)")));
|
|
333
|
-
renderedLines = 1;
|
|
334
|
-
} else {
|
|
335
|
-
const startIdx = Math.max(
|
|
336
|
-
0,
|
|
337
|
-
logLines.length - MAX_LOG_LINES - this.logScrollOffset,
|
|
338
|
-
);
|
|
339
|
-
const endIdx = Math.max(0, logLines.length - this.logScrollOffset);
|
|
340
|
-
const visibleLines = logLines.slice(startIdx, endIdx);
|
|
341
|
-
|
|
342
|
-
// Track scroll info for footer
|
|
343
|
-
this.scrollInfo.above = startIdx;
|
|
344
|
-
this.scrollInfo.below =
|
|
345
|
-
this.logScrollOffset > 0 ? logLines.length - endIdx : 0;
|
|
346
|
-
|
|
347
|
-
for (const line of visibleLines) {
|
|
348
|
-
const displayLine = truncate(line.text, innerWidth - 2);
|
|
349
|
-
if (line.type === "stderr") {
|
|
350
|
-
lines.push(padLine(warning(displayLine)));
|
|
351
|
-
} else {
|
|
352
|
-
lines.push(padLine(displayLine));
|
|
353
|
-
}
|
|
354
|
-
renderedLines++;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Pad to fixed height
|
|
360
|
-
while (renderedLines < MAX_LOG_LINES) {
|
|
361
|
-
lines.push(padLine(""));
|
|
362
|
-
renderedLines++;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Footer divider
|
|
368
|
-
lines.push(border("─".repeat(width)));
|
|
369
|
-
|
|
370
|
-
// Footer with controls
|
|
371
|
-
const footerLeft =
|
|
372
|
-
`${dim("j/k")} select ` +
|
|
373
|
-
`${dim("x")} kill ` +
|
|
374
|
-
`${dim("c")} clear ` +
|
|
375
|
-
`${dim("q")} quit`;
|
|
376
|
-
|
|
377
|
-
let footerRight = "";
|
|
378
|
-
if (this.scrollInfo.above > 0 || this.scrollInfo.below > 0) {
|
|
379
|
-
const parts: string[] = [];
|
|
380
|
-
if (this.scrollInfo.above > 0) {
|
|
381
|
-
parts.push(`↑${this.scrollInfo.above}`);
|
|
382
|
-
}
|
|
383
|
-
if (this.scrollInfo.below > 0) {
|
|
384
|
-
parts.push(`↓${this.scrollInfo.below}`);
|
|
385
|
-
}
|
|
386
|
-
footerRight = `${dim("J/K")} scroll ${dim(parts.join(" "))}`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const footerLeftLen = visibleWidth(footerLeft);
|
|
390
|
-
const footerRightLen = visibleWidth(footerRight);
|
|
391
|
-
const footerGap = Math.max(2, innerWidth - footerLeftLen - footerRightLen);
|
|
392
|
-
const footer = footerLeft + " ".repeat(footerGap) + footerRight;
|
|
393
|
-
|
|
394
|
-
lines.push(padLine(footer));
|
|
395
|
-
|
|
396
|
-
// Bottom border
|
|
397
|
-
lines.push(border("─".repeat(width)));
|
|
398
|
-
|
|
399
|
-
this.cachedLines = lines;
|
|
400
|
-
this.cachedWidth = width;
|
|
401
|
-
|
|
402
|
-
return this.cachedLines;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private formatStatus(proc: ProcessInfo): string {
|
|
406
|
-
const theme = this.theme;
|
|
407
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
408
|
-
const success = (s: string) => theme.fg("success", s);
|
|
409
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
410
|
-
const error = (s: string) => theme.fg("error", s);
|
|
411
|
-
|
|
412
|
-
const icon = this.getStatusIcon(proc.status, proc.success);
|
|
413
|
-
|
|
414
|
-
switch (proc.status) {
|
|
415
|
-
case "running":
|
|
416
|
-
return success(`${icon} running`);
|
|
417
|
-
case "exited":
|
|
418
|
-
if (proc.success) {
|
|
419
|
-
return dim(`${icon} exit(0)`);
|
|
420
|
-
}
|
|
421
|
-
return error(`${icon} exit(${proc.exitCode ?? "?"})`);
|
|
422
|
-
case "killed":
|
|
423
|
-
return warning(`${icon} killed`);
|
|
424
|
-
default:
|
|
425
|
-
return proc.status;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private getStatusIcon(
|
|
430
|
-
status: ProcessInfo["status"],
|
|
431
|
-
success: boolean | null,
|
|
432
|
-
): string {
|
|
433
|
-
switch (status) {
|
|
434
|
-
case "running":
|
|
435
|
-
return "\u25CF"; // filled circle
|
|
436
|
-
case "exited":
|
|
437
|
-
return success ? "\u2713" : "\u2717"; // check or x
|
|
438
|
-
case "killed":
|
|
439
|
-
return "\u2717"; // x mark
|
|
440
|
-
default:
|
|
441
|
-
return "?";
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const WIDGET_ID = "processes-status";
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { ProcessesComponent } from "../components/processes-component";
|
|
3
|
+
import type { ProcessManager } from "../manager";
|
|
447
4
|
|
|
448
5
|
export function setupProcessesCommands(
|
|
449
6
|
pi: ExtensionAPI,
|
|
@@ -466,23 +23,4 @@ export function setupProcessesCommands(
|
|
|
466
23
|
});
|
|
467
24
|
},
|
|
468
25
|
});
|
|
469
|
-
|
|
470
|
-
pi.registerCommand("processes:clear", {
|
|
471
|
-
description: "Clear finished processes and hide widget",
|
|
472
|
-
handler: async (_args, ctx) => {
|
|
473
|
-
const cleared = manager.clearFinished();
|
|
474
|
-
const remaining = manager.list();
|
|
475
|
-
|
|
476
|
-
// Hide widget if no processes remain
|
|
477
|
-
if (remaining.length === 0 && ctx.hasUI) {
|
|
478
|
-
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (cleared > 0) {
|
|
482
|
-
ctx.ui.notify(`Cleared ${cleared} finished process(es)`, "info");
|
|
483
|
-
} else {
|
|
484
|
-
ctx.ui.notify("No finished processes to clear", "info");
|
|
485
|
-
}
|
|
486
|
-
},
|
|
487
|
-
});
|
|
488
26
|
}
|
package/hooks/cleanup.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import type { ProcessManager } from "../manager";
|
|
3
3
|
|
|
4
4
|
export function setupCleanupHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
5
|
-
pi.on("session_shutdown",
|
|
5
|
+
pi.on("session_shutdown", () => {
|
|
6
|
+
manager.stopWatcher();
|
|
7
|
+
manager.shutdownKillAll();
|
|
6
8
|
manager.cleanup();
|
|
7
9
|
});
|
|
8
10
|
}
|
package/hooks/process-end.ts
CHANGED
|
@@ -2,24 +2,9 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { MESSAGE_TYPE_PROCESS_UPDATE } from "../constants";
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
function formatRuntime(startTime: number, endTime: number | null): string {
|
|
9
|
-
const end = endTime ?? Date.now();
|
|
10
|
-
const ms = end - startTime;
|
|
11
|
-
const seconds = Math.floor(ms / 1000);
|
|
12
|
-
const minutes = Math.floor(seconds / 60);
|
|
13
|
-
const hours = Math.floor(minutes / 60);
|
|
14
|
-
|
|
15
|
-
if (hours > 0) {
|
|
16
|
-
return `${hours}h ${minutes % 60}m`;
|
|
17
|
-
}
|
|
18
|
-
if (minutes > 0) {
|
|
19
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
20
|
-
}
|
|
21
|
-
return `${seconds}s`;
|
|
22
|
-
}
|
|
5
|
+
import { MESSAGE_TYPE_PROCESS_UPDATE, type ProcessInfo } from "../constants";
|
|
6
|
+
import type { ProcessManager } from "../manager";
|
|
7
|
+
import { formatRuntime } from "../utils";
|
|
23
8
|
|
|
24
9
|
interface ProcessUpdateDetails {
|
|
25
10
|
processId: string;
|
|
@@ -47,8 +32,11 @@ export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
47
32
|
latestContext = ctx;
|
|
48
33
|
});
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
manager.onEvent((event) => {
|
|
36
|
+
if (event.type !== "process_ended") return;
|
|
37
|
+
|
|
38
|
+
const info: ProcessInfo = event.info;
|
|
39
|
+
|
|
52
40
|
// Check notification preferences
|
|
53
41
|
const shouldNotify =
|
|
54
42
|
(info.status === "killed" && info.notifyOnKill) ||
|
|
@@ -99,5 +87,5 @@ export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
99
87
|
{ triggerTurn: false },
|
|
100
88
|
);
|
|
101
89
|
}
|
|
102
|
-
};
|
|
90
|
+
});
|
|
103
91
|
}
|