@aliou/pi-processes 0.5.0 → 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 +2 -2
- 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/processes-component.ts +0 -1
- package/src/config.ts +28 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/types.ts +7 -1
- package/src/hooks/index.ts +5 -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} +7 -74
- package/src/hooks/widget/types.ts +21 -0
- package/src/index.ts +8 -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.ts +1 -1
- package/src/utils/keybindings.ts +76 -0
- package/src/commands/settings-command.ts +0 -179
- 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/src/tools/index.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { executeAction } from "./actions";
|
|
|
15
15
|
|
|
16
16
|
const ProcessesParams = Type.Object({
|
|
17
17
|
action: StringEnum(
|
|
18
|
-
["start", "list", "output", "logs", "kill", "clear"] as const,
|
|
18
|
+
["start", "list", "output", "logs", "kill", "clear", "write"] as const,
|
|
19
19
|
{
|
|
20
20
|
description:
|
|
21
|
-
"Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished)",
|
|
21
|
+
"Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin)",
|
|
22
22
|
},
|
|
23
23
|
),
|
|
24
24
|
command: Type.Optional(
|
|
@@ -33,7 +33,18 @@ const ProcessesParams = Type.Object({
|
|
|
33
33
|
id: Type.Optional(
|
|
34
34
|
Type.String({
|
|
35
35
|
description:
|
|
36
|
-
"Process ID or name to match (required for output/kill/logs). Can be proc_N or friendly name.",
|
|
36
|
+
"Process ID or name to match (required for output/kill/logs/write). Can be proc_N or friendly name.",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
input: Type.Optional(
|
|
40
|
+
Type.String({
|
|
41
|
+
description: "Data to write to process stdin (required for write action)",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
end: Type.Optional(
|
|
45
|
+
Type.Boolean({
|
|
46
|
+
description:
|
|
47
|
+
"Close stdin after writing (optional for write action, use for programs reading until EOF)",
|
|
37
48
|
}),
|
|
38
49
|
),
|
|
39
50
|
alertOnSuccess: Type.Optional(
|
|
@@ -72,6 +83,7 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
72
83
|
- logs: Get log file paths to inspect with read tool (requires 'id')
|
|
73
84
|
- kill: Terminate a process (requires 'id' - can be proc_N or name match like "backend")
|
|
74
85
|
- clear: Remove all finished processes from the list
|
|
86
|
+
- write: Write to process stdin (requires 'id' and 'input', optional 'end' to close stdin)
|
|
75
87
|
|
|
76
88
|
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.
|
|
77
89
|
|
|
@@ -107,12 +119,20 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
107
119
|
if (
|
|
108
120
|
(args.action === "output" ||
|
|
109
121
|
args.action === "kill" ||
|
|
110
|
-
args.action === "logs"
|
|
122
|
+
args.action === "logs" ||
|
|
123
|
+
args.action === "write") &&
|
|
111
124
|
args.id
|
|
112
125
|
) {
|
|
113
126
|
mainArg = args.id;
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
if (args.action === "write" && args.input) {
|
|
130
|
+
optionArgs.push({ label: "input", value: args.input });
|
|
131
|
+
if (args.end) {
|
|
132
|
+
optionArgs.push({ label: "end", value: "true" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
return new ToolCallHeader(
|
|
117
137
|
{
|
|
118
138
|
toolName: "Process",
|
|
@@ -204,6 +224,23 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
227
|
+
|
|
228
|
+
// Collapsed summary
|
|
229
|
+
const previewSource =
|
|
230
|
+
details.output.stdout.length > 0
|
|
231
|
+
? details.output.stdout
|
|
232
|
+
: details.output.stderr;
|
|
233
|
+
const preview = previewSource
|
|
234
|
+
.slice(-2)
|
|
235
|
+
.map((l) => stripAnsi(l))
|
|
236
|
+
.join("\n");
|
|
237
|
+
fields.push({
|
|
238
|
+
label: "Output",
|
|
239
|
+
value: preview
|
|
240
|
+
? `${theme.fg("muted", preview)}`
|
|
241
|
+
: theme.fg("muted", "(empty)"),
|
|
242
|
+
showCollapsed: true,
|
|
243
|
+
});
|
|
207
244
|
} else if (
|
|
208
245
|
details.action === "list" &&
|
|
209
246
|
details.processes &&
|
|
@@ -243,6 +280,31 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
243
280
|
}
|
|
244
281
|
|
|
245
282
|
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
283
|
+
|
|
284
|
+
// Collapsed summary: first 3 processes
|
|
285
|
+
const summary = details.processes
|
|
286
|
+
.slice(0, 3)
|
|
287
|
+
.map((p) => {
|
|
288
|
+
const s =
|
|
289
|
+
p.status === "running"
|
|
290
|
+
? theme.fg("accent", "running")
|
|
291
|
+
: p.status === "exited" && p.success
|
|
292
|
+
? theme.fg("success", "exit(0)")
|
|
293
|
+
: p.status === "exited"
|
|
294
|
+
? theme.fg("error", `exit(${p.exitCode ?? "?"})`)
|
|
295
|
+
: theme.fg("muted", p.status);
|
|
296
|
+
return `${theme.fg("accent", `"${p.name}"`)} [${s}]`;
|
|
297
|
+
})
|
|
298
|
+
.join(", ");
|
|
299
|
+
const more =
|
|
300
|
+
details.processes.length > 3
|
|
301
|
+
? theme.fg("muted", ` +${details.processes.length - 3} more`)
|
|
302
|
+
: "";
|
|
303
|
+
fields.push({
|
|
304
|
+
label: "Processes",
|
|
305
|
+
value: summary + more,
|
|
306
|
+
showCollapsed: true,
|
|
307
|
+
});
|
|
246
308
|
} else if (details.action === "logs" && details.logFiles) {
|
|
247
309
|
fields.push(
|
|
248
310
|
new Text(
|
|
@@ -263,16 +325,22 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
263
325
|
});
|
|
264
326
|
}
|
|
265
327
|
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
const footerItems: Array<{
|
|
329
|
+
label: string;
|
|
330
|
+
value: string;
|
|
331
|
+
tone: "accent" | "success" | "error" | "warning" | "muted";
|
|
332
|
+
}> = [];
|
|
333
|
+
if (!details.success) {
|
|
334
|
+
footerItems.push({
|
|
335
|
+
label: "status",
|
|
336
|
+
value: "error",
|
|
337
|
+
tone: "error",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const footer =
|
|
341
|
+
footerItems.length > 0
|
|
342
|
+
? new ToolFooter(theme, { items: footerItems })
|
|
343
|
+
: undefined;
|
|
276
344
|
|
|
277
345
|
return new ToolBody({ fields, footer }, options, theme);
|
|
278
346
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcuts configuration for the Process Dock.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProcessesKeybindings {
|
|
6
|
+
/** Toggle dock visibility (global) */
|
|
7
|
+
toggleDock: string;
|
|
8
|
+
/** Scroll logs up */
|
|
9
|
+
scrollUp: string;
|
|
10
|
+
/** Scroll logs down */
|
|
11
|
+
scrollDown: string;
|
|
12
|
+
/** Focus previous process */
|
|
13
|
+
prevProcess: string;
|
|
14
|
+
/** Focus next process */
|
|
15
|
+
nextProcess: string;
|
|
16
|
+
/** Toggle focus mode */
|
|
17
|
+
toggleFocus: string;
|
|
18
|
+
/** Toggle follow mode */
|
|
19
|
+
toggleFollow: string;
|
|
20
|
+
/** Kill focused process */
|
|
21
|
+
killProcess: string;
|
|
22
|
+
/** Clear finished processes */
|
|
23
|
+
clearFinished: string;
|
|
24
|
+
/** Collapse/close dock */
|
|
25
|
+
closeDock: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_KEYBINDINGS: ProcessesKeybindings = {
|
|
29
|
+
toggleDock: "", // Disabled - conflicts with editor shortcuts
|
|
30
|
+
scrollUp: "k",
|
|
31
|
+
scrollDown: "j",
|
|
32
|
+
prevProcess: "h",
|
|
33
|
+
nextProcess: "l",
|
|
34
|
+
toggleFocus: "f",
|
|
35
|
+
toggleFollow: "Shift+F",
|
|
36
|
+
killProcess: "x",
|
|
37
|
+
clearFinished: "c",
|
|
38
|
+
closeDock: "q",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interface for config that may contain keybindings overrides
|
|
43
|
+
*/
|
|
44
|
+
export interface ProcessesConfigKeybindings {
|
|
45
|
+
toggleDock?: string;
|
|
46
|
+
scrollUp?: string;
|
|
47
|
+
scrollDown?: string;
|
|
48
|
+
prevProcess?: string;
|
|
49
|
+
nextProcess?: string;
|
|
50
|
+
toggleFocus?: string;
|
|
51
|
+
toggleFollow?: string;
|
|
52
|
+
killProcess?: string;
|
|
53
|
+
clearFinished?: string;
|
|
54
|
+
closeDock?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load keybindings from config, falling back to defaults.
|
|
59
|
+
*/
|
|
60
|
+
export function loadKeybindings(config: {
|
|
61
|
+
keybindings?: ProcessesConfigKeybindings;
|
|
62
|
+
}): ProcessesKeybindings {
|
|
63
|
+
const user = config.keybindings ?? {};
|
|
64
|
+
return {
|
|
65
|
+
toggleDock: user.toggleDock ?? DEFAULT_KEYBINDINGS.toggleDock,
|
|
66
|
+
scrollUp: user.scrollUp ?? DEFAULT_KEYBINDINGS.scrollUp,
|
|
67
|
+
scrollDown: user.scrollDown ?? DEFAULT_KEYBINDINGS.scrollDown,
|
|
68
|
+
prevProcess: user.prevProcess ?? DEFAULT_KEYBINDINGS.prevProcess,
|
|
69
|
+
nextProcess: user.nextProcess ?? DEFAULT_KEYBINDINGS.nextProcess,
|
|
70
|
+
toggleFocus: user.toggleFocus ?? DEFAULT_KEYBINDINGS.toggleFocus,
|
|
71
|
+
toggleFollow: user.toggleFollow ?? DEFAULT_KEYBINDINGS.toggleFollow,
|
|
72
|
+
killProcess: user.killProcess ?? DEFAULT_KEYBINDINGS.killProcess,
|
|
73
|
+
clearFinished: user.clearFinished ?? DEFAULT_KEYBINDINGS.clearFinished,
|
|
74
|
+
closeDock: user.closeDock ?? DEFAULT_KEYBINDINGS.closeDock,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,179 +0,0 @@
|
|
|
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: "Interception",
|
|
98
|
-
items: [
|
|
99
|
-
{
|
|
100
|
-
id: "interception.blockBackgroundCommands",
|
|
101
|
-
label: "Block background commands",
|
|
102
|
-
description:
|
|
103
|
-
"Block bash background commands (&, nohup, disown, setsid) and guide the model to use the process tool",
|
|
104
|
-
currentValue:
|
|
105
|
-
(tabConfig?.interception?.blockBackgroundCommands ??
|
|
106
|
-
resolved.interception.blockBackgroundCommands)
|
|
107
|
-
? "on"
|
|
108
|
-
: "off",
|
|
109
|
-
values: ["on", "off"],
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
label: "Widget",
|
|
115
|
-
items: [
|
|
116
|
-
{
|
|
117
|
-
id: "widget.showStatusWidget",
|
|
118
|
-
label: "Show status widget",
|
|
119
|
-
description: "Show process status widget below the editor",
|
|
120
|
-
currentValue:
|
|
121
|
-
(tabConfig?.widget?.showStatusWidget ??
|
|
122
|
-
resolved.widget.showStatusWidget)
|
|
123
|
-
? "on"
|
|
124
|
-
: "off",
|
|
125
|
-
values: ["on", "off"],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
];
|
|
130
|
-
},
|
|
131
|
-
onSettingChange: (id, newValue, config) => {
|
|
132
|
-
const updated = structuredClone(config);
|
|
133
|
-
// Boolean fields.
|
|
134
|
-
if (id === "interception.blockBackgroundCommands") {
|
|
135
|
-
if (!updated.interception) updated.interception = {};
|
|
136
|
-
updated.interception.blockBackgroundCommands = newValue === "on";
|
|
137
|
-
return updated;
|
|
138
|
-
}
|
|
139
|
-
if (id === "widget.showStatusWidget") {
|
|
140
|
-
if (!updated.widget) updated.widget = {};
|
|
141
|
-
updated.widget.showStatusWidget = newValue === "on";
|
|
142
|
-
return updated;
|
|
143
|
-
}
|
|
144
|
-
if (id === "execution.shellPath") {
|
|
145
|
-
if (!updated.execution) updated.execution = {};
|
|
146
|
-
updated.execution.shellPath =
|
|
147
|
-
newValue === "auto" ? undefined : newValue;
|
|
148
|
-
return updated;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Numeric fields.
|
|
152
|
-
const num = Number.parseInt(newValue, 10);
|
|
153
|
-
if (Number.isNaN(num)) return null;
|
|
154
|
-
|
|
155
|
-
switch (id) {
|
|
156
|
-
case "processList.maxVisibleProcesses":
|
|
157
|
-
if (!updated.processList) updated.processList = {};
|
|
158
|
-
updated.processList.maxVisibleProcesses = num;
|
|
159
|
-
break;
|
|
160
|
-
case "processList.maxPreviewLines":
|
|
161
|
-
if (!updated.processList) updated.processList = {};
|
|
162
|
-
updated.processList.maxPreviewLines = num;
|
|
163
|
-
break;
|
|
164
|
-
case "output.defaultTailLines":
|
|
165
|
-
if (!updated.output) updated.output = {};
|
|
166
|
-
updated.output.defaultTailLines = num;
|
|
167
|
-
break;
|
|
168
|
-
case "output.maxOutputLines":
|
|
169
|
-
if (!updated.output) updated.output = {};
|
|
170
|
-
updated.output.maxOutputLines = num;
|
|
171
|
-
break;
|
|
172
|
-
default:
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
return updated;
|
|
176
|
-
},
|
|
177
|
-
onSave,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createPanelPadder,
|
|
3
|
-
renderPanelRule,
|
|
4
|
-
renderPanelTitleLine,
|
|
5
|
-
} from "@aliou/pi-utils-ui";
|
|
6
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
type Component,
|
|
9
|
-
truncateToWidth,
|
|
10
|
-
visibleWidth,
|
|
11
|
-
} from "@mariozechner/pi-tui";
|
|
12
|
-
import type { ProcessManager } from "../manager";
|
|
13
|
-
import { stripAnsi } from "../utils";
|
|
14
|
-
import { statusIcon, statusLabel } from "./status-format";
|
|
15
|
-
|
|
16
|
-
const MAX_LOG_LINES = 16;
|
|
17
|
-
const POLL_INTERVAL_MS = 500;
|
|
18
|
-
|
|
19
|
-
export class LogStreamComponent implements Component {
|
|
20
|
-
private tui: { requestRender: () => void };
|
|
21
|
-
private theme: Theme;
|
|
22
|
-
private manager: ProcessManager;
|
|
23
|
-
private processId: string;
|
|
24
|
-
private timer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
-
private unsubscribe: (() => void) | null = null;
|
|
26
|
-
private cachedLines: string[] = [];
|
|
27
|
-
private cachedWidth = 0;
|
|
28
|
-
|
|
29
|
-
constructor(
|
|
30
|
-
tui: { requestRender: () => void },
|
|
31
|
-
theme: Theme,
|
|
32
|
-
manager: ProcessManager,
|
|
33
|
-
processId: string,
|
|
34
|
-
) {
|
|
35
|
-
this.tui = tui;
|
|
36
|
-
this.theme = theme;
|
|
37
|
-
this.manager = manager;
|
|
38
|
-
this.processId = processId;
|
|
39
|
-
|
|
40
|
-
// Poll log file for new output.
|
|
41
|
-
this.timer = setInterval(() => {
|
|
42
|
-
this.invalidate();
|
|
43
|
-
this.tui.requestRender();
|
|
44
|
-
}, POLL_INTERVAL_MS);
|
|
45
|
-
|
|
46
|
-
// Also re-render on process events (status changes, etc.).
|
|
47
|
-
this.unsubscribe = this.manager.onEvent(() => {
|
|
48
|
-
this.invalidate();
|
|
49
|
-
this.tui.requestRender();
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
handleInput(_data: string): boolean {
|
|
54
|
-
// Widget doesn't handle input — the editor is still active.
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
invalidate(): void {
|
|
59
|
-
this.cachedWidth = 0;
|
|
60
|
-
this.cachedLines = [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
render(width: number): string[] {
|
|
64
|
-
if (width === this.cachedWidth && this.cachedLines.length > 0) {
|
|
65
|
-
return this.cachedLines;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const theme = this.theme;
|
|
69
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
70
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
71
|
-
const innerWidth = width - 2;
|
|
72
|
-
|
|
73
|
-
const basePadLine = createPanelPadder(width);
|
|
74
|
-
const padLine = (content: string): string => {
|
|
75
|
-
const contentWidth = visibleWidth(content);
|
|
76
|
-
return basePadLine(
|
|
77
|
-
contentWidth > innerWidth
|
|
78
|
-
? truncateToWidth(content, innerWidth)
|
|
79
|
-
: content,
|
|
80
|
-
);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const lines: string[] = [];
|
|
84
|
-
const proc = this.manager.get(this.processId);
|
|
85
|
-
|
|
86
|
-
if (!proc) {
|
|
87
|
-
lines.push(renderPanelRule(width, theme));
|
|
88
|
-
lines.push(padLine(warning("Process not found")));
|
|
89
|
-
lines.push(renderPanelRule(width, theme));
|
|
90
|
-
this.cachedLines = lines;
|
|
91
|
-
this.cachedWidth = width;
|
|
92
|
-
return this.cachedLines;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Header
|
|
96
|
-
const icon = statusIcon(proc.status, proc.success);
|
|
97
|
-
const label = statusLabel(proc);
|
|
98
|
-
lines.push(
|
|
99
|
-
renderPanelTitleLine(
|
|
100
|
-
`Process: ${proc.name} (${proc.id}) ${icon} ${label}`,
|
|
101
|
-
width,
|
|
102
|
-
theme,
|
|
103
|
-
),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// Log lines (interleaved stdout + stderr in temporal order).
|
|
107
|
-
const logLines = this.manager.getCombinedOutput(
|
|
108
|
-
this.processId,
|
|
109
|
-
MAX_LOG_LINES,
|
|
110
|
-
);
|
|
111
|
-
if (logLines && logLines.length > 0) {
|
|
112
|
-
for (const line of logLines) {
|
|
113
|
-
const cleaned = stripAnsi(line.text);
|
|
114
|
-
const display = truncateToWidth(cleaned, innerWidth - 2);
|
|
115
|
-
if (line.type === "stderr") {
|
|
116
|
-
lines.push(padLine(warning(display)));
|
|
117
|
-
} else {
|
|
118
|
-
lines.push(padLine(display));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
lines.push(padLine(dim("(no output yet)")));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Pad to MAX_LOG_LINES for stable height.
|
|
126
|
-
const renderedLogLines = lines.length - 1; // minus header
|
|
127
|
-
for (let i = renderedLogLines; i < MAX_LOG_LINES; i++) {
|
|
128
|
-
lines.push(padLine(""));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Footer hint
|
|
132
|
-
lines.push(renderPanelRule(width, theme));
|
|
133
|
-
lines.push(padLine(dim("Run /process:stream to dismiss")));
|
|
134
|
-
lines.push(renderPanelRule(width, theme));
|
|
135
|
-
|
|
136
|
-
this.cachedLines = lines;
|
|
137
|
-
this.cachedWidth = width;
|
|
138
|
-
return this.cachedLines;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
dispose(): void {
|
|
142
|
-
if (this.timer) {
|
|
143
|
-
clearInterval(this.timer);
|
|
144
|
-
this.timer = null;
|
|
145
|
-
}
|
|
146
|
-
this.unsubscribe?.();
|
|
147
|
-
this.unsubscribe = null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Test script that simulates a crash (exit code 137 - like SIGKILL)
|
|
3
|
-
# Usage: ./test-exit-crash.sh [seconds]
|
|
4
|
-
|
|
5
|
-
WAIT_SECONDS=${1:-17}
|
|
6
|
-
|
|
7
|
-
echo "Starting unstable task..."
|
|
8
|
-
echo "Will crash in ${WAIT_SECONDS} seconds"
|
|
9
|
-
|
|
10
|
-
for i in $(seq 1 $WAIT_SECONDS); do
|
|
11
|
-
echo "[$(date '+%H:%M:%S')] Running... ($i/$WAIT_SECONDS)"
|
|
12
|
-
if [ $i -eq $((WAIT_SECONDS - 1)) ]; then
|
|
13
|
-
echo "[WARN] Memory pressure detected" >&2
|
|
14
|
-
fi
|
|
15
|
-
sleep 1
|
|
16
|
-
done
|
|
17
|
-
|
|
18
|
-
echo "FATAL: Segmentation fault (core dumped)" >&2
|
|
19
|
-
exit 137
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Test script that exits with failure (exit code 1)
|
|
3
|
-
# Usage: ./test-exit-failure.sh [seconds]
|
|
4
|
-
|
|
5
|
-
WAIT_SECONDS=${1:-15}
|
|
6
|
-
|
|
7
|
-
echo "Starting failing task..."
|
|
8
|
-
echo "Will fail in ${WAIT_SECONDS} seconds"
|
|
9
|
-
|
|
10
|
-
for i in $(seq 1 $WAIT_SECONDS); do
|
|
11
|
-
echo "[$(date '+%H:%M:%S')] Processing... ($i/$WAIT_SECONDS)"
|
|
12
|
-
sleep 1
|
|
13
|
-
done
|
|
14
|
-
|
|
15
|
-
echo "ERROR: Task failed!" >&2
|
|
16
|
-
echo "Something went wrong!" >&2
|
|
17
|
-
exit 1
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Test script that exits successfully (exit code 0)
|
|
3
|
-
# Usage: ./test-exit-success.sh [seconds]
|
|
4
|
-
|
|
5
|
-
WAIT_SECONDS=${1:-13}
|
|
6
|
-
|
|
7
|
-
echo "Starting successful task..."
|
|
8
|
-
echo "Will complete in ${WAIT_SECONDS} seconds"
|
|
9
|
-
|
|
10
|
-
for i in $(seq 1 $WAIT_SECONDS); do
|
|
11
|
-
echo "[$(date '+%H:%M:%S')] Working... ($i/$WAIT_SECONDS)"
|
|
12
|
-
sleep 1
|
|
13
|
-
done
|
|
14
|
-
|
|
15
|
-
echo "Task completed successfully!"
|
|
16
|
-
exit 0
|
package/src/test/test-output.sh
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Test script for processes extension
|
|
3
|
-
# Writes 80 characters every second, empty line every 10 seconds
|
|
4
|
-
|
|
5
|
-
counter=0
|
|
6
|
-
while true; do
|
|
7
|
-
counter=$((counter + 1))
|
|
8
|
-
|
|
9
|
-
# Generate 80 characters: timestamp + padding
|
|
10
|
-
timestamp=$(date '+%H:%M:%S')
|
|
11
|
-
line=$(printf "[%s] Line %05d: " "$timestamp" "$counter")
|
|
12
|
-
# Pad to 80 chars with random chars
|
|
13
|
-
padding_len=$((80 - ${#line}))
|
|
14
|
-
padding=$(head -c $padding_len /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' 2>/dev/null || printf '%*s' "$padding_len" '' | tr ' ' 'x')
|
|
15
|
-
echo "${line}${padding}"
|
|
16
|
-
|
|
17
|
-
# Every 10 seconds, print an empty line
|
|
18
|
-
if [ $((counter % 10)) -eq 0 ]; then
|
|
19
|
-
echo ""
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
# Every 5 lines, write something to stderr
|
|
23
|
-
if [ $((counter % 5)) -eq 0 ]; then
|
|
24
|
-
echo "[WARN] Counter reached $counter" >&2
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
sleep 1
|
|
28
|
-
done
|