@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.
Files changed (171) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +47 -20
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +499 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -106
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -148
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output.ts +0 -269
  166. package/src/tools/actions/start.ts +0 -245
  167. package/src/tools/actions/write.ts +0 -113
  168. package/src/tools/index.ts +0 -207
  169. package/src/utils/ansi.test.ts +0 -22
  170. package/src/utils/command-executor.test.ts +0 -48
  171. package/src/utils/keybindings.ts +0 -71
@@ -0,0 +1,336 @@
1
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { ProcessManager } from "../../../src/manager";
4
+ import { CHANNELS } from "../../../src/protocol";
5
+ import type { ManagerEvent, ProcessInfo } from "../../../src/types";
6
+ import { classifyProcessEnd } from "./classify";
7
+ import {
8
+ type CompiledLogMatcher,
9
+ compileLogMatchers,
10
+ evaluateLogMatchers,
11
+ type LogMatchResult,
12
+ } from "./log-matchers";
13
+ import type { NotifyConfig, WatchState } from "./registry";
14
+ import {
15
+ createNotificationRegistry,
16
+ type NotificationRegistry,
17
+ } from "./registry";
18
+ import type {
19
+ Attention,
20
+ ProcessNotificationDetails,
21
+ ProcessNotificationKind,
22
+ } from "./types";
23
+
24
+ const DEFAULT_ATTENTION: Record<
25
+ "onSuccess" | "onFailure" | "onKilled",
26
+ Attention
27
+ > = {
28
+ onSuccess: "context",
29
+ onFailure: "turn",
30
+ // External kills surface as context by default (see DEFAULT_NOTIFY_CONFIG
31
+ // in tools/notify.ts for rationale). Intentional stops are classified
32
+ // separately and never reach this branch.
33
+ onKilled: "context",
34
+ };
35
+
36
+ export interface NotificationServiceDeps {
37
+ /**
38
+ * Event bus used to fan out notification events on CHANNELS.NOTIFICATION.
39
+ * The service never calls pi.sendMessage directly; a core delivery listener
40
+ * converts the emitted payload into a persisted custom message. This keeps
41
+ * notification flow event-driven and lets UI extensions observe the same
42
+ * events (e.g. for log-match highlighting).
43
+ */
44
+ events: EventBus;
45
+ manager: ProcessManager;
46
+ registry: NotificationRegistry;
47
+ getProcess: (id: string) => ProcessInfo | null;
48
+ }
49
+
50
+ interface ProcessMatcherState {
51
+ matchers: CompiledLogMatcher[];
52
+ revision: number;
53
+ generation: number;
54
+ }
55
+
56
+ export function createNotificationService(deps: NotificationServiceDeps): {
57
+ dispose: () => void;
58
+ } {
59
+ const { events, manager, registry, getProcess } = deps;
60
+ let disposed = false;
61
+ const matcherStates = new Map<string, ProcessMatcherState>();
62
+
63
+ const unsubscribe = manager.onEvent(handleEvent);
64
+
65
+ function handleEvent(event: ManagerEvent): void {
66
+ if (disposed) return;
67
+
68
+ if (event.type === "process_ended") {
69
+ // Defer to a microtask so that tool code (e.g. executeStart) can
70
+ // register the notify config in the registry before we read it.
71
+ // manager.start() may synchronously emit process_ended for missing_pid.
72
+ queueMicrotask(() => {
73
+ if (disposed) return;
74
+ handleProcessEnded(event.info);
75
+ });
76
+ return;
77
+ }
78
+
79
+ if (event.type === "process_output_changed") {
80
+ handleOutputChanged(event);
81
+ return;
82
+ }
83
+ }
84
+
85
+ function handleProcessEnded(info: ProcessInfo): void {
86
+ const isIntentionalStop = registry.consumeIntentionalStop(info.id);
87
+ const kind = classifyProcessEnd(info);
88
+
89
+ if (isIntentionalStop) {
90
+ cleanupMatcherState(info.id);
91
+ registry.unregister(info.id);
92
+ return;
93
+ }
94
+
95
+ const config = registry.get(info.id);
96
+ const attention = resolveAttention(kind, config);
97
+
98
+ const shouldForceDisplay = kind === "crash" || kind === "failure";
99
+
100
+ if (attention === "ignore" && !shouldForceDisplay) {
101
+ cleanupMatcherState(info.id);
102
+ registry.unregister(info.id);
103
+ return;
104
+ }
105
+
106
+ const effectiveAttention: Attention =
107
+ attention === "ignore" && shouldForceDisplay ? "context" : attention;
108
+
109
+ const details = buildLifecycleDetails(info, kind, effectiveAttention);
110
+ events.emit(CHANNELS.NOTIFICATION, details);
111
+
112
+ cleanupMatcherState(info.id);
113
+ registry.unregister(info.id);
114
+ }
115
+
116
+ function handleOutputChanged(
117
+ event: ManagerEvent & { type: "process_output_changed" },
118
+ ): void {
119
+ if (!event.appendedText || event.appendedText.length === 0) return;
120
+
121
+ const watchState = registry.getWatchState(event.id);
122
+ if (!watchState) return;
123
+
124
+ const state = syncMatcherState(event.id, watchState);
125
+ if (!state) return;
126
+
127
+ const matchTime = performance.now();
128
+ const matches = evaluateLogMatchers(
129
+ state.matchers,
130
+ event.appendedText,
131
+ matchTime,
132
+ );
133
+
134
+ const processInfo = getProcess(event.id);
135
+ const timestamp = Date.now();
136
+
137
+ for (const match of matches) {
138
+ const attention = match.on;
139
+ const details = buildLogMatchDetails(
140
+ event.id,
141
+ processInfo,
142
+ match,
143
+ attention,
144
+ timestamp,
145
+ );
146
+ events.emit(CHANNELS.NOTIFICATION, details);
147
+ }
148
+ }
149
+
150
+ function resolveAttention(
151
+ kind: ProcessNotificationKind,
152
+ config: NotifyConfig | null,
153
+ ): Attention {
154
+ switch (kind) {
155
+ case "success":
156
+ return config?.onSuccess ?? DEFAULT_ATTENTION.onSuccess;
157
+ case "failure":
158
+ case "crash":
159
+ return config?.onFailure ?? DEFAULT_ATTENTION.onFailure;
160
+ case "killed":
161
+ return config?.onKilled ?? DEFAULT_ATTENTION.onKilled;
162
+ case "log_match":
163
+ return "turn";
164
+ case "log_match_suppressed":
165
+ return "context";
166
+ }
167
+ }
168
+
169
+ function buildLifecycleDetails(
170
+ info: ProcessInfo,
171
+ kind: ProcessNotificationKind,
172
+ attention: Attention,
173
+ ): ProcessNotificationDetails {
174
+ const elapsed =
175
+ info.endTime !== null && info.startTime > 0
176
+ ? Math.round((info.endTime - info.startTime) / 1000)
177
+ : null;
178
+
179
+ let summary: string;
180
+ if (kind === "success") {
181
+ summary =
182
+ elapsed !== null
183
+ ? `Process "${info.name}" succeeded after ${elapsed}s.`
184
+ : `Process "${info.name}" succeeded.`;
185
+ } else if (kind === "killed" && info.signal) {
186
+ const number =
187
+ info.signal.number === null
188
+ ? ""
189
+ : ` (${info.signal.number}, ${info.signal.description})`;
190
+ summary = `Process "${info.name}" ended after receiving ${info.signal.name}${number}.`;
191
+ } else if (info.exitCode !== null && info.exitCode !== 0) {
192
+ summary =
193
+ elapsed !== null
194
+ ? `Process "${info.name}" failed with exit code ${info.exitCode} after ${elapsed}s.`
195
+ : `Process "${info.name}" failed with exit code ${info.exitCode}.`;
196
+ } else {
197
+ summary = `Process "${info.name}" failed.`;
198
+ }
199
+
200
+ return {
201
+ kind,
202
+ processId: info.id,
203
+ processName: info.name,
204
+ command: info.command,
205
+ timestamp: info.endTime ?? Date.now(),
206
+ summary,
207
+ status: info.status,
208
+ exitCode: info.exitCode,
209
+ endReason: info.endReason,
210
+ signal: info.signal,
211
+ attention,
212
+ };
213
+ }
214
+
215
+ function buildLogMatchDetails(
216
+ processId: string,
217
+ processInfo: ProcessInfo | null,
218
+ match: LogMatchResult,
219
+ attention: Attention,
220
+ timestamp: number,
221
+ ): ProcessNotificationDetails {
222
+ const name = processInfo?.name ?? processId;
223
+ const command = processInfo?.command ?? "";
224
+ const truncatedLine =
225
+ match.line.length > 160 ? `${match.line.slice(0, 157)}...` : match.line;
226
+
227
+ return {
228
+ kind: "log_match",
229
+ processId,
230
+ processName: name,
231
+ command,
232
+ timestamp,
233
+ summary: `Process "${name}" matched log pattern "${match.pattern}" on ${match.stream}.`,
234
+ logMatch: {
235
+ pattern: match.pattern,
236
+ mode: match.mode,
237
+ stream: match.stream,
238
+ line: truncatedLine,
239
+ matcherIndex: match.matcherIndex,
240
+ },
241
+ attention,
242
+ };
243
+ }
244
+
245
+ function syncMatcherState(
246
+ processId: string,
247
+ watchState: WatchState,
248
+ ): ProcessMatcherState | null {
249
+ const existing = matcherStates.get(processId);
250
+
251
+ if (!existing) {
252
+ const matchers = compileLogMatchers({
253
+ logMatches: watchState.logMatches,
254
+ });
255
+ if (matchers.length === 0) return null;
256
+ const state: ProcessMatcherState = {
257
+ matchers,
258
+ revision: watchState.revision,
259
+ generation: watchState.generation,
260
+ };
261
+ matcherStates.set(processId, state);
262
+ return state;
263
+ }
264
+
265
+ if (existing.revision === watchState.revision) return existing;
266
+
267
+ const next = compileLogMatchers({ logMatches: watchState.logMatches });
268
+
269
+ if (existing.generation === watchState.generation) {
270
+ preserveMatcherRuntimeState(existing.matchers, next);
271
+ }
272
+
273
+ if (next.length === 0) {
274
+ matcherStates.delete(processId);
275
+ return null;
276
+ }
277
+
278
+ const state: ProcessMatcherState = {
279
+ matchers: next,
280
+ revision: watchState.revision,
281
+ generation: watchState.generation,
282
+ };
283
+ matcherStates.set(processId, state);
284
+ return state;
285
+ }
286
+
287
+ function preserveMatcherRuntimeState(
288
+ prev: CompiledLogMatcher[],
289
+ next: CompiledLogMatcher[],
290
+ ): void {
291
+ const byKey = new Map<string, CompiledLogMatcher[]>();
292
+ for (const m of prev) {
293
+ const key = matcherIdentityKey(m);
294
+ const bucket = byKey.get(key);
295
+ if (bucket) {
296
+ bucket.push(m);
297
+ } else {
298
+ byKey.set(key, [m]);
299
+ }
300
+ }
301
+
302
+ for (const m of next) {
303
+ const key = matcherIdentityKey(m);
304
+ const bucket = byKey.get(key);
305
+ if (!bucket || bucket.length === 0) continue;
306
+ const donor = bucket.shift();
307
+ if (!donor) continue;
308
+ m.fired = donor.fired;
309
+ m.lastMatchTime = donor.lastMatchTime;
310
+ }
311
+ }
312
+
313
+ function matcherIdentityKey(m: {
314
+ pattern: string;
315
+ mode: string;
316
+ stream: string;
317
+ repeat: boolean;
318
+ on: string;
319
+ }): string {
320
+ return JSON.stringify([m.pattern, m.mode, m.stream, m.repeat, m.on]);
321
+ }
322
+
323
+ function cleanupMatcherState(processId: string): void {
324
+ matcherStates.delete(processId);
325
+ }
326
+
327
+ return {
328
+ dispose() {
329
+ disposed = true;
330
+ unsubscribe();
331
+ matcherStates.clear();
332
+ },
333
+ };
334
+ }
335
+
336
+ export { createNotificationRegistry };
@@ -0,0 +1,11 @@
1
+ // Re-export the protocol-safe notification types so the core extension and the
2
+ // protocol layer cannot drift apart. The canonical shape lives in
3
+ // `src/protocol/notifications.ts`; these names keep existing import sites
4
+ // stable while guaranteeing structural compatibility with the events emitted on
5
+ // CHANNELS.NOTIFICATION.
6
+ export type {
7
+ ProcessProtocolAttention as Attention,
8
+ ProcessProtocolNotificationKind as ProcessNotificationKind,
9
+ ProcessProtocolNotificationLogMatch as ProcessNotificationLogMatchDetails,
10
+ ProcessProtocolNotificationPayload as ProcessNotificationDetails,
11
+ } from "../../../src/protocol";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Apply a setting change to the core processes config.
3
+ *
4
+ * Logs/overview/dock settings are handled by their owning extensions.
5
+ */
6
+
7
+ import { setNestedValue } from "@aliou/pi-utils-settings";
8
+
9
+ import type { ProcessConfig } from "../config";
10
+
11
+ const BOOLEAN_FIELDS = new Set([
12
+ "interception.blockBackgroundCommands",
13
+ "widget.showStatusWidget",
14
+ ]);
15
+
16
+ export function applySettingChange(
17
+ id: string,
18
+ newValue: string,
19
+ config: ProcessConfig,
20
+ ): ProcessConfig | null {
21
+ const updated = structuredClone(config);
22
+
23
+ if (BOOLEAN_FIELDS.has(id)) {
24
+ setNestedValue(updated, id, newValue === "on");
25
+ return updated;
26
+ }
27
+
28
+ // Unknown field: fall through to default string storage
29
+ return null;
30
+ }