@aliou/pi-processes 0.9.5 → 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 (173) 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 +34 -9
  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 -119
  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 -155
  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-truncate.ts +0 -187
  166. package/src/tools/actions/output.test.ts +0 -392
  167. package/src/tools/actions/output.ts +0 -447
  168. package/src/tools/actions/start.ts +0 -245
  169. package/src/tools/actions/write.ts +0 -113
  170. package/src/tools/index.ts +0 -207
  171. package/src/utils/ansi.test.ts +0 -22
  172. package/src/utils/command-executor.test.ts +0 -48
  173. package/src/utils/keybindings.ts +0 -71
@@ -1,447 +0,0 @@
1
- import { ToolBody, ToolCallHeader } from "@aliou/pi-utils-ui";
2
- import type {
3
- AgentToolResult,
4
- Theme,
5
- ToolRenderResultOptions,
6
- } from "@earendil-works/pi-coding-agent";
7
- import { Text } from "@earendil-works/pi-tui";
8
- import { configLoader } from "../../config";
9
- import type { ExecuteResult, ProcessesDetails } from "../../constants";
10
- import type { ProcessManager } from "../../manager";
11
- import { formatStatus, hasAnsi, stripAnsi } from "../../utils";
12
- import {
13
- countLines,
14
- formatTruncationNotice,
15
- MAX_OUTPUT_BYTES,
16
- MAX_OUTPUT_JSON_BYTES,
17
- type TruncationDetails,
18
- type TruncationResult,
19
- truncateTail,
20
- } from "./output-truncate";
21
-
22
- interface OutputParams {
23
- id?: string;
24
- }
25
-
26
- /** Marker delimiting the always-present complete-log footer in content. */
27
- const LOG_FOOTER_MARKER = "[Complete currently-retained logs:";
28
-
29
- export function renderOutputCall(
30
- args: OutputParams,
31
- theme: Theme,
32
- ): ToolCallHeader {
33
- return new ToolCallHeader(
34
- {
35
- toolName: "Process",
36
- action: "output",
37
- mainArg: args.id,
38
- },
39
- theme,
40
- );
41
- }
42
-
43
- export function renderOutputResult(
44
- result: AgentToolResult<ProcessesDetails>,
45
- options: ToolRenderResultOptions,
46
- theme: Theme,
47
- ): ToolBody {
48
- const { details, content } = result;
49
-
50
- const textBlock = Array.isArray(content)
51
- ? content.find((block) => block.type === "text")
52
- : undefined;
53
- const contentText =
54
- textBlock && textBlock.type === "text" ? textBlock.text : "";
55
-
56
- // Legacy session results still carry raw stdout/stderr arrays in details.
57
- // Render them so historical entries remain visible without errors.
58
- if (details.output) {
59
- return renderLegacyOutput(details, theme, options);
60
- }
61
-
62
- const bodyLines = extractOutputBody(contentText, details);
63
- let hadAnsi = false;
64
-
65
- const lines: string[] = [theme.fg("muted", details.message)];
66
-
67
- if (bodyLines.length > 0) {
68
- lines.push("");
69
- for (const line of bodyLines) {
70
- if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
71
- lines.push(line);
72
- }
73
- } else {
74
- lines.push("", theme.fg("muted", "(no output)"));
75
- }
76
-
77
- if (details.truncation) {
78
- lines.push(
79
- "",
80
- theme.fg("muted", buildTruncationSummary(details.truncation)),
81
- );
82
- }
83
-
84
- if (details.logFiles) {
85
- lines.push(
86
- "",
87
- theme.fg("success", "Log files:"),
88
- ` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
89
- ` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
90
- );
91
- }
92
-
93
- if (hadAnsi) {
94
- lines.push(
95
- "",
96
- theme.fg("muted", "ANSI escape codes were stripped from output"),
97
- );
98
- }
99
-
100
- const fields: Array<
101
- { label: string; value: string; showCollapsed?: boolean } | Text
102
- > = [new Text(lines.join("\n"), 0, 0)];
103
-
104
- // Collapsed preview: the last couple of body lines.
105
- const preview =
106
- bodyLines.slice(-2).join("\n") || theme.fg("muted", "(empty)");
107
- fields.push({
108
- label: "Output",
109
- value: theme.fg("muted", preview),
110
- showCollapsed: true,
111
- });
112
-
113
- return new ToolBody({ fields }, options, theme);
114
- }
115
-
116
- function renderLegacyOutput(
117
- details: ProcessesDetails,
118
- theme: Theme,
119
- options: ToolRenderResultOptions,
120
- ): ToolBody {
121
- const lines: string[] = [theme.fg("muted", details.message)];
122
- let hadAnsi = false;
123
-
124
- if (details.output?.stdout.length) {
125
- lines.push("", theme.fg("accent", "stdout:"));
126
- for (const line of details.output.stdout.slice(-20)) {
127
- if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
128
- lines.push(stripAnsi(line));
129
- }
130
- if (details.output.stdout.length > 20) {
131
- lines.push(
132
- theme.fg(
133
- "muted",
134
- `... (${details.output.stdout.length - 20} more lines)`,
135
- ),
136
- );
137
- }
138
- }
139
-
140
- if (details.output?.stderr.length) {
141
- lines.push("", theme.fg("warning", "stderr:"));
142
- for (const line of details.output.stderr.slice(-10)) {
143
- if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
144
- lines.push(theme.fg("warning", stripAnsi(line)));
145
- }
146
- if (details.output.stderr.length > 10) {
147
- lines.push(
148
- theme.fg(
149
- "muted",
150
- `... (${details.output.stderr.length - 10} more lines)`,
151
- ),
152
- );
153
- }
154
- }
155
-
156
- if (details.logFiles) {
157
- lines.push(
158
- "",
159
- theme.fg("success", "Log files:"),
160
- ` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
161
- ` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
162
- );
163
- }
164
-
165
- if (hadAnsi) {
166
- lines.push(
167
- "",
168
- theme.fg("muted", "ANSI escape codes were stripped from output"),
169
- );
170
- }
171
-
172
- const previewSource = details.output?.stdout.length
173
- ? details.output.stdout
174
- : (details.output?.stderr ?? []);
175
- const preview = previewSource
176
- .slice(-2)
177
- .map((l) => stripAnsi(l))
178
- .join("\n");
179
-
180
- const fields: Array<
181
- { label: string; value: string; showCollapsed?: boolean } | Text
182
- > = [
183
- new Text(lines.join("\n"), 0, 0),
184
- {
185
- label: "Output",
186
- value: preview
187
- ? theme.fg("muted", preview)
188
- : theme.fg("muted", "(empty)"),
189
- showCollapsed: true,
190
- },
191
- ];
192
-
193
- return new ToolBody({ fields }, options, theme);
194
- }
195
-
196
- /**
197
- * Extract the bounded process-output body from tool-result content text.
198
- *
199
- * Content is structured as:
200
- * - a one-line header (`details.message`);
201
- * - the bounded output body (already ANSI-stripped);
202
- * - an optional truncation notice line;
203
- * - an always-present complete-log footer.
204
- *
205
- * Only the body is returned. The renderer uses `details` for metadata,
206
- * truncation state, and log paths; this function never re-parses stream
207
- * labels, so a real log line containing `stderr:` cannot confuse it.
208
- *
209
- * Legacy content that lost its header through tail truncation is accepted: if
210
- * the first line does not match the expected header, the whole content is
211
- * treated as body up to the footer.
212
- */
213
- function extractOutputBody(
214
- contentText: string,
215
- details: ProcessesDetails,
216
- ): string[] {
217
- if (!contentText) return [];
218
-
219
- const lines = contentText.split("\n");
220
- const header = details.message;
221
-
222
- let bodyStart = lines[0] === header ? 1 : 0;
223
- // Skip the blank separator following the header.
224
- if (bodyStart > 0 && lines[bodyStart] === "") {
225
- bodyStart++;
226
- }
227
-
228
- const footerStart = findLastLineIndex(
229
- lines,
230
- (line) => line === LOG_FOOTER_MARKER,
231
- );
232
- let bodyEnd = footerStart >= 0 ? footerStart : lines.length;
233
-
234
- // Exclude the running-guidance line, which sits between the body and the
235
- // footer/notice. It is metadata, not process output.
236
- const guidance = "Process is still running. Use watches instead of polling.";
237
- const guidanceIndex = findLastLineIndex(
238
- lines,
239
- (line, index) => index < bodyEnd && line === guidance,
240
- );
241
- if (guidanceIndex >= bodyStart) {
242
- bodyEnd = guidanceIndex;
243
- }
244
-
245
- // Exclude a preceding blank line that separated body from the footer/notice.
246
- while (bodyEnd > bodyStart && (lines[bodyEnd - 1] ?? "") === "") {
247
- bodyEnd--;
248
- }
249
-
250
- return lines.slice(bodyStart, bodyEnd);
251
- }
252
-
253
- function buildTruncationSummary(truncation: TruncationDetails): string {
254
- const partialNote = truncation.lastLinePartial ? " · partial final line" : "";
255
- return `Preview truncated · ${truncation.outputLines}/${truncation.totalLines} lines${partialNote}`;
256
- }
257
-
258
- function findLastLineIndex(
259
- lines: string[],
260
- predicate: (line: string, index: number) => boolean,
261
- ): number {
262
- for (let i = lines.length - 1; i >= 0; i--) {
263
- if (predicate(lines[i] ?? "", i)) return i;
264
- }
265
- return -1;
266
- }
267
-
268
- export function executeOutput(
269
- params: OutputParams,
270
- manager: ProcessManager,
271
- ): ExecuteResult {
272
- if (!params.id) {
273
- return {
274
- content: [{ type: "text", text: "Missing required parameter: id" }],
275
- details: {
276
- action: "output",
277
- success: false,
278
- message: "Missing required parameter: id",
279
- },
280
- };
281
- }
282
-
283
- const proc = manager.get(params.id);
284
- if (!proc) {
285
- const message = `Process not found: ${params.id}`;
286
- return {
287
- content: [{ type: "text", text: message }],
288
- details: {
289
- action: "output",
290
- success: false,
291
- message,
292
- },
293
- };
294
- }
295
-
296
- const { defaultTailLines, maxOutputLines } = configLoader.getConfig().output;
297
- const output = manager.getOutput(proc.id, defaultTailLines);
298
- if (!output) {
299
- const message = `Could not read output for "${proc.name}" (${proc.id})`;
300
- return {
301
- content: [{ type: "text", text: message }],
302
- details: {
303
- action: "output",
304
- success: false,
305
- message,
306
- },
307
- };
308
- }
309
-
310
- const logFiles = manager.getLogFiles(proc.id);
311
- const stdoutLines = output.stdout.length;
312
- const stderrLines = output.stderr.length;
313
- const message = `"${proc.name}" (${proc.id}) [${formatStatus(proc)}]: ${stdoutLines} stdout lines, ${stderrLines} stderr lines`;
314
-
315
- // Build the stripped body text. stdout/stderr stay local and are never
316
- // persisted in `details`; only the bounded preview survives in `content`.
317
- const bodyLines: string[] = [];
318
- if (output.stdout.length > 0) {
319
- bodyLines.push("stdout:");
320
- bodyLines.push(...output.stdout.map(stripAnsi));
321
- }
322
- if (output.stderr.length > 0) {
323
- if (bodyLines.length > 0) bodyLines.push("");
324
- bodyLines.push("stderr:");
325
- bodyLines.push(...output.stderr.map(stripAnsi));
326
- }
327
-
328
- const guidance =
329
- output.status === "running"
330
- ? "Process is still running. Use watches instead of polling."
331
- : null;
332
-
333
- const { contentText, truncation } = buildBoundedOutput(
334
- message,
335
- bodyLines.join("\n"),
336
- guidance,
337
- logFiles,
338
- maxOutputLines,
339
- );
340
-
341
- const details: ProcessesDetails = {
342
- action: "output",
343
- success: true,
344
- message,
345
- logFiles: logFiles
346
- ? {
347
- stdoutFile: logFiles.stdoutFile,
348
- stderrFile: logFiles.stderrFile,
349
- }
350
- : undefined,
351
- };
352
-
353
- if (truncation.truncated) {
354
- const { content: _content, ...rest } = truncation;
355
- details.truncation = rest;
356
- }
357
-
358
- return {
359
- content: [{ type: "text", text: contentText }],
360
- details,
361
- };
362
- }
363
-
364
- /**
365
- * Compose the bounded content text. The header, optional guidance, truncation
366
- * notice, and complete-log footer live outside the truncation window, so the
367
- * agent always receives log paths even when the body is truncated.
368
- *
369
- * The byte and line budgets apply to the composed+JSON-escaped result. If the
370
- * first pass overflows (because JSON escaping expands control characters or
371
- * the fixed metadata consumes the budget), the body budget is shrunk and the
372
- * body re-truncated while keeping the newest output.
373
- */
374
- function buildBoundedOutput(
375
- header: string,
376
- body: string,
377
- guidance: string | null,
378
- logFiles: { stdoutFile: string; stderrFile: string } | null,
379
- maxLines: number,
380
- ): { contentText: string; truncation: TruncationResult } {
381
- let maxBodyBytes = MAX_OUTPUT_BYTES;
382
- let maxBodyLines = maxLines;
383
-
384
- let truncation = truncateTail(body, {
385
- maxBytes: maxBodyBytes,
386
- maxLines: maxBodyLines,
387
- });
388
- let contentText = composeContent(header, truncation, guidance, logFiles);
389
-
390
- for (let attempt = 0; attempt < 10; attempt++) {
391
- const excessBytes =
392
- Buffer.byteLength(contentText, "utf-8") - MAX_OUTPUT_BYTES;
393
- const excessLines = countLines(contentText) - maxLines;
394
- const excessJsonBytes =
395
- Buffer.byteLength(JSON.stringify(contentText), "utf-8") -
396
- MAX_OUTPUT_JSON_BYTES;
397
-
398
- if (excessBytes <= 0 && excessLines <= 0 && excessJsonBytes <= 0) {
399
- return { contentText, truncation };
400
- }
401
-
402
- maxBodyBytes = Math.max(0, maxBodyBytes - Math.max(0, excessBytes));
403
- if (excessJsonBytes > 0) {
404
- maxBodyBytes = Math.floor(maxBodyBytes / 2);
405
- }
406
- maxBodyLines = Math.max(1, maxBodyLines - Math.max(0, excessLines));
407
-
408
- truncation = truncateTail(body, {
409
- maxBytes: maxBodyBytes,
410
- maxLines: maxBodyLines,
411
- });
412
- contentText = composeContent(header, truncation, guidance, logFiles);
413
- }
414
-
415
- // Pathological metadata can still consume the whole budget. Bound the final
416
- // composed string so a session entry cannot grow unbounded.
417
- const finalTruncation = truncateTail(contentText, {
418
- maxBytes: MAX_OUTPUT_BYTES,
419
- maxLines: maxLines,
420
- });
421
- return { contentText: finalTruncation.content, truncation };
422
- }
423
-
424
- function composeContent(
425
- header: string,
426
- truncation: TruncationResult,
427
- guidance: string | null,
428
- logFiles: { stdoutFile: string; stderrFile: string } | null,
429
- ): string {
430
- const sections: string[] = [header, truncation.content];
431
-
432
- if (guidance) {
433
- sections.push(guidance);
434
- }
435
-
436
- if (truncation.truncated) {
437
- sections.push(formatTruncationNotice(truncation));
438
- }
439
-
440
- if (logFiles) {
441
- sections.push(
442
- `${LOG_FOOTER_MARKER}\nstdout=${logFiles.stdoutFile}\nstderr=${logFiles.stderrFile}]`,
443
- );
444
- }
445
-
446
- return sections.filter((section) => section.length > 0).join("\n\n");
447
- }
@@ -1,245 +0,0 @@
1
- import { ToolBody, ToolCallHeader } from "@aliou/pi-utils-ui";
2
- import type {
3
- AgentToolResult,
4
- ExtensionContext,
5
- Theme,
6
- ToolRenderResultOptions,
7
- } from "@earendil-works/pi-coding-agent";
8
- import { Text } from "@earendil-works/pi-tui";
9
- import type { ExecuteResult, ProcessesDetails } from "../../constants";
10
- import type { ProcessManager } from "../../manager";
11
-
12
- type WatchStream = "stdout" | "stderr" | "both";
13
-
14
- interface StartLogWatch {
15
- pattern: string;
16
- stream?: WatchStream;
17
- repeat?: boolean;
18
- }
19
-
20
- interface StartParams {
21
- name?: string;
22
- command?: string;
23
- cwd?: string;
24
- alertOnSuccess?: boolean;
25
- alertOnFailure?: boolean;
26
- alertOnKill?: boolean;
27
- logWatches?: StartLogWatch[];
28
- }
29
-
30
- export function renderStartCall(
31
- args: StartParams,
32
- theme: Theme,
33
- ): ToolCallHeader {
34
- const longArgs: Array<{ label?: string; value: string }> = [];
35
- const optionArgs: Array<{ label: string; value: string }> = [];
36
- let mainArg: string | undefined;
37
-
38
- if (args.name) {
39
- mainArg = `"${args.name}"`;
40
- }
41
-
42
- if (args.command) {
43
- if (!mainArg && args.command.length <= 60) {
44
- mainArg = args.command;
45
- } else if (args.command.length <= 60) {
46
- optionArgs.push({ label: "command", value: args.command });
47
- } else {
48
- longArgs.push({ label: "command", value: args.command });
49
- }
50
- }
51
-
52
- if (args.logWatches && args.logWatches.length > 0) {
53
- optionArgs.push({
54
- label: "watches",
55
- value: String(args.logWatches.length),
56
- });
57
- }
58
-
59
- return new ToolCallHeader(
60
- {
61
- toolName: "Process",
62
- action: "start",
63
- mainArg,
64
- optionArgs,
65
- longArgs,
66
- },
67
- theme,
68
- );
69
- }
70
-
71
- export function renderStartResult(
72
- result: AgentToolResult<ProcessesDetails>,
73
- options: ToolRenderResultOptions,
74
- theme: Theme,
75
- ): ToolBody {
76
- const { details } = result;
77
- const process = details.process;
78
-
79
- if (!process) {
80
- return new ToolBody(
81
- {
82
- fields: [
83
- {
84
- label: "Error",
85
- value: "Missing process details",
86
- showCollapsed: true,
87
- },
88
- ],
89
- },
90
- options,
91
- theme,
92
- );
93
- }
94
-
95
- const fields: Array<
96
- { label: string; value: string; showCollapsed?: boolean } | Text
97
- > = [
98
- new Text(
99
- [
100
- theme.fg("success", "Started process"),
101
- ` name: ${theme.fg("accent", process.name)}`,
102
- ` command: ${process.command}`,
103
- ` id: ${theme.fg("accent", process.id)}`,
104
- ` pid: ${String(process.pid)}`,
105
- " Log files:",
106
- ` - stdout: ${theme.fg("accent", process.stdoutFile)}`,
107
- ` - stderr: ${theme.fg("accent", process.stderrFile)}`,
108
- ].join("\n"),
109
- 0,
110
- 0,
111
- ),
112
- {
113
- label: "Status",
114
- value:
115
- theme.fg("success", "Started") +
116
- ` ${theme.fg("accent", `"${process.name}"`)} (${process.id}, PID: ${process.pid})`,
117
- showCollapsed: true,
118
- },
119
- ];
120
-
121
- return new ToolBody({ fields }, options, theme);
122
- }
123
-
124
- export function executeStart(
125
- params: StartParams,
126
- manager: ProcessManager,
127
- ctx: ExtensionContext,
128
- ): ExecuteResult {
129
- if (!params.name) {
130
- return {
131
- content: [{ type: "text", text: "Missing required parameter: name" }],
132
- details: {
133
- action: "start",
134
- success: false,
135
- message: "Missing required parameter: name",
136
- },
137
- };
138
- }
139
- if (!params.command) {
140
- return {
141
- content: [{ type: "text", text: "Missing required parameter: command" }],
142
- details: {
143
- action: "start",
144
- success: false,
145
- message: "Missing required parameter: command",
146
- },
147
- };
148
- }
149
-
150
- const watchValidationError = validateLogWatches(params.logWatches);
151
- if (watchValidationError) {
152
- return {
153
- content: [{ type: "text", text: watchValidationError }],
154
- details: {
155
- action: "start",
156
- success: false,
157
- message: watchValidationError,
158
- },
159
- };
160
- }
161
-
162
- let proc: ReturnType<ProcessManager["start"]>;
163
- try {
164
- proc = manager.start(params.name, params.command, params.cwd ?? ctx.cwd, {
165
- alertOnSuccess: params.alertOnSuccess,
166
- alertOnFailure: params.alertOnFailure,
167
- alertOnKill: params.alertOnKill,
168
- logWatches: params.logWatches,
169
- });
170
- } catch (error) {
171
- const message =
172
- error instanceof Error
173
- ? `Invalid start options: ${error.message}`
174
- : "Invalid start options";
175
- return {
176
- content: [{ type: "text", text: message }],
177
- details: {
178
- action: "start",
179
- success: false,
180
- message,
181
- },
182
- };
183
- }
184
-
185
- const message = [
186
- `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})`,
187
- "Log files:",
188
- ` stdout: ${proc.stdoutFile}`,
189
- ` stderr: ${proc.stderrFile}`,
190
- ].join("\n");
191
- return {
192
- content: [{ type: "text", text: message }],
193
- details: {
194
- action: "start",
195
- success: true,
196
- message,
197
- process: proc,
198
- },
199
- };
200
- }
201
-
202
- function validateLogWatches(watches?: StartLogWatch[]): string | null {
203
- if (!watches) return null;
204
-
205
- if (!Array.isArray(watches)) {
206
- return "Invalid parameter: logWatches must be an array";
207
- }
208
-
209
- for (const [index, watch] of watches.entries()) {
210
- if (!watch || typeof watch !== "object") {
211
- return `Invalid logWatches[${index}]: expected an object`;
212
- }
213
-
214
- if (
215
- typeof watch.pattern !== "string" ||
216
- watch.pattern.trim().length === 0
217
- ) {
218
- return `Invalid logWatches[${index}].pattern: expected non-empty string`;
219
- }
220
-
221
- try {
222
- // Validate regex syntax at process start for fast feedback.
223
- new RegExp(watch.pattern);
224
- } catch (error) {
225
- const message =
226
- error instanceof Error ? error.message : "invalid regular expression";
227
- return `Invalid logWatches[${index}].pattern: ${message}`;
228
- }
229
-
230
- if (
231
- watch.stream !== undefined &&
232
- watch.stream !== "stdout" &&
233
- watch.stream !== "stderr" &&
234
- watch.stream !== "both"
235
- ) {
236
- return `Invalid logWatches[${index}].stream: expected stdout, stderr, or both`;
237
- }
238
-
239
- if (watch.repeat !== undefined && typeof watch.repeat !== "boolean") {
240
- return `Invalid logWatches[${index}].repeat: expected boolean`;
241
- }
242
- }
243
-
244
- return null;
245
- }