@aliou/pi-processes 0.6.4 → 0.7.1
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 +49 -0
- package/package.json +6 -5
- package/src/commands/kill/command.ts +6 -6
- package/src/commands/logs/command.ts +1 -1
- package/src/commands/pin/command.ts +2 -1
- package/src/components/log-dock-component.ts +0 -11
- package/src/components/log-overlay-component.ts +0 -9
- package/src/components/processes-component.ts +32 -16
- package/src/constants/index.ts +4 -0
- package/src/constants/types.ts +36 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/message-renderer.ts +35 -3
- package/src/hooks/process-end.ts +2 -0
- package/src/hooks/process-watch.ts +83 -0
- package/src/hooks/widget/setup.ts +0 -4
- package/src/manager.test.ts +331 -0
- package/src/manager.ts +214 -30
- package/src/tools/actions/debug.ts +148 -0
- package/src/tools/actions/index.ts +126 -10
- package/src/tools/actions/kill.ts +14 -1
- package/src/tools/actions/list.ts +153 -2
- package/src/tools/actions/logs.ts +61 -3
- package/src/tools/actions/output.ts +129 -4
- package/src/tools/actions/start.ts +197 -8
- package/src/tools/actions/write.ts +28 -2
- package/src/tools/index.ts +95 -247
- package/src/utils/command-executor.ts +3 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/index.ts +7 -1
package/README.md
CHANGED
|
@@ -114,6 +114,55 @@ Available settings include:
|
|
|
114
114
|
- Linux: supported
|
|
115
115
|
- Windows: not supported
|
|
116
116
|
|
|
117
|
+
## Runtime log watch alerts
|
|
118
|
+
|
|
119
|
+
Use `process` tool `start` with `logWatches` to trigger immediate alerts while the process is still running.
|
|
120
|
+
|
|
121
|
+
- default behavior: each watch fires once (`repeat: false`)
|
|
122
|
+
- set `repeat: true` to trigger on every match
|
|
123
|
+
- scope by stream (`stdout`, `stderr`, `both`) to reduce noise
|
|
124
|
+
|
|
125
|
+
Example: server ready marker (one-time default)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"action": "start",
|
|
130
|
+
"name": "dev-server",
|
|
131
|
+
"command": "pnpm dev",
|
|
132
|
+
"logWatches": [
|
|
133
|
+
{ "pattern": "ready on http://localhost:3000" }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Example: error marker from stderr
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"action": "start",
|
|
143
|
+
"name": "builder",
|
|
144
|
+
"command": "pnpm build --watch",
|
|
145
|
+
"logWatches": [
|
|
146
|
+
{ "pattern": "TypeError|ReferenceError", "stream": "stderr" }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Example: repeatable watch on stdout only
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"action": "start",
|
|
156
|
+
"name": "worker",
|
|
157
|
+
"command": "pnpm worker",
|
|
158
|
+
"logWatches": [
|
|
159
|
+
{ "pattern": "job completed", "stream": "stdout", "repeat": true }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Invalid regex patterns fail fast at process start with a clear error.
|
|
165
|
+
|
|
117
166
|
## Troubleshooting
|
|
118
167
|
|
|
119
168
|
### Pi started something and I want to see more output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
"@sinclair/typebox": "^0.34.41"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
43
|
-
"@mariozechner/pi-tui": "0.
|
|
42
|
+
"@mariozechner/pi-coding-agent": "0.65.0",
|
|
43
|
+
"@mariozechner/pi-tui": "0.65.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@aliou/biome-plugins": "^0.3.2",
|
|
47
47
|
"@aliou/pi-evals": "^0.3.0",
|
|
48
48
|
"@biomejs/biome": "^2.3.13",
|
|
49
49
|
"@changesets/cli": "^2.27.11",
|
|
50
|
-
"@mariozechner/pi-ai": "0.
|
|
51
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
50
|
+
"@mariozechner/pi-ai": "0.65.0",
|
|
51
|
+
"@mariozechner/pi-coding-agent": "0.65.0",
|
|
52
52
|
"@types/node": "^25.0.10",
|
|
53
53
|
"husky": "^9.1.7",
|
|
54
54
|
"typescript": "^5.9.3",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
70
70
|
"changeset": "changeset",
|
|
71
71
|
"version": "changeset version",
|
|
72
|
+
"test": "vitest run",
|
|
72
73
|
"release": "pnpm changeset publish"
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -19,7 +19,7 @@ export function registerPsKillCommand(
|
|
|
19
19
|
let processId: string | undefined;
|
|
20
20
|
|
|
21
21
|
if (arg) {
|
|
22
|
-
const proc = manager.
|
|
22
|
+
const proc = manager.get(arg);
|
|
23
23
|
if (!proc) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -49,18 +49,18 @@ export function registerPsKillCommand(
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (!processId) return;
|
|
53
|
+
|
|
52
54
|
const proc = manager.get(processId);
|
|
53
|
-
if (!proc)
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
55
|
+
if (!proc) return;
|
|
56
56
|
|
|
57
57
|
const signal =
|
|
58
58
|
proc.status === "terminate_timeout" ? "SIGKILL" : "SIGTERM";
|
|
59
59
|
const timeoutMs = signal === "SIGKILL" ? 200 : 3000;
|
|
60
|
-
const result = await manager.kill(
|
|
60
|
+
const result = await manager.kill(proc.id, { signal, timeoutMs });
|
|
61
61
|
|
|
62
62
|
if (result.ok) {
|
|
63
|
-
if (dockActions.getFocusedProcessId() ===
|
|
63
|
+
if (dockActions.getFocusedProcessId() === proc.id) {
|
|
64
64
|
dockActions.setFocus(null);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -17,7 +17,7 @@ export function registerPsPinCommand(
|
|
|
17
17
|
let processId: string | undefined;
|
|
18
18
|
|
|
19
19
|
if (arg) {
|
|
20
|
-
const proc = manager.
|
|
20
|
+
const proc = manager.get(arg);
|
|
21
21
|
if (!proc) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
@@ -27,6 +27,7 @@ export function registerPsPinCommand(
|
|
|
27
27
|
if (!processId) return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
if (!processId) return;
|
|
30
31
|
dockActions.setFocus(processId);
|
|
31
32
|
},
|
|
32
33
|
});
|
|
@@ -17,8 +17,6 @@ import { LIVE_STATUSES } from "../constants";
|
|
|
17
17
|
import type { ProcessManager } from "../manager";
|
|
18
18
|
import { LogFileViewer } from "./log-file-viewer";
|
|
19
19
|
|
|
20
|
-
const POLL_INTERVAL_MS = 500;
|
|
21
|
-
|
|
22
20
|
const PROCESS_COLORS: ThemeColor[] = [
|
|
23
21
|
"accent",
|
|
24
22
|
"warning",
|
|
@@ -47,7 +45,6 @@ export class LogDockComponent implements Component {
|
|
|
47
45
|
private mode: "collapsed" | "open";
|
|
48
46
|
private focusedProcessId: string | null;
|
|
49
47
|
|
|
50
|
-
private timer: ReturnType<typeof setInterval> | null = null;
|
|
51
48
|
private unsubscribeManager: (() => void) | null = null;
|
|
52
49
|
|
|
53
50
|
/** One viewer per process, lazily created, follow:true. */
|
|
@@ -64,10 +61,6 @@ export class LogDockComponent implements Component {
|
|
|
64
61
|
this.mode = options.mode;
|
|
65
62
|
this.focusedProcessId = options.focusedProcessId;
|
|
66
63
|
|
|
67
|
-
this.timer = setInterval(() => {
|
|
68
|
-
this.tui.requestRender();
|
|
69
|
-
}, POLL_INTERVAL_MS);
|
|
70
|
-
|
|
71
64
|
this.unsubscribeManager = this.manager.onEvent(() => {
|
|
72
65
|
this.tui.requestRender();
|
|
73
66
|
});
|
|
@@ -227,10 +220,6 @@ export class LogDockComponent implements Component {
|
|
|
227
220
|
}
|
|
228
221
|
|
|
229
222
|
dispose(): void {
|
|
230
|
-
if (this.timer) {
|
|
231
|
-
clearInterval(this.timer);
|
|
232
|
-
this.timer = null;
|
|
233
|
-
}
|
|
234
223
|
this.unsubscribeManager?.();
|
|
235
224
|
this.viewers.clear();
|
|
236
225
|
this.processColors.clear();
|
|
@@ -61,7 +61,6 @@ export class LogOverlayComponent implements Component {
|
|
|
61
61
|
private mode: OverlayMode = "normal";
|
|
62
62
|
private searchInput: Input = new Input();
|
|
63
63
|
|
|
64
|
-
private timer: ReturnType<typeof setInterval> | null = null;
|
|
65
64
|
private unsubscribeManager: (() => void) | null = null;
|
|
66
65
|
|
|
67
66
|
constructor(opts: LogOverlayOptions) {
|
|
@@ -91,10 +90,6 @@ export class LogOverlayComponent implements Component {
|
|
|
91
90
|
this.tui.requestRender();
|
|
92
91
|
});
|
|
93
92
|
|
|
94
|
-
this.timer = setInterval(() => {
|
|
95
|
-
this.tui.requestRender();
|
|
96
|
-
}, 300);
|
|
97
|
-
|
|
98
93
|
this.searchInput.onSubmit = (query) => {
|
|
99
94
|
const trimmed = query.trim();
|
|
100
95
|
if (trimmed) {
|
|
@@ -163,10 +158,6 @@ export class LogOverlayComponent implements Component {
|
|
|
163
158
|
// ---------------------------------------------------------------------------
|
|
164
159
|
|
|
165
160
|
private close(): void {
|
|
166
|
-
if (this.timer) {
|
|
167
|
-
clearInterval(this.timer);
|
|
168
|
-
this.timer = null;
|
|
169
|
-
}
|
|
170
161
|
this.unsubscribeManager?.();
|
|
171
162
|
this.unsubscribeManager = null;
|
|
172
163
|
this.done();
|
|
@@ -48,6 +48,19 @@ function truncate(str: string, maxLen: number): string {
|
|
|
48
48
|
return `${str.slice(0, maxLen - 3)}...`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function fitCell(
|
|
52
|
+
value: string,
|
|
53
|
+
width: number,
|
|
54
|
+
align: "left" | "right" = "left",
|
|
55
|
+
): string {
|
|
56
|
+
const truncated = truncateToWidth(value, Math.max(0, width));
|
|
57
|
+
const pad = Math.max(0, width - visibleWidth(truncated));
|
|
58
|
+
if (align === "right") {
|
|
59
|
+
return " ".repeat(pad) + truncated;
|
|
60
|
+
}
|
|
61
|
+
return truncated + " ".repeat(pad);
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
export class ProcessesComponent implements Component {
|
|
52
65
|
private tui: { requestRender: () => void };
|
|
53
66
|
private theme: Theme;
|
|
@@ -243,8 +256,7 @@ export class ProcessesComponent implements Component {
|
|
|
243
256
|
const scaleFactor =
|
|
244
257
|
innerWidth < minTotalWidth ? innerWidth / minTotalWidth : 1;
|
|
245
258
|
|
|
246
|
-
const
|
|
247
|
-
const nameWidth = Math.max(8, Math.floor(15 * scaleFactor));
|
|
259
|
+
const processWidth = Math.max(14, Math.floor(24 * scaleFactor));
|
|
248
260
|
const statusWidth = Math.max(10, Math.floor(18 * scaleFactor));
|
|
249
261
|
const timeWidth = Math.max(4, Math.floor(8 * scaleFactor));
|
|
250
262
|
const sizeWidth = Math.max(4, Math.floor(8 * scaleFactor));
|
|
@@ -258,8 +270,7 @@ export class ProcessesComponent implements Component {
|
|
|
258
270
|
// Calculate command column width based on remaining space
|
|
259
271
|
const fixedWidth =
|
|
260
272
|
prefixWidth +
|
|
261
|
-
|
|
262
|
-
nameWidth +
|
|
273
|
+
processWidth +
|
|
263
274
|
statusWidth +
|
|
264
275
|
timeWidth +
|
|
265
276
|
sizeWidth +
|
|
@@ -269,8 +280,7 @@ export class ProcessesComponent implements Component {
|
|
|
269
280
|
lines.push(padLine(""));
|
|
270
281
|
const header =
|
|
271
282
|
" " +
|
|
272
|
-
dim("
|
|
273
|
-
dim("Name".padEnd(nameWidth)) +
|
|
283
|
+
dim("Process".padEnd(processWidth)) +
|
|
274
284
|
dim("Command".padEnd(cmdWidth)) +
|
|
275
285
|
dim("Status".padEnd(statusWidth)) +
|
|
276
286
|
dim("Time".padEnd(timeWidth)) +
|
|
@@ -294,18 +304,24 @@ export class ProcessesComponent implements Component {
|
|
|
294
304
|
const totalSize = sizes ? sizes.stdout + sizes.stderr : 0;
|
|
295
305
|
|
|
296
306
|
const statusText = this.formatStatus(proc);
|
|
297
|
-
|
|
298
|
-
|
|
307
|
+
|
|
308
|
+
// Keep process cell bounded even with large IDs.
|
|
309
|
+
const idPlain = `(${proc.id})`;
|
|
310
|
+
const maxNameLen = Math.max(
|
|
311
|
+
1,
|
|
312
|
+
processWidth - visibleWidth(idPlain) - 1,
|
|
313
|
+
);
|
|
314
|
+
const tName = truncate(proc.name, maxNameLen);
|
|
315
|
+
const processCell = isSelected
|
|
316
|
+
? `${accent(tName)} ${dim(` ${idPlain}`)}`
|
|
317
|
+
: `${tName}${dim(` ${idPlain}`)}`;
|
|
299
318
|
|
|
300
319
|
const row =
|
|
301
|
-
(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
statusText.padEnd(statusPadding) +
|
|
307
|
-
formatRuntime(proc.startTime, proc.endTime).padEnd(timeWidth) +
|
|
308
|
-
formatBytes(totalSize).padStart(sizeWidth);
|
|
320
|
+
fitCell(processCell, processWidth) +
|
|
321
|
+
fitCell(truncate(proc.command, cmdWidth - 1), cmdWidth) +
|
|
322
|
+
fitCell(statusText, statusWidth) +
|
|
323
|
+
fitCell(formatRuntime(proc.startTime, proc.endTime), timeWidth) +
|
|
324
|
+
fitCell(formatBytes(totalSize), sizeWidth, "right");
|
|
309
325
|
|
|
310
326
|
if (isSelected) {
|
|
311
327
|
lines.push(padLine(`${accent(">")} ${row}`));
|
package/src/constants/index.ts
CHANGED
package/src/constants/types.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Custom message type for process update notifications
|
|
2
2
|
export const MESSAGE_TYPE_PROCESS_UPDATE = "ad-process:update";
|
|
3
3
|
|
|
4
|
+
export type ProcessAction =
|
|
5
|
+
| "start"
|
|
6
|
+
| "list"
|
|
7
|
+
| "output"
|
|
8
|
+
| "logs"
|
|
9
|
+
| "kill"
|
|
10
|
+
| "clear"
|
|
11
|
+
| "write"
|
|
12
|
+
| "debug_preview";
|
|
13
|
+
|
|
4
14
|
export type ProcessStatus =
|
|
5
15
|
| "running"
|
|
6
16
|
| "terminating"
|
|
@@ -14,6 +24,14 @@ export const LIVE_STATUSES: ReadonlySet<ProcessStatus> = new Set([
|
|
|
14
24
|
"terminate_timeout",
|
|
15
25
|
]);
|
|
16
26
|
|
|
27
|
+
export type LogWatchStream = "stdout" | "stderr" | "both";
|
|
28
|
+
|
|
29
|
+
export interface LogWatch {
|
|
30
|
+
pattern: string;
|
|
31
|
+
stream?: LogWatchStream;
|
|
32
|
+
repeat?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
export interface ProcessInfo {
|
|
18
36
|
id: string;
|
|
19
37
|
name: string;
|
|
@@ -32,9 +50,25 @@ export interface ProcessInfo {
|
|
|
32
50
|
alertOnKill: boolean;
|
|
33
51
|
}
|
|
34
52
|
|
|
53
|
+
export interface LogWatchMatchEvent {
|
|
54
|
+
processId: string;
|
|
55
|
+
processName: string;
|
|
56
|
+
processCommand: string;
|
|
57
|
+
source: "stdout" | "stderr";
|
|
58
|
+
line: string;
|
|
59
|
+
watch: {
|
|
60
|
+
index: number;
|
|
61
|
+
pattern: string;
|
|
62
|
+
stream: LogWatchStream;
|
|
63
|
+
repeat: boolean;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
export type ManagerEvent =
|
|
36
68
|
| { type: "process_started"; info: ProcessInfo }
|
|
37
69
|
| { type: "process_ended"; info: ProcessInfo }
|
|
70
|
+
| { type: "process_output_changed"; id: string }
|
|
71
|
+
| { type: "process_watch_matched"; match: LogWatchMatchEvent }
|
|
38
72
|
| { type: "processes_changed" };
|
|
39
73
|
|
|
40
74
|
export type KillResult =
|
|
@@ -52,10 +86,11 @@ export interface StartOptions {
|
|
|
52
86
|
alertOnSuccess?: boolean;
|
|
53
87
|
alertOnFailure?: boolean;
|
|
54
88
|
alertOnKill?: boolean;
|
|
89
|
+
logWatches?: LogWatch[];
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
export interface ProcessesDetails {
|
|
58
|
-
action:
|
|
93
|
+
action: ProcessAction;
|
|
59
94
|
success: boolean;
|
|
60
95
|
message: string;
|
|
61
96
|
process?: ProcessInfo;
|
package/src/hooks/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { setupBackgroundBlocker } from "./background-blocker";
|
|
|
5
5
|
import { setupCleanupHook } from "./cleanup";
|
|
6
6
|
import { setupMessageRenderer } from "./message-renderer";
|
|
7
7
|
import { setupProcessEndHook } from "./process-end";
|
|
8
|
+
import { setupProcessWatchHook } from "./process-watch";
|
|
8
9
|
import { type DockActions, setupProcessWidget } from "./widget";
|
|
9
10
|
|
|
10
11
|
export type { DockActions };
|
|
@@ -16,6 +17,7 @@ export function setupProcessesHooks(
|
|
|
16
17
|
): { update: () => void; dockActions: DockActions } {
|
|
17
18
|
setupCleanupHook(pi, manager);
|
|
18
19
|
setupProcessEndHook(pi, manager);
|
|
20
|
+
setupProcessWatchHook(pi, manager);
|
|
19
21
|
|
|
20
22
|
if (config.interception.blockBackgroundCommands) {
|
|
21
23
|
setupBackgroundBlocker(pi);
|
|
@@ -6,7 +6,8 @@ import type {
|
|
|
6
6
|
import { Text } from "@mariozechner/pi-tui";
|
|
7
7
|
import { MESSAGE_TYPE_PROCESS_UPDATE } from "../constants";
|
|
8
8
|
|
|
9
|
-
interface
|
|
9
|
+
interface ProcessLifecycleDetails {
|
|
10
|
+
kind?: "lifecycle";
|
|
10
11
|
processId: string;
|
|
11
12
|
processName: string;
|
|
12
13
|
command: string;
|
|
@@ -16,10 +17,25 @@ interface ProcessUpdateDetails {
|
|
|
16
17
|
runtime: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
interface ProcessWatchMatchDetails {
|
|
21
|
+
kind: "watch_matched";
|
|
22
|
+
processId: string;
|
|
23
|
+
processName: string;
|
|
24
|
+
command: string;
|
|
25
|
+
source: "stdout" | "stderr";
|
|
26
|
+
line: string;
|
|
27
|
+
watch: {
|
|
28
|
+
index: number;
|
|
29
|
+
pattern: string;
|
|
30
|
+
stream: "stdout" | "stderr" | "both";
|
|
31
|
+
repeat: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
interface ProcessUpdateMessage {
|
|
20
36
|
customType: string;
|
|
21
37
|
content: string | Array<{ type: string; text?: string }>;
|
|
22
|
-
details?:
|
|
38
|
+
details?: ProcessLifecycleDetails | ProcessWatchMatchDetails;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
function getContentText(
|
|
@@ -35,7 +51,9 @@ function getContentText(
|
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
export function setupMessageRenderer(pi: ExtensionAPI) {
|
|
38
|
-
pi.registerMessageRenderer<
|
|
54
|
+
pi.registerMessageRenderer<
|
|
55
|
+
ProcessLifecycleDetails | ProcessWatchMatchDetails
|
|
56
|
+
>(
|
|
39
57
|
MESSAGE_TYPE_PROCESS_UPDATE,
|
|
40
58
|
(
|
|
41
59
|
message: ProcessUpdateMessage,
|
|
@@ -48,6 +66,20 @@ export function setupMessageRenderer(pi: ExtensionAPI) {
|
|
|
48
66
|
return new Text(getContentText(message.content), 0, 0);
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
if (details.kind === "watch_matched") {
|
|
70
|
+
const streamColor = details.source === "stderr" ? "warning" : "accent";
|
|
71
|
+
const text =
|
|
72
|
+
theme.fg("success", "* ") +
|
|
73
|
+
theme.fg("accent", `"${details.processName}"`) +
|
|
74
|
+
theme.fg("muted", ` (${details.processId}) `) +
|
|
75
|
+
theme.fg("success", "watch matched ") +
|
|
76
|
+
theme.fg("muted", `/${details.watch.pattern}/ `) +
|
|
77
|
+
theme.fg(streamColor, `[${details.source}]`) +
|
|
78
|
+
theme.fg("muted", ` ${details.line}`);
|
|
79
|
+
|
|
80
|
+
return new Text(text, 0, 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
51
83
|
let icon: string;
|
|
52
84
|
let color: "success" | "error" | "warning";
|
|
53
85
|
|
package/src/hooks/process-end.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ProcessManager } from "../manager";
|
|
|
4
4
|
import { formatRuntime } from "../utils";
|
|
5
5
|
|
|
6
6
|
interface ProcessUpdateDetails {
|
|
7
|
+
kind: "lifecycle";
|
|
7
8
|
processId: string;
|
|
8
9
|
processName: string;
|
|
9
10
|
command: string;
|
|
@@ -43,6 +44,7 @@ export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
43
44
|
// Send the message to the conversation - displayed via custom renderer in UI
|
|
44
45
|
// Only trigger an agent turn when the notification preferences say so.
|
|
45
46
|
const details: ProcessUpdateDetails = {
|
|
47
|
+
kind: "lifecycle",
|
|
46
48
|
processId: info.id,
|
|
47
49
|
processName: info.name,
|
|
48
50
|
command: info.command,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { MESSAGE_TYPE_PROCESS_UPDATE } from "../constants";
|
|
3
|
+
import type { ProcessManager } from "../manager";
|
|
4
|
+
|
|
5
|
+
interface ProcessWatchUpdateDetails {
|
|
6
|
+
kind: "watch_matched";
|
|
7
|
+
processId: string;
|
|
8
|
+
processName: string;
|
|
9
|
+
command: string;
|
|
10
|
+
source: "stdout" | "stderr";
|
|
11
|
+
line: string;
|
|
12
|
+
watch: {
|
|
13
|
+
index: number;
|
|
14
|
+
pattern: string;
|
|
15
|
+
stream: "stdout" | "stderr" | "both";
|
|
16
|
+
repeat: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const REPEAT_WATCH_TURN_COOLDOWN_MS = 5000;
|
|
21
|
+
|
|
22
|
+
export function setupProcessWatchHook(
|
|
23
|
+
pi: ExtensionAPI,
|
|
24
|
+
manager: ProcessManager,
|
|
25
|
+
) {
|
|
26
|
+
const lastRepeatTurnAt = new Map<string, number>();
|
|
27
|
+
|
|
28
|
+
manager.onEvent((event) => {
|
|
29
|
+
if (event.type === "process_ended") {
|
|
30
|
+
// Cleanup cooldown state for this process.
|
|
31
|
+
const prefix = `${event.info.id}:`;
|
|
32
|
+
for (const key of lastRepeatTurnAt.keys()) {
|
|
33
|
+
if (key.startsWith(prefix)) {
|
|
34
|
+
lastRepeatTurnAt.delete(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (event.type !== "process_watch_matched") return;
|
|
41
|
+
|
|
42
|
+
const match = event.match;
|
|
43
|
+
const message =
|
|
44
|
+
`Watch matched for '${match.processName}' (${match.processId}) ` +
|
|
45
|
+
`[${match.source}] /${match.watch.pattern}/`;
|
|
46
|
+
|
|
47
|
+
const details: ProcessWatchUpdateDetails = {
|
|
48
|
+
kind: "watch_matched",
|
|
49
|
+
processId: match.processId,
|
|
50
|
+
processName: match.processName,
|
|
51
|
+
command: match.processCommand,
|
|
52
|
+
source: match.source,
|
|
53
|
+
line: match.line,
|
|
54
|
+
watch: {
|
|
55
|
+
index: match.watch.index,
|
|
56
|
+
pattern: match.watch.pattern,
|
|
57
|
+
stream: match.watch.stream,
|
|
58
|
+
repeat: match.watch.repeat,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let triggerTurn = true;
|
|
63
|
+
if (match.watch.repeat) {
|
|
64
|
+
const watchKey = `${match.processId}:${match.watch.index}`;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const last = lastRepeatTurnAt.get(watchKey) ?? 0;
|
|
67
|
+
triggerTurn = now - last >= REPEAT_WATCH_TURN_COOLDOWN_MS;
|
|
68
|
+
if (triggerTurn) {
|
|
69
|
+
lastRepeatTurnAt.set(watchKey, now);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pi.sendMessage(
|
|
74
|
+
{
|
|
75
|
+
customType: MESSAGE_TYPE_PROCESS_UPDATE,
|
|
76
|
+
content: message,
|
|
77
|
+
display: true,
|
|
78
|
+
details,
|
|
79
|
+
},
|
|
80
|
+
{ triggerTurn },
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -147,10 +147,6 @@ export function setupProcessWidget(
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
pi.on("session_start", async (_event, ctx) => {
|
|
150
|
-
latestContext = ctx;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
pi.on("session_switch", async (_event, ctx) => {
|
|
154
150
|
if (logDockComponent) {
|
|
155
151
|
logDockComponent.dispose();
|
|
156
152
|
logDockComponent = null;
|