@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.
Files changed (51) hide show
  1. package/README.md +83 -13
  2. package/package.json +10 -4
  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/process-picker-component.ts +14 -2
  26. package/src/components/processes-component.ts +41 -21
  27. package/src/config.ts +38 -1
  28. package/src/constants/index.ts +1 -0
  29. package/src/constants/types.ts +7 -1
  30. package/src/hooks/background-blocker.ts +66 -0
  31. package/src/hooks/index.ts +15 -3
  32. package/src/hooks/process-end.ts +3 -30
  33. package/src/hooks/widget/index.ts +2 -0
  34. package/src/hooks/widget/setup.ts +168 -0
  35. package/src/hooks/{widget.ts → widget/status-widget.ts} +27 -68
  36. package/src/hooks/widget/types.ts +21 -0
  37. package/src/index.ts +9 -3
  38. package/src/manager.ts +61 -9
  39. package/src/tools/actions/index.ts +5 -0
  40. package/src/tools/actions/write.ts +87 -0
  41. package/src/tools/index.ts +82 -14
  42. package/src/utils/command-executor.test.ts +2 -1
  43. package/src/utils/command-executor.ts +1 -1
  44. package/src/utils/keybindings.ts +76 -0
  45. package/src/utils/shell-utils.ts +133 -0
  46. package/src/commands/settings-command.ts +0 -157
  47. package/src/components/log-stream-component.ts +0 -149
  48. package/src/test/test-exit-crash.sh +0 -19
  49. package/src/test/test-exit-failure.sh +0 -17
  50. package/src/test/test-exit-success.sh +0 -16
  51. 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: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
- 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: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
- - [ ] **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).
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.4.7",
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.4.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";
@@ -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
+ }