@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.
- 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 +34 -9
- 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 -119
- 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 -155
- 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-truncate.ts +0 -187
- package/src/tools/actions/output.test.ts +0 -392
- package/src/tools/actions/output.ts +0 -447
- 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
|
@@ -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
|
-
}
|