@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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AgentToolResult,
|
|
3
|
+
defineTool,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
type Theme,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { type Component, Container, Text } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
import type { ProcessManager } from "../../../src/manager";
|
|
11
|
+
import type { NotificationRegistry } from "../notifications/registry";
|
|
12
|
+
import { type ClearDetails, executeClear, formatClearDetails } from "./clear";
|
|
13
|
+
import * as clearRender from "./clear/render";
|
|
14
|
+
import { ToolLayout } from "./components";
|
|
15
|
+
import { executeList, formatListDetails, type ListDetails } from "./list";
|
|
16
|
+
import * as listRender from "./list/render";
|
|
17
|
+
import { executeOutput, type OutputDetails } from "./output";
|
|
18
|
+
import * as outputRender from "./output/render";
|
|
19
|
+
import { ProcessesParams, type ProcessesParamsType } from "./schema";
|
|
20
|
+
import { executeStart, formatStartDetails, type StartDetails } from "./start";
|
|
21
|
+
import * as startRender from "./start/render";
|
|
22
|
+
import { executeStop, formatStopDetails, type StopDetails } from "./stop";
|
|
23
|
+
import * as stopRender from "./stop/render";
|
|
24
|
+
import {
|
|
25
|
+
executeUpdate,
|
|
26
|
+
formatUpdateDetails,
|
|
27
|
+
type UpdateDetails,
|
|
28
|
+
} from "./update";
|
|
29
|
+
import * as updateRender from "./update/render";
|
|
30
|
+
import { executeWrite, formatWriteDetails, type WriteDetails } from "./write";
|
|
31
|
+
import * as writeRender from "./write/render";
|
|
32
|
+
|
|
33
|
+
type ProcessDetails =
|
|
34
|
+
| StartDetails
|
|
35
|
+
| ListDetails
|
|
36
|
+
| StopDetails
|
|
37
|
+
| OutputDetails
|
|
38
|
+
| WriteDetails
|
|
39
|
+
| UpdateDetails
|
|
40
|
+
| ClearDetails;
|
|
41
|
+
|
|
42
|
+
export function registerProcessTool(
|
|
43
|
+
pi: ExtensionAPI,
|
|
44
|
+
manager: ProcessManager,
|
|
45
|
+
notifications: NotificationRegistry,
|
|
46
|
+
): void {
|
|
47
|
+
pi.registerTool(
|
|
48
|
+
defineTool({
|
|
49
|
+
name: "process",
|
|
50
|
+
label: "Process",
|
|
51
|
+
description:
|
|
52
|
+
"Start, list, stop, write to stdin, update, clear, and inspect output of long-running background processes.",
|
|
53
|
+
promptSnippet:
|
|
54
|
+
"Manage long-running background processes: start, list, stop, write to stdin, update watches, clear finished entries, and inspect recent output. After starting a process, do not wait - notifications bring you back on exit and on log matches.",
|
|
55
|
+
promptGuidelines: [
|
|
56
|
+
"process tool: use process start for long-running commands (dev servers, watchers, builds) instead of shell background patterns like &, nohup, or setsid; give each process a specific name and check process list first when a duplicate would be noisy.",
|
|
57
|
+
"process tool: after process start, do not sleep, poll, or hold your turn. End your turn or move on. The process notifies you on exit (success, failure, killed) and on notify.logMatches matches, which brings you back.",
|
|
58
|
+
"process tool: use notify.logMatches to get brought back on readiness or error signals instead of polling process output. If a watch is too noisy, use process update (watches.mode append/replace/remove/clear) to fix it without restarting.",
|
|
59
|
+
"process tool: for the full lifecycle (start, list, output, update, write, stop, clear), notify options, use cases, and noisy-watch handling, read the pi-processes skill.",
|
|
60
|
+
],
|
|
61
|
+
parameters: ProcessesParams,
|
|
62
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
63
|
+
if (params.action === "output") {
|
|
64
|
+
const result = executeOutput(params, manager);
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: result.content }],
|
|
67
|
+
details: result.details,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const details = await execute(params, manager, ctx, notifications);
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: formatDetails(details) }],
|
|
74
|
+
details,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
renderCall: renderProcessCall,
|
|
78
|
+
renderResult: renderProcessResult,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function execute(
|
|
84
|
+
params: ProcessesParamsType,
|
|
85
|
+
manager: ProcessManager,
|
|
86
|
+
ctx: ExtensionContext,
|
|
87
|
+
notifications: NotificationRegistry,
|
|
88
|
+
): Promise<ProcessDetails> {
|
|
89
|
+
switch (params.action) {
|
|
90
|
+
case "start":
|
|
91
|
+
return executeStart(params, manager, ctx, notifications);
|
|
92
|
+
case "list":
|
|
93
|
+
return executeList(manager, params, notifications);
|
|
94
|
+
case "stop":
|
|
95
|
+
return executeStop(params, manager, notifications);
|
|
96
|
+
case "write":
|
|
97
|
+
return executeWrite(params, manager);
|
|
98
|
+
case "update":
|
|
99
|
+
return executeUpdate(params, manager, notifications);
|
|
100
|
+
case "clear":
|
|
101
|
+
return executeClear(manager);
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`unsupported process action: ${String(params.action)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderProcessCall(
|
|
108
|
+
args: ProcessesParamsType,
|
|
109
|
+
theme: Theme,
|
|
110
|
+
context?: { expanded?: boolean },
|
|
111
|
+
): Component {
|
|
112
|
+
switch (args.action) {
|
|
113
|
+
case "start":
|
|
114
|
+
return new ToolLayout().setHeader(
|
|
115
|
+
startRender.buildHeader(args, theme, context),
|
|
116
|
+
);
|
|
117
|
+
case "list":
|
|
118
|
+
return new ToolLayout().setHeader(listRender.buildHeader(args, theme));
|
|
119
|
+
case "stop":
|
|
120
|
+
return new ToolLayout().setHeader(
|
|
121
|
+
stopRender.buildHeader(args, theme, context),
|
|
122
|
+
);
|
|
123
|
+
case "output":
|
|
124
|
+
return new ToolLayout().setHeader(
|
|
125
|
+
outputRender.buildHeader(args, theme, context),
|
|
126
|
+
);
|
|
127
|
+
case "write":
|
|
128
|
+
return new ToolLayout().setHeader(
|
|
129
|
+
writeRender.buildHeader(args, theme, context),
|
|
130
|
+
);
|
|
131
|
+
case "update":
|
|
132
|
+
return new ToolLayout().setHeader(
|
|
133
|
+
updateRender.buildHeader(args, theme, context),
|
|
134
|
+
);
|
|
135
|
+
case "clear":
|
|
136
|
+
return new ToolLayout().setHeader(clearRender.buildHeader(args, theme));
|
|
137
|
+
default:
|
|
138
|
+
return fallbackContainer(theme);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderProcessResult(
|
|
143
|
+
result: AgentToolResult<ProcessDetails>,
|
|
144
|
+
options: { expanded?: boolean; isPartial?: boolean },
|
|
145
|
+
theme: Theme,
|
|
146
|
+
context?: { isError?: boolean },
|
|
147
|
+
): Component {
|
|
148
|
+
if (options.isPartial) {
|
|
149
|
+
return fallbackContainer(theme, "process running...");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (context?.isError) {
|
|
153
|
+
return fallbackContainer(theme, "process failed");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const details = result.details as ProcessDetails | undefined;
|
|
157
|
+
|
|
158
|
+
if (!details) {
|
|
159
|
+
return fallbackContainer(theme, "process completed");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const contentText = getContentText(result);
|
|
163
|
+
|
|
164
|
+
return new ToolLayout()
|
|
165
|
+
.withSectionSpacing(options.expanded)
|
|
166
|
+
.setBody(buildBody(details, contentText, options, theme))
|
|
167
|
+
.setFooter(buildFooter(details, contentText, options, theme));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getContentText(
|
|
171
|
+
result: AgentToolResult<ProcessDetails>,
|
|
172
|
+
): string | undefined {
|
|
173
|
+
const first = result.content[0];
|
|
174
|
+
if (first?.type === "text" && typeof first.text === "string") {
|
|
175
|
+
return first.text;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildBody(
|
|
181
|
+
details: ProcessDetails,
|
|
182
|
+
contentText: string | undefined,
|
|
183
|
+
options: { expanded?: boolean },
|
|
184
|
+
theme: Theme,
|
|
185
|
+
): Container {
|
|
186
|
+
switch (details.action) {
|
|
187
|
+
case "start":
|
|
188
|
+
return options.expanded
|
|
189
|
+
? startRender.buildExpanded(details, theme)
|
|
190
|
+
: startRender.buildCollapsed(details, theme);
|
|
191
|
+
case "list":
|
|
192
|
+
return options.expanded
|
|
193
|
+
? listRender.buildExpanded(details, theme)
|
|
194
|
+
: listRender.buildCollapsed(details, theme);
|
|
195
|
+
case "output":
|
|
196
|
+
return options.expanded
|
|
197
|
+
? outputRender.buildExpanded(contentText ?? "", details, theme)
|
|
198
|
+
: outputRender.buildCollapsed(contentText ?? "", details, theme);
|
|
199
|
+
case "update":
|
|
200
|
+
return options.expanded
|
|
201
|
+
? updateRender.buildExpanded(details, theme)
|
|
202
|
+
: updateRender.buildCollapsed(details, theme);
|
|
203
|
+
case "write":
|
|
204
|
+
return options.expanded
|
|
205
|
+
? writeRender.buildExpanded(details, theme)
|
|
206
|
+
: writeRender.buildCollapsed(details, theme);
|
|
207
|
+
case "stop":
|
|
208
|
+
return options.expanded
|
|
209
|
+
? stopRender.buildExpanded(details, theme)
|
|
210
|
+
: stopRender.buildCollapsed(details, theme);
|
|
211
|
+
case "clear":
|
|
212
|
+
return options.expanded
|
|
213
|
+
? clearRender.buildExpanded(details, theme)
|
|
214
|
+
: clearRender.buildCollapsed(details, theme);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildFooter(
|
|
219
|
+
details: ProcessDetails,
|
|
220
|
+
_contentText: string | undefined,
|
|
221
|
+
options: { expanded?: boolean },
|
|
222
|
+
theme: Theme,
|
|
223
|
+
): Container | null {
|
|
224
|
+
switch (details.action) {
|
|
225
|
+
case "start":
|
|
226
|
+
return startRender.buildFooter(details, options, theme);
|
|
227
|
+
case "list":
|
|
228
|
+
return listRender.buildFooter(details, options, theme);
|
|
229
|
+
case "stop":
|
|
230
|
+
return stopRender.buildFooter(details, options, theme);
|
|
231
|
+
case "output":
|
|
232
|
+
return outputRender.buildFooter(details, options, theme);
|
|
233
|
+
case "update":
|
|
234
|
+
return updateRender.buildFooter(details, options, theme);
|
|
235
|
+
case "write":
|
|
236
|
+
return writeRender.buildFooter(details, options, theme);
|
|
237
|
+
case "clear":
|
|
238
|
+
return clearRender.buildFooter(details, options, theme);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatDetails(details: ProcessDetails): string {
|
|
243
|
+
switch (details.action) {
|
|
244
|
+
case "start":
|
|
245
|
+
return formatStartDetails(details);
|
|
246
|
+
case "list":
|
|
247
|
+
return formatListDetails(details);
|
|
248
|
+
case "stop":
|
|
249
|
+
return formatStopDetails(details);
|
|
250
|
+
case "update":
|
|
251
|
+
return formatUpdateDetails(details);
|
|
252
|
+
case "write":
|
|
253
|
+
return formatWriteDetails(details);
|
|
254
|
+
case "clear":
|
|
255
|
+
return formatClearDetails(details);
|
|
256
|
+
case "output":
|
|
257
|
+
throw new Error("output action uses pre-formatted content");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function fallbackContainer(theme: Theme, message = "process"): Container {
|
|
262
|
+
const container = new Container();
|
|
263
|
+
container.addChild(new Text(theme.fg("muted", message), 0, 0));
|
|
264
|
+
return container;
|
|
265
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ProcessManager } from "../../../../src/manager";
|
|
2
|
+
import { LIVE_STATUSES, type ProcessInfo } from "../../../../src/types";
|
|
3
|
+
import type {
|
|
4
|
+
LogMatcherConfig,
|
|
5
|
+
NotificationRegistry,
|
|
6
|
+
} from "../../notifications/registry";
|
|
7
|
+
import type {
|
|
8
|
+
ProcessesParamsType,
|
|
9
|
+
ProcessListSort,
|
|
10
|
+
ProcessListStatusFilter,
|
|
11
|
+
} from "../schema";
|
|
12
|
+
import { formatProcessRuntime } from "../utils";
|
|
13
|
+
import { formatPatternsForModel } from "../watch-format";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A process plus its duration computed when the list was produced.
|
|
17
|
+
*
|
|
18
|
+
* For stopped processes this is the actual run duration (endTime - startTime).
|
|
19
|
+
* For running processes it is measured against the list call's reference time.
|
|
20
|
+
*/
|
|
21
|
+
export interface ListProcess extends ProcessInfo {
|
|
22
|
+
duration: string;
|
|
23
|
+
watches: LogMatcherConfig[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ListDetails {
|
|
27
|
+
action: "list";
|
|
28
|
+
processes: ListProcess[];
|
|
29
|
+
filters: {
|
|
30
|
+
limit: number | null;
|
|
31
|
+
sortBy: ProcessListSort;
|
|
32
|
+
statuses: ProcessListStatusFilter[];
|
|
33
|
+
};
|
|
34
|
+
counts: ProcessListCounts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProcessListCounts {
|
|
38
|
+
running: number;
|
|
39
|
+
exited: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
killed: number;
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function executeList(
|
|
46
|
+
manager: ProcessManager,
|
|
47
|
+
params: ProcessesParamsType,
|
|
48
|
+
notifications: NotificationRegistry,
|
|
49
|
+
): ListDetails {
|
|
50
|
+
const filters = resolveListFilters(params);
|
|
51
|
+
const allProcesses = manager.list();
|
|
52
|
+
const filtered = allProcesses
|
|
53
|
+
.filter((process) => matchesStatusFilter(process, filters.statuses))
|
|
54
|
+
.sort((a, b) => compareProcesses(a, b, filters.sortBy));
|
|
55
|
+
|
|
56
|
+
const limited =
|
|
57
|
+
filters.limit === null ? filtered : filtered.slice(0, filters.limit);
|
|
58
|
+
|
|
59
|
+
// Capture the reference time after listing so every process in `limited`
|
|
60
|
+
// has startTime <= now (avoids negative durations for just-spawned ones).
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const processes: ListProcess[] = limited.map((process) => ({
|
|
63
|
+
...process,
|
|
64
|
+
duration: formatProcessRuntime(process, now),
|
|
65
|
+
watches: LIVE_STATUSES.has(process.status)
|
|
66
|
+
? (notifications.getWatchState(process.id)?.logMatches ?? [])
|
|
67
|
+
: [],
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
action: "list",
|
|
72
|
+
processes,
|
|
73
|
+
filters,
|
|
74
|
+
counts: countProcesses(processes),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatListDetails(details: ListDetails): string {
|
|
79
|
+
if (details.processes.length === 0) {
|
|
80
|
+
return "No matching background processes.";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return details.processes
|
|
84
|
+
.map(
|
|
85
|
+
(process) =>
|
|
86
|
+
`${process.id}\t${process.status}\t${process.name}\t${process.command}\t${process.stdoutFile}\t${process.stderrFile}\tpatterns=${formatPatternsForModel(process.watches)}`,
|
|
87
|
+
)
|
|
88
|
+
.join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveListFilters(
|
|
92
|
+
params: ProcessesParamsType,
|
|
93
|
+
): ListDetails["filters"] {
|
|
94
|
+
return {
|
|
95
|
+
limit: params.limit && params.limit > 0 ? Math.floor(params.limit) : null,
|
|
96
|
+
sortBy: params.sortBy ?? "startTime_desc",
|
|
97
|
+
statuses:
|
|
98
|
+
params.statuses && params.statuses.length > 0 ? params.statuses : ["all"],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function matchesStatusFilter(
|
|
103
|
+
process: ProcessInfo,
|
|
104
|
+
filters: ProcessListStatusFilter[],
|
|
105
|
+
): boolean {
|
|
106
|
+
if (filters.includes("all")) return true;
|
|
107
|
+
|
|
108
|
+
return filters.some((filter) => {
|
|
109
|
+
if (filter === "finished") {
|
|
110
|
+
return process.status === "exited" && process.success === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (filter === "failed") {
|
|
114
|
+
return (
|
|
115
|
+
process.status === "terminate_timeout" ||
|
|
116
|
+
(process.status === "exited" && process.success === false)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return process.status === filter;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function compareProcesses(
|
|
125
|
+
a: ProcessInfo,
|
|
126
|
+
b: ProcessInfo,
|
|
127
|
+
sortBy: ProcessListSort,
|
|
128
|
+
): number {
|
|
129
|
+
switch (sortBy) {
|
|
130
|
+
case "startTime_asc":
|
|
131
|
+
return a.startTime - b.startTime;
|
|
132
|
+
case "name_asc":
|
|
133
|
+
return a.name.localeCompare(b.name);
|
|
134
|
+
case "name_desc":
|
|
135
|
+
return b.name.localeCompare(a.name);
|
|
136
|
+
case "status_asc":
|
|
137
|
+
return a.status.localeCompare(b.status);
|
|
138
|
+
case "startTime_desc":
|
|
139
|
+
return b.startTime - a.startTime;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function countProcesses(processes: ProcessInfo[]): ProcessListCounts {
|
|
144
|
+
return {
|
|
145
|
+
running: processes.filter((process) => process.status === "running").length,
|
|
146
|
+
exited: processes.filter(
|
|
147
|
+
(process) => process.status === "exited" && process.success === true,
|
|
148
|
+
).length,
|
|
149
|
+
failed: processes.filter(
|
|
150
|
+
(process) =>
|
|
151
|
+
process.status === "terminate_timeout" ||
|
|
152
|
+
(process.status === "exited" && process.success === false),
|
|
153
|
+
).length,
|
|
154
|
+
killed: processes.filter((process) => process.status === "killed").length,
|
|
155
|
+
total: processes.length,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatTimestamp, shortenPath } from "../../../../src/utils";
|
|
4
|
+
import { LinesComponent } from "../../../shared/ui";
|
|
5
|
+
import { truncateToWidth } from "../../utils/truncate";
|
|
6
|
+
import { formatPatternForDisplay, ProcessActionTitle } from "../components";
|
|
7
|
+
import type {
|
|
8
|
+
ProcessesParamsType,
|
|
9
|
+
ProcessListSort,
|
|
10
|
+
ProcessListStatusFilter,
|
|
11
|
+
} from "../schema";
|
|
12
|
+
import { formatColoredProcessStatus, formatCount } from "../utils";
|
|
13
|
+
import type { ListDetails, ListProcess, ProcessListCounts } from ".";
|
|
14
|
+
|
|
15
|
+
type CountStatusItem = {
|
|
16
|
+
filter: ProcessListStatusFilter;
|
|
17
|
+
countKey: keyof Pick<
|
|
18
|
+
ProcessListCounts,
|
|
19
|
+
"running" | "exited" | "failed" | "killed"
|
|
20
|
+
>;
|
|
21
|
+
label: string;
|
|
22
|
+
tone: "success" | "warning" | "error" | "muted";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const COUNT_STATUS_ITEMS: CountStatusItem[] = [
|
|
26
|
+
{
|
|
27
|
+
filter: "running",
|
|
28
|
+
countKey: "running",
|
|
29
|
+
label: "running",
|
|
30
|
+
tone: "success",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
filter: "finished",
|
|
34
|
+
countKey: "exited",
|
|
35
|
+
label: "exited",
|
|
36
|
+
tone: "muted",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
filter: "failed",
|
|
40
|
+
countKey: "failed",
|
|
41
|
+
label: "failed",
|
|
42
|
+
tone: "error",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
filter: "killed",
|
|
46
|
+
countKey: "killed",
|
|
47
|
+
label: "killed",
|
|
48
|
+
tone: "warning",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function buildHeader(
|
|
53
|
+
args: ProcessesParamsType,
|
|
54
|
+
theme: Theme,
|
|
55
|
+
): Container {
|
|
56
|
+
const header = new Container();
|
|
57
|
+
header.addChild(
|
|
58
|
+
new ProcessActionTitle("list", theme, formatListHeaderSuffix(args, theme)),
|
|
59
|
+
);
|
|
60
|
+
return header;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildExpanded(details: ListDetails, theme: Theme): Container {
|
|
64
|
+
const container = new Container();
|
|
65
|
+
container.addChild(
|
|
66
|
+
new LinesComponent((width) =>
|
|
67
|
+
formatExpandedProcessLines(details.processes, theme, width),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
return container;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildCollapsed(details: ListDetails, theme: Theme): Container {
|
|
74
|
+
const container = new Container();
|
|
75
|
+
|
|
76
|
+
for (const process of details.processes.slice(0, 2)) {
|
|
77
|
+
const parts = [
|
|
78
|
+
process.name,
|
|
79
|
+
theme.fg("accent", process.id),
|
|
80
|
+
`pid ${process.pid}`,
|
|
81
|
+
formatColoredProcessStatus(process, theme),
|
|
82
|
+
theme.fg(
|
|
83
|
+
"muted",
|
|
84
|
+
`${process.watches.length} ${process.watches.length === 1 ? "watch" : "watches"}`,
|
|
85
|
+
),
|
|
86
|
+
];
|
|
87
|
+
container.addChild(new Text(parts.join(" "), 0, 0));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return container;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildFooter(
|
|
94
|
+
details: ListDetails,
|
|
95
|
+
_options: { expanded?: boolean },
|
|
96
|
+
theme: Theme,
|
|
97
|
+
): Container {
|
|
98
|
+
const footer = new Container();
|
|
99
|
+
footer.addChild(new Text(formatCounts(details, theme), 0, 0));
|
|
100
|
+
return footer;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatListHeaderSuffix(
|
|
104
|
+
args: ProcessesParamsType,
|
|
105
|
+
theme: Theme,
|
|
106
|
+
): string {
|
|
107
|
+
const statuses = normalizeStatuses(args.statuses);
|
|
108
|
+
const parts = [
|
|
109
|
+
`${theme.fg("muted", "sorted")} ${theme.fg("accent", formatSort(args.sortBy ?? "startTime_desc"))}`,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (args.limit) {
|
|
113
|
+
parts.unshift(
|
|
114
|
+
`${theme.fg("muted", "limit")} ${theme.fg("accent", String(args.limit))}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!statuses.includes("all")) {
|
|
119
|
+
parts.push(
|
|
120
|
+
`${theme.fg("muted", "statuses")} ${theme.fg("accent", statuses.join(", "))}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return parts.join(theme.fg("dim", " / "));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeStatuses(
|
|
128
|
+
statuses: ProcessListStatusFilter[] | undefined,
|
|
129
|
+
): ProcessListStatusFilter[] {
|
|
130
|
+
return statuses && statuses.length > 0 ? statuses : ["all"];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatSort(sort: ProcessListSort): string {
|
|
134
|
+
switch (sort) {
|
|
135
|
+
case "startTime_desc":
|
|
136
|
+
return "newest first";
|
|
137
|
+
case "startTime_asc":
|
|
138
|
+
return "oldest first";
|
|
139
|
+
case "name_asc":
|
|
140
|
+
return "name A-Z";
|
|
141
|
+
case "name_desc":
|
|
142
|
+
return "name Z-A";
|
|
143
|
+
case "status_asc":
|
|
144
|
+
return "status A-Z";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatCounts(details: ListDetails, theme: Theme): string {
|
|
149
|
+
if (details.processes.length === 0) {
|
|
150
|
+
return "no process running";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const statuses = details.filters.statuses;
|
|
154
|
+
const showAll = statuses.includes("all");
|
|
155
|
+
|
|
156
|
+
return COUNT_STATUS_ITEMS.filter(
|
|
157
|
+
(item) => showAll || statuses.includes(item.filter),
|
|
158
|
+
)
|
|
159
|
+
.map((item) =>
|
|
160
|
+
theme.fg(
|
|
161
|
+
item.tone,
|
|
162
|
+
`${formatCount(details.counts[item.countKey], "process")} ${item.label}`,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
.join(theme.fg("dim", " / "));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatExpandedProcessLines(
|
|
169
|
+
processes: ListProcess[],
|
|
170
|
+
theme: Theme,
|
|
171
|
+
width: number,
|
|
172
|
+
): string[] {
|
|
173
|
+
if (processes.length === 0) {
|
|
174
|
+
return ["No matching background processes."];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (const process of processes) {
|
|
180
|
+
if (lines.length > 0) lines.push("");
|
|
181
|
+
|
|
182
|
+
const summary = [
|
|
183
|
+
theme.bold(theme.fg("text", formatPatternForDisplay(process.name))),
|
|
184
|
+
`${theme.fg("muted", "pid:")} ${process.pid}`,
|
|
185
|
+
formatColoredProcessStatus(process, theme),
|
|
186
|
+
theme.fg("muted", process.duration),
|
|
187
|
+
theme.fg("muted", `(${formatTimestamp(process.startTime)})`),
|
|
188
|
+
theme.fg("accent", process.id),
|
|
189
|
+
].join(" ");
|
|
190
|
+
lines.push(fitStyledLine(summary, width));
|
|
191
|
+
|
|
192
|
+
const command = `${theme.fg("muted", " $")} ${theme.fg("text", formatPatternForDisplay(process.command))}`;
|
|
193
|
+
lines.push(fitStyledLine(command, width));
|
|
194
|
+
const cwd = `${theme.fg("muted", " cwd")} ${theme.fg("text", formatPatternForDisplay(shortenPath(process.cwd)))}`;
|
|
195
|
+
lines.push(fitStyledLine(cwd, width));
|
|
196
|
+
|
|
197
|
+
for (const matcher of process.watches) {
|
|
198
|
+
const stream = matcher.stream ?? "both";
|
|
199
|
+
const watch = `${theme.fg("muted", " ↳")} ${theme.fg("muted", `[${stream}]`)} ${theme.fg("accent", formatPatternForDisplay(matcher.pattern))}`;
|
|
200
|
+
lines.push(fitStyledLine(watch, width));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fitStyledLine(line: string, width: number): string {
|
|
208
|
+
if (width <= 0) return "";
|
|
209
|
+
if (visibleWidth(line) <= width) return line;
|
|
210
|
+
return `${truncateToWidth(line, width)}\u001b[0m`;
|
|
211
|
+
}
|