@aliou/pi-processes 0.9.5 → 0.10.1

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 (174) 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 +29 -10
  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 +519 -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/process-group.ts +4 -0
  114. package/src/utils/shorten-path.ts +11 -0
  115. package/src/utils/signals.ts +22 -0
  116. package/src/commands/clear/command.ts +0 -14
  117. package/src/commands/clear/index.ts +0 -1
  118. package/src/commands/completions.ts +0 -38
  119. package/src/commands/dock/command.ts +0 -29
  120. package/src/commands/dock/index.ts +0 -1
  121. package/src/commands/index.ts +0 -33
  122. package/src/commands/kill/command.ts +0 -69
  123. package/src/commands/kill/index.ts +0 -1
  124. package/src/commands/logs/command.ts +0 -47
  125. package/src/commands/logs/index.ts +0 -1
  126. package/src/commands/pick-process.ts +0 -34
  127. package/src/commands/pin/command.ts +0 -34
  128. package/src/commands/pin/index.ts +0 -1
  129. package/src/commands/processes/command.ts +0 -39
  130. package/src/commands/processes/index.ts +0 -1
  131. package/src/commands/settings/apply-setting-change.ts +0 -72
  132. package/src/commands/settings/build-sections.ts +0 -160
  133. package/src/commands/settings/command.ts +0 -20
  134. package/src/commands/settings/index.ts +0 -1
  135. package/src/components/log-dock-component.test.ts +0 -29
  136. package/src/components/log-dock-component.ts +0 -236
  137. package/src/components/log-file-viewer.ts +0 -317
  138. package/src/components/log-overlay-component.ts +0 -546
  139. package/src/components/panel-helpers.ts +0 -37
  140. package/src/components/process-picker-component.ts +0 -166
  141. package/src/components/processes-component.ts +0 -485
  142. package/src/components/status-format.ts +0 -38
  143. package/src/config.ts +0 -107
  144. package/src/constants/index.ts +0 -16
  145. package/src/constants/types.ts +0 -119
  146. package/src/hooks/background-blocker.ts +0 -66
  147. package/src/hooks/cleanup.ts +0 -10
  148. package/src/hooks/index.ts +0 -32
  149. package/src/hooks/message-renderer.ts +0 -115
  150. package/src/hooks/process-end.ts +0 -69
  151. package/src/hooks/process-watch.ts +0 -85
  152. package/src/hooks/utils.ts +0 -23
  153. package/src/hooks/widget/index.ts +0 -2
  154. package/src/hooks/widget/setup.ts +0 -167
  155. package/src/hooks/widget/status-widget.ts +0 -105
  156. package/src/hooks/widget/types.ts +0 -20
  157. package/src/index.ts +0 -35
  158. package/src/manager.test.ts +0 -331
  159. package/src/manager.ts +0 -758
  160. package/src/tools/actions/clear.ts +0 -20
  161. package/src/tools/actions/debug.ts +0 -155
  162. package/src/tools/actions/index.ts +0 -171
  163. package/src/tools/actions/kill.ts +0 -89
  164. package/src/tools/actions/list.ts +0 -188
  165. package/src/tools/actions/logs.ts +0 -117
  166. package/src/tools/actions/output-truncate.ts +0 -187
  167. package/src/tools/actions/output.test.ts +0 -392
  168. package/src/tools/actions/output.ts +0 -447
  169. package/src/tools/actions/start.ts +0 -245
  170. package/src/tools/actions/write.ts +0 -113
  171. package/src/tools/index.ts +0 -207
  172. package/src/utils/ansi.test.ts +0 -22
  173. package/src/utils/command-executor.test.ts +0 -48
  174. 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
+ }