@aliou/pi-processes 0.5.0 → 0.6.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.
Files changed (47) hide show
  1. package/README.md +82 -13
  2. package/package.json +2 -2
  3. package/src/commands/clear/command.ts +14 -0
  4. package/src/commands/clear/index.ts +1 -0
  5. package/src/commands/completions.ts +38 -0
  6. package/src/commands/dock/command.ts +29 -0
  7. package/src/commands/dock/index.ts +1 -0
  8. package/src/commands/index.ts +26 -327
  9. package/src/commands/kill/command.ts +69 -0
  10. package/src/commands/kill/index.ts +1 -0
  11. package/src/commands/logs/command.ts +47 -0
  12. package/src/commands/logs/index.ts +1 -0
  13. package/src/commands/pick-process.ts +34 -0
  14. package/src/commands/pin/command.ts +33 -0
  15. package/src/commands/pin/index.ts +1 -0
  16. package/src/commands/processes/command.ts +39 -0
  17. package/src/commands/processes/index.ts +1 -0
  18. package/src/commands/settings/apply-setting-change.ts +72 -0
  19. package/src/commands/settings/build-sections.ts +160 -0
  20. package/src/commands/settings/command.ts +20 -0
  21. package/src/commands/settings/index.ts +1 -0
  22. package/src/components/log-dock-component.ts +238 -0
  23. package/src/components/log-file-viewer.ts +317 -0
  24. package/src/components/log-overlay-component.ts +555 -0
  25. package/src/components/processes-component.ts +0 -1
  26. package/src/config.ts +28 -1
  27. package/src/constants/index.ts +1 -0
  28. package/src/constants/types.ts +7 -1
  29. package/src/hooks/index.ts +5 -3
  30. package/src/hooks/process-end.ts +3 -30
  31. package/src/hooks/widget/index.ts +2 -0
  32. package/src/hooks/widget/setup.ts +164 -0
  33. package/src/hooks/{widget.ts → widget/status-widget.ts} +7 -74
  34. package/src/hooks/widget/types.ts +20 -0
  35. package/src/index.ts +8 -3
  36. package/src/manager.ts +61 -9
  37. package/src/tools/actions/index.ts +5 -0
  38. package/src/tools/actions/write.ts +87 -0
  39. package/src/tools/index.ts +82 -14
  40. package/src/utils/command-executor.ts +1 -1
  41. package/src/utils/keybindings.ts +71 -0
  42. package/src/commands/settings-command.ts +0 -179
  43. package/src/components/log-stream-component.ts +0 -149
  44. package/src/test/test-exit-crash.sh +0 -19
  45. package/src/test/test-exit-failure.sh +0 -17
  46. package/src/test/test-exit-success.sh +0 -16
  47. 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**: `/process:list` (interactive panel), `/process:stream` (stream logs), `/process:logs` (log paths), `/process:kill` (kill process), `/process:clear` (clear finished)
24
+ - **Commands**: `/ps` (interactive panel), `/ps:pin` (pin dock to process), `/ps:logs` (open log overlay), `/ps:kill` (kill process), `/ps:clear` (clear finished), `/ps:dock` (control dock visibility), `/ps:settings`
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,84 @@ Note: User always sees process updates in the UI. The alert flags control whethe
52
55
 
53
56
  ### Commands (interactive)
54
57
 
55
- Run `/process:list` to open the panel:
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` - stream logs for selected process
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
- Other commands:
64
- - `/process:stream [id|name]` - stream logs from a running process
65
- - `/process:logs [id|name]` - show log file paths
66
- - `/process:kill [id|name]` - kill a running process
67
- - `/process:clear` - clear finished processes
68
+ #### `/ps:pin [id|name]` - Pin dock to a process
69
+
70
+ Pin the dock to a specific process. Opens the dock automatically if hidden.
71
+
72
+ Without arguments, shows a picker to select a process.
73
+
74
+ #### `/ps:logs [id|name]` - Open log overlay
75
+
76
+ Open the interactive log viewer overlay (search, scroll, stream filter).
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 [show|hide|toggle]` - Control dock
87
+
88
+ Control dock visibility. Without arguments, it toggles state. With arguments:
89
+ - `show` - Show dock (open)
90
+ - `hide` - Hide dock
91
+ - `toggle` - Cycle dock visibility state
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
+ - Follow mode toggle is only available via settings (`/ps:settings`)
112
+
113
+ **Dock States:**
114
+ - `collapsed` (1-2 lines): Summary + last log line
115
+ - `open` (8-12 lines): Full interleaved or focused logs
116
+
117
+ ### Deprecated Commands (backward compatible)
118
+
119
+ The following commands are deprecated but still work with a warning:
120
+ - `/process:list` → Use `/ps` instead
121
+ - `/process:stream` → Use `/ps:pin` instead
122
+ - `/process:logs` → Use `/ps:logs` instead
123
+ - `/process:kill` → Use `/ps:kill` instead
124
+ - `/process:clear` → Use `/ps:clear` instead
125
+
126
+ ## Settings
127
+
128
+ Configure via `/ps:settings` or `~/.pi/agent/extensions/processes.json`:
129
+
130
+ - **Widget**: Dock default state, dock height
131
+ - **Follow Mode**: Enable by default, auto-hide on finish
132
+ - **Process List**: Max visible processes, max preview lines
133
+ - **Output Limits**: Default tail lines, max output lines
134
+ - **Execution**: Shell path override
135
+ - **Interception**: Block background commands
68
136
 
69
137
  ## Test Scripts
70
138
 
@@ -79,8 +147,9 @@ Test scripts in `src/test/` directory:
79
147
 
80
148
  ## Future Improvements
81
149
 
82
- - [ ] **Expandable log view**: Allow toggling between collapsed (current fixed height) and expanded (full height) log view in the `/process:list` panel.
83
-
84
- - [ ] **Copy log file path**: Add keyboard shortcut to copy the stdout/stderr log file path to clipboard for easy access.
85
-
86
- - [ ] **Open logs in editor**: Add keyboard shortcut to open log files directly in the configured editor (`$EDITOR` or VS Code).
150
+ - [ ] **Configurable keybindings UI**: Allow editing keybindings in settings panel
151
+ - [ ] **Process filtering**: Filter by name/status in full panel
152
+ - [ ] **Log search**: Search within logs (Ctrl+F in dock)
153
+ - [ ] **Copy log path**: Keyboard shortcut to copy log file path
154
+ - [ ] **Open in editor**: Keyboard shortcut to open logs in $EDITOR
155
+ - [ ] **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.5.0",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -28,7 +28,7 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@aliou/pi-utils-settings": "^0.4.0",
31
+ "@aliou/pi-utils-settings": "^0.6.0",
32
32
  "@aliou/pi-utils-ui": "^0.1.0",
33
33
  "@aliou/sh": "^0.1.0",
34
34
  "@sinclair/typebox": "^0.34.41"
@@ -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";
@@ -1,334 +1,33 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- Theme,
5
- } from "@mariozechner/pi-coding-agent";
6
- import { LogStreamComponent } from "../components/log-stream-component";
7
- import { ProcessPickerComponent } from "../components/process-picker-component";
8
- import { ProcessesComponent } from "../components/processes-component";
9
- import { LIVE_STATUSES, type ProcessInfo } from "../constants";
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
- const LOG_STREAM_WIDGET_ID = "processes-log-stream";
13
-
14
- function runningProcessCompletions(manager: ProcessManager) {
15
- return (prefix: string) => {
16
- const processes = manager.list();
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
- let streamingProcessId: string | null = null;
56
-
57
- // ── /process:list ──────────────────────────────────────────────────
58
- // Registered first so it appears first in autocomplete.
59
- pi.registerCommand("process:list", {
60
- description: "View and manage background processes",
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
+ }
@@ -0,0 +1 @@
1
+ export { registerPsKillCommand } from "./command";