@aliou/pi-processes 0.9.4 → 0.10.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/CONTRIBUTING.md +40 -42
- package/README.md +54 -84
- package/extensions/process-tabs.ts +26 -0
- package/extensions/processes/client.ts +194 -0
- package/extensions/processes/commands/clear.ts +29 -0
- package/extensions/processes/commands/kill.ts +152 -0
- package/extensions/processes/commands/overview.ts +102 -0
- package/extensions/processes/components/overview-component.ts +729 -0
- package/extensions/processes/components/overview-panel.ts +177 -0
- package/extensions/processes/config/defaults.ts +28 -0
- package/extensions/processes/config/index.ts +13 -0
- package/extensions/processes/config/loader.ts +62 -0
- package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
- package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
- package/extensions/processes/config/migrations/index.ts +25 -0
- package/extensions/processes/config/schema.ts +9 -0
- package/extensions/processes/config/types.ts +72 -0
- package/extensions/processes/constants.ts +1 -0
- package/extensions/processes/handlers/commands.ts +113 -0
- package/extensions/processes/handlers/kill-process.ts +31 -0
- package/extensions/processes/handlers/notifications.ts +147 -0
- package/extensions/processes/handlers/requests.ts +114 -0
- package/extensions/processes/handlers/subscriptions.ts +144 -0
- package/extensions/processes/hooks/background-blocker.ts +201 -0
- package/extensions/processes/hooks/cleanup.ts +43 -0
- package/extensions/processes/hooks/event-bridge.ts +32 -0
- package/extensions/processes/i18n/index.ts +3 -0
- package/extensions/processes/i18n/messages.ts +27 -0
- package/extensions/processes/i18n/translator.ts +49 -0
- package/extensions/processes/index.ts +109 -0
- package/extensions/processes/message-renderer.ts +117 -0
- package/extensions/processes/notification-sender.ts +43 -0
- package/extensions/processes/notifications/classify.ts +27 -0
- package/extensions/processes/notifications/log-matchers.ts +146 -0
- package/extensions/processes/notifications/registry.ts +225 -0
- package/extensions/processes/notifications/render-content.ts +122 -0
- package/extensions/processes/notifications/service.ts +336 -0
- package/extensions/processes/notifications/types.ts +11 -0
- package/extensions/processes/settings/apply-setting-change.ts +30 -0
- package/extensions/processes/settings/build-sections.ts +420 -0
- package/extensions/processes/settings/index.ts +42 -0
- package/extensions/processes/tools/clear/index.ts +21 -0
- package/extensions/processes/tools/clear/render.ts +40 -0
- package/extensions/processes/tools/components/index.ts +4 -0
- package/extensions/processes/tools/components/process-action-header.ts +31 -0
- package/extensions/processes/tools/components/process-action-title.ts +17 -0
- package/extensions/processes/tools/components/tool-layout.ts +70 -0
- package/extensions/processes/tools/components/watch.ts +53 -0
- package/extensions/processes/tools/index.ts +265 -0
- package/extensions/processes/tools/list/index.ts +157 -0
- package/extensions/processes/tools/list/render.ts +211 -0
- package/extensions/processes/tools/notify.ts +191 -0
- package/extensions/processes/tools/output/index.ts +307 -0
- package/extensions/processes/tools/output/render.ts +229 -0
- package/extensions/processes/tools/schema.ts +263 -0
- package/extensions/processes/tools/start/index.ts +64 -0
- package/extensions/processes/tools/start/render.ts +67 -0
- package/extensions/processes/tools/stop/index.ts +35 -0
- package/extensions/processes/tools/stop/render.ts +43 -0
- package/extensions/processes/tools/update/index.ts +320 -0
- package/extensions/processes/tools/update/render.ts +251 -0
- package/extensions/processes/tools/utils.ts +112 -0
- package/extensions/processes/tools/watch-format.ts +14 -0
- package/extensions/processes/tools/write/index.ts +75 -0
- package/extensions/processes/tools/write/render.ts +68 -0
- package/extensions/processes/utils/truncate.ts +285 -0
- package/extensions/processes-dock/client.ts +60 -0
- package/extensions/processes-dock/commands/dock.ts +70 -0
- package/extensions/processes-dock/commands/pin.ts +166 -0
- package/extensions/processes-dock/components/log-dock-component.ts +375 -0
- package/extensions/processes-dock/dock-state.ts +33 -0
- package/extensions/processes-dock/index.ts +28 -0
- package/extensions/processes-dock/logs-client.ts +5 -0
- package/extensions/processes-dock/widget/setup.ts +520 -0
- package/extensions/processes-dock/widget/status.ts +149 -0
- package/extensions/processes-dock/widget/types.ts +16 -0
- package/extensions/processes-logs/client.ts +51 -0
- package/extensions/processes-logs/commands/logs.ts +108 -0
- package/extensions/processes-logs/completions.ts +31 -0
- package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
- package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
- package/extensions/processes-logs/index.ts +27 -0
- package/extensions/processes-logs/logs-client.ts +101 -0
- package/extensions/shared/line-buffer.ts +35 -0
- package/extensions/shared/output-payload.ts +28 -0
- package/extensions/shared/ui.ts +146 -0
- package/package.json +47 -20
- package/schema.json +134 -0
- package/skills/pi-processes/SKILL.md +496 -25
- package/src/get-manager.ts +15 -0
- package/src/manager/index.ts +182 -0
- package/src/manager/internal-types.ts +130 -0
- package/src/manager/limits.ts +15 -0
- package/src/manager/process-log-store.ts +422 -0
- package/src/manager/process-output.ts +345 -0
- package/src/manager/process-registry.ts +78 -0
- package/src/manager/process-runtime-controller.ts +499 -0
- package/src/protocol/broadcasts.ts +14 -0
- package/src/protocol/channels.ts +30 -0
- package/src/protocol/commands.ts +24 -0
- package/src/protocol/index.ts +34 -0
- package/src/protocol/logs.ts +23 -0
- package/src/protocol/notifications.ts +48 -0
- package/src/protocol/requests.ts +76 -0
- package/src/types.ts +66 -0
- package/src/utils/ansi.ts +2 -4
- package/src/utils/format.ts +24 -32
- package/src/utils/index.ts +5 -1
- package/src/utils/is-record.ts +3 -0
- package/src/utils/match-line.ts +32 -0
- package/src/utils/platform.ts +13 -0
- package/src/utils/shorten-path.ts +11 -0
- package/src/utils/signals.ts +22 -0
- package/src/commands/clear/command.ts +0 -14
- package/src/commands/clear/index.ts +0 -1
- package/src/commands/completions.ts +0 -38
- package/src/commands/dock/command.ts +0 -29
- package/src/commands/dock/index.ts +0 -1
- package/src/commands/index.ts +0 -33
- package/src/commands/kill/command.ts +0 -69
- package/src/commands/kill/index.ts +0 -1
- package/src/commands/logs/command.ts +0 -47
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/pick-process.ts +0 -34
- package/src/commands/pin/command.ts +0 -34
- package/src/commands/pin/index.ts +0 -1
- package/src/commands/processes/command.ts +0 -39
- package/src/commands/processes/index.ts +0 -1
- package/src/commands/settings/apply-setting-change.ts +0 -72
- package/src/commands/settings/build-sections.ts +0 -160
- package/src/commands/settings/command.ts +0 -20
- package/src/commands/settings/index.ts +0 -1
- package/src/components/log-dock-component.test.ts +0 -29
- package/src/components/log-dock-component.ts +0 -236
- package/src/components/log-file-viewer.ts +0 -317
- package/src/components/log-overlay-component.ts +0 -546
- package/src/components/panel-helpers.ts +0 -37
- package/src/components/process-picker-component.ts +0 -166
- package/src/components/processes-component.ts +0 -485
- package/src/components/status-format.ts +0 -38
- package/src/config.ts +0 -107
- package/src/constants/index.ts +0 -16
- package/src/constants/types.ts +0 -106
- package/src/hooks/background-blocker.ts +0 -66
- package/src/hooks/cleanup.ts +0 -10
- package/src/hooks/index.ts +0 -32
- package/src/hooks/message-renderer.ts +0 -115
- package/src/hooks/process-end.ts +0 -69
- package/src/hooks/process-watch.ts +0 -85
- package/src/hooks/utils.ts +0 -23
- package/src/hooks/widget/index.ts +0 -2
- package/src/hooks/widget/setup.ts +0 -167
- package/src/hooks/widget/status-widget.ts +0 -105
- package/src/hooks/widget/types.ts +0 -20
- package/src/index.ts +0 -35
- package/src/manager.test.ts +0 -331
- package/src/manager.ts +0 -758
- package/src/tools/actions/clear.ts +0 -20
- package/src/tools/actions/debug.ts +0 -148
- package/src/tools/actions/index.ts +0 -171
- package/src/tools/actions/kill.ts +0 -89
- package/src/tools/actions/list.ts +0 -188
- package/src/tools/actions/logs.ts +0 -117
- package/src/tools/actions/output.ts +0 -269
- package/src/tools/actions/start.ts +0 -245
- package/src/tools/actions/write.ts +0 -113
- package/src/tools/index.ts +0 -207
- package/src/utils/ansi.test.ts +0 -22
- package/src/utils/command-executor.test.ts +0 -48
- package/src/utils/keybindings.ts +0 -71
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
type Component,
|
|
4
|
-
matchesKey,
|
|
5
|
-
truncateToWidth,
|
|
6
|
-
visibleWidth,
|
|
7
|
-
} from "@earendil-works/pi-tui";
|
|
8
|
-
import type { ProcessInfo } from "../constants";
|
|
9
|
-
import type { ProcessManager } from "../manager";
|
|
10
|
-
import {
|
|
11
|
-
createPanelPadder,
|
|
12
|
-
renderPanelRule,
|
|
13
|
-
renderPanelTitleLine,
|
|
14
|
-
} from "./panel-helpers";
|
|
15
|
-
import { statusIcon, statusLabel } from "./status-format";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A simple process picker component. Shows a list of processes and lets the
|
|
19
|
-
* user select one with up/down + Enter, or dismiss with Escape/q.
|
|
20
|
-
*/
|
|
21
|
-
export class ProcessPickerComponent implements Component {
|
|
22
|
-
private tui: { requestRender: () => void };
|
|
23
|
-
private theme: Theme;
|
|
24
|
-
private onClose: (processId?: string) => void;
|
|
25
|
-
private manager: ProcessManager;
|
|
26
|
-
private title: string;
|
|
27
|
-
private filter: (proc: ProcessInfo) => boolean;
|
|
28
|
-
|
|
29
|
-
private selectedIndex = 0;
|
|
30
|
-
private cachedLines: string[] = [];
|
|
31
|
-
private cachedWidth = 0;
|
|
32
|
-
private unsubscribe: (() => void) | null = null;
|
|
33
|
-
|
|
34
|
-
constructor(
|
|
35
|
-
tui: { requestRender: () => void },
|
|
36
|
-
theme: Theme,
|
|
37
|
-
onClose: (processId?: string) => void,
|
|
38
|
-
manager: ProcessManager,
|
|
39
|
-
title: string,
|
|
40
|
-
filter?: (proc: ProcessInfo) => boolean,
|
|
41
|
-
) {
|
|
42
|
-
this.tui = tui;
|
|
43
|
-
this.theme = theme;
|
|
44
|
-
this.onClose = onClose;
|
|
45
|
-
this.manager = manager;
|
|
46
|
-
this.title = title;
|
|
47
|
-
this.filter = filter ?? (() => true);
|
|
48
|
-
|
|
49
|
-
this.unsubscribe = this.manager.onEvent(() => {
|
|
50
|
-
this.invalidate();
|
|
51
|
-
this.tui.requestRender();
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private getProcesses(): ProcessInfo[] {
|
|
56
|
-
return this.manager.list().filter(this.filter);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
handleInput(data: string): boolean {
|
|
60
|
-
const processes = this.getProcesses();
|
|
61
|
-
|
|
62
|
-
if (matchesKey(data, "down") || data === "j") {
|
|
63
|
-
if (processes.length > 0) {
|
|
64
|
-
this.selectedIndex = Math.min(
|
|
65
|
-
this.selectedIndex + 1,
|
|
66
|
-
processes.length - 1,
|
|
67
|
-
);
|
|
68
|
-
this.invalidate();
|
|
69
|
-
this.tui.requestRender();
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (matchesKey(data, "up") || data === "k") {
|
|
75
|
-
if (processes.length > 0) {
|
|
76
|
-
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
77
|
-
this.invalidate();
|
|
78
|
-
this.tui.requestRender();
|
|
79
|
-
}
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (matchesKey(data, "return")) {
|
|
84
|
-
if (processes.length > 0 && this.selectedIndex < processes.length) {
|
|
85
|
-
const proc = processes[this.selectedIndex];
|
|
86
|
-
if (proc) {
|
|
87
|
-
this.unsubscribe?.();
|
|
88
|
-
this.unsubscribe = null;
|
|
89
|
-
this.onClose(proc.id);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
96
|
-
this.unsubscribe?.();
|
|
97
|
-
this.unsubscribe = null;
|
|
98
|
-
this.onClose();
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
invalidate(): void {
|
|
106
|
-
this.cachedWidth = 0;
|
|
107
|
-
this.cachedLines = [];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
render(width: number): string[] {
|
|
111
|
-
if (width === this.cachedWidth && this.cachedLines.length > 0) {
|
|
112
|
-
return this.cachedLines;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const theme = this.theme;
|
|
116
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
117
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
118
|
-
|
|
119
|
-
const innerWidth = width - 2;
|
|
120
|
-
const basePadLine = createPanelPadder(width);
|
|
121
|
-
const padLine = (content: string): string =>
|
|
122
|
-
basePadLine(
|
|
123
|
-
visibleWidth(content) > innerWidth
|
|
124
|
-
? truncateToWidth(content, innerWidth, "", true)
|
|
125
|
-
: content,
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const lines: string[] = [];
|
|
129
|
-
const processes = this.getProcesses();
|
|
130
|
-
|
|
131
|
-
lines.push(renderPanelTitleLine(this.title, width, theme));
|
|
132
|
-
|
|
133
|
-
if (processes.length === 0) {
|
|
134
|
-
lines.push(padLine(""));
|
|
135
|
-
lines.push(padLine(dim("No processes available")));
|
|
136
|
-
lines.push(padLine(""));
|
|
137
|
-
} else {
|
|
138
|
-
lines.push(padLine(""));
|
|
139
|
-
for (let i = 0; i < processes.length; i++) {
|
|
140
|
-
const proc = processes[i];
|
|
141
|
-
if (!proc) continue;
|
|
142
|
-
const isSelected = i === this.selectedIndex;
|
|
143
|
-
const icon = statusIcon(proc.status, proc.success);
|
|
144
|
-
const label = statusLabel(proc);
|
|
145
|
-
const prefix = isSelected ? accent("> ") : " ";
|
|
146
|
-
const name = isSelected ? accent(proc.name) : proc.name;
|
|
147
|
-
const id = dim(`(${proc.id})`);
|
|
148
|
-
const status = dim(`${icon} ${label}`);
|
|
149
|
-
lines.push(padLine(`${prefix}${name} ${id} ${status}`));
|
|
150
|
-
}
|
|
151
|
-
lines.push(padLine(""));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
lines.push(renderPanelRule(width, theme));
|
|
155
|
-
lines.push(
|
|
156
|
-
padLine(
|
|
157
|
-
`${dim("j/k")} select ${dim("enter")} confirm ${dim("q")} cancel`,
|
|
158
|
-
),
|
|
159
|
-
);
|
|
160
|
-
lines.push(renderPanelRule(width, theme));
|
|
161
|
-
|
|
162
|
-
this.cachedLines = lines;
|
|
163
|
-
this.cachedWidth = width;
|
|
164
|
-
return this.cachedLines;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
type Component,
|
|
4
|
-
matchesKey,
|
|
5
|
-
truncateToWidth,
|
|
6
|
-
visibleWidth,
|
|
7
|
-
} from "@earendil-works/pi-tui";
|
|
8
|
-
import { configLoader } from "../config";
|
|
9
|
-
import type { ProcessInfo } from "../constants";
|
|
10
|
-
import type { ProcessManager } from "../manager";
|
|
11
|
-
import { stripAnsi } from "../utils";
|
|
12
|
-
import {
|
|
13
|
-
createPanelPadder,
|
|
14
|
-
renderPanelRule,
|
|
15
|
-
renderPanelTitleLine,
|
|
16
|
-
} from "./panel-helpers";
|
|
17
|
-
import { statusIcon, statusLabel } from "./status-format";
|
|
18
|
-
|
|
19
|
-
function formatRuntime(startTime: number, endTime: number | null): string {
|
|
20
|
-
const end = endTime ?? Date.now();
|
|
21
|
-
const ms = end - startTime;
|
|
22
|
-
const seconds = Math.floor(ms / 1000);
|
|
23
|
-
const minutes = Math.floor(seconds / 60);
|
|
24
|
-
const hours = Math.floor(minutes / 60);
|
|
25
|
-
|
|
26
|
-
if (hours > 0) {
|
|
27
|
-
return `${hours}h ${minutes % 60}m`;
|
|
28
|
-
}
|
|
29
|
-
if (minutes > 0) {
|
|
30
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
31
|
-
}
|
|
32
|
-
return `${seconds}s`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatBytes(bytes: number): string {
|
|
36
|
-
if (bytes >= 1024 * 1024) {
|
|
37
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
38
|
-
}
|
|
39
|
-
if (bytes >= 1024) {
|
|
40
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
41
|
-
}
|
|
42
|
-
return `${bytes}B`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function truncate(str: string, maxLen: number): string {
|
|
46
|
-
if (maxLen <= 3) return str.slice(0, maxLen);
|
|
47
|
-
if (str.length <= maxLen) return str;
|
|
48
|
-
return `${str.slice(0, maxLen - 3)}...`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function fitCell(
|
|
52
|
-
value: string,
|
|
53
|
-
width: number,
|
|
54
|
-
align: "left" | "right" = "left",
|
|
55
|
-
): string {
|
|
56
|
-
const truncated = truncateToWidth(value, Math.max(0, width));
|
|
57
|
-
const pad = Math.max(0, width - visibleWidth(truncated));
|
|
58
|
-
if (align === "right") {
|
|
59
|
-
return " ".repeat(pad) + truncated;
|
|
60
|
-
}
|
|
61
|
-
return truncated + " ".repeat(pad);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class ProcessesComponent implements Component {
|
|
65
|
-
private tui: { requestRender: () => void };
|
|
66
|
-
private theme: Theme;
|
|
67
|
-
private onClose: (processId?: string) => void;
|
|
68
|
-
private manager: ProcessManager;
|
|
69
|
-
|
|
70
|
-
private selectedIndex = 0;
|
|
71
|
-
private processScrollOffset = 0;
|
|
72
|
-
private logScrollOffset = 0;
|
|
73
|
-
private scrollInfo = { above: 0, below: 0 };
|
|
74
|
-
private cachedLines: string[] = [];
|
|
75
|
-
private cachedWidth = 0;
|
|
76
|
-
private unsubscribe: (() => void) | null = null;
|
|
77
|
-
|
|
78
|
-
constructor(
|
|
79
|
-
tui: { requestRender: () => void },
|
|
80
|
-
theme: Theme,
|
|
81
|
-
onClose: (processId?: string) => void,
|
|
82
|
-
manager: ProcessManager,
|
|
83
|
-
) {
|
|
84
|
-
this.tui = tui;
|
|
85
|
-
this.theme = theme;
|
|
86
|
-
this.onClose = onClose;
|
|
87
|
-
this.manager = manager;
|
|
88
|
-
|
|
89
|
-
this.unsubscribe = this.manager.onEvent(() => {
|
|
90
|
-
this.invalidate();
|
|
91
|
-
this.tui.requestRender();
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
handleInput(data: string): boolean {
|
|
96
|
-
const processes = this.manager.list();
|
|
97
|
-
|
|
98
|
-
// Navigation
|
|
99
|
-
if (matchesKey(data, "down") || data === "j") {
|
|
100
|
-
if (processes.length > 0) {
|
|
101
|
-
this.selectedIndex = Math.min(
|
|
102
|
-
this.selectedIndex + 1,
|
|
103
|
-
processes.length - 1,
|
|
104
|
-
);
|
|
105
|
-
this.logScrollOffset = 0;
|
|
106
|
-
this.ensureProcessVisible(processes.length);
|
|
107
|
-
this.invalidate();
|
|
108
|
-
this.tui.requestRender();
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (matchesKey(data, "up") || data === "k") {
|
|
114
|
-
if (processes.length > 0) {
|
|
115
|
-
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
116
|
-
this.logScrollOffset = 0;
|
|
117
|
-
this.ensureProcessVisible(processes.length);
|
|
118
|
-
this.invalidate();
|
|
119
|
-
this.tui.requestRender();
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Scroll logs
|
|
125
|
-
if (data === "J") {
|
|
126
|
-
this.logScrollOffset = Math.max(0, this.logScrollOffset - 5);
|
|
127
|
-
this.invalidate();
|
|
128
|
-
this.tui.requestRender();
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (data === "K") {
|
|
133
|
-
this.logScrollOffset += 5;
|
|
134
|
-
this.invalidate();
|
|
135
|
-
this.tui.requestRender();
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Stream logs for selected process
|
|
140
|
-
if (matchesKey(data, "return")) {
|
|
141
|
-
if (processes.length > 0 && this.selectedIndex < processes.length) {
|
|
142
|
-
const proc = processes[this.selectedIndex];
|
|
143
|
-
if (proc) {
|
|
144
|
-
this.unsubscribe?.();
|
|
145
|
-
this.unsubscribe = null;
|
|
146
|
-
this.onClose(proc.id);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Kill selected process
|
|
153
|
-
if (data === "x") {
|
|
154
|
-
if (processes.length > 0 && this.selectedIndex < processes.length) {
|
|
155
|
-
const proc = processes[this.selectedIndex];
|
|
156
|
-
if (proc?.status === "running") {
|
|
157
|
-
void this.manager.kill(proc.id, {
|
|
158
|
-
signal: "SIGTERM",
|
|
159
|
-
timeoutMs: 3000,
|
|
160
|
-
});
|
|
161
|
-
} else if (proc?.status === "terminate_timeout") {
|
|
162
|
-
void this.manager.kill(proc.id, {
|
|
163
|
-
signal: "SIGKILL",
|
|
164
|
-
timeoutMs: 200,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Clear finished processes
|
|
172
|
-
if (data === "c" || data === "C") {
|
|
173
|
-
const cleared = this.manager.clearFinished();
|
|
174
|
-
if (cleared > 0) {
|
|
175
|
-
const remaining = this.manager.list();
|
|
176
|
-
if (this.selectedIndex >= remaining.length) {
|
|
177
|
-
this.selectedIndex = Math.max(0, remaining.length - 1);
|
|
178
|
-
}
|
|
179
|
-
this.ensureProcessVisible(remaining.length);
|
|
180
|
-
this.invalidate();
|
|
181
|
-
this.tui.requestRender();
|
|
182
|
-
}
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Close
|
|
187
|
-
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
188
|
-
this.unsubscribe?.();
|
|
189
|
-
this.unsubscribe = null;
|
|
190
|
-
this.onClose();
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private ensureProcessVisible(totalProcesses: number): void {
|
|
198
|
-
const maxVisibleProcesses =
|
|
199
|
-
configLoader.getConfig().processList.maxVisibleProcesses;
|
|
200
|
-
const visibleCount = Math.min(maxVisibleProcesses, totalProcesses);
|
|
201
|
-
if (this.selectedIndex < this.processScrollOffset) {
|
|
202
|
-
this.processScrollOffset = this.selectedIndex;
|
|
203
|
-
} else if (this.selectedIndex >= this.processScrollOffset + visibleCount) {
|
|
204
|
-
this.processScrollOffset = this.selectedIndex - visibleCount + 1;
|
|
205
|
-
}
|
|
206
|
-
this.processScrollOffset = Math.max(
|
|
207
|
-
0,
|
|
208
|
-
Math.min(this.processScrollOffset, totalProcesses - visibleCount),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
invalidate(): void {
|
|
213
|
-
this.cachedWidth = 0;
|
|
214
|
-
this.cachedLines = [];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
render(width: number): string[] {
|
|
218
|
-
if (width === this.cachedWidth && this.cachedLines.length > 0) {
|
|
219
|
-
return this.cachedLines;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const cfg = configLoader.getConfig().processList;
|
|
223
|
-
const maxVisibleProcesses = cfg.maxVisibleProcesses;
|
|
224
|
-
const maxPreviewLines = cfg.maxPreviewLines;
|
|
225
|
-
|
|
226
|
-
const theme = this.theme;
|
|
227
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
228
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
229
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
230
|
-
|
|
231
|
-
const lines: string[] = [];
|
|
232
|
-
const processes = this.manager.list();
|
|
233
|
-
const innerWidth = width - 2;
|
|
234
|
-
|
|
235
|
-
const basePadLine = createPanelPadder(width);
|
|
236
|
-
const padLine = (content: string): string =>
|
|
237
|
-
basePadLine(
|
|
238
|
-
visibleWidth(content) > innerWidth
|
|
239
|
-
? truncateToWidth(content, innerWidth, "", true)
|
|
240
|
-
: content,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
lines.push(renderPanelTitleLine("Background Processes", width, theme));
|
|
244
|
-
|
|
245
|
-
if (processes.length === 0) {
|
|
246
|
-
lines.push(padLine(""));
|
|
247
|
-
lines.push(padLine(dim("No background processes")));
|
|
248
|
-
lines.push(padLine(dim("Use the processes tool to start commands")));
|
|
249
|
-
lines.push(padLine(""));
|
|
250
|
-
} else {
|
|
251
|
-
const prefixWidth = 2;
|
|
252
|
-
|
|
253
|
-
// Responsive column widths based on available space
|
|
254
|
-
// Minimum widths: id=6, name=8, cmd=4, status=10, time=4, size=4 = ~40 chars minimum
|
|
255
|
-
const minTotalWidth = 40;
|
|
256
|
-
const scaleFactor =
|
|
257
|
-
innerWidth < minTotalWidth ? innerWidth / minTotalWidth : 1;
|
|
258
|
-
|
|
259
|
-
const processWidth = Math.max(14, Math.floor(24 * scaleFactor));
|
|
260
|
-
const statusWidth = Math.max(10, Math.floor(18 * scaleFactor));
|
|
261
|
-
const timeWidth = Math.max(4, Math.floor(8 * scaleFactor));
|
|
262
|
-
const sizeWidth = Math.max(4, Math.floor(8 * scaleFactor));
|
|
263
|
-
|
|
264
|
-
const hasProcessScroll = processes.length > maxVisibleProcesses;
|
|
265
|
-
const headerSuffixText = hasProcessScroll
|
|
266
|
-
? ` [${this.processScrollOffset + 1}-${Math.min(this.processScrollOffset + maxVisibleProcesses, processes.length)}/${processes.length}]`
|
|
267
|
-
: "";
|
|
268
|
-
const headerSuffixLen = hasProcessScroll ? headerSuffixText.length : 0;
|
|
269
|
-
|
|
270
|
-
// Calculate command column width based on remaining space
|
|
271
|
-
const fixedWidth =
|
|
272
|
-
prefixWidth +
|
|
273
|
-
processWidth +
|
|
274
|
-
statusWidth +
|
|
275
|
-
timeWidth +
|
|
276
|
-
sizeWidth +
|
|
277
|
-
headerSuffixLen;
|
|
278
|
-
const cmdWidth = Math.max(4, innerWidth - fixedWidth);
|
|
279
|
-
|
|
280
|
-
lines.push(padLine(""));
|
|
281
|
-
const header =
|
|
282
|
-
" " +
|
|
283
|
-
dim("Process".padEnd(processWidth)) +
|
|
284
|
-
dim("Command".padEnd(cmdWidth)) +
|
|
285
|
-
dim("Status".padEnd(statusWidth)) +
|
|
286
|
-
dim("Time".padEnd(timeWidth)) +
|
|
287
|
-
dim("Size".padStart(sizeWidth)) +
|
|
288
|
-
(hasProcessScroll ? dim(headerSuffixText) : "");
|
|
289
|
-
lines.push(padLine(header));
|
|
290
|
-
lines.push(renderPanelRule(width, theme));
|
|
291
|
-
|
|
292
|
-
const visibleProcessCount = Math.min(
|
|
293
|
-
maxVisibleProcesses,
|
|
294
|
-
processes.length,
|
|
295
|
-
);
|
|
296
|
-
const startIdx = this.processScrollOffset;
|
|
297
|
-
const endIdx = startIdx + visibleProcessCount;
|
|
298
|
-
|
|
299
|
-
for (let i = startIdx; i < endIdx; i++) {
|
|
300
|
-
const proc = processes[i];
|
|
301
|
-
if (!proc) continue;
|
|
302
|
-
const isSelected = i === this.selectedIndex;
|
|
303
|
-
const sizes = this.manager.getFileSize(proc.id);
|
|
304
|
-
const totalSize = sizes ? sizes.stdout + sizes.stderr : 0;
|
|
305
|
-
|
|
306
|
-
const statusText = this.formatStatus(proc);
|
|
307
|
-
|
|
308
|
-
// Keep process cell bounded even with large IDs.
|
|
309
|
-
const idPlain = `(${proc.id})`;
|
|
310
|
-
const maxNameLen = Math.max(
|
|
311
|
-
1,
|
|
312
|
-
processWidth - visibleWidth(idPlain) - 1,
|
|
313
|
-
);
|
|
314
|
-
const tName = truncate(proc.name, maxNameLen);
|
|
315
|
-
const processCell = isSelected
|
|
316
|
-
? `${accent(tName)} ${dim(` ${idPlain}`)}`
|
|
317
|
-
: `${tName}${dim(` ${idPlain}`)}`;
|
|
318
|
-
|
|
319
|
-
const row =
|
|
320
|
-
fitCell(processCell, processWidth) +
|
|
321
|
-
fitCell(truncate(proc.command, cmdWidth - 1), cmdWidth) +
|
|
322
|
-
fitCell(statusText, statusWidth) +
|
|
323
|
-
fitCell(formatRuntime(proc.startTime, proc.endTime), timeWidth) +
|
|
324
|
-
fitCell(formatBytes(totalSize), sizeWidth, "right");
|
|
325
|
-
|
|
326
|
-
if (isSelected) {
|
|
327
|
-
lines.push(padLine(`${accent(">")} ${row}`));
|
|
328
|
-
} else {
|
|
329
|
-
lines.push(padLine(` ${row}`));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
for (let i = visibleProcessCount; i < maxVisibleProcesses; i++) {
|
|
334
|
-
lines.push(padLine(""));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (this.selectedIndex < processes.length) {
|
|
338
|
-
const selected = processes[this.selectedIndex];
|
|
339
|
-
if (!selected) {
|
|
340
|
-
this.cachedLines = lines;
|
|
341
|
-
this.cachedWidth = width;
|
|
342
|
-
return this.cachedLines;
|
|
343
|
-
}
|
|
344
|
-
const output = this.manager.getOutput(selected.id, maxPreviewLines * 2);
|
|
345
|
-
const sizes = this.manager.getFileSize(selected.id);
|
|
346
|
-
|
|
347
|
-
lines.push(renderPanelRule(width, theme));
|
|
348
|
-
|
|
349
|
-
const logTitlePlain = `Output: ${selected.name} (${selected.id})`;
|
|
350
|
-
const sizeInfoPlain = sizes
|
|
351
|
-
? ` stdout: ${formatBytes(sizes.stdout)}, stderr: ${formatBytes(sizes.stderr)}`
|
|
352
|
-
: "";
|
|
353
|
-
const combinedPlain = logTitlePlain + sizeInfoPlain;
|
|
354
|
-
// Truncate if combined exceeds innerWidth, prioritizing the title
|
|
355
|
-
if (combinedPlain.length <= innerWidth) {
|
|
356
|
-
const logTitle = `Output: ${accent(selected.name)} ${dim(`(${selected.id})`)}`;
|
|
357
|
-
const sizeInfo = sizes ? dim(sizeInfoPlain) : "";
|
|
358
|
-
lines.push(padLine(logTitle + sizeInfo));
|
|
359
|
-
} else {
|
|
360
|
-
const maxNameLen = Math.max(
|
|
361
|
-
8,
|
|
362
|
-
innerWidth -
|
|
363
|
-
(`Output: (${selected.id})`.length + sizeInfoPlain.length),
|
|
364
|
-
);
|
|
365
|
-
const tName = truncate(selected.name, maxNameLen);
|
|
366
|
-
const logTitle = `Output: ${accent(tName)} ${dim(`(${selected.id})`)}`;
|
|
367
|
-
const sizeInfo = sizes ? dim(sizeInfoPlain) : "";
|
|
368
|
-
lines.push(padLine(logTitle + sizeInfo));
|
|
369
|
-
}
|
|
370
|
-
lines.push(padLine(""));
|
|
371
|
-
|
|
372
|
-
let renderedLines = 0;
|
|
373
|
-
|
|
374
|
-
if (output) {
|
|
375
|
-
const logLines: { type: "stdout" | "stderr"; text: string }[] = [];
|
|
376
|
-
for (const line of output.stdout) {
|
|
377
|
-
logLines.push({ type: "stdout", text: line });
|
|
378
|
-
}
|
|
379
|
-
for (const line of output.stderr) {
|
|
380
|
-
logLines.push({ type: "stderr", text: line });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (logLines.length === 0) {
|
|
384
|
-
lines.push(padLine(dim("(no output yet)")));
|
|
385
|
-
renderedLines = 1;
|
|
386
|
-
} else {
|
|
387
|
-
const startIdx = Math.max(
|
|
388
|
-
0,
|
|
389
|
-
logLines.length - maxPreviewLines - this.logScrollOffset,
|
|
390
|
-
);
|
|
391
|
-
const endIdx = Math.max(0, logLines.length - this.logScrollOffset);
|
|
392
|
-
const visibleLines = logLines.slice(startIdx, endIdx);
|
|
393
|
-
|
|
394
|
-
this.scrollInfo.above = startIdx;
|
|
395
|
-
this.scrollInfo.below =
|
|
396
|
-
this.logScrollOffset > 0 ? logLines.length - endIdx : 0;
|
|
397
|
-
|
|
398
|
-
for (const line of visibleLines) {
|
|
399
|
-
const displayLine = truncate(
|
|
400
|
-
stripAnsi(line.text),
|
|
401
|
-
innerWidth - 2,
|
|
402
|
-
);
|
|
403
|
-
if (line.type === "stderr") {
|
|
404
|
-
lines.push(padLine(warning(displayLine)));
|
|
405
|
-
} else {
|
|
406
|
-
lines.push(padLine(displayLine));
|
|
407
|
-
}
|
|
408
|
-
renderedLines++;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
while (renderedLines < maxPreviewLines) {
|
|
414
|
-
lines.push(padLine(""));
|
|
415
|
-
renderedLines++;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const footerLeft =
|
|
421
|
-
`${dim("enter")} stream ` +
|
|
422
|
-
`${dim("j/k")} select ` +
|
|
423
|
-
`${dim("x")} term/kill ` +
|
|
424
|
-
`${dim("c")} clear ` +
|
|
425
|
-
`${dim("q")} quit`;
|
|
426
|
-
|
|
427
|
-
let footerRight = "";
|
|
428
|
-
if (this.scrollInfo.above > 0 || this.scrollInfo.below > 0) {
|
|
429
|
-
const parts: string[] = [];
|
|
430
|
-
if (this.scrollInfo.above > 0) {
|
|
431
|
-
parts.push(`↑${this.scrollInfo.above}`);
|
|
432
|
-
}
|
|
433
|
-
if (this.scrollInfo.below > 0) {
|
|
434
|
-
parts.push(`↓${this.scrollInfo.below}`);
|
|
435
|
-
}
|
|
436
|
-
footerRight = `${dim("J/K")} scroll ${dim(parts.join(" "))}`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const footerLeftLen = visibleWidth(footerLeft);
|
|
440
|
-
const footerRightLen = visibleWidth(footerRight);
|
|
441
|
-
const footerGap = Math.max(2, innerWidth - footerLeftLen - footerRightLen);
|
|
442
|
-
let footer = footerLeft + " ".repeat(footerGap) + footerRight;
|
|
443
|
-
|
|
444
|
-
// Truncate footer if it exceeds inner width (e.g., on very narrow terminals)
|
|
445
|
-
if (footerLeftLen + footerGap + footerRightLen > innerWidth) {
|
|
446
|
-
footer = truncateToWidth(footer, innerWidth);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
lines.push(renderPanelRule(width, theme));
|
|
450
|
-
lines.push(padLine(footer));
|
|
451
|
-
|
|
452
|
-
this.cachedLines = lines;
|
|
453
|
-
this.cachedWidth = width;
|
|
454
|
-
|
|
455
|
-
return this.cachedLines;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private formatStatus(proc: ProcessInfo): string {
|
|
459
|
-
const theme = this.theme;
|
|
460
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
461
|
-
const success = (s: string) => theme.fg("success", s);
|
|
462
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
463
|
-
const error = (s: string) => theme.fg("error", s);
|
|
464
|
-
|
|
465
|
-
const icon = statusIcon(proc.status, proc.success);
|
|
466
|
-
const label = statusLabel(proc);
|
|
467
|
-
|
|
468
|
-
switch (proc.status) {
|
|
469
|
-
case "running":
|
|
470
|
-
return success(`${icon} ${label}`);
|
|
471
|
-
case "terminating":
|
|
472
|
-
return warning(`${icon} ${label}`);
|
|
473
|
-
case "terminate_timeout":
|
|
474
|
-
return error(`${icon} ${label}`);
|
|
475
|
-
case "killed":
|
|
476
|
-
return warning(`${icon} ${label}`);
|
|
477
|
-
case "exited":
|
|
478
|
-
return proc.success
|
|
479
|
-
? dim(`${icon} ${label}`)
|
|
480
|
-
: error(`${icon} ${label}`);
|
|
481
|
-
default:
|
|
482
|
-
return dim(`${icon} ${label}`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ProcessInfo, ProcessStatus } from "../constants";
|
|
2
|
-
|
|
3
|
-
export function statusLabel(proc: ProcessInfo): string {
|
|
4
|
-
switch (proc.status) {
|
|
5
|
-
case "running":
|
|
6
|
-
return "running";
|
|
7
|
-
case "terminating":
|
|
8
|
-
return "terminating";
|
|
9
|
-
case "terminate_timeout":
|
|
10
|
-
return "terminate_timeout";
|
|
11
|
-
case "killed":
|
|
12
|
-
return "killed";
|
|
13
|
-
case "exited":
|
|
14
|
-
return proc.success ? "exit(0)" : `exit(${proc.exitCode ?? "?"})`;
|
|
15
|
-
default:
|
|
16
|
-
return proc.status;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function statusIcon(
|
|
21
|
-
status: ProcessStatus,
|
|
22
|
-
success: boolean | null,
|
|
23
|
-
): string {
|
|
24
|
-
switch (status) {
|
|
25
|
-
case "running":
|
|
26
|
-
return "\u25CF"; // filled circle
|
|
27
|
-
case "terminating":
|
|
28
|
-
return "\u25CF"; // filled circle
|
|
29
|
-
case "terminate_timeout":
|
|
30
|
-
return "\u2717"; // x mark
|
|
31
|
-
case "exited":
|
|
32
|
-
return success ? "\u2713" : "\u2717";
|
|
33
|
-
case "killed":
|
|
34
|
-
return "\u2717";
|
|
35
|
-
default:
|
|
36
|
-
return "?";
|
|
37
|
-
}
|
|
38
|
-
}
|