@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,167 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { LogDockComponent } from "../../components/log-dock-component";
|
|
6
|
-
import { configLoader, type ResolvedProcessesConfig } from "../../config";
|
|
7
|
-
import { LIVE_STATUSES } from "../../constants";
|
|
8
|
-
import type { ProcessManager } from "../../manager";
|
|
9
|
-
import { renderStatusWidget } from "./status-widget";
|
|
10
|
-
import {
|
|
11
|
-
type DockActions,
|
|
12
|
-
type DockState,
|
|
13
|
-
LOG_DOCK_WIDGET_ID,
|
|
14
|
-
STATUS_WIDGET_ID,
|
|
15
|
-
} from "./types";
|
|
16
|
-
|
|
17
|
-
export function setupProcessWidget(
|
|
18
|
-
pi: ExtensionAPI,
|
|
19
|
-
manager: ProcessManager,
|
|
20
|
-
config: ResolvedProcessesConfig,
|
|
21
|
-
) {
|
|
22
|
-
let activeCtx: ExtensionContext | null = null;
|
|
23
|
-
let logDockComponent: LogDockComponent | null = null;
|
|
24
|
-
let logDockComponentTui: { requestRender(): void } | null = null;
|
|
25
|
-
|
|
26
|
-
const dockState: DockState = {
|
|
27
|
-
visibility: "hidden",
|
|
28
|
-
followEnabled: config.follow.enabledByDefault,
|
|
29
|
-
focusedProcessId: null,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function updateWidget() {
|
|
33
|
-
if (!activeCtx?.hasUI) return;
|
|
34
|
-
|
|
35
|
-
if (!configLoader.getConfig().widget.showStatusWidget) {
|
|
36
|
-
activeCtx.ui.setWidget(STATUS_WIDGET_ID, undefined);
|
|
37
|
-
} else {
|
|
38
|
-
const processes = manager.list();
|
|
39
|
-
const maxWidth = process.stdout.columns || 120;
|
|
40
|
-
const lines = renderStatusWidget(processes, activeCtx.ui.theme, maxWidth);
|
|
41
|
-
|
|
42
|
-
if (lines.length === 0) {
|
|
43
|
-
activeCtx.ui.setWidget(STATUS_WIDGET_ID, undefined);
|
|
44
|
-
} else {
|
|
45
|
-
activeCtx.ui.setWidget(STATUS_WIDGET_ID, lines, {
|
|
46
|
-
placement: "belowEditor",
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (dockState.visibility === "hidden") {
|
|
52
|
-
activeCtx.ui.setWidget(LOG_DOCK_WIDGET_ID, undefined);
|
|
53
|
-
if (logDockComponent) {
|
|
54
|
-
logDockComponent.dispose();
|
|
55
|
-
logDockComponent = null;
|
|
56
|
-
logDockComponentTui = null;
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const mode = dockState.visibility as "collapsed" | "open";
|
|
62
|
-
const height = mode === "collapsed" ? 3 : config.widget.dockHeight;
|
|
63
|
-
|
|
64
|
-
if (logDockComponent && logDockComponentTui) {
|
|
65
|
-
logDockComponent.update({
|
|
66
|
-
mode,
|
|
67
|
-
focusedProcessId: dockState.focusedProcessId,
|
|
68
|
-
dockHeight: height,
|
|
69
|
-
});
|
|
70
|
-
} else {
|
|
71
|
-
const ctx = activeCtx;
|
|
72
|
-
ctx.ui.setWidget(
|
|
73
|
-
LOG_DOCK_WIDGET_ID,
|
|
74
|
-
(tui: { requestRender(): void }, theme: typeof ctx.ui.theme) => {
|
|
75
|
-
logDockComponent = new LogDockComponent({
|
|
76
|
-
manager,
|
|
77
|
-
tui,
|
|
78
|
-
theme,
|
|
79
|
-
mode,
|
|
80
|
-
focusedProcessId: dockState.focusedProcessId,
|
|
81
|
-
dockHeight: height,
|
|
82
|
-
});
|
|
83
|
-
logDockComponentTui = tui;
|
|
84
|
-
return logDockComponent;
|
|
85
|
-
},
|
|
86
|
-
{ placement: "aboveEditor" },
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const dockActions: DockActions = {
|
|
92
|
-
getFocusedProcessId: () => dockState.focusedProcessId,
|
|
93
|
-
isFollowEnabled: () => dockState.followEnabled,
|
|
94
|
-
setFocus(id) {
|
|
95
|
-
dockState.focusedProcessId = id;
|
|
96
|
-
if (id && dockState.visibility === "hidden")
|
|
97
|
-
dockState.visibility = "open";
|
|
98
|
-
updateWidget();
|
|
99
|
-
},
|
|
100
|
-
expand() {
|
|
101
|
-
dockState.visibility = "open";
|
|
102
|
-
updateWidget();
|
|
103
|
-
},
|
|
104
|
-
collapse() {
|
|
105
|
-
dockState.visibility = "collapsed";
|
|
106
|
-
updateWidget();
|
|
107
|
-
},
|
|
108
|
-
hide() {
|
|
109
|
-
dockState.visibility = "hidden";
|
|
110
|
-
updateWidget();
|
|
111
|
-
},
|
|
112
|
-
toggle() {
|
|
113
|
-
if (dockState.visibility === "hidden") dockState.visibility = "collapsed";
|
|
114
|
-
else if (dockState.visibility === "collapsed")
|
|
115
|
-
dockState.visibility = "open";
|
|
116
|
-
else dockState.visibility = "collapsed";
|
|
117
|
-
updateWidget();
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
manager.onEvent((event) => {
|
|
122
|
-
if (event.type === "process_started") {
|
|
123
|
-
if (dockState.followEnabled && dockState.visibility === "hidden") {
|
|
124
|
-
dockState.visibility = "collapsed";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (event.type === "process_ended") {
|
|
129
|
-
if (dockState.focusedProcessId === event.info.id) {
|
|
130
|
-
dockState.focusedProcessId = null;
|
|
131
|
-
}
|
|
132
|
-
const running = manager.list().filter((p) => LIVE_STATUSES.has(p.status));
|
|
133
|
-
if (
|
|
134
|
-
running.length === 0 &&
|
|
135
|
-
config.follow.autoHideOnFinish &&
|
|
136
|
-
dockState.followEnabled
|
|
137
|
-
) {
|
|
138
|
-
dockState.visibility = "hidden";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
updateWidget();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
146
|
-
if (logDockComponent) {
|
|
147
|
-
logDockComponent.dispose();
|
|
148
|
-
logDockComponent = null;
|
|
149
|
-
logDockComponentTui = null;
|
|
150
|
-
}
|
|
151
|
-
activeCtx = ctx;
|
|
152
|
-
updateWidget();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
156
|
-
activeCtx = null;
|
|
157
|
-
if (logDockComponent) {
|
|
158
|
-
logDockComponent.dispose();
|
|
159
|
-
logDockComponent = null;
|
|
160
|
-
logDockComponentTui = null;
|
|
161
|
-
}
|
|
162
|
-
ctx.ui.setWidget(STATUS_WIDGET_ID, undefined);
|
|
163
|
-
ctx.ui.setWidget(LOG_DOCK_WIDGET_ID, undefined);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return { update: updateWidget, dockActions };
|
|
167
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { ProcessInfo } from "../../constants";
|
|
4
|
-
|
|
5
|
-
function formatProcessStatus(
|
|
6
|
-
proc: ProcessInfo,
|
|
7
|
-
theme: ExtensionContext["ui"]["theme"],
|
|
8
|
-
): string {
|
|
9
|
-
const name =
|
|
10
|
-
proc.name.length > 20 ? `${proc.name.slice(0, 17)}...` : proc.name;
|
|
11
|
-
|
|
12
|
-
switch (proc.status) {
|
|
13
|
-
case "running":
|
|
14
|
-
return `${theme.fg("accent", name)} ${theme.fg("dim", "running")}`;
|
|
15
|
-
case "terminating":
|
|
16
|
-
return `${theme.fg("warning", name)} ${theme.fg("dim", "terminating")}`;
|
|
17
|
-
case "terminate_timeout":
|
|
18
|
-
return `${theme.fg("error", name)} ${theme.fg("error", "terminate_timeout")}`;
|
|
19
|
-
case "killed":
|
|
20
|
-
return `${theme.fg("warning", name)} ${theme.fg("dim", "killed")}`;
|
|
21
|
-
case "exited":
|
|
22
|
-
if (proc.success) {
|
|
23
|
-
return `${theme.fg("dim", name)} ${theme.fg("success", "done")}`;
|
|
24
|
-
}
|
|
25
|
-
return `${theme.fg("error", name)} ${theme.fg("error", `exit(${proc.exitCode ?? "?"})`)}`;
|
|
26
|
-
default:
|
|
27
|
-
return `${theme.fg("dim", name)} ${theme.fg("dim", proc.status)}`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function renderStatusWidget(
|
|
32
|
-
processes: ProcessInfo[],
|
|
33
|
-
theme: ExtensionContext["ui"]["theme"],
|
|
34
|
-
maxWidth?: number,
|
|
35
|
-
): string[] {
|
|
36
|
-
if (processes.length === 0) return [];
|
|
37
|
-
|
|
38
|
-
const aliveish = processes.filter(
|
|
39
|
-
(p) =>
|
|
40
|
-
p.status === "running" ||
|
|
41
|
-
p.status === "terminating" ||
|
|
42
|
-
p.status === "terminate_timeout",
|
|
43
|
-
);
|
|
44
|
-
const finished = processes.filter(
|
|
45
|
-
(p) =>
|
|
46
|
-
p.status !== "running" &&
|
|
47
|
-
p.status !== "terminating" &&
|
|
48
|
-
p.status !== "terminate_timeout",
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const allProcs: ProcessInfo[] = [
|
|
52
|
-
...aliveish,
|
|
53
|
-
...finished.sort((a, b) => (b.endTime ?? 0) - (a.endTime ?? 0)),
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const prefix = theme.fg("dim", "processes: ");
|
|
57
|
-
const prefixLen = visibleWidth(prefix);
|
|
58
|
-
const separator = theme.fg("dim", " | ");
|
|
59
|
-
const separatorLen = visibleWidth(separator);
|
|
60
|
-
const effectiveMax = maxWidth ?? 200;
|
|
61
|
-
|
|
62
|
-
const parts: string[] = [];
|
|
63
|
-
let currentLen = prefixLen;
|
|
64
|
-
let includedCount = 0;
|
|
65
|
-
|
|
66
|
-
for (const proc of allProcs) {
|
|
67
|
-
const formatted = formatProcessStatus(proc, theme);
|
|
68
|
-
const formattedLen = visibleWidth(formatted);
|
|
69
|
-
const remaining = allProcs.length - includedCount - 1;
|
|
70
|
-
const needed =
|
|
71
|
-
includedCount > 0 ? separatorLen + formattedLen : formattedLen;
|
|
72
|
-
|
|
73
|
-
let reservedForSuffix = 0;
|
|
74
|
-
if (remaining > 0) {
|
|
75
|
-
const suffixText = `+${remaining} more`;
|
|
76
|
-
reservedForSuffix = separatorLen + visibleWidth(suffixText);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
currentLen + needed + reservedForSuffix > effectiveMax &&
|
|
81
|
-
includedCount > 0
|
|
82
|
-
) {
|
|
83
|
-
const hiddenCount = allProcs.length - includedCount;
|
|
84
|
-
if (hiddenCount > 0) parts.push(theme.fg("dim", `+${hiddenCount} more`));
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
parts.push(formatted);
|
|
89
|
-
currentLen += needed;
|
|
90
|
-
includedCount++;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (includedCount === 0 && allProcs.length > 0) {
|
|
94
|
-
parts.push(formatProcessStatus(allProcs[0], theme));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (parts.length === 0) return [];
|
|
98
|
-
|
|
99
|
-
const line = prefix + parts.join(separator);
|
|
100
|
-
return [
|
|
101
|
-
visibleWidth(line) > effectiveMax
|
|
102
|
-
? truncateToWidth(line, effectiveMax)
|
|
103
|
-
: line,
|
|
104
|
-
];
|
|
105
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type DockVisibility = "hidden" | "collapsed" | "open";
|
|
2
|
-
|
|
3
|
-
export interface DockState {
|
|
4
|
-
visibility: DockVisibility;
|
|
5
|
-
followEnabled: boolean;
|
|
6
|
-
focusedProcessId: string | null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface DockActions {
|
|
10
|
-
getFocusedProcessId(): string | null;
|
|
11
|
-
isFollowEnabled(): boolean;
|
|
12
|
-
setFocus(id: string | null): void;
|
|
13
|
-
expand(): void;
|
|
14
|
-
collapse(): void;
|
|
15
|
-
hide(): void;
|
|
16
|
-
toggle(): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const STATUS_WIDGET_ID = "processes-status";
|
|
20
|
-
export const LOG_DOCK_WIDGET_ID = "processes-dock";
|
package/src/index.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { setupProcessesCommands } from "./commands";
|
|
3
|
-
import { registerProcessesSettings } from "./commands/settings";
|
|
4
|
-
import { configLoader } from "./config";
|
|
5
|
-
import { setupProcessesHooks } from "./hooks";
|
|
6
|
-
import { ProcessManager } from "./manager";
|
|
7
|
-
import { setupProcessesTools } from "./tools";
|
|
8
|
-
|
|
9
|
-
export default async function (pi: ExtensionAPI) {
|
|
10
|
-
if (process.platform === "win32") {
|
|
11
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
12
|
-
if (!ctx.hasUI) return;
|
|
13
|
-
ctx.ui.notify("processes extension not available on Windows", "warning");
|
|
14
|
-
});
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
await configLoader.load();
|
|
19
|
-
const manager = new ProcessManager({
|
|
20
|
-
getConfiguredShellPath: () => configLoader.getConfig().execution.shellPath,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const config = configLoader.getConfig();
|
|
24
|
-
|
|
25
|
-
const { update: updateWidget, dockActions } = setupProcessesHooks(
|
|
26
|
-
pi,
|
|
27
|
-
manager,
|
|
28
|
-
config,
|
|
29
|
-
);
|
|
30
|
-
setupProcessesCommands(pi, manager, dockActions);
|
|
31
|
-
setupProcessesTools(pi, manager);
|
|
32
|
-
registerProcessesSettings(pi, () => {
|
|
33
|
-
updateWidget();
|
|
34
|
-
});
|
|
35
|
-
}
|
package/src/manager.test.ts
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import type { ManagerEvent } from "./constants";
|
|
3
|
-
import { ProcessManager } from "./manager";
|
|
4
|
-
|
|
5
|
-
function waitForEnd(manager: ProcessManager, id: string): Promise<void> {
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
const unsub = manager.onEvent((e) => {
|
|
8
|
-
if (e.type === "process_ended" && e.info.id === id) {
|
|
9
|
-
unsub();
|
|
10
|
-
resolve();
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function collectEvents(manager: ProcessManager): ManagerEvent[] {
|
|
17
|
-
const events: ManagerEvent[] = [];
|
|
18
|
-
// Unsubscribe not stored; manager.cleanup() in afterEach clears all listeners.
|
|
19
|
-
manager.onEvent((e) => events.push(e));
|
|
20
|
-
return events;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("process_output_changed", () => {
|
|
24
|
-
let manager: ProcessManager;
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
manager.cleanup();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("emits process_output_changed on stdout", async () => {
|
|
31
|
-
manager = new ProcessManager();
|
|
32
|
-
const events = collectEvents(manager);
|
|
33
|
-
const info = manager.start("test", "echo hello", "/tmp");
|
|
34
|
-
await waitForEnd(manager, info.id);
|
|
35
|
-
|
|
36
|
-
const outputEvents = events.filter(
|
|
37
|
-
(e) => e.type === "process_output_changed",
|
|
38
|
-
);
|
|
39
|
-
expect(outputEvents.length).toBeGreaterThanOrEqual(1);
|
|
40
|
-
expect(outputEvents[0]).toEqual({
|
|
41
|
-
type: "process_output_changed",
|
|
42
|
-
id: info.id,
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("emits process_output_changed on stderr", async () => {
|
|
47
|
-
manager = new ProcessManager();
|
|
48
|
-
const events = collectEvents(manager);
|
|
49
|
-
const info = manager.start("test", "echo err >&2", "/tmp");
|
|
50
|
-
await waitForEnd(manager, info.id);
|
|
51
|
-
|
|
52
|
-
const outputEvents = events.filter(
|
|
53
|
-
(e) => e.type === "process_output_changed",
|
|
54
|
-
);
|
|
55
|
-
expect(outputEvents.length).toBeGreaterThanOrEqual(1);
|
|
56
|
-
expect(outputEvents[0]).toEqual({
|
|
57
|
-
type: "process_output_changed",
|
|
58
|
-
id: info.id,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("throttles rapid output", async () => {
|
|
63
|
-
manager = new ProcessManager();
|
|
64
|
-
const events = collectEvents(manager);
|
|
65
|
-
const info = manager.start("test", "seq 1 200", "/tmp");
|
|
66
|
-
await waitForEnd(manager, info.id);
|
|
67
|
-
|
|
68
|
-
const outputEvents = events.filter(
|
|
69
|
-
(e) => e.type === "process_output_changed",
|
|
70
|
-
);
|
|
71
|
-
// Should be significantly fewer than 200 due to throttling
|
|
72
|
-
expect(outputEvents.length).toBeGreaterThanOrEqual(1);
|
|
73
|
-
expect(outputEvents.length).toBeLessThan(50);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("stdout and stderr share one throttle bucket", async () => {
|
|
77
|
-
manager = new ProcessManager();
|
|
78
|
-
|
|
79
|
-
// Dual-stream burst: writes to both stdout and stderr rapidly
|
|
80
|
-
const events2 = collectEvents(manager);
|
|
81
|
-
const info2 = manager.start(
|
|
82
|
-
"dual",
|
|
83
|
-
"bash -c 'for i in $(seq 1 50); do echo out$i; echo err$i >&2; done'",
|
|
84
|
-
"/tmp",
|
|
85
|
-
);
|
|
86
|
-
await waitForEnd(manager, info2.id);
|
|
87
|
-
const dualCount = events2.filter(
|
|
88
|
-
(e) => e.type === "process_output_changed",
|
|
89
|
-
).length;
|
|
90
|
-
|
|
91
|
-
// Both streams share one throttle bucket, so total events should be low
|
|
92
|
-
expect(dualCount).toBeLessThan(30);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("trailing emit fires after burst ends", async () => {
|
|
96
|
-
manager = new ProcessManager();
|
|
97
|
-
const events = collectEvents(manager);
|
|
98
|
-
const info = manager.start("test", "seq 1 100", "/tmp");
|
|
99
|
-
await waitForEnd(manager, info.id);
|
|
100
|
-
|
|
101
|
-
// There should be at least one output event, and a process_ended event
|
|
102
|
-
const outputEvents = events.filter(
|
|
103
|
-
(e) => e.type === "process_output_changed",
|
|
104
|
-
);
|
|
105
|
-
const endEvents = events.filter((e) => e.type === "process_ended");
|
|
106
|
-
expect(outputEvents.length).toBeGreaterThanOrEqual(1);
|
|
107
|
-
expect(endEvents.length).toBe(1);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("final output event before process_ended", async () => {
|
|
111
|
-
manager = new ProcessManager();
|
|
112
|
-
const events = collectEvents(manager);
|
|
113
|
-
const info = manager.start("test", "echo hello", "/tmp");
|
|
114
|
-
await waitForEnd(manager, info.id);
|
|
115
|
-
|
|
116
|
-
let lastOutputIdx = -1;
|
|
117
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
118
|
-
if (events[i].type === "process_output_changed") {
|
|
119
|
-
lastOutputIdx = i;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const endIdx = events.findIndex((e) => e.type === "process_ended");
|
|
124
|
-
|
|
125
|
-
expect(lastOutputIdx).toBeGreaterThanOrEqual(0);
|
|
126
|
-
expect(endIdx).toBeGreaterThanOrEqual(0);
|
|
127
|
-
expect(lastOutputIdx).toBeLessThan(endIdx);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("no output events for silent process", async () => {
|
|
131
|
-
manager = new ProcessManager();
|
|
132
|
-
const events = collectEvents(manager);
|
|
133
|
-
const info = manager.start("test", "true", "/tmp");
|
|
134
|
-
await waitForEnd(manager, info.id);
|
|
135
|
-
|
|
136
|
-
// Wait a bit for any stale trailing emits
|
|
137
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
138
|
-
|
|
139
|
-
const outputEvents = events.filter(
|
|
140
|
-
(e) => e.type === "process_output_changed",
|
|
141
|
-
);
|
|
142
|
-
expect(outputEvents.length).toBe(0);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("no stale events after clearFinished", async () => {
|
|
146
|
-
manager = new ProcessManager();
|
|
147
|
-
const info = manager.start("test", "seq 1 50", "/tmp");
|
|
148
|
-
await waitForEnd(manager, info.id);
|
|
149
|
-
|
|
150
|
-
manager.clearFinished();
|
|
151
|
-
|
|
152
|
-
const lateEvents: ManagerEvent[] = [];
|
|
153
|
-
manager.onEvent((e) => lateEvents.push(e));
|
|
154
|
-
|
|
155
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
156
|
-
|
|
157
|
-
const staleOutput = lateEvents.filter(
|
|
158
|
-
(e) => e.type === "process_output_changed",
|
|
159
|
-
);
|
|
160
|
-
expect(staleOutput.length).toBe(0);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("events carry correct process id with multiple processes", async () => {
|
|
164
|
-
manager = new ProcessManager();
|
|
165
|
-
const events = collectEvents(manager);
|
|
166
|
-
|
|
167
|
-
const info1 = manager.start("proc1", "echo one", "/tmp");
|
|
168
|
-
const info2 = manager.start("proc2", "echo two", "/tmp");
|
|
169
|
-
|
|
170
|
-
await Promise.all([
|
|
171
|
-
waitForEnd(manager, info1.id),
|
|
172
|
-
waitForEnd(manager, info2.id),
|
|
173
|
-
]);
|
|
174
|
-
|
|
175
|
-
const outputEvents = events.filter(
|
|
176
|
-
(e) => e.type === "process_output_changed",
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
for (const e of outputEvents) {
|
|
180
|
-
if (e.type === "process_output_changed") {
|
|
181
|
-
expect([info1.id, info2.id]).toContain(e.id);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Both processes should have at least one output event
|
|
186
|
-
const ids = new Set(
|
|
187
|
-
outputEvents
|
|
188
|
-
.filter(
|
|
189
|
-
(e): e is Extract<ManagerEvent, { type: "process_output_changed" }> =>
|
|
190
|
-
e.type === "process_output_changed",
|
|
191
|
-
)
|
|
192
|
-
.map((e) => e.id),
|
|
193
|
-
);
|
|
194
|
-
expect(ids.has(info1.id)).toBe(true);
|
|
195
|
-
expect(ids.has(info2.id)).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe("process_watch_matched", () => {
|
|
200
|
-
let manager: ProcessManager;
|
|
201
|
-
|
|
202
|
-
afterEach(() => {
|
|
203
|
-
manager.cleanup();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("fires once by default on first matching line", async () => {
|
|
207
|
-
manager = new ProcessManager();
|
|
208
|
-
const events = collectEvents(manager);
|
|
209
|
-
|
|
210
|
-
const info = manager.start(
|
|
211
|
-
"watch-once",
|
|
212
|
-
"bash -c 'echo ready; echo ready; echo ready'",
|
|
213
|
-
"/tmp",
|
|
214
|
-
{
|
|
215
|
-
logWatches: [{ pattern: "ready" }],
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
await waitForEnd(manager, info.id);
|
|
220
|
-
|
|
221
|
-
const matches = events.filter((e) => e.type === "process_watch_matched");
|
|
222
|
-
expect(matches).toHaveLength(1);
|
|
223
|
-
|
|
224
|
-
const first = matches[0];
|
|
225
|
-
if (first.type === "process_watch_matched") {
|
|
226
|
-
expect(first.match.processId).toBe(info.id);
|
|
227
|
-
expect(first.match.source).toBe("stdout");
|
|
228
|
-
expect(first.match.watch.repeat).toBe(false);
|
|
229
|
-
expect(first.match.line).toBe("ready");
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("supports repeat watches", async () => {
|
|
234
|
-
manager = new ProcessManager();
|
|
235
|
-
const events = collectEvents(manager);
|
|
236
|
-
|
|
237
|
-
const info = manager.start(
|
|
238
|
-
"watch-repeat",
|
|
239
|
-
"bash -c 'echo done; echo done; echo done'",
|
|
240
|
-
"/tmp",
|
|
241
|
-
{
|
|
242
|
-
logWatches: [{ pattern: "done", repeat: true }],
|
|
243
|
-
},
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
await waitForEnd(manager, info.id);
|
|
247
|
-
|
|
248
|
-
const matches = events.filter((e) => e.type === "process_watch_matched");
|
|
249
|
-
expect(matches).toHaveLength(3);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("respects stream scoping", async () => {
|
|
253
|
-
manager = new ProcessManager();
|
|
254
|
-
const events = collectEvents(manager);
|
|
255
|
-
|
|
256
|
-
const info = manager.start(
|
|
257
|
-
"watch-stream",
|
|
258
|
-
"bash -c 'echo out; echo err >&2'",
|
|
259
|
-
"/tmp",
|
|
260
|
-
{
|
|
261
|
-
logWatches: [{ pattern: "err", stream: "stderr" }],
|
|
262
|
-
},
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
await waitForEnd(manager, info.id);
|
|
266
|
-
|
|
267
|
-
const matches = events.filter((e) => e.type === "process_watch_matched");
|
|
268
|
-
expect(matches).toHaveLength(1);
|
|
269
|
-
|
|
270
|
-
const match = matches[0];
|
|
271
|
-
if (match.type === "process_watch_matched") {
|
|
272
|
-
expect(match.match.source).toBe("stderr");
|
|
273
|
-
expect(match.match.line).toBe("err");
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("stream both matches stdout and stderr", async () => {
|
|
278
|
-
manager = new ProcessManager();
|
|
279
|
-
const events = collectEvents(manager);
|
|
280
|
-
|
|
281
|
-
const info = manager.start(
|
|
282
|
-
"watch-both",
|
|
283
|
-
"bash -c 'echo marker; echo marker >&2'",
|
|
284
|
-
"/tmp",
|
|
285
|
-
{
|
|
286
|
-
logWatches: [{ pattern: "marker", stream: "both", repeat: true }],
|
|
287
|
-
},
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
await waitForEnd(manager, info.id);
|
|
291
|
-
|
|
292
|
-
const matches = events.filter((e) => e.type === "process_watch_matched");
|
|
293
|
-
expect(matches).toHaveLength(2);
|
|
294
|
-
|
|
295
|
-
const sources = new Set(
|
|
296
|
-
matches
|
|
297
|
-
.filter(
|
|
298
|
-
(e): e is Extract<ManagerEvent, { type: "process_watch_matched" }> =>
|
|
299
|
-
e.type === "process_watch_matched",
|
|
300
|
-
)
|
|
301
|
-
.map((e) => e.match.source),
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
expect(sources.has("stdout")).toBe(true);
|
|
305
|
-
expect(sources.has("stderr")).toBe(true);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("matches trailing partial line at process end", async () => {
|
|
309
|
-
manager = new ProcessManager();
|
|
310
|
-
const events = collectEvents(manager);
|
|
311
|
-
|
|
312
|
-
const info = manager.start("watch-trailing", "printf ready", "/tmp", {
|
|
313
|
-
logWatches: [{ pattern: "ready" }],
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
await waitForEnd(manager, info.id);
|
|
317
|
-
|
|
318
|
-
const matches = events.filter((e) => e.type === "process_watch_matched");
|
|
319
|
-
expect(matches).toHaveLength(1);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("throws for invalid watch regex", () => {
|
|
323
|
-
manager = new ProcessManager();
|
|
324
|
-
|
|
325
|
-
expect(() =>
|
|
326
|
-
manager.start("bad-watch", "echo ok", "/tmp", {
|
|
327
|
-
logWatches: [{ pattern: "(" }],
|
|
328
|
-
}),
|
|
329
|
-
).toThrowError(/Invalid log watch pattern/);
|
|
330
|
-
});
|
|
331
|
-
});
|