@aliou/pi-processes 0.4.5 → 0.4.7
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 +30 -23
- package/package.json +6 -1
- package/src/commands/index.ts +334 -0
- package/src/commands/settings-command.ts +157 -0
- package/src/components/log-stream-component.ts +149 -0
- package/src/components/process-picker-component.ts +155 -0
- package/src/components/processes-component.ts +450 -0
- package/src/components/status-format.ts +38 -0
- package/src/config.ts +70 -0
- package/src/constants/index.ts +11 -0
- package/src/constants/types.ts +65 -0
- package/src/hooks/cleanup.ts +10 -0
- package/src/hooks/index.ts +18 -0
- package/src/hooks/message-renderer.ts +83 -0
- package/src/hooks/process-end.ts +92 -0
- package/src/hooks/widget.ts +146 -0
- package/src/index.ts +22 -20
- package/src/manager.ts +522 -0
- package/src/test/test-exit-crash.sh +19 -0
- package/src/test/test-exit-failure.sh +17 -0
- package/src/test/test-exit-success.sh +16 -0
- package/src/test/test-output.sh +28 -0
- package/src/tools/actions/clear.ts +20 -0
- package/src/tools/actions/index.ts +49 -0
- package/src/tools/actions/kill.ts +76 -0
- package/src/tools/actions/list.ts +37 -0
- package/src/tools/actions/logs.ts +59 -0
- package/src/tools/actions/output.ts +144 -0
- package/src/tools/actions/start.ts +55 -0
- package/src/tools/index.ts +280 -0
- package/src/utils/ansi.ts +36 -0
- package/src/utils/command-executor.test.ts +47 -0
- package/src/utils/command-executor.ts +56 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/process-group.ts +22 -0
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ pi install git:github.com/aliou/pi-processes
|
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
|
-
- **Tool**: `
|
|
24
|
-
- **
|
|
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)
|
|
25
25
|
- Auto-cleanup on session exit
|
|
26
26
|
- File-based logging (logs written to temp files, not memory)
|
|
27
27
|
- Friendly process names (auto-inferred or custom)
|
|
@@ -31,48 +31,55 @@ pi install git:github.com/aliou/pi-processes
|
|
|
31
31
|
### Tool (for agent)
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
process start "pnpm dev" name="backend-dev"
|
|
35
|
+
process start "pnpm build" name="build" alertOnSuccess=true
|
|
36
|
+
process start "pnpm test" alertOnFailure=true
|
|
37
|
+
process list
|
|
38
|
+
process output id="backend"
|
|
39
|
+
process logs id="proc_1"
|
|
40
|
+
process kill id="backend"
|
|
41
|
+
process clear
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**
|
|
45
|
-
- `
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
44
|
+
**Alert parameters** (for `start` action):
|
|
45
|
+
- `alertOnSuccess` (default: false) - Get a turn to react when process completes successfully. Use for builds/tests where you need confirmation.
|
|
46
|
+
- `alertOnFailure` (default: true) - Get a turn to react when process fails/crashes. Use to be alerted of unexpected failures.
|
|
47
|
+
- `alertOnKill` (default: false) - Get a turn to react if killed by external signal. Note: killing via tool never triggers a turn.
|
|
48
48
|
|
|
49
49
|
**Important:** You don't need to poll or wait for processes. Notifications arrive automatically based on your preferences. Start processes and continue with other work - you'll be informed if something requires attention.
|
|
50
50
|
|
|
51
|
-
Note: User always sees
|
|
51
|
+
Note: User always sees process updates in the UI. The alert flags control whether the agent gets a turn to react (e.g. check results, fix code, restart).
|
|
52
52
|
|
|
53
|
-
###
|
|
53
|
+
### Commands (interactive)
|
|
54
54
|
|
|
55
|
-
Run `/
|
|
55
|
+
Run `/process:list` to open the panel:
|
|
56
56
|
- `j/k` - select process
|
|
57
57
|
- `J/K` - scroll logs
|
|
58
|
+
- `enter` - stream logs for selected process
|
|
58
59
|
- `x` - kill selected process
|
|
59
60
|
- `c` - clear finished processes
|
|
60
61
|
- `q` - quit
|
|
61
62
|
|
|
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
|
+
|
|
62
69
|
## Test Scripts
|
|
63
70
|
|
|
64
|
-
Test scripts in `test/` directory:
|
|
71
|
+
Test scripts in `src/test/` directory:
|
|
65
72
|
|
|
66
73
|
```bash
|
|
67
|
-
./test/test-output.sh # Continuous output (80 chars/sec)
|
|
68
|
-
./test/test-exit-success.sh 5 # Exits successfully after 5s
|
|
69
|
-
./test/test-exit-failure.sh 5 # Exits with code 1 after 5s
|
|
70
|
-
./test/test-exit-crash.sh 5 # Exits with code 137 after 5s
|
|
74
|
+
./src/test/test-output.sh # Continuous output (80 chars/sec)
|
|
75
|
+
./src/test/test-exit-success.sh 5 # Exits successfully after 5s
|
|
76
|
+
./src/test/test-exit-failure.sh 5 # Exits with code 1 after 5s
|
|
77
|
+
./src/test/test-exit-crash.sh 5 # Exits with code 137 after 5s
|
|
71
78
|
```
|
|
72
79
|
|
|
73
80
|
## Future Improvements
|
|
74
81
|
|
|
75
|
-
- [ ] **Expandable log view**: Allow toggling between collapsed (current fixed height) and expanded (full height) log view in the `/
|
|
82
|
+
- [ ] **Expandable log view**: Allow toggling between collapsed (current fixed height) and expanded (full height) log view in the `/process:list` panel.
|
|
76
83
|
|
|
77
84
|
- [ ] **Copy log file path**: Add keyboard shortcut to copy the stdout/stderr log file path to clipboard for easy access.
|
|
78
85
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-processes",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
48
|
"vitest": "^4.0.18"
|
|
49
49
|
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@mariozechner/pi-coding-agent": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
50
55
|
"scripts": {
|
|
51
56
|
"typecheck": "tsc --noEmit",
|
|
52
57
|
"lint": "biome check",
|
|
@@ -0,0 +1,334 @@
|
|
|
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";
|
|
10
|
+
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
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setupProcessesCommands(
|
|
52
|
+
pi: ExtensionAPI,
|
|
53
|
+
manager: ProcessManager,
|
|
54
|
+
): 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;
|
|
334
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerSettingsCommand,
|
|
3
|
+
type SettingsSection,
|
|
4
|
+
} from "@aliou/pi-utils-settings";
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type { ProcessesConfig, ResolvedProcessesConfig } from "../config";
|
|
7
|
+
import { configLoader } from "../config";
|
|
8
|
+
|
|
9
|
+
export function registerProcessesSettings(
|
|
10
|
+
pi: ExtensionAPI,
|
|
11
|
+
onSave?: () => void,
|
|
12
|
+
): void {
|
|
13
|
+
registerSettingsCommand<ProcessesConfig, ResolvedProcessesConfig>(pi, {
|
|
14
|
+
commandName: "process:settings",
|
|
15
|
+
title: "Processes Settings",
|
|
16
|
+
configStore: configLoader,
|
|
17
|
+
buildSections: (
|
|
18
|
+
tabConfig: ProcessesConfig | null,
|
|
19
|
+
resolved: ResolvedProcessesConfig,
|
|
20
|
+
): SettingsSection[] => {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
label: "Process List",
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
id: "processList.maxVisibleProcesses",
|
|
27
|
+
label: "Max visible processes",
|
|
28
|
+
description:
|
|
29
|
+
"Maximum processes shown in the /processes list before scrolling",
|
|
30
|
+
currentValue: String(
|
|
31
|
+
tabConfig?.processList?.maxVisibleProcesses ??
|
|
32
|
+
resolved.processList.maxVisibleProcesses,
|
|
33
|
+
),
|
|
34
|
+
values: ["4", "6", "8", "12", "16"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "processList.maxPreviewLines",
|
|
38
|
+
label: "Max preview lines",
|
|
39
|
+
description: "Log preview lines shown below the selected process",
|
|
40
|
+
currentValue: String(
|
|
41
|
+
tabConfig?.processList?.maxPreviewLines ??
|
|
42
|
+
resolved.processList.maxPreviewLines,
|
|
43
|
+
),
|
|
44
|
+
values: ["6", "8", "12", "16", "24"],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Output Limits",
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
id: "output.defaultTailLines",
|
|
53
|
+
label: "Default tail lines",
|
|
54
|
+
description:
|
|
55
|
+
"Number of tail lines returned to the agent by default",
|
|
56
|
+
currentValue: String(
|
|
57
|
+
tabConfig?.output?.defaultTailLines ??
|
|
58
|
+
resolved.output.defaultTailLines,
|
|
59
|
+
),
|
|
60
|
+
values: ["50", "100", "200", "500"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "output.maxOutputLines",
|
|
64
|
+
label: "Max output lines",
|
|
65
|
+
description: "Hard cap on output lines returned to the agent",
|
|
66
|
+
currentValue: String(
|
|
67
|
+
tabConfig?.output?.maxOutputLines ??
|
|
68
|
+
resolved.output.maxOutputLines,
|
|
69
|
+
),
|
|
70
|
+
values: ["100", "200", "500", "1000"],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Execution",
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
id: "execution.shellPath",
|
|
79
|
+
label: "Shell path",
|
|
80
|
+
description:
|
|
81
|
+
"Absolute shell path override used to execute commands",
|
|
82
|
+
currentValue:
|
|
83
|
+
tabConfig?.execution?.shellPath ??
|
|
84
|
+
resolved.execution.shellPath ??
|
|
85
|
+
"auto",
|
|
86
|
+
values: [
|
|
87
|
+
"auto",
|
|
88
|
+
"/run/current-system/sw/bin/bash",
|
|
89
|
+
"/bin/bash",
|
|
90
|
+
"/usr/bin/bash",
|
|
91
|
+
"/usr/local/bin/bash",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "Widget",
|
|
98
|
+
items: [
|
|
99
|
+
{
|
|
100
|
+
id: "widget.showStatusWidget",
|
|
101
|
+
label: "Show status widget",
|
|
102
|
+
description: "Show process status widget below the editor",
|
|
103
|
+
currentValue:
|
|
104
|
+
(tabConfig?.widget?.showStatusWidget ??
|
|
105
|
+
resolved.widget.showStatusWidget)
|
|
106
|
+
? "on"
|
|
107
|
+
: "off",
|
|
108
|
+
values: ["on", "off"],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
},
|
|
114
|
+
onSettingChange: (id, newValue, config) => {
|
|
115
|
+
const updated = structuredClone(config);
|
|
116
|
+
// Boolean fields.
|
|
117
|
+
if (id === "widget.showStatusWidget") {
|
|
118
|
+
if (!updated.widget) updated.widget = {};
|
|
119
|
+
updated.widget.showStatusWidget = newValue === "on";
|
|
120
|
+
return updated;
|
|
121
|
+
}
|
|
122
|
+
if (id === "execution.shellPath") {
|
|
123
|
+
if (!updated.execution) updated.execution = {};
|
|
124
|
+
updated.execution.shellPath =
|
|
125
|
+
newValue === "auto" ? undefined : newValue;
|
|
126
|
+
return updated;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Numeric fields.
|
|
130
|
+
const num = Number.parseInt(newValue, 10);
|
|
131
|
+
if (Number.isNaN(num)) return null;
|
|
132
|
+
|
|
133
|
+
switch (id) {
|
|
134
|
+
case "processList.maxVisibleProcesses":
|
|
135
|
+
if (!updated.processList) updated.processList = {};
|
|
136
|
+
updated.processList.maxVisibleProcesses = num;
|
|
137
|
+
break;
|
|
138
|
+
case "processList.maxPreviewLines":
|
|
139
|
+
if (!updated.processList) updated.processList = {};
|
|
140
|
+
updated.processList.maxPreviewLines = num;
|
|
141
|
+
break;
|
|
142
|
+
case "output.defaultTailLines":
|
|
143
|
+
if (!updated.output) updated.output = {};
|
|
144
|
+
updated.output.defaultTailLines = num;
|
|
145
|
+
break;
|
|
146
|
+
case "output.maxOutputLines":
|
|
147
|
+
if (!updated.output) updated.output = {};
|
|
148
|
+
updated.output.maxOutputLines = num;
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return updated;
|
|
154
|
+
},
|
|
155
|
+
onSave,
|
|
156
|
+
});
|
|
157
|
+
}
|