@aliou/pi-processes 0.4.7 → 0.6.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 +83 -13
- package/package.json +10 -4
- package/src/commands/clear/command.ts +14 -0
- package/src/commands/clear/index.ts +1 -0
- package/src/commands/completions.ts +38 -0
- package/src/commands/dock/command.ts +29 -0
- package/src/commands/dock/index.ts +1 -0
- package/src/commands/index.ts +26 -327
- package/src/commands/kill/command.ts +69 -0
- package/src/commands/kill/index.ts +1 -0
- package/src/commands/logs/command.ts +47 -0
- package/src/commands/logs/index.ts +1 -0
- package/src/commands/pick-process.ts +34 -0
- package/src/commands/pin/command.ts +33 -0
- package/src/commands/pin/index.ts +1 -0
- package/src/commands/processes/command.ts +39 -0
- package/src/commands/processes/index.ts +1 -0
- package/src/commands/settings/apply-setting-change.ts +72 -0
- package/src/commands/settings/build-sections.ts +160 -0
- package/src/commands/settings/command.ts +20 -0
- package/src/commands/settings/index.ts +1 -0
- package/src/components/log-dock-component.ts +238 -0
- package/src/components/log-file-viewer.ts +317 -0
- package/src/components/log-overlay-component.ts +555 -0
- package/src/components/process-picker-component.ts +14 -2
- package/src/components/processes-component.ts +41 -21
- package/src/config.ts +38 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/types.ts +7 -1
- package/src/hooks/background-blocker.ts +66 -0
- package/src/hooks/index.ts +15 -3
- package/src/hooks/process-end.ts +3 -30
- package/src/hooks/widget/index.ts +2 -0
- package/src/hooks/widget/setup.ts +168 -0
- package/src/hooks/{widget.ts → widget/status-widget.ts} +27 -68
- package/src/hooks/widget/types.ts +21 -0
- package/src/index.ts +9 -3
- package/src/manager.ts +61 -9
- package/src/tools/actions/index.ts +5 -0
- package/src/tools/actions/write.ts +87 -0
- package/src/tools/index.ts +82 -14
- package/src/utils/command-executor.test.ts +2 -1
- package/src/utils/command-executor.ts +1 -1
- package/src/utils/keybindings.ts +76 -0
- package/src/utils/shell-utils.ts +133 -0
- package/src/commands/settings-command.ts +0 -157
- package/src/components/log-stream-component.ts +0 -149
- package/src/test/test-exit-crash.sh +0 -19
- package/src/test/test-exit-failure.sh +0 -17
- package/src/test/test-exit-success.sh +0 -16
- package/src/test/test-output.sh +0 -28
package/README.md
CHANGED
|
@@ -21,7 +21,10 @@ pi install git:github.com/aliou/pi-processes
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
23
|
- **Tool**: `process` with actions: `start`, `list`, `output`, `logs`, `kill`, `clear`
|
|
24
|
-
- **Commands**: `/
|
|
24
|
+
- **Commands**: `/ps` (interactive panel), `/ps:focus` (focus on process), `/ps:logs` (focus + expand), `/ps:kill` (kill process), `/ps:clear` (clear finished), `/ps:dock` (toggle dock)
|
|
25
|
+
- **Log Dock**: Unified view for all process logs with color-coded prefixes
|
|
26
|
+
- **Follow Mode**: Automatically shows dock when processes start (enabled by default)
|
|
27
|
+
- **Focus Mode**: Filter to a single process's logs
|
|
25
28
|
- Auto-cleanup on session exit
|
|
26
29
|
- File-based logging (logs written to temp files, not memory)
|
|
27
30
|
- Friendly process names (auto-inferred or custom)
|
|
@@ -52,19 +55,85 @@ Note: User always sees process updates in the UI. The alert flags control whethe
|
|
|
52
55
|
|
|
53
56
|
### Commands (interactive)
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
#### `/ps` - Open full panel
|
|
59
|
+
|
|
60
|
+
View and manage all processes in an interactive panel:
|
|
56
61
|
- `j/k` - select process
|
|
57
62
|
- `J/K` - scroll logs
|
|
58
|
-
- `enter` -
|
|
63
|
+
- `enter` - focus on selected process
|
|
59
64
|
- `x` - kill selected process
|
|
60
65
|
- `c` - clear finished processes
|
|
61
66
|
- `q` - quit
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
#### `/ps:focus [id|name]` - Focus on a process
|
|
69
|
+
|
|
70
|
+
Focus on a specific process to view its logs in the dock. Opens the dock automatically if hidden.
|
|
71
|
+
|
|
72
|
+
Without arguments, shows a picker to select a process.
|
|
73
|
+
|
|
74
|
+
#### `/ps:logs [id|name]` - Show log paths
|
|
75
|
+
|
|
76
|
+
Display the stdout and stderr log file paths for a process.
|
|
77
|
+
|
|
78
|
+
#### `/ps:kill [id|name]` - Kill a process
|
|
79
|
+
|
|
80
|
+
Kill a running process. Without arguments, shows a picker.
|
|
81
|
+
|
|
82
|
+
#### `/ps:clear` - Clear finished
|
|
83
|
+
|
|
84
|
+
Remove all finished processes from the list.
|
|
85
|
+
|
|
86
|
+
#### `/ps:dock [on|off|expanded]` - Toggle dock
|
|
87
|
+
|
|
88
|
+
Control dock visibility. Without arguments, toggles between hidden/open. With arguments:
|
|
89
|
+
- `on` - Show dock (open)
|
|
90
|
+
- `off` - Hide dock
|
|
91
|
+
- `expanded` - Show dock open (same as on)
|
|
92
|
+
|
|
93
|
+
### Log Dock
|
|
94
|
+
|
|
95
|
+
The log dock shows interleaved logs from all processes with color-coded prefixes:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
[dev] GET /api/users 200 45ms (cyan)
|
|
99
|
+
[test] Running auth.test.ts... (yellow)
|
|
100
|
+
[dev] POST /api/auth 200 12ms
|
|
101
|
+
[test] ✓ login flow
|
|
102
|
+
...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Dock Keyboard Shortcuts** (when dock is open):
|
|
106
|
+
- `h` - focus previous process
|
|
107
|
+
- `l` - focus next process
|
|
108
|
+
- `f` - toggle focus mode (filter to single process)
|
|
109
|
+
- `x` - kill focused process
|
|
110
|
+
- `q` - close/unfocus dock
|
|
111
|
+
- `Shift+F` - toggle follow mode
|
|
112
|
+
- `Ctrl+Shift+P` - toggle dock visibility (global)
|
|
113
|
+
|
|
114
|
+
**Dock States:**
|
|
115
|
+
- `collapsed` (1-2 lines): Summary + last log line
|
|
116
|
+
- `open` (8-12 lines): Full interleaved or focused logs
|
|
117
|
+
|
|
118
|
+
### Deprecated Commands (backward compatible)
|
|
119
|
+
|
|
120
|
+
The following commands are deprecated but still work with a warning:
|
|
121
|
+
- `/process:list` → Use `/ps` instead
|
|
122
|
+
- `/process:stream` → Use `/ps:focus` instead
|
|
123
|
+
- `/process:logs` → Use `/ps:logs` instead
|
|
124
|
+
- `/process:kill` → Use `/ps:kill` instead
|
|
125
|
+
- `/process:clear` → Use `/ps:clear` instead
|
|
126
|
+
|
|
127
|
+
## Settings
|
|
128
|
+
|
|
129
|
+
Configure via `/ps:settings` or `~/.pi/agent/extensions/processes.json`:
|
|
130
|
+
|
|
131
|
+
- **Widget**: Dock default state, dock height
|
|
132
|
+
- **Follow Mode**: Enable by default, auto-hide on finish
|
|
133
|
+
- **Process List**: Max visible processes, max preview lines
|
|
134
|
+
- **Output Limits**: Default tail lines, max output lines
|
|
135
|
+
- **Execution**: Shell path override
|
|
136
|
+
- **Interception**: Block background commands
|
|
68
137
|
|
|
69
138
|
## Test Scripts
|
|
70
139
|
|
|
@@ -79,8 +148,9 @@ Test scripts in `src/test/` directory:
|
|
|
79
148
|
|
|
80
149
|
## Future Improvements
|
|
81
150
|
|
|
82
|
-
- [ ] **
|
|
83
|
-
|
|
84
|
-
- [ ] **
|
|
85
|
-
|
|
86
|
-
- [ ] **Open
|
|
151
|
+
- [ ] **Configurable keybindings UI**: Allow editing keybindings in settings panel
|
|
152
|
+
- [ ] **Process filtering**: Filter by name/status in full panel
|
|
153
|
+
- [ ] **Log search**: Search within logs (Ctrl+F in dock)
|
|
154
|
+
- [ ] **Copy log path**: Keyboard shortcut to copy log file path
|
|
155
|
+
- [ ] **Open in editor**: Keyboard shortcut to open logs in $EDITOR
|
|
156
|
+
- [ ] **Sound notifications**: Play sound on process completion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -28,20 +28,22 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aliou/pi-utils-settings": "^0.
|
|
31
|
+
"@aliou/pi-utils-settings": "^0.6.0",
|
|
32
32
|
"@aliou/pi-utils-ui": "^0.1.0",
|
|
33
|
+
"@aliou/sh": "^0.1.0",
|
|
33
34
|
"@sinclair/typebox": "^0.34.41"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
|
-
"@mariozechner/pi-coding-agent": ">=0.51.0"
|
|
37
|
+
"@mariozechner/pi-coding-agent": ">=0.51.0",
|
|
38
|
+
"@mariozechner/pi-tui": ">=0.51.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"@aliou/biome-plugins": "^0.3.2",
|
|
39
42
|
"@aliou/pi-evals": "^0.3.0",
|
|
40
43
|
"@biomejs/biome": "^2.3.13",
|
|
41
44
|
"@changesets/cli": "^2.27.11",
|
|
42
45
|
"@mariozechner/pi-ai": "0.52.7",
|
|
43
46
|
"@mariozechner/pi-coding-agent": "0.52.7",
|
|
44
|
-
"@mariozechner/pi-tui": "0.52.7",
|
|
45
47
|
"@types/node": "^25.0.10",
|
|
46
48
|
"husky": "^9.1.7",
|
|
47
49
|
"typescript": "^5.9.3",
|
|
@@ -50,12 +52,16 @@
|
|
|
50
52
|
"peerDependenciesMeta": {
|
|
51
53
|
"@mariozechner/pi-coding-agent": {
|
|
52
54
|
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@mariozechner/pi-tui": {
|
|
57
|
+
"optional": true
|
|
53
58
|
}
|
|
54
59
|
},
|
|
55
60
|
"scripts": {
|
|
56
61
|
"typecheck": "tsc --noEmit",
|
|
57
62
|
"lint": "biome check",
|
|
58
63
|
"format": "biome check --write",
|
|
64
|
+
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
59
65
|
"changeset": "changeset",
|
|
60
66
|
"version": "changeset version",
|
|
61
67
|
"release": "pnpm changeset publish"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ProcessManager } from "../../manager";
|
|
3
|
+
|
|
4
|
+
export function registerPsClearCommand(
|
|
5
|
+
pi: ExtensionAPI,
|
|
6
|
+
manager: ProcessManager,
|
|
7
|
+
): void {
|
|
8
|
+
pi.registerCommand("ps:clear", {
|
|
9
|
+
description: "Remove all finished processes from the list",
|
|
10
|
+
handler: async (_args, _ctx) => {
|
|
11
|
+
manager.clearFinished();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerPsClearCommand } from "./command";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ProcessManager } from "../manager";
|
|
2
|
+
|
|
3
|
+
export function runningProcessCompletions(manager: ProcessManager) {
|
|
4
|
+
return (prefix: string) => {
|
|
5
|
+
const processes = manager.list();
|
|
6
|
+
const lower = prefix.toLowerCase();
|
|
7
|
+
return processes
|
|
8
|
+
.filter(
|
|
9
|
+
(p) =>
|
|
10
|
+
p.status === "running" &&
|
|
11
|
+
(p.id.toLowerCase().startsWith(lower) ||
|
|
12
|
+
p.name.toLowerCase().startsWith(lower)),
|
|
13
|
+
)
|
|
14
|
+
.map((p) => ({
|
|
15
|
+
value: p.id,
|
|
16
|
+
label: p.id,
|
|
17
|
+
description: p.name,
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function allProcessCompletions(manager: ProcessManager) {
|
|
23
|
+
return (prefix: string) => {
|
|
24
|
+
const processes = manager.list();
|
|
25
|
+
const lower = prefix.toLowerCase();
|
|
26
|
+
return processes
|
|
27
|
+
.filter(
|
|
28
|
+
(p) =>
|
|
29
|
+
p.id.toLowerCase().startsWith(lower) ||
|
|
30
|
+
p.name.toLowerCase().startsWith(lower),
|
|
31
|
+
)
|
|
32
|
+
.map((p) => ({
|
|
33
|
+
value: p.id,
|
|
34
|
+
label: p.id,
|
|
35
|
+
description: p.name,
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { DockActions } from "../../hooks/widget";
|
|
3
|
+
|
|
4
|
+
export function registerPsDockCommand(
|
|
5
|
+
pi: ExtensionAPI,
|
|
6
|
+
dockActions: DockActions,
|
|
7
|
+
): void {
|
|
8
|
+
pi.registerCommand("ps:dock", {
|
|
9
|
+
description: "Control dock visibility",
|
|
10
|
+
getArgumentCompletions: () => [
|
|
11
|
+
{ value: "show", label: "show — make the dock visible" },
|
|
12
|
+
{ value: "hide", label: "hide — hide the dock" },
|
|
13
|
+
{ value: "toggle", label: "toggle — cycle visibility" },
|
|
14
|
+
],
|
|
15
|
+
handler: async (args, _ctx) => {
|
|
16
|
+
const arg = args.trim().toLowerCase();
|
|
17
|
+
|
|
18
|
+
if (arg === "show") {
|
|
19
|
+
dockActions.expand();
|
|
20
|
+
} else if (arg === "hide") {
|
|
21
|
+
dockActions.hide();
|
|
22
|
+
} else if (arg === "toggle" || arg === "") {
|
|
23
|
+
dockActions.toggle();
|
|
24
|
+
} else {
|
|
25
|
+
dockActions.toggle();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerPsDockCommand } from "./command";
|
package/src/commands/index.ts
CHANGED
|
@@ -1,334 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Process commands with /ps: prefix.
|
|
3
|
+
*
|
|
4
|
+
* /ps - View and manage all background processes
|
|
5
|
+
* /ps:logs [id] - Open log viewer overlay (search, scroll, stream filter)
|
|
6
|
+
* /ps:pin [id] - Pin the dock to a specific process
|
|
7
|
+
* /ps:kill [id] - Kill a running process
|
|
8
|
+
* /ps:clear - Remove all finished processes from the list
|
|
9
|
+
* /ps:dock [show|hide|toggle] - Control dock visibility
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { DockActions } from "../hooks/widget";
|
|
10
14
|
import type { ProcessManager } from "../manager";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const lower = prefix.toLowerCase();
|
|
18
|
-
return processes
|
|
19
|
-
.filter(
|
|
20
|
-
(p) =>
|
|
21
|
-
LIVE_STATUSES.has(p.status) &&
|
|
22
|
-
(p.id.toLowerCase().startsWith(lower) ||
|
|
23
|
-
p.name.toLowerCase().startsWith(lower)),
|
|
24
|
-
)
|
|
25
|
-
.map((p) => ({
|
|
26
|
-
value: p.id,
|
|
27
|
-
label: p.id,
|
|
28
|
-
description: p.name,
|
|
29
|
-
}));
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function allProcessCompletions(manager: ProcessManager) {
|
|
34
|
-
return (prefix: string) => {
|
|
35
|
-
const processes = manager.list();
|
|
36
|
-
const lower = prefix.toLowerCase();
|
|
37
|
-
return processes
|
|
38
|
-
.filter(
|
|
39
|
-
(p) =>
|
|
40
|
-
p.id.toLowerCase().startsWith(lower) ||
|
|
41
|
-
p.name.toLowerCase().startsWith(lower),
|
|
42
|
-
)
|
|
43
|
-
.map((p) => ({
|
|
44
|
-
value: p.id,
|
|
45
|
-
label: p.id,
|
|
46
|
-
description: p.name,
|
|
47
|
-
}));
|
|
48
|
-
};
|
|
49
|
-
}
|
|
15
|
+
import { registerPsClearCommand } from "./clear";
|
|
16
|
+
import { registerPsDockCommand } from "./dock";
|
|
17
|
+
import { registerPsKillCommand } from "./kill";
|
|
18
|
+
import { registerPsLogsCommand } from "./logs";
|
|
19
|
+
import { registerPsPinCommand } from "./pin";
|
|
20
|
+
import { registerPsCommand } from "./processes";
|
|
50
21
|
|
|
51
22
|
export function setupProcessesCommands(
|
|
52
23
|
pi: ExtensionAPI,
|
|
53
24
|
manager: ProcessManager,
|
|
25
|
+
dockActions: DockActions,
|
|
54
26
|
): void {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
pi
|
|
60
|
-
|
|
61
|
-
handler: async (_args, ctx) => {
|
|
62
|
-
if (!ctx.hasUI) {
|
|
63
|
-
ctx.ui.notify("/process:list requires interactive mode", "error");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// If currently streaming, dismiss the stream widget and show the list.
|
|
68
|
-
if (streamingProcessId) {
|
|
69
|
-
ctx.ui.setWidget(LOG_STREAM_WIDGET_ID, undefined);
|
|
70
|
-
streamingProcessId = null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const result = await ctx.ui.custom<string | null>(
|
|
74
|
-
(tui, theme, _keybindings, done) => {
|
|
75
|
-
return new ProcessesComponent(
|
|
76
|
-
tui,
|
|
77
|
-
theme,
|
|
78
|
-
(processId?: string) => {
|
|
79
|
-
if (processId) {
|
|
80
|
-
done(processId);
|
|
81
|
-
} else {
|
|
82
|
-
done(null);
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
manager,
|
|
86
|
-
);
|
|
87
|
-
},
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// RPC fallback.
|
|
91
|
-
if (result === undefined) {
|
|
92
|
-
ctx.ui.notify("/process:list requires interactive mode", "info");
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// User dismissed with Escape/q.
|
|
97
|
-
if (result === null) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// User selected a process — start streaming its logs.
|
|
102
|
-
startStreaming(ctx.ui, manager, result);
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ── /process:stream [id|name] ──────────────────────────────────────
|
|
107
|
-
pi.registerCommand("process:stream", {
|
|
108
|
-
description: "Stream logs from a running process",
|
|
109
|
-
getArgumentCompletions: runningProcessCompletions(manager),
|
|
110
|
-
handler: async (args, ctx) => {
|
|
111
|
-
const arg = args.trim();
|
|
112
|
-
|
|
113
|
-
// Explicit argument: stream that process.
|
|
114
|
-
if (arg) {
|
|
115
|
-
const proc = manager.find(arg);
|
|
116
|
-
if (!proc) {
|
|
117
|
-
ctx.ui.notify(`Process not found: ${arg}`, "error");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (!LIVE_STATUSES.has(proc.status)) {
|
|
121
|
-
ctx.ui.notify(
|
|
122
|
-
`${proc.name} (${proc.id}) is not running (${proc.status})`,
|
|
123
|
-
"info",
|
|
124
|
-
);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
startStreaming(ctx.ui, manager, proc.id);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// No argument + currently streaming: dismiss.
|
|
132
|
-
if (streamingProcessId) {
|
|
133
|
-
ctx.ui.setWidget(LOG_STREAM_WIDGET_ID, undefined);
|
|
134
|
-
streamingProcessId = null;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// No argument + not streaming: pick from running processes.
|
|
139
|
-
const running = manager.list().filter((p) => LIVE_STATUSES.has(p.status));
|
|
140
|
-
|
|
141
|
-
// No running processes.
|
|
142
|
-
if (running.length === 0) {
|
|
143
|
-
ctx.ui.notify("No running processes", "info");
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Single running process: auto-select.
|
|
148
|
-
if (running.length === 1 && running[0]) {
|
|
149
|
-
startStreaming(ctx.ui, manager, running[0].id);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Multiple running processes: show picker.
|
|
154
|
-
const processId = await pickProcess(
|
|
155
|
-
ctx,
|
|
156
|
-
manager,
|
|
157
|
-
"Select process to stream",
|
|
158
|
-
(p) => LIVE_STATUSES.has(p.status),
|
|
159
|
-
);
|
|
160
|
-
if (!processId) return;
|
|
161
|
-
startStreaming(ctx.ui, manager, processId);
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ── /process:logs [id|name] ────────────────────────────────────────
|
|
166
|
-
pi.registerCommand("process:logs", {
|
|
167
|
-
description: "Show log file paths for a process",
|
|
168
|
-
getArgumentCompletions: allProcessCompletions(manager),
|
|
169
|
-
handler: async (args, ctx) => {
|
|
170
|
-
const arg = args.trim();
|
|
171
|
-
|
|
172
|
-
let processId: string | undefined;
|
|
173
|
-
|
|
174
|
-
if (arg) {
|
|
175
|
-
const proc = manager.find(arg);
|
|
176
|
-
if (!proc) {
|
|
177
|
-
ctx.ui.notify(`Process not found: ${arg}`, "error");
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
processId = proc.id;
|
|
181
|
-
} else {
|
|
182
|
-
// No argument: show picker.
|
|
183
|
-
processId = await pickProcess(ctx, manager, "Select process for logs");
|
|
184
|
-
if (!processId) return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const logFiles = manager.getLogFiles(processId);
|
|
188
|
-
const proc = manager.get(processId);
|
|
189
|
-
if (!logFiles || !proc) {
|
|
190
|
-
ctx.ui.notify(`Process not found: ${processId}`, "error");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
ctx.ui.notify(
|
|
195
|
-
`${proc.name} (${proc.id})\nstdout: ${logFiles.stdoutFile}\nstderr: ${logFiles.stderrFile}`,
|
|
196
|
-
"info",
|
|
197
|
-
);
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// ── /process:kill [id|name] ────────────────────────────────────────
|
|
202
|
-
pi.registerCommand("process:kill", {
|
|
203
|
-
description: "Kill a running background process",
|
|
204
|
-
getArgumentCompletions: runningProcessCompletions(manager),
|
|
205
|
-
handler: async (args, ctx) => {
|
|
206
|
-
const arg = args.trim();
|
|
207
|
-
|
|
208
|
-
let processId: string | undefined;
|
|
209
|
-
|
|
210
|
-
if (arg) {
|
|
211
|
-
const proc = manager.find(arg);
|
|
212
|
-
if (!proc) {
|
|
213
|
-
ctx.ui.notify(`Process not found: ${arg}`, "error");
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (!LIVE_STATUSES.has(proc.status)) {
|
|
217
|
-
ctx.ui.notify(
|
|
218
|
-
`${proc.name} (${proc.id}) is not running (${proc.status})`,
|
|
219
|
-
"info",
|
|
220
|
-
);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
processId = proc.id;
|
|
224
|
-
} else {
|
|
225
|
-
// No argument: show picker (only running processes).
|
|
226
|
-
const running = manager
|
|
227
|
-
.list()
|
|
228
|
-
.filter((p) => LIVE_STATUSES.has(p.status));
|
|
229
|
-
|
|
230
|
-
if (running.length === 0) {
|
|
231
|
-
ctx.ui.notify("No running processes to kill", "info");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (running.length === 1 && running[0]) {
|
|
236
|
-
processId = running[0].id;
|
|
237
|
-
} else {
|
|
238
|
-
processId = await pickProcess(
|
|
239
|
-
ctx,
|
|
240
|
-
manager,
|
|
241
|
-
"Select process to kill",
|
|
242
|
-
(p) => LIVE_STATUSES.has(p.status),
|
|
243
|
-
);
|
|
244
|
-
if (!processId) return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const proc = manager.get(processId);
|
|
249
|
-
if (!proc) {
|
|
250
|
-
ctx.ui.notify(`Process not found: ${processId}`, "error");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const signal =
|
|
255
|
-
proc.status === "terminate_timeout" ? "SIGKILL" : "SIGTERM";
|
|
256
|
-
const timeoutMs = signal === "SIGKILL" ? 200 : 3000;
|
|
257
|
-
const result = await manager.kill(processId, { signal, timeoutMs });
|
|
258
|
-
|
|
259
|
-
if (result.ok) {
|
|
260
|
-
ctx.ui.notify(`Killed ${proc.name} (${proc.id})`, "info");
|
|
261
|
-
} else {
|
|
262
|
-
ctx.ui.notify(
|
|
263
|
-
`Failed to kill ${proc.name} (${proc.id}): ${result.reason}`,
|
|
264
|
-
"error",
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// ── /process:clear ─────────────────────────────────────────────────
|
|
271
|
-
pi.registerCommand("process:clear", {
|
|
272
|
-
description: "Clear finished processes",
|
|
273
|
-
handler: async (_args, ctx) => {
|
|
274
|
-
const cleared = manager.clearFinished();
|
|
275
|
-
if (cleared > 0) {
|
|
276
|
-
ctx.ui.notify(
|
|
277
|
-
`Cleared ${cleared} finished process${cleared > 1 ? "es" : ""}`,
|
|
278
|
-
"info",
|
|
279
|
-
);
|
|
280
|
-
} else {
|
|
281
|
-
ctx.ui.notify("No finished processes to clear", "info");
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
function startStreaming(
|
|
289
|
-
ui: ExtensionCommandContext["ui"],
|
|
290
|
-
mgr: ProcessManager,
|
|
291
|
-
processId: string,
|
|
292
|
-
) {
|
|
293
|
-
streamingProcessId = processId;
|
|
294
|
-
ui.setWidget(
|
|
295
|
-
LOG_STREAM_WIDGET_ID,
|
|
296
|
-
(tui: { requestRender: () => void }, theme: Theme) => {
|
|
297
|
-
return new LogStreamComponent(tui, theme, mgr, processId);
|
|
298
|
-
},
|
|
299
|
-
{ placement: "aboveEditor" },
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function pickProcess(
|
|
305
|
-
ctx: ExtensionCommandContext,
|
|
306
|
-
manager: ProcessManager,
|
|
307
|
-
title: string,
|
|
308
|
-
filter?: (proc: ProcessInfo) => boolean,
|
|
309
|
-
): Promise<string | undefined> {
|
|
310
|
-
if (!ctx.hasUI) {
|
|
311
|
-
ctx.ui.notify("Interactive mode required", "error");
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
316
|
-
return new ProcessPickerComponent(
|
|
317
|
-
tui,
|
|
318
|
-
theme,
|
|
319
|
-
(processId?: string) => {
|
|
320
|
-
done(processId ?? null);
|
|
321
|
-
},
|
|
322
|
-
manager,
|
|
323
|
-
title,
|
|
324
|
-
filter,
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// RPC fallback or user cancelled.
|
|
329
|
-
if (result === undefined || result === null) {
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return result;
|
|
27
|
+
registerPsCommand(pi, manager, dockActions);
|
|
28
|
+
registerPsPinCommand(pi, manager, dockActions);
|
|
29
|
+
registerPsLogsCommand(pi, manager);
|
|
30
|
+
registerPsKillCommand(pi, manager, dockActions);
|
|
31
|
+
registerPsClearCommand(pi, manager);
|
|
32
|
+
registerPsDockCommand(pi, dockActions);
|
|
334
33
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { LIVE_STATUSES } from "../../constants";
|
|
3
|
+
import type { DockActions } from "../../hooks/widget";
|
|
4
|
+
import type { ProcessManager } from "../../manager";
|
|
5
|
+
import { runningProcessCompletions } from "../completions";
|
|
6
|
+
import { pickProcess } from "../pick-process";
|
|
7
|
+
|
|
8
|
+
export function registerPsKillCommand(
|
|
9
|
+
pi: ExtensionAPI,
|
|
10
|
+
manager: ProcessManager,
|
|
11
|
+
dockActions: DockActions,
|
|
12
|
+
): void {
|
|
13
|
+
pi.registerCommand("ps:kill", {
|
|
14
|
+
description: "Kill a running process",
|
|
15
|
+
getArgumentCompletions: runningProcessCompletions(manager),
|
|
16
|
+
handler: async (args, ctx) => {
|
|
17
|
+
const arg = args.trim();
|
|
18
|
+
|
|
19
|
+
let processId: string | undefined;
|
|
20
|
+
|
|
21
|
+
if (arg) {
|
|
22
|
+
const proc = manager.find(arg);
|
|
23
|
+
if (!proc) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!LIVE_STATUSES.has(proc.status)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
processId = proc.id;
|
|
30
|
+
} else {
|
|
31
|
+
const running = manager
|
|
32
|
+
.list()
|
|
33
|
+
.filter((p) => LIVE_STATUSES.has(p.status));
|
|
34
|
+
|
|
35
|
+
if (running.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (running.length === 1 && running[0]) {
|
|
40
|
+
processId = running[0].id;
|
|
41
|
+
} else {
|
|
42
|
+
processId = await pickProcess(
|
|
43
|
+
ctx,
|
|
44
|
+
manager,
|
|
45
|
+
"Select process to kill",
|
|
46
|
+
(p) => LIVE_STATUSES.has(p.status),
|
|
47
|
+
);
|
|
48
|
+
if (!processId) return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const proc = manager.get(processId);
|
|
53
|
+
if (!proc) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const signal =
|
|
58
|
+
proc.status === "terminate_timeout" ? "SIGKILL" : "SIGTERM";
|
|
59
|
+
const timeoutMs = signal === "SIGKILL" ? 200 : 3000;
|
|
60
|
+
const result = await manager.kill(processId, { signal, timeoutMs });
|
|
61
|
+
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
if (dockActions.getFocusedProcessId() === processId) {
|
|
64
|
+
dockActions.setFocus(null);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|