@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.
- 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 +29 -10
- 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 +519 -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/process-group.ts +4 -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,117 +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 type { ExecuteResult, ProcessesDetails } from "../../constants";
|
|
9
|
-
import type { ProcessManager } from "../../manager";
|
|
10
|
-
|
|
11
|
-
interface LogsParams {
|
|
12
|
-
id?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function renderLogsCall(args: LogsParams, theme: Theme): ToolCallHeader {
|
|
16
|
-
return new ToolCallHeader(
|
|
17
|
-
{
|
|
18
|
-
toolName: "Process",
|
|
19
|
-
action: "logs",
|
|
20
|
-
mainArg: args.id,
|
|
21
|
-
},
|
|
22
|
-
theme,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function renderLogsResult(
|
|
27
|
-
result: AgentToolResult<ProcessesDetails>,
|
|
28
|
-
options: ToolRenderResultOptions,
|
|
29
|
-
theme: Theme,
|
|
30
|
-
): ToolBody {
|
|
31
|
-
const { details } = result;
|
|
32
|
-
|
|
33
|
-
if (!details.logFiles) {
|
|
34
|
-
return new ToolBody(
|
|
35
|
-
{
|
|
36
|
-
fields: [
|
|
37
|
-
{
|
|
38
|
-
label: "Error",
|
|
39
|
-
value: "Missing log file details",
|
|
40
|
-
showCollapsed: true,
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
options,
|
|
45
|
-
theme,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const fields: Array<
|
|
50
|
-
{ label: string; value: string; showCollapsed?: boolean } | Text
|
|
51
|
-
> = [
|
|
52
|
-
new Text(
|
|
53
|
-
[
|
|
54
|
-
theme.fg("success", "Log files:"),
|
|
55
|
-
` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
|
|
56
|
-
` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
|
|
57
|
-
].join("\n"),
|
|
58
|
-
0,
|
|
59
|
-
0,
|
|
60
|
-
),
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
return new ToolBody({ fields }, options, theme);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function executeLogs(
|
|
67
|
-
params: LogsParams,
|
|
68
|
-
manager: ProcessManager,
|
|
69
|
-
): ExecuteResult {
|
|
70
|
-
if (!params.id) {
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text", text: "Missing required parameter: id" }],
|
|
73
|
-
details: {
|
|
74
|
-
action: "logs",
|
|
75
|
-
success: false,
|
|
76
|
-
message: "Missing required parameter: id",
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const proc = manager.get(params.id);
|
|
82
|
-
if (!proc) {
|
|
83
|
-
const message = `Process not found: ${params.id}`;
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "text", text: message }],
|
|
86
|
-
details: {
|
|
87
|
-
action: "logs",
|
|
88
|
-
success: false,
|
|
89
|
-
message,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const logFiles = manager.getLogFiles(proc.id);
|
|
95
|
-
if (!logFiles) {
|
|
96
|
-
const message = `Could not get log files for "${proc.name}" (${proc.id})`;
|
|
97
|
-
return {
|
|
98
|
-
content: [{ type: "text", text: message }],
|
|
99
|
-
details: {
|
|
100
|
-
action: "logs",
|
|
101
|
-
success: false,
|
|
102
|
-
message,
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const message = `Log files for "${proc.name}" (${proc.id}):\n stdout: ${logFiles.stdoutFile}\n stderr: ${logFiles.stderrFile}\n\nUse the read tool to inspect these files.`;
|
|
108
|
-
return {
|
|
109
|
-
content: [{ type: "text", text: message }],
|
|
110
|
-
details: {
|
|
111
|
-
action: "logs",
|
|
112
|
-
success: true,
|
|
113
|
-
message,
|
|
114
|
-
logFiles,
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local tail-truncation for the `process output` action.
|
|
3
|
-
*
|
|
4
|
-
* The installed Pi version (0.75.x) does not export its shared truncation
|
|
5
|
-
* helper, so this module keeps a local implementation that mirrors the v0.10
|
|
6
|
-
* behavior the agent depends on.
|
|
7
|
-
*
|
|
8
|
-
* Invariants:
|
|
9
|
-
* - Always keeps the newest output within the byte and line budgets.
|
|
10
|
-
* - A single oversized line yields a UTF-8-safe suffix instead of being
|
|
11
|
-
* dropped entirely.
|
|
12
|
-
* - ANSI and terminal control characters must be stripped by callers before
|
|
13
|
-
* truncation; this module operates on already-cleaned text.
|
|
14
|
-
*
|
|
15
|
-
* The `truncation` metadata returned alongside the content matches the shape
|
|
16
|
-
* Pi 0.10 exposes through `TruncationResult`, minus the `content` field, so it
|
|
17
|
-
* can be persisted in `details` without re-embedding raw output.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Ceiling applied to the JSON-escaped serialized content. Newline-heavy or
|
|
24
|
-
* tab-heavy output inflates under `JSON.stringify`, so the composed result is
|
|
25
|
-
* measured against this tighter budget in addition to the raw byte limit.
|
|
26
|
-
*/
|
|
27
|
-
export const MAX_OUTPUT_JSON_BYTES = 96 * 1024;
|
|
28
|
-
|
|
29
|
-
export interface TruncationResult {
|
|
30
|
-
content: string;
|
|
31
|
-
truncated: boolean;
|
|
32
|
-
truncatedBy: "bytes" | "lines" | null;
|
|
33
|
-
lastLinePartial: boolean;
|
|
34
|
-
outputLines: number;
|
|
35
|
-
outputBytes: number;
|
|
36
|
-
totalLines: number;
|
|
37
|
-
totalBytes: number;
|
|
38
|
-
maxBytes: number;
|
|
39
|
-
maxLines: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Metadata persisted in tool-result `details`. Mirrors `TruncationResult`
|
|
44
|
-
* minus the `content` field, so the bounded preview stays in `content` only.
|
|
45
|
-
*/
|
|
46
|
-
export type TruncationDetails = Omit<TruncationResult, "content">;
|
|
47
|
-
|
|
48
|
-
interface TruncateOptions {
|
|
49
|
-
maxBytes: number;
|
|
50
|
-
maxLines: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Truncate `text` from the tail, keeping the newest lines within the byte and
|
|
55
|
-
* line budgets. A single oversized line is sliced to a UTF-8-safe suffix
|
|
56
|
-
* rather than dropped entirely.
|
|
57
|
-
*/
|
|
58
|
-
export function truncateTail(
|
|
59
|
-
text: string,
|
|
60
|
-
options: TruncateOptions,
|
|
61
|
-
): TruncationResult {
|
|
62
|
-
const { maxBytes, maxLines } = options;
|
|
63
|
-
const totalBytes = Buffer.byteLength(text, "utf-8");
|
|
64
|
-
const lines = text.split("\n");
|
|
65
|
-
const totalLines = lines.length;
|
|
66
|
-
|
|
67
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
68
|
-
return {
|
|
69
|
-
content: text,
|
|
70
|
-
truncated: false,
|
|
71
|
-
truncatedBy: null,
|
|
72
|
-
lastLinePartial: false,
|
|
73
|
-
outputLines: totalLines,
|
|
74
|
-
outputBytes: totalBytes,
|
|
75
|
-
totalLines,
|
|
76
|
-
totalBytes,
|
|
77
|
-
maxBytes,
|
|
78
|
-
maxLines,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Walk backwards, keeping whole lines that fit. Track whether the byte
|
|
83
|
-
// budget was the binding constraint so the notice wording is accurate.
|
|
84
|
-
const kept: string[] = [];
|
|
85
|
-
let keptBytes = 0;
|
|
86
|
-
let hitBytes = false;
|
|
87
|
-
|
|
88
|
-
for (let i = lines.length - 1; i >= 0 && kept.length < maxLines; i--) {
|
|
89
|
-
const line = lines[i] ?? "";
|
|
90
|
-
const lineBytes =
|
|
91
|
-
Buffer.byteLength(line, "utf-8") + (kept.length > 0 ? 1 : 0);
|
|
92
|
-
|
|
93
|
-
if (keptBytes + lineBytes > maxBytes) {
|
|
94
|
-
hitBytes = true;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
kept.unshift(line);
|
|
99
|
-
keptBytes += lineBytes;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let lastLinePartial = false;
|
|
103
|
-
|
|
104
|
-
if (kept.length === 0) {
|
|
105
|
-
// The newest line alone exceeds the byte budget. Keep a UTF-8-safe suffix
|
|
106
|
-
// instead of returning an empty body, so the agent still sees recent
|
|
107
|
-
// output. The single byte budget minus one byte for the skipped joiner.
|
|
108
|
-
const lastLine = lines[lines.length - 1] ?? "";
|
|
109
|
-
const suffix = utf8Suffix(lastLine, maxBytes - 1);
|
|
110
|
-
kept.push(suffix);
|
|
111
|
-
keptBytes = Buffer.byteLength(suffix, "utf-8");
|
|
112
|
-
lastLinePartial = true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const content = kept.join("\n");
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
content,
|
|
119
|
-
truncated: true,
|
|
120
|
-
truncatedBy: hitBytes ? "bytes" : "lines",
|
|
121
|
-
lastLinePartial,
|
|
122
|
-
outputLines: kept.length,
|
|
123
|
-
outputBytes: keptBytes,
|
|
124
|
-
totalLines,
|
|
125
|
-
totalBytes,
|
|
126
|
-
maxBytes,
|
|
127
|
-
maxLines,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Slice a UTF-8-safe suffix from `line` at most `maxBytes` long. The suffix
|
|
133
|
-
* keeps the newest content (the tail of the line) and is prefixed with an
|
|
134
|
-
* ellipsis marker. The cut point walks forward past any leading continuation
|
|
135
|
-
* bytes (0b10xxxxxx) so the result never starts mid-code-point.
|
|
136
|
-
*/
|
|
137
|
-
function utf8Suffix(line: string, maxBytes: number): string {
|
|
138
|
-
const buffer = Buffer.from(line, "utf-8");
|
|
139
|
-
if (buffer.length <= maxBytes) return line;
|
|
140
|
-
|
|
141
|
-
const marker = "…";
|
|
142
|
-
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
143
|
-
const budget = Math.max(0, maxBytes - markerBytes);
|
|
144
|
-
if (budget <= 0) return marker;
|
|
145
|
-
|
|
146
|
-
// Start the cut so the suffix is the last `budget` bytes of the line.
|
|
147
|
-
let start = buffer.length - budget;
|
|
148
|
-
// If the cut lands inside a multibyte code point, the first byte is a
|
|
149
|
-
// continuation byte (0b10xxxxxx). Walk forward to the next lead byte.
|
|
150
|
-
while (start < buffer.length && (buffer[start] ?? 0) >> 6 === 0b10) {
|
|
151
|
-
start++;
|
|
152
|
-
}
|
|
153
|
-
if (start >= buffer.length) return marker;
|
|
154
|
-
|
|
155
|
-
return `${marker}${buffer.subarray(start).toString("utf-8")}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function formatSize(bytes: number): string {
|
|
159
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
160
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
161
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Count logical lines, matching `text.split("\n").length`. Empty text has
|
|
166
|
-
* zero lines so the line budget is not consumed by a stray newline.
|
|
167
|
-
*/
|
|
168
|
-
export function countLines(text: string): number {
|
|
169
|
-
if (text.length === 0) return 0;
|
|
170
|
-
return text.split("\n").length;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Compose a truncation notice string matching the agent-facing wording other
|
|
175
|
-
* tools use, so renderers can locate it reliably.
|
|
176
|
-
*/
|
|
177
|
-
export function formatTruncationNotice(truncation: TruncationResult): string {
|
|
178
|
-
if (!truncation.truncated) return "";
|
|
179
|
-
const limit =
|
|
180
|
-
truncation.truncatedBy === "bytes"
|
|
181
|
-
? `${formatSize(truncation.maxBytes)} byte limit`
|
|
182
|
-
: `${truncation.maxLines} line limit`;
|
|
183
|
-
const partialNote = truncation.lastLinePartial
|
|
184
|
-
? " (final line is a partial suffix)"
|
|
185
|
-
: "";
|
|
186
|
-
return `[Preview truncated by ${limit}${partialNote}; showing ${truncation.outputLines} lines / ${formatSize(truncation.outputBytes)} of ${truncation.totalLines} lines / ${formatSize(truncation.totalBytes)}.]`;
|
|
187
|
-
}
|
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { configLoader } from "../../config";
|
|
4
|
-
import type { ExecuteResult, ProcessInfo } from "../../constants";
|
|
5
|
-
import type { ProcessManager } from "../../manager";
|
|
6
|
-
import { executeOutput, renderOutputResult } from "./output";
|
|
7
|
-
import { MAX_OUTPUT_BYTES, truncateTail } from "./output-truncate";
|
|
8
|
-
|
|
9
|
-
const STDOUT_FILE = "/tmp/proc_1-stdout.log";
|
|
10
|
-
const STDERR_FILE = "/tmp/proc_1-stderr.log";
|
|
11
|
-
|
|
12
|
-
interface OutputSnapshot {
|
|
13
|
-
stdout: string[];
|
|
14
|
-
stderr: string[];
|
|
15
|
-
status: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function mockProcess(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
|
|
19
|
-
return {
|
|
20
|
-
id: "proc_1",
|
|
21
|
-
name: "server",
|
|
22
|
-
pid: 1234,
|
|
23
|
-
command: "npm start",
|
|
24
|
-
cwd: "/project",
|
|
25
|
-
startTime: 1,
|
|
26
|
-
endTime: null,
|
|
27
|
-
status: "running",
|
|
28
|
-
exitCode: null,
|
|
29
|
-
success: null,
|
|
30
|
-
stdoutFile: STDOUT_FILE,
|
|
31
|
-
stderrFile: STDERR_FILE,
|
|
32
|
-
alertOnSuccess: false,
|
|
33
|
-
alertOnFailure: true,
|
|
34
|
-
alertOnKill: false,
|
|
35
|
-
...overrides,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function mockManager(
|
|
40
|
-
output: OutputSnapshot | null,
|
|
41
|
-
processOverrides: Partial<ProcessInfo> = {},
|
|
42
|
-
): ProcessManager {
|
|
43
|
-
const process = mockProcess(processOverrides);
|
|
44
|
-
return {
|
|
45
|
-
get: vi.fn().mockReturnValue(process),
|
|
46
|
-
getOutput: vi.fn().mockReturnValue(output),
|
|
47
|
-
getLogFiles: vi.fn().mockReturnValue({
|
|
48
|
-
stdoutFile: STDOUT_FILE,
|
|
49
|
-
stderrFile: STDERR_FILE,
|
|
50
|
-
combinedFile: "/tmp/proc_1-combined.log",
|
|
51
|
-
}),
|
|
52
|
-
} as unknown as ProcessManager;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function runOutput(output: OutputSnapshot): ExecuteResult {
|
|
56
|
-
return executeOutput({ id: "proc_1" }, mockManager(output));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
beforeAll(async () => {
|
|
60
|
-
await configLoader.load();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
function contentText(result: ExecuteResult): string {
|
|
64
|
-
const block = result.content[0];
|
|
65
|
-
return block && block.type === "text" ? block.text : "";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
describe("executeOutput", () => {
|
|
69
|
-
it("formats normal stdout and stderr", () => {
|
|
70
|
-
const result = runOutput({
|
|
71
|
-
stdout: ["line 1", "line 2"],
|
|
72
|
-
stderr: ["err 1"],
|
|
73
|
-
status: "running",
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const content = contentText(result);
|
|
77
|
-
expect(content).toContain("stdout:");
|
|
78
|
-
expect(content).toContain("line 1");
|
|
79
|
-
expect(content).toContain("line 2");
|
|
80
|
-
expect(content).toContain("stderr:");
|
|
81
|
-
expect(content).toContain("err 1");
|
|
82
|
-
expect(content).toContain("2 stdout lines, 1 stderr lines");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("strips ANSI and terminal control characters before persistence", () => {
|
|
86
|
-
const result = runOutput({
|
|
87
|
-
stdout: ["\u001b[31mred\u001b[0m", "step 1\rstep 2\b done"],
|
|
88
|
-
stderr: [],
|
|
89
|
-
status: "exited",
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const content = contentText(result);
|
|
93
|
-
expect(content).not.toContain("\u001b[31m");
|
|
94
|
-
expect(content).not.toContain("\r");
|
|
95
|
-
expect(content).not.toContain("\b");
|
|
96
|
-
expect(content).toContain("red");
|
|
97
|
-
expect(content).toContain("step 1step 2 done");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("never persists stdout, stderr, or output arrays in new result details", () => {
|
|
101
|
-
const result = runOutput({
|
|
102
|
-
stdout: ["line 1"],
|
|
103
|
-
stderr: ["err 1"],
|
|
104
|
-
status: "running",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const { details } = result;
|
|
108
|
-
expect(details.output).toBeUndefined();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("does not embed raw process output in serialized details", () => {
|
|
112
|
-
const result = runOutput({
|
|
113
|
-
stdout: ["SECRET-stdout-token"],
|
|
114
|
-
stderr: ["SECRET-stderr-token"],
|
|
115
|
-
status: "running",
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const serialized = JSON.stringify(result.details);
|
|
119
|
-
expect(serialized).not.toContain("SECRET-stdout-token");
|
|
120
|
-
expect(serialized).not.toContain("SECRET-stderr-token");
|
|
121
|
-
// The raw-output property is absent from new result details.
|
|
122
|
-
expect(result.details.output).toBeUndefined();
|
|
123
|
-
expect("output" in JSON.parse(serialized)).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("bounds a 2 MiB single line without growing the session entry", () => {
|
|
127
|
-
const huge = "x".repeat(2 * 1024 * 1024);
|
|
128
|
-
const result = runOutput({ stdout: [huge], stderr: [], status: "running" });
|
|
129
|
-
|
|
130
|
-
const serialized = JSON.stringify(result);
|
|
131
|
-
// Serialized result (content + details) stays below a fixed safe ceiling.
|
|
132
|
-
expect(Buffer.byteLength(serialized, "utf-8")).toBeLessThan(128 * 1024);
|
|
133
|
-
// The newest output still surfaces a partial suffix.
|
|
134
|
-
expect(contentText(result)).toContain("x");
|
|
135
|
-
expect(result.details.truncation?.truncated).toBe(true);
|
|
136
|
-
expect(result.details.truncation?.lastLinePartial).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("handles CR-only progress output without session growth", () => {
|
|
140
|
-
// CR-only progress collapses into a single line carrying the final state.
|
|
141
|
-
const progress = `${Array.from({ length: 1000 }, (_, i) => `phase ${i}\r`).join("")}done`;
|
|
142
|
-
const result = runOutput({
|
|
143
|
-
stdout: [progress],
|
|
144
|
-
stderr: [],
|
|
145
|
-
status: "running",
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const serialized = JSON.stringify(result);
|
|
149
|
-
expect(Buffer.byteLength(serialized, "utf-8")).toBeLessThan(128 * 1024);
|
|
150
|
-
expect(contentText(result)).not.toContain("\r");
|
|
151
|
-
expect(contentText(result)).toContain("done");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("accounts for JSON-escaping expansion for a tab-heavy line", () => {
|
|
155
|
-
// Tabs survive stripAnsi but expand under JSON.stringify.
|
|
156
|
-
const tabHeavy = `${"\t".repeat(200_000)}tail-marker`;
|
|
157
|
-
const result = runOutput({
|
|
158
|
-
stdout: [tabHeavy],
|
|
159
|
-
stderr: [],
|
|
160
|
-
status: "running",
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const serialized = JSON.stringify(result);
|
|
164
|
-
expect(Buffer.byteLength(serialized, "utf-8")).toBeLessThan(128 * 1024);
|
|
165
|
-
expect(contentText(result)).toContain("tail-marker");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("keeps a UTF-8-safe suffix when an oversized multibyte line is truncated", () => {
|
|
169
|
-
// Use a 4-byte UTF-8 codepoint (U+1F680) so an arbitrary mid-byte cut
|
|
170
|
-
// would produce replacement characters. Keep enough multibyte content so
|
|
171
|
-
// the suffix definitely lands inside a code point run.
|
|
172
|
-
const emoji = "\u{1F680}".repeat(50_000); // ~200 KiB
|
|
173
|
-
const result = runOutput({
|
|
174
|
-
stdout: [emoji],
|
|
175
|
-
stderr: [],
|
|
176
|
-
status: "running",
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(result.details.truncation?.lastLinePartial).toBe(true);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("bounds combined large stdout and stderr", () => {
|
|
183
|
-
const stdout = Array.from({ length: 5000 }, (_, i) => `out ${i}`);
|
|
184
|
-
const stderr = Array.from({ length: 5000 }, (_, i) => `err ${i}`);
|
|
185
|
-
const result = runOutput({ stdout, stderr, status: "running" });
|
|
186
|
-
|
|
187
|
-
const serialized = JSON.stringify(result);
|
|
188
|
-
expect(Buffer.byteLength(serialized, "utf-8")).toBeLessThan(128 * 1024);
|
|
189
|
-
// Newest stderr lines are kept (they form the tail of the combined body).
|
|
190
|
-
expect(contentText(result)).toContain("err 4999");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("keeps the final serialized tool result below a fixed safe ceiling", () => {
|
|
194
|
-
const stdout = Array.from({ length: 10_000 }, (_, i) => `line ${i}`);
|
|
195
|
-
const stderr = Array.from({ length: 10_000 }, (_, i) => `err ${i}`);
|
|
196
|
-
const result = runOutput({ stdout, stderr, status: "running" });
|
|
197
|
-
|
|
198
|
-
expect(Buffer.byteLength(JSON.stringify(result), "utf-8")).toBeLessThan(
|
|
199
|
-
128 * 1024,
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("always retains complete log file paths in the textual result", () => {
|
|
204
|
-
const stdout = Array.from({ length: 10_000 }, (_, i) => `line ${i}`);
|
|
205
|
-
const result = runOutput({ stdout, stderr: [], status: "running" });
|
|
206
|
-
|
|
207
|
-
const content = contentText(result);
|
|
208
|
-
expect(content).toContain(STDOUT_FILE);
|
|
209
|
-
expect(content).toContain(STDERR_FILE);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("returns failure details without log files when the process is missing", () => {
|
|
213
|
-
const manager = {
|
|
214
|
-
get: vi.fn().mockReturnValue(null),
|
|
215
|
-
getOutput: vi.fn(),
|
|
216
|
-
getLogFiles: vi.fn(),
|
|
217
|
-
} as unknown as ProcessManager;
|
|
218
|
-
|
|
219
|
-
const result = executeOutput({ id: "missing" }, manager);
|
|
220
|
-
expect(result.details.success).toBe(false);
|
|
221
|
-
expect(result.details.logFiles).toBeUndefined();
|
|
222
|
-
expect(result.details.output).toBeUndefined();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("includes truncation metadata in details when content is truncated", () => {
|
|
226
|
-
const result = runOutput({
|
|
227
|
-
stdout: Array.from({ length: 5000 }, (_, i) => `line ${i}`),
|
|
228
|
-
stderr: [],
|
|
229
|
-
status: "running",
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(result.details.truncation).toBeDefined();
|
|
233
|
-
expect(result.details.truncation?.truncated).toBe(true);
|
|
234
|
-
expect(result.details.truncation?.totalLines).toBeGreaterThan(
|
|
235
|
-
result.details.truncation?.outputLines ?? 0,
|
|
236
|
-
);
|
|
237
|
-
// Raw output never leaks through truncation metadata.
|
|
238
|
-
expect(JSON.stringify(result.details.truncation)).not.toContain(
|
|
239
|
-
"line 4999",
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe("truncateTail (hardening)", () => {
|
|
245
|
-
it("keeps the newest lines within the byte and line budgets", () => {
|
|
246
|
-
const text = Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n");
|
|
247
|
-
const result = truncateTail(text, {
|
|
248
|
-
maxBytes: MAX_OUTPUT_BYTES,
|
|
249
|
-
maxLines: 10,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
expect(result.truncated).toBe(true);
|
|
253
|
-
expect(result.content).toContain("line 99");
|
|
254
|
-
expect(result.content).not.toContain("line 0");
|
|
255
|
-
expect(result.outputLines).toBeLessThanOrEqual(10);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("returns a UTF-8-safe suffix from a single oversized line", () => {
|
|
259
|
-
const huge = "\u{1F680}".repeat(50_000);
|
|
260
|
-
const result = truncateTail(huge, { maxBytes: 1024, maxLines: 10 });
|
|
261
|
-
|
|
262
|
-
expect(result.truncated).toBe(true);
|
|
263
|
-
expect(result.lastLinePartial).toBe(true);
|
|
264
|
-
expect(result.content.length).toBeGreaterThan(0);
|
|
265
|
-
// No partial UTF-8 sequences reach the output.
|
|
266
|
-
expect(Buffer.from(result.content, "utf-8").toString("utf-8")).toBe(
|
|
267
|
-
result.content,
|
|
268
|
-
);
|
|
269
|
-
expect(result.content).not.toContain("\uFFFD");
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// --- Renderer coverage ---
|
|
274
|
-
|
|
275
|
-
function mockTheme(): Theme {
|
|
276
|
-
return {
|
|
277
|
-
fg: (_color: string, text: string) => text,
|
|
278
|
-
bg: (_color: string, text: string) => text,
|
|
279
|
-
bold: (text: string) => text,
|
|
280
|
-
italic: (text: string) => text,
|
|
281
|
-
underline: (text: string) => text,
|
|
282
|
-
inverse: (text: string) => text,
|
|
283
|
-
strikethrough: (text: string) => text,
|
|
284
|
-
getFgAnsi: () => "",
|
|
285
|
-
getBgAnsi: () => "",
|
|
286
|
-
getColorMode: () => "truecolor",
|
|
287
|
-
getThinkingBorderColor: () => (text: string) => text,
|
|
288
|
-
getBashModeBorderColor: () => (text: string) => text,
|
|
289
|
-
} as unknown as Theme;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function render(
|
|
293
|
-
result: ExecuteResult,
|
|
294
|
-
options: { expanded?: boolean } = {},
|
|
295
|
-
): string[] {
|
|
296
|
-
const body = renderOutputResult(
|
|
297
|
-
result as never,
|
|
298
|
-
{ expanded: options.expanded ?? false } as never,
|
|
299
|
-
mockTheme(),
|
|
300
|
-
);
|
|
301
|
-
return body.render(120);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
describe("renderOutputResult", () => {
|
|
305
|
-
it("renders expanded content-based output from tool-result content", () => {
|
|
306
|
-
const result = runOutput({
|
|
307
|
-
stdout: ["ready on http://localhost:3000"],
|
|
308
|
-
stderr: [],
|
|
309
|
-
status: "running",
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const lines = render(result, { expanded: true });
|
|
313
|
-
const joined = lines.join("\n");
|
|
314
|
-
|
|
315
|
-
expect(joined).toContain("ready on http://localhost:3000");
|
|
316
|
-
expect(joined).toContain("Log files:");
|
|
317
|
-
expect(joined).toContain(STDOUT_FILE);
|
|
318
|
-
expect(joined).toContain(STDERR_FILE);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("renders a collapsed preview from the bounded content", () => {
|
|
322
|
-
const result = runOutput({
|
|
323
|
-
stdout: ["first", "second", "third"],
|
|
324
|
-
stderr: [],
|
|
325
|
-
status: "running",
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
const lines = render(result, { expanded: false });
|
|
329
|
-
const joined = lines.join("\n");
|
|
330
|
-
|
|
331
|
-
expect(joined).toContain("third");
|
|
332
|
-
expect(joined).toContain("Output");
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it("surfaces the truncation notice and log paths in expanded view", () => {
|
|
336
|
-
const result = runOutput({
|
|
337
|
-
stdout: Array.from({ length: 5000 }, (_, i) => `line ${i}`),
|
|
338
|
-
stderr: [],
|
|
339
|
-
status: "running",
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
const lines = render(result, { expanded: true });
|
|
343
|
-
const joined = lines.join("\n");
|
|
344
|
-
|
|
345
|
-
expect(result.details.truncation?.truncated).toBe(true);
|
|
346
|
-
expect(joined).toContain("Preview truncated");
|
|
347
|
-
expect(joined).toContain(STDOUT_FILE);
|
|
348
|
-
expect(joined).toContain(STDERR_FILE);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("renders legacy session results that still carry details.output", () => {
|
|
352
|
-
const legacy = {
|
|
353
|
-
content: [{ type: "text" as const, text: "legacy content" }],
|
|
354
|
-
details: {
|
|
355
|
-
action: "output" as const,
|
|
356
|
-
success: true,
|
|
357
|
-
message: '"server" (proc_1) [running]: 1 stdout lines, 0 stderr lines',
|
|
358
|
-
output: {
|
|
359
|
-
stdout: ["legacy stdout line"],
|
|
360
|
-
stderr: [],
|
|
361
|
-
status: "running",
|
|
362
|
-
},
|
|
363
|
-
logFiles: {
|
|
364
|
-
stdoutFile: STDOUT_FILE,
|
|
365
|
-
stderrFile: STDERR_FILE,
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const lines = render(legacy, { expanded: true });
|
|
371
|
-
const joined = lines.join("\n");
|
|
372
|
-
|
|
373
|
-
expect(joined).toContain("legacy stdout line");
|
|
374
|
-
expect(joined).toContain("Log files:");
|
|
375
|
-
expect(joined).toContain(STDOUT_FILE);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("does not mutate result details during rendering", () => {
|
|
379
|
-
const result = runOutput({
|
|
380
|
-
stdout: ["line 1"],
|
|
381
|
-
stderr: ["err 1"],
|
|
382
|
-
status: "running",
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const before = JSON.stringify(result.details);
|
|
386
|
-
render(result, { expanded: true });
|
|
387
|
-
render(result, { expanded: false });
|
|
388
|
-
const after = JSON.stringify(result.details);
|
|
389
|
-
|
|
390
|
-
expect(after).toBe(before);
|
|
391
|
-
});
|
|
392
|
-
});
|