@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,320 @@
|
|
|
1
|
+
import type { ProcessManager } from "../../../../src/manager";
|
|
2
|
+
import type { ProcessInfo } from "../../../../src/types";
|
|
3
|
+
import { LIVE_STATUSES } from "../../../../src/types";
|
|
4
|
+
import { isRecord } from "../../../../src/utils/is-record";
|
|
5
|
+
import type {
|
|
6
|
+
LogMatcherConfig,
|
|
7
|
+
NotificationRegistry,
|
|
8
|
+
WatchRemoveSpec,
|
|
9
|
+
WatchUpdateResult,
|
|
10
|
+
} from "../../notifications/registry";
|
|
11
|
+
import { normalizeLogMatchItems } from "../notify";
|
|
12
|
+
import type { ProcessesParamsType } from "../schema";
|
|
13
|
+
|
|
14
|
+
export interface UpdateDetails {
|
|
15
|
+
action: "update";
|
|
16
|
+
ok: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
process?: ProcessInfo;
|
|
19
|
+
renamed: boolean;
|
|
20
|
+
previousName: string | null;
|
|
21
|
+
watches: {
|
|
22
|
+
mode: "append" | "replace" | "remove" | "clear" | null;
|
|
23
|
+
before: LogMatcherConfig[];
|
|
24
|
+
applied: LogMatcherConfig[];
|
|
25
|
+
count: number;
|
|
26
|
+
items: LogMatcherConfig[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function executeUpdate(
|
|
31
|
+
params: ProcessesParamsType,
|
|
32
|
+
manager: ProcessManager,
|
|
33
|
+
notifications: NotificationRegistry,
|
|
34
|
+
): UpdateDetails {
|
|
35
|
+
if (!params.id) {
|
|
36
|
+
return {
|
|
37
|
+
action: "update",
|
|
38
|
+
ok: false,
|
|
39
|
+
error: "process update requires id",
|
|
40
|
+
renamed: false,
|
|
41
|
+
previousName: null,
|
|
42
|
+
watches: { mode: null, before: [], applied: [], count: 0, items: [] },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const before = manager.get(params.id);
|
|
47
|
+
if (!before) {
|
|
48
|
+
return {
|
|
49
|
+
action: "update",
|
|
50
|
+
ok: false,
|
|
51
|
+
error: `process not found: ${params.id}`,
|
|
52
|
+
renamed: false,
|
|
53
|
+
previousName: null,
|
|
54
|
+
watches: { mode: null, before: [], applied: [], count: 0, items: [] },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!LIVE_STATUSES.has(before.status)) {
|
|
59
|
+
return {
|
|
60
|
+
action: "update",
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `process update requires a running process; ${params.id} has status ${before.status}`,
|
|
63
|
+
process: before,
|
|
64
|
+
renamed: false,
|
|
65
|
+
previousName: null,
|
|
66
|
+
watches: { mode: null, before: [], applied: [], count: 0, items: [] },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let process = before;
|
|
71
|
+
let renamed = false;
|
|
72
|
+
let previousName: string | null = null;
|
|
73
|
+
|
|
74
|
+
if (params.name !== undefined) {
|
|
75
|
+
const name = params.name.trim();
|
|
76
|
+
if (!name) {
|
|
77
|
+
return {
|
|
78
|
+
action: "update",
|
|
79
|
+
ok: false,
|
|
80
|
+
error: "process update name must be non-empty",
|
|
81
|
+
process: before,
|
|
82
|
+
renamed: false,
|
|
83
|
+
previousName: null,
|
|
84
|
+
watches: { mode: null, before: [], applied: [], count: 0, items: [] },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const renamedProcess = manager.rename(params.id, name);
|
|
89
|
+
if (!renamedProcess) {
|
|
90
|
+
return {
|
|
91
|
+
action: "update",
|
|
92
|
+
ok: false,
|
|
93
|
+
error: `process not found: ${params.id}`,
|
|
94
|
+
renamed: false,
|
|
95
|
+
previousName: null,
|
|
96
|
+
watches: { mode: null, before: [], applied: [], count: 0, items: [] },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
renamed = before.name !== renamedProcess.name;
|
|
101
|
+
if (renamed) {
|
|
102
|
+
previousName = before.name;
|
|
103
|
+
}
|
|
104
|
+
process = renamedProcess;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let watchError: string | undefined;
|
|
108
|
+
const watchesBefore =
|
|
109
|
+
notifications.getWatchState(params.id)?.logMatches ?? [];
|
|
110
|
+
let watchesApplied: LogMatcherConfig[] = [];
|
|
111
|
+
const watchResult = applyWatchUpdate(
|
|
112
|
+
params.id,
|
|
113
|
+
params.watches,
|
|
114
|
+
notifications,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (params.watches && !watchResult) {
|
|
118
|
+
watchError = `process update could not update watches for ${params.id}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (watchResult) {
|
|
122
|
+
watchesApplied =
|
|
123
|
+
params.watches?.mode === "append" || params.watches?.mode === "replace"
|
|
124
|
+
? watchResult.logMatches.slice(
|
|
125
|
+
params.watches.mode === "append" ? watchesBefore.length : 0,
|
|
126
|
+
)
|
|
127
|
+
: [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const currentWatches =
|
|
131
|
+
watchResult?.logMatches ??
|
|
132
|
+
notifications.getWatchState(params.id)?.logMatches ??
|
|
133
|
+
[];
|
|
134
|
+
|
|
135
|
+
if (watchError) {
|
|
136
|
+
return {
|
|
137
|
+
action: "update",
|
|
138
|
+
ok: false,
|
|
139
|
+
error: watchError,
|
|
140
|
+
process,
|
|
141
|
+
renamed,
|
|
142
|
+
previousName,
|
|
143
|
+
watches: {
|
|
144
|
+
mode: params.watches?.mode ?? null,
|
|
145
|
+
before: watchesBefore,
|
|
146
|
+
applied: [],
|
|
147
|
+
count: currentWatches.length,
|
|
148
|
+
items: currentWatches,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
action: "update",
|
|
155
|
+
ok: true,
|
|
156
|
+
process,
|
|
157
|
+
renamed,
|
|
158
|
+
previousName,
|
|
159
|
+
watches: {
|
|
160
|
+
mode: params.watches?.mode ?? null,
|
|
161
|
+
before: watchesBefore,
|
|
162
|
+
applied: watchesApplied,
|
|
163
|
+
count: currentWatches.length,
|
|
164
|
+
items: currentWatches,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function applyWatchUpdate(
|
|
170
|
+
processId: string,
|
|
171
|
+
watches: ProcessesParamsType["watches"],
|
|
172
|
+
notifications: NotificationRegistry,
|
|
173
|
+
): WatchUpdateResult | null {
|
|
174
|
+
if (!watches) return null;
|
|
175
|
+
|
|
176
|
+
const { mode } = watches;
|
|
177
|
+
|
|
178
|
+
if (mode === "clear") {
|
|
179
|
+
return notifications.clearWatches(processId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const items = watches.items;
|
|
183
|
+
if (!items || items.length === 0) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`process update watches.items is required for mode=${mode}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (mode === "append" || mode === "replace") {
|
|
190
|
+
const normalized = normalizeLogMatchItems(items, {
|
|
191
|
+
actionLabel: "process update",
|
|
192
|
+
pathPrefix: "watches.items",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return mode === "append"
|
|
196
|
+
? notifications.appendWatches(processId, normalized)
|
|
197
|
+
: notifications.replaceWatches(processId, normalized);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (mode === "remove") {
|
|
201
|
+
const specs = normalizeWatchRemoveSpecs(items);
|
|
202
|
+
return notifications.removeWatches(processId, specs);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error(`unsupported watch update mode: ${String(mode)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface RawRemoveItem {
|
|
209
|
+
index?: number;
|
|
210
|
+
pattern?: string;
|
|
211
|
+
mode?: string;
|
|
212
|
+
stream?: string;
|
|
213
|
+
repeat?: boolean;
|
|
214
|
+
on?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeWatchRemoveSpecs(items: unknown[]): WatchRemoveSpec[] {
|
|
218
|
+
return items.map((raw, index) => {
|
|
219
|
+
if (!isRecord(raw)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`process update watches.items[${index}] must be an object`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const item = raw as RawRemoveItem;
|
|
226
|
+
|
|
227
|
+
if (item.index !== undefined) {
|
|
228
|
+
if (
|
|
229
|
+
typeof item.index !== "number" ||
|
|
230
|
+
!Number.isInteger(item.index) ||
|
|
231
|
+
item.index < 0
|
|
232
|
+
) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`process update watches.items[${index}].index must be a non-negative integer`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { index: item.index };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (item.pattern !== undefined) {
|
|
242
|
+
if (typeof item.pattern !== "string") {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`process update watches.items[${index}].pattern must be a string`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (item.pattern.trim().length === 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`process update watches.items[${index}].pattern must not be empty`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (item.mode === "regex") {
|
|
255
|
+
try {
|
|
256
|
+
new RegExp(item.pattern);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const message =
|
|
259
|
+
error instanceof Error ? error.message : String(error);
|
|
260
|
+
throw new Error(
|
|
261
|
+
`process update watches.items[${index}].pattern is not a valid regular expression: ${message}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
pattern: item.pattern,
|
|
268
|
+
mode: item.mode as WatchRemoveSpec["mode"],
|
|
269
|
+
stream: item.stream as WatchRemoveSpec["stream"],
|
|
270
|
+
repeat: item.repeat,
|
|
271
|
+
on: item.on as WatchRemoveSpec["on"],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error(
|
|
276
|
+
`process update watches.items[${index}] requires either index or pattern for remove mode`,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function formatUpdateDetails(details: UpdateDetails): string {
|
|
282
|
+
if (!details.ok) {
|
|
283
|
+
return details.error ?? "process update failed";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const parts: string[] = [];
|
|
287
|
+
|
|
288
|
+
if (details.renamed && details.previousName && details.process) {
|
|
289
|
+
parts.push(
|
|
290
|
+
`Renamed process from "${details.previousName}" to "${details.process.name}" (${details.process.id}).`,
|
|
291
|
+
);
|
|
292
|
+
} else if (details.process) {
|
|
293
|
+
parts.push(
|
|
294
|
+
`Updated process ${details.process.name} (${details.process.id}).`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { mode, applied, before, count } = details.watches;
|
|
299
|
+
|
|
300
|
+
if (mode === "append") {
|
|
301
|
+
parts.push(
|
|
302
|
+
`Appended ${applied.length} watch${applied.length === 1 ? "" : "es"}; ${count} active.`,
|
|
303
|
+
);
|
|
304
|
+
} else if (mode === "replace") {
|
|
305
|
+
parts.push(
|
|
306
|
+
`Replaced ${before.length} with ${applied.length} watch${applied.length === 1 ? "" : "es"}; ${count} active.`,
|
|
307
|
+
);
|
|
308
|
+
} else if (mode === "remove") {
|
|
309
|
+
const removed = before.length - count;
|
|
310
|
+
parts.push(
|
|
311
|
+
`Removed ${removed} watch${removed === 1 ? "" : "es"}; ${count} active.`,
|
|
312
|
+
);
|
|
313
|
+
} else if (mode === "clear") {
|
|
314
|
+
parts.push(
|
|
315
|
+
`Cleared ${before.length} watch${before.length === 1 ? "" : "es"}; ${count} active.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return parts.join(" ") || "Updated process.";
|
|
320
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { LogMatcherConfig } from "../../notifications/registry";
|
|
4
|
+
import { buildMatcherLine, ProcessActionHeader } from "../components";
|
|
5
|
+
import type { ProcessesParamsType } from "../schema";
|
|
6
|
+
import { buildField } from "../utils";
|
|
7
|
+
import type { UpdateDetails } from ".";
|
|
8
|
+
|
|
9
|
+
export function buildHeader(
|
|
10
|
+
args: ProcessesParamsType,
|
|
11
|
+
theme: Theme,
|
|
12
|
+
options?: { expanded?: boolean },
|
|
13
|
+
): Container {
|
|
14
|
+
return new ProcessActionHeader(args, theme, {
|
|
15
|
+
action: "update",
|
|
16
|
+
expanded: options?.expanded,
|
|
17
|
+
suffix: args.id ? theme.fg("accent", `(${args.id})`) : undefined,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildExpanded(details: UpdateDetails, theme: Theme): Container {
|
|
22
|
+
const container = new Container();
|
|
23
|
+
|
|
24
|
+
if (!details.ok) {
|
|
25
|
+
container.addChild(
|
|
26
|
+
new Text(theme.fg("error", details.error ?? "update failed"), 0, 0),
|
|
27
|
+
);
|
|
28
|
+
return container;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (details.renamed && details.previousName && details.process) {
|
|
32
|
+
container.addChild(
|
|
33
|
+
new Text(
|
|
34
|
+
`${theme.fg("muted", "renamed:")} ${details.previousName} -> ${theme.fg("accent", details.process.name)}`,
|
|
35
|
+
0,
|
|
36
|
+
0,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (details.watches.mode) {
|
|
42
|
+
container.addChild(buildWatchSection(details, theme));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return container;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildCollapsed(
|
|
49
|
+
details: UpdateDetails,
|
|
50
|
+
theme: Theme,
|
|
51
|
+
): Container {
|
|
52
|
+
const container = new Container();
|
|
53
|
+
|
|
54
|
+
if (!details.ok) {
|
|
55
|
+
container.addChild(
|
|
56
|
+
new Text(theme.fg("error", details.error ?? "update failed"), 0, 0),
|
|
57
|
+
);
|
|
58
|
+
return container;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (details.renamed && details.previousName && details.process) {
|
|
62
|
+
container.addChild(
|
|
63
|
+
buildField(
|
|
64
|
+
"renamed",
|
|
65
|
+
`${details.previousName} -> ${details.process.name}`,
|
|
66
|
+
theme,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (details.watches.mode) {
|
|
72
|
+
container.addChild(buildWatchSummary(details, theme));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return container;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildFooter(
|
|
79
|
+
_details: UpdateDetails,
|
|
80
|
+
_options: { expanded?: boolean },
|
|
81
|
+
_theme: Theme,
|
|
82
|
+
): Container | null {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Collapsed summary ---
|
|
87
|
+
|
|
88
|
+
function buildWatchSummary(details: UpdateDetails, theme: Theme): Container {
|
|
89
|
+
const container = new Container();
|
|
90
|
+
const { mode, applied, before, count } = details.watches;
|
|
91
|
+
|
|
92
|
+
if (mode === "append") {
|
|
93
|
+
container.addChild(
|
|
94
|
+
buildField(
|
|
95
|
+
"watches",
|
|
96
|
+
`${theme.fg("success", `appended ${applied.length}`)}; ${count} active`,
|
|
97
|
+
theme,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
} else if (mode === "replace") {
|
|
101
|
+
container.addChild(
|
|
102
|
+
buildField(
|
|
103
|
+
"watches",
|
|
104
|
+
`${theme.fg("accent", `replaced ${before.length} with ${applied.length}`)}; ${count} active`,
|
|
105
|
+
theme,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
} else if (mode === "remove") {
|
|
109
|
+
const removed = before.length - count;
|
|
110
|
+
container.addChild(
|
|
111
|
+
buildField(
|
|
112
|
+
"watches",
|
|
113
|
+
`${theme.fg("error", `removed ${removed}`)}; ${count} active`,
|
|
114
|
+
theme,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
} else if (mode === "clear") {
|
|
118
|
+
container.addChild(
|
|
119
|
+
buildField(
|
|
120
|
+
"watches",
|
|
121
|
+
`${theme.fg("error", `cleared ${before.length}`)}; ${count} active`,
|
|
122
|
+
theme,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return container;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Expanded watch details ---
|
|
131
|
+
|
|
132
|
+
function buildWatchSection(details: UpdateDetails, theme: Theme): Container {
|
|
133
|
+
const container = new Container();
|
|
134
|
+
const { mode, applied, before, count } = details.watches;
|
|
135
|
+
|
|
136
|
+
// Summary line
|
|
137
|
+
if (mode === "append") {
|
|
138
|
+
container.addChild(
|
|
139
|
+
new Text(
|
|
140
|
+
theme.fg(
|
|
141
|
+
"muted",
|
|
142
|
+
`appended ${applied.length} watch${applied.length === 1 ? "" : "es"}; ${count} active`,
|
|
143
|
+
),
|
|
144
|
+
0,
|
|
145
|
+
0,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
} else if (mode === "replace") {
|
|
149
|
+
container.addChild(
|
|
150
|
+
new Text(
|
|
151
|
+
theme.fg(
|
|
152
|
+
"muted",
|
|
153
|
+
`replaced ${before.length} with ${applied.length}; ${count} active`,
|
|
154
|
+
),
|
|
155
|
+
0,
|
|
156
|
+
0,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
} else if (mode === "remove") {
|
|
160
|
+
const removed = before.length - count;
|
|
161
|
+
container.addChild(
|
|
162
|
+
new Text(
|
|
163
|
+
theme.fg(
|
|
164
|
+
"muted",
|
|
165
|
+
`removed ${removed} watch${removed === 1 ? "" : "es"}; ${count} active`,
|
|
166
|
+
),
|
|
167
|
+
0,
|
|
168
|
+
0,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
} else if (mode === "clear") {
|
|
172
|
+
container.addChild(
|
|
173
|
+
new Text(
|
|
174
|
+
theme.fg(
|
|
175
|
+
"muted",
|
|
176
|
+
`cleared ${before.length} watch${before.length === 1 ? "" : "es"}; ${count} active`,
|
|
177
|
+
),
|
|
178
|
+
0,
|
|
179
|
+
0,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mode === "append") {
|
|
185
|
+
for (const matcher of applied) {
|
|
186
|
+
container.addChild(
|
|
187
|
+
buildMatcherLine(matcher, theme, theme.fg("success", "+ ")),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (mode === "replace") {
|
|
193
|
+
for (const matcher of before) {
|
|
194
|
+
container.addChild(
|
|
195
|
+
buildMatcherLine(matcher, theme, theme.fg("error", "- ")),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
for (const matcher of details.watches.items) {
|
|
199
|
+
container.addChild(
|
|
200
|
+
buildMatcherLine(matcher, theme, theme.fg("success", "+ ")),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (mode === "remove") {
|
|
206
|
+
for (const matcher of findRemovedMatchers(before, details.watches.items)) {
|
|
207
|
+
container.addChild(
|
|
208
|
+
buildMatcherLine(matcher, theme, theme.fg("error", "- ")),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (mode === "clear") {
|
|
214
|
+
for (const matcher of before) {
|
|
215
|
+
container.addChild(
|
|
216
|
+
buildMatcherLine(matcher, theme, theme.fg("error", "- ")),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return container;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function findRemovedMatchers(
|
|
225
|
+
before: LogMatcherConfig[],
|
|
226
|
+
after: LogMatcherConfig[],
|
|
227
|
+
): LogMatcherConfig[] {
|
|
228
|
+
const remaining = new Map<string, number>();
|
|
229
|
+
for (const matcher of after) {
|
|
230
|
+
const key = matcherKey(matcher);
|
|
231
|
+
remaining.set(key, (remaining.get(key) ?? 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return before.filter((matcher) => {
|
|
235
|
+
const key = matcherKey(matcher);
|
|
236
|
+
const count = remaining.get(key) ?? 0;
|
|
237
|
+
if (count === 0) return true;
|
|
238
|
+
remaining.set(key, count - 1);
|
|
239
|
+
return false;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function matcherKey(matcher: LogMatcherConfig): string {
|
|
244
|
+
return JSON.stringify([
|
|
245
|
+
matcher.pattern,
|
|
246
|
+
matcher.mode ?? "literal",
|
|
247
|
+
matcher.stream ?? "both",
|
|
248
|
+
matcher.repeat ?? false,
|
|
249
|
+
matcher.on ?? "turn",
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import type { ProcessInfo } from "../../../src/types";
|
|
5
|
+
import {
|
|
6
|
+
formatRuntime,
|
|
7
|
+
formatStatus,
|
|
8
|
+
formatTimestamp,
|
|
9
|
+
shortenPath,
|
|
10
|
+
} from "../../../src/utils";
|
|
11
|
+
import { statusColor } from "../../shared/ui";
|
|
12
|
+
import { truncateToWidth } from "../utils/truncate";
|
|
13
|
+
|
|
14
|
+
export interface RenderOptions {
|
|
15
|
+
expanded?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildField(
|
|
19
|
+
label: string,
|
|
20
|
+
value: string | number | null,
|
|
21
|
+
theme: Theme,
|
|
22
|
+
): Text {
|
|
23
|
+
return new Text(
|
|
24
|
+
`${theme.fg("muted", `${label}:`)} ${String(value ?? "-")}`,
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildCommandField(
|
|
31
|
+
command: string,
|
|
32
|
+
theme: Theme,
|
|
33
|
+
options?: { truncate?: boolean },
|
|
34
|
+
): Text {
|
|
35
|
+
const value = options?.truncate
|
|
36
|
+
? truncateToWidth(command, 80, theme.fg("accent", "…"))
|
|
37
|
+
: command;
|
|
38
|
+
return buildField("command", theme.fg("accent", `\`${value}\``), theme);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildProcessDetails(
|
|
42
|
+
process: ProcessInfo,
|
|
43
|
+
theme: Theme,
|
|
44
|
+
options?: { runtime?: boolean },
|
|
45
|
+
): Container {
|
|
46
|
+
const container = new Container();
|
|
47
|
+
container.addChild(buildField("id", process.id, theme));
|
|
48
|
+
container.addChild(buildField("name", process.name, theme));
|
|
49
|
+
container.addChild(
|
|
50
|
+
buildField("status", formatColoredProcessStatus(process, theme), theme),
|
|
51
|
+
);
|
|
52
|
+
container.addChild(buildField("pid", process.pid, theme));
|
|
53
|
+
if (options?.runtime !== false) {
|
|
54
|
+
container.addChild(
|
|
55
|
+
buildField("duration", formatProcessRuntime(process), theme),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
container.addChild(
|
|
59
|
+
buildField("started", formatTimestamp(process.startTime), theme),
|
|
60
|
+
);
|
|
61
|
+
container.addChild(buildField("cwd", shortenPath(process.cwd), theme));
|
|
62
|
+
container.addChild(buildCommandField(process.command, theme));
|
|
63
|
+
container.addChild(buildField("stdout", process.stdoutFile, theme));
|
|
64
|
+
container.addChild(buildField("stderr", process.stderrFile, theme));
|
|
65
|
+
return container;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildProcessSummaryRow(
|
|
69
|
+
process: ProcessInfo,
|
|
70
|
+
theme: Theme,
|
|
71
|
+
): Text {
|
|
72
|
+
return new Text(
|
|
73
|
+
[
|
|
74
|
+
process.name,
|
|
75
|
+
theme.fg("accent", process.id),
|
|
76
|
+
`pid ${process.pid}`,
|
|
77
|
+
formatColoredProcessStatus(process, theme),
|
|
78
|
+
].join(" "),
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildCompactProcessLine(
|
|
85
|
+
process: ProcessInfo,
|
|
86
|
+
theme: Theme,
|
|
87
|
+
): Text {
|
|
88
|
+
return buildField("process", `${process.id} / pid ${process.pid}`, theme);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatProcessRuntime(
|
|
92
|
+
process: ProcessInfo,
|
|
93
|
+
now?: number,
|
|
94
|
+
): string {
|
|
95
|
+
if (process.startTime <= 0) return "-";
|
|
96
|
+
return formatRuntime(process.startTime, process.endTime, now);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatColoredProcessStatus(
|
|
100
|
+
process: ProcessInfo,
|
|
101
|
+
theme: Theme,
|
|
102
|
+
): string {
|
|
103
|
+
return theme.fg(statusColor(process), formatStatus(process));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function plural(noun: string, count: number): string {
|
|
107
|
+
return count === 1 ? noun : `${noun}s`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatCount(count: number, singular: string): string {
|
|
111
|
+
return `${count} ${plural(singular, count)}`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LogMatcherConfig } from "../notifications/registry";
|
|
2
|
+
|
|
3
|
+
export function formatMatcherForModel(matcher: LogMatcherConfig): string {
|
|
4
|
+
const mode = matcher.mode ?? "literal";
|
|
5
|
+
const stream = matcher.stream ?? "both";
|
|
6
|
+
const repeat = matcher.repeat ? " (repeat)" : "";
|
|
7
|
+
const on = matcher.on ?? "turn";
|
|
8
|
+
|
|
9
|
+
return `[${stream}] ${mode} ${on}${repeat} ${JSON.stringify(matcher.pattern)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatPatternsForModel(matchers: LogMatcherConfig[]): string {
|
|
13
|
+
return JSON.stringify(matchers.map((matcher) => matcher.pattern));
|
|
14
|
+
}
|