@aliou/pi-processes 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +47 -20
- 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 -106
- 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 -148
- 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.ts +0 -269
- 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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ProcessManager } from "../../../../src/manager";
|
|
2
|
+
import type { ProcessInfo } from "../../../../src/types";
|
|
3
|
+
import type { ProcessesParamsType } from "../schema";
|
|
4
|
+
|
|
5
|
+
export interface WriteDetails {
|
|
6
|
+
action: "write";
|
|
7
|
+
id: string;
|
|
8
|
+
processName: string;
|
|
9
|
+
process: ProcessInfo | null;
|
|
10
|
+
bytes: number;
|
|
11
|
+
end: boolean;
|
|
12
|
+
ok: boolean;
|
|
13
|
+
reason: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write text to a running process's stdin.
|
|
18
|
+
*
|
|
19
|
+
* `input` defaults to an empty string so a caller can close stdin with
|
|
20
|
+
* `{ action: "write", id, end: true }` to signal EOF without writing anything.
|
|
21
|
+
*/
|
|
22
|
+
export function executeWrite(
|
|
23
|
+
params: ProcessesParamsType,
|
|
24
|
+
manager: ProcessManager,
|
|
25
|
+
): WriteDetails {
|
|
26
|
+
if (!params.id) {
|
|
27
|
+
throw new Error("process write requires id");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const id = params.id;
|
|
31
|
+
const input = params.input ?? "";
|
|
32
|
+
const end = params.end ?? false;
|
|
33
|
+
|
|
34
|
+
if (input.length === 0 && !end) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'process write requires "input" or "end"; provide text to write or set end to close stdin',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const process = manager.get(id);
|
|
41
|
+
const result = manager.writeToStdin(id, input, { end });
|
|
42
|
+
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
return {
|
|
45
|
+
action: "write",
|
|
46
|
+
id,
|
|
47
|
+
processName: process?.name ?? "(unknown)",
|
|
48
|
+
process,
|
|
49
|
+
bytes: Buffer.byteLength(input, "utf-8"),
|
|
50
|
+
end,
|
|
51
|
+
ok: true,
|
|
52
|
+
reason: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
action: "write",
|
|
58
|
+
id,
|
|
59
|
+
processName: process?.name ?? "(unknown)",
|
|
60
|
+
process,
|
|
61
|
+
bytes: 0,
|
|
62
|
+
end,
|
|
63
|
+
ok: false,
|
|
64
|
+
reason: result.reason,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatWriteDetails(details: WriteDetails): string {
|
|
69
|
+
if (!details.ok) {
|
|
70
|
+
return `Failed to write to stdin for "${details.processName}" (${details.id}): ${details.reason}.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const suffix = details.end ? " and closed stdin" : "";
|
|
74
|
+
return `Wrote ${details.bytes} byte${details.bytes === 1 ? "" : "s"} to "${details.processName}" (${details.id})${suffix}.`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { ProcessActionHeader } from "../components";
|
|
4
|
+
import type { ProcessesParamsType } from "../schema";
|
|
5
|
+
import { buildField } from "../utils";
|
|
6
|
+
import type { WriteDetails } from ".";
|
|
7
|
+
|
|
8
|
+
export function buildHeader(
|
|
9
|
+
args: ProcessesParamsType,
|
|
10
|
+
theme: Theme,
|
|
11
|
+
options?: { expanded?: boolean },
|
|
12
|
+
): Container {
|
|
13
|
+
return new ProcessActionHeader(args, theme, {
|
|
14
|
+
action: "write",
|
|
15
|
+
expanded: options?.expanded,
|
|
16
|
+
suffix: args.id ? theme.fg("accent", `(${args.id})`) : undefined,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildExpanded(details: WriteDetails, theme: Theme): Container {
|
|
21
|
+
const container = new Container();
|
|
22
|
+
container.addChild(buildSummary(details, theme));
|
|
23
|
+
return container;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCollapsed(details: WriteDetails, theme: Theme): Container {
|
|
27
|
+
const container = new Container();
|
|
28
|
+
container.addChild(buildSummary(details, theme));
|
|
29
|
+
return container;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildFooter(
|
|
33
|
+
_details: WriteDetails,
|
|
34
|
+
_options: { expanded?: boolean },
|
|
35
|
+
_theme: Theme,
|
|
36
|
+
): Container | null {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildSummary(details: WriteDetails, theme: Theme): Container {
|
|
41
|
+
const container = new Container();
|
|
42
|
+
|
|
43
|
+
if (!details.ok) {
|
|
44
|
+
container.addChild(
|
|
45
|
+
new Text(
|
|
46
|
+
theme.fg("error", `stdin write failed: ${details.reason ?? "unknown"}`),
|
|
47
|
+
0,
|
|
48
|
+
0,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
container.addChild(buildField("process", details.processName, theme));
|
|
52
|
+
return container;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const closeTag = details.end ? " (stdin closed)" : "";
|
|
56
|
+
container.addChild(
|
|
57
|
+
new Text(
|
|
58
|
+
theme.fg(
|
|
59
|
+
"muted",
|
|
60
|
+
`wrote ${details.bytes} byte${details.bytes === 1 ? "" : "s"}${closeTag}`,
|
|
61
|
+
),
|
|
62
|
+
0,
|
|
63
|
+
0,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
container.addChild(buildField("process", details.processName, theme));
|
|
67
|
+
return container;
|
|
68
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function truncateToWidth(
|
|
4
|
+
text: string,
|
|
5
|
+
maxWidth: number,
|
|
6
|
+
ellipsis = "…",
|
|
7
|
+
pad = false,
|
|
8
|
+
): string {
|
|
9
|
+
if (maxWidth <= 0) return "";
|
|
10
|
+
if (text.length === 0) return pad ? " ".repeat(maxWidth) : "";
|
|
11
|
+
|
|
12
|
+
const ellipsisWidth = visibleWidth(ellipsis);
|
|
13
|
+
if (ellipsisWidth >= maxWidth) {
|
|
14
|
+
const textWidth = visibleWidth(text);
|
|
15
|
+
if (textWidth <= maxWidth) {
|
|
16
|
+
return pad ? text + " ".repeat(maxWidth - textWidth) : text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const clippedEllipsis = truncateFragmentToWidth(ellipsis, maxWidth);
|
|
20
|
+
if (clippedEllipsis.width === 0) {
|
|
21
|
+
return pad ? " ".repeat(maxWidth) : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return finalizeTruncatedResult(
|
|
25
|
+
"",
|
|
26
|
+
0,
|
|
27
|
+
clippedEllipsis.text,
|
|
28
|
+
clippedEllipsis.width,
|
|
29
|
+
maxWidth,
|
|
30
|
+
pad,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isPrintableAscii(text)) {
|
|
35
|
+
if (text.length <= maxWidth) {
|
|
36
|
+
return pad ? text + " ".repeat(maxWidth - text.length) : text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
40
|
+
return finalizeTruncatedResult(
|
|
41
|
+
text.slice(0, targetWidth),
|
|
42
|
+
targetWidth,
|
|
43
|
+
ellipsis,
|
|
44
|
+
ellipsisWidth,
|
|
45
|
+
maxWidth,
|
|
46
|
+
pad,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
51
|
+
let result = "";
|
|
52
|
+
let visibleSoFar = 0;
|
|
53
|
+
let keptWidth = 0;
|
|
54
|
+
let keepContiguousPrefix = true;
|
|
55
|
+
let overflowed = false;
|
|
56
|
+
let exhaustedInput = false;
|
|
57
|
+
const hasAnsi = text.includes("\u001b");
|
|
58
|
+
const hasTabs = text.includes("\t");
|
|
59
|
+
|
|
60
|
+
if (!hasAnsi && !hasTabs) {
|
|
61
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
62
|
+
const width = visibleWidth(segment);
|
|
63
|
+
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
64
|
+
result += segment;
|
|
65
|
+
keptWidth += width;
|
|
66
|
+
} else {
|
|
67
|
+
keepContiguousPrefix = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
visibleSoFar += width;
|
|
71
|
+
if (visibleSoFar > maxWidth) {
|
|
72
|
+
overflowed = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exhaustedInput = !overflowed;
|
|
77
|
+
} else {
|
|
78
|
+
let index = 0;
|
|
79
|
+
let pendingAnsi = "";
|
|
80
|
+
while (index < text.length) {
|
|
81
|
+
const ansi = readAnsiSequence(text, index);
|
|
82
|
+
if (ansi) {
|
|
83
|
+
pendingAnsi += ansi;
|
|
84
|
+
index += ansi.length;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (text[index] === "\t") {
|
|
89
|
+
if (keepContiguousPrefix && keptWidth + 3 <= targetWidth) {
|
|
90
|
+
if (pendingAnsi) {
|
|
91
|
+
result += pendingAnsi;
|
|
92
|
+
pendingAnsi = "";
|
|
93
|
+
}
|
|
94
|
+
result += "\t";
|
|
95
|
+
keptWidth += 3;
|
|
96
|
+
} else {
|
|
97
|
+
keepContiguousPrefix = false;
|
|
98
|
+
pendingAnsi = "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
visibleSoFar += 3;
|
|
102
|
+
if (visibleSoFar > maxWidth) {
|
|
103
|
+
overflowed = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
index++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let end = index;
|
|
111
|
+
while (end < text.length && text[end] !== "\t") {
|
|
112
|
+
const nextAnsi = readAnsiSequence(text, end);
|
|
113
|
+
if (nextAnsi) break;
|
|
114
|
+
end++;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const { segment } of segmenter.segment(text.slice(index, end))) {
|
|
118
|
+
const width = visibleWidth(segment);
|
|
119
|
+
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
120
|
+
if (pendingAnsi) {
|
|
121
|
+
result += pendingAnsi;
|
|
122
|
+
pendingAnsi = "";
|
|
123
|
+
}
|
|
124
|
+
result += segment;
|
|
125
|
+
keptWidth += width;
|
|
126
|
+
} else {
|
|
127
|
+
keepContiguousPrefix = false;
|
|
128
|
+
pendingAnsi = "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
visibleSoFar += width;
|
|
132
|
+
if (visibleSoFar > maxWidth) {
|
|
133
|
+
overflowed = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (overflowed) break;
|
|
139
|
+
index = end;
|
|
140
|
+
}
|
|
141
|
+
exhaustedInput = index >= text.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!overflowed && exhaustedInput) {
|
|
145
|
+
return pad ? text + " ".repeat(Math.max(0, maxWidth - visibleSoFar)) : text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return finalizeTruncatedResult(
|
|
149
|
+
result,
|
|
150
|
+
keptWidth,
|
|
151
|
+
ellipsis,
|
|
152
|
+
ellipsisWidth,
|
|
153
|
+
maxWidth,
|
|
154
|
+
pad,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
159
|
+
|
|
160
|
+
function isPrintableAscii(text: string): boolean {
|
|
161
|
+
for (let index = 0; index < text.length; index++) {
|
|
162
|
+
const code = text.charCodeAt(index);
|
|
163
|
+
if (code < 0x20 || code > 0x7e) return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function truncateFragmentToWidth(
|
|
169
|
+
text: string,
|
|
170
|
+
maxWidth: number,
|
|
171
|
+
): { text: string; width: number } {
|
|
172
|
+
if (maxWidth <= 0 || text.length === 0) {
|
|
173
|
+
return { text: "", width: 0 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isPrintableAscii(text)) {
|
|
177
|
+
const clipped = text.slice(0, maxWidth);
|
|
178
|
+
return { text: clipped, width: clipped.length };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const hasAnsi = text.includes("\u001b");
|
|
182
|
+
const hasTabs = text.includes("\t");
|
|
183
|
+
if (!hasAnsi && !hasTabs) {
|
|
184
|
+
let result = "";
|
|
185
|
+
let width = 0;
|
|
186
|
+
|
|
187
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
188
|
+
const segmentWidth = visibleWidth(segment);
|
|
189
|
+
if (width + segmentWidth > maxWidth) break;
|
|
190
|
+
result += segment;
|
|
191
|
+
width += segmentWidth;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { text: result, width };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let result = "";
|
|
198
|
+
let width = 0;
|
|
199
|
+
let index = 0;
|
|
200
|
+
let pendingAnsi = "";
|
|
201
|
+
|
|
202
|
+
while (index < text.length) {
|
|
203
|
+
const ansi = readAnsiSequence(text, index);
|
|
204
|
+
if (ansi) {
|
|
205
|
+
pendingAnsi += ansi;
|
|
206
|
+
index += ansi.length;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (text[index] === "\t") {
|
|
211
|
+
if (width + 3 > maxWidth) break;
|
|
212
|
+
if (pendingAnsi) {
|
|
213
|
+
result += pendingAnsi;
|
|
214
|
+
pendingAnsi = "";
|
|
215
|
+
}
|
|
216
|
+
result += "\t";
|
|
217
|
+
width += 3;
|
|
218
|
+
index++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let end = index;
|
|
223
|
+
while (end < text.length && text[end] !== "\t") {
|
|
224
|
+
const nextAnsi = readAnsiSequence(text, end);
|
|
225
|
+
if (nextAnsi) break;
|
|
226
|
+
end++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const { segment } of segmenter.segment(text.slice(index, end))) {
|
|
230
|
+
const segmentWidth = visibleWidth(segment);
|
|
231
|
+
if (width + segmentWidth > maxWidth) {
|
|
232
|
+
return { text: result, width };
|
|
233
|
+
}
|
|
234
|
+
if (pendingAnsi) {
|
|
235
|
+
result += pendingAnsi;
|
|
236
|
+
pendingAnsi = "";
|
|
237
|
+
}
|
|
238
|
+
result += segment;
|
|
239
|
+
width += segmentWidth;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
index = end;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { text: result, width };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function finalizeTruncatedResult(
|
|
249
|
+
prefix: string,
|
|
250
|
+
prefixWidth: number,
|
|
251
|
+
ellipsis: string,
|
|
252
|
+
ellipsisWidth: number,
|
|
253
|
+
maxWidth: number,
|
|
254
|
+
pad: boolean,
|
|
255
|
+
): string {
|
|
256
|
+
const width = prefixWidth + ellipsisWidth;
|
|
257
|
+
const result = ellipsis.length > 0 ? `${prefix}${ellipsis}` : prefix;
|
|
258
|
+
return pad ? result + " ".repeat(Math.max(0, maxWidth - width)) : result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readAnsiSequence(text: string, index: number): string | null {
|
|
262
|
+
const ESCAPE = "\u001b";
|
|
263
|
+
if (text[index] !== ESCAPE) return null;
|
|
264
|
+
|
|
265
|
+
const next = text[index + 1];
|
|
266
|
+
if (next === "[") {
|
|
267
|
+
const code = text.slice(index);
|
|
268
|
+
const pattern = new RegExp(`^${ESCAPE}\\[[0-?]*[ -/]*[@-~]`, "u");
|
|
269
|
+
const match = code.match(pattern);
|
|
270
|
+
return match?.[0] ?? text[index];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (next === "]" || next === "_") {
|
|
274
|
+
const rest = text.slice(index);
|
|
275
|
+
const bel = rest.indexOf("\u0007");
|
|
276
|
+
const st = rest.indexOf("\u001b\\");
|
|
277
|
+
const end = [bel, st]
|
|
278
|
+
.filter((value) => value >= 0)
|
|
279
|
+
.sort((a, b) => a - b)[0];
|
|
280
|
+
if (end === undefined) return text[index];
|
|
281
|
+
return rest.slice(0, end + (end === st ? 2 : 1));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return text.slice(index, Math.min(index + 2, text.length));
|
|
285
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
CHANNELS,
|
|
4
|
+
type ProcessProtocolConfig,
|
|
5
|
+
type RequestCombinedOutputPayload,
|
|
6
|
+
type RequestConfigPayload,
|
|
7
|
+
type RequestGetPayload,
|
|
8
|
+
type RequestListPayload,
|
|
9
|
+
} from "../../src/protocol";
|
|
10
|
+
import type { ProcessInfo } from "../../src/types";
|
|
11
|
+
|
|
12
|
+
export type ProcessLogLine = { type: "stdout" | "stderr"; text: string };
|
|
13
|
+
|
|
14
|
+
export function requestProcessList(events: EventBus): ProcessInfo[] {
|
|
15
|
+
let processes: ProcessInfo[] = [];
|
|
16
|
+
const payload: RequestListPayload = {
|
|
17
|
+
reply: (result) => (processes = result),
|
|
18
|
+
};
|
|
19
|
+
events.emit(CHANNELS.REQUEST_LIST, payload);
|
|
20
|
+
return processes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requestProcess(
|
|
24
|
+
events: EventBus,
|
|
25
|
+
id: string,
|
|
26
|
+
): ProcessInfo | null {
|
|
27
|
+
let process: ProcessInfo | null = null;
|
|
28
|
+
const payload: RequestGetPayload = {
|
|
29
|
+
id,
|
|
30
|
+
reply: (result) => (process = result),
|
|
31
|
+
};
|
|
32
|
+
events.emit(CHANNELS.REQUEST_GET, payload);
|
|
33
|
+
return process;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requestCombinedOutput(
|
|
37
|
+
events: EventBus,
|
|
38
|
+
id: string,
|
|
39
|
+
tailLines?: number,
|
|
40
|
+
): ProcessLogLine[] {
|
|
41
|
+
let lines: ProcessLogLine[] | null = null;
|
|
42
|
+
const payload: RequestCombinedOutputPayload = {
|
|
43
|
+
id,
|
|
44
|
+
tailLines,
|
|
45
|
+
reply: (result) => (lines = result),
|
|
46
|
+
};
|
|
47
|
+
events.emit(CHANNELS.REQUEST_COMBINED_OUTPUT, payload);
|
|
48
|
+
return lines ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function requestConfig(events: EventBus): ProcessProtocolConfig {
|
|
52
|
+
let config: ProcessProtocolConfig | null = null;
|
|
53
|
+
const payload: RequestConfigPayload = {
|
|
54
|
+
reply: (result) => (config = result),
|
|
55
|
+
};
|
|
56
|
+
events.emit(CHANNELS.REQUEST_CONFIG, payload);
|
|
57
|
+
if (!config)
|
|
58
|
+
throw new Error("processes core extension did not reply to config request");
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
6
|
+
import type { DockController } from "../widget/setup";
|
|
7
|
+
|
|
8
|
+
const ACTIONS = [
|
|
9
|
+
{
|
|
10
|
+
value: "expand",
|
|
11
|
+
description: "Show the expanded dock.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: "collapse",
|
|
15
|
+
description: "Show the compact dock.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
value: "close",
|
|
19
|
+
description: "Close the dock.",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function registerDockCommand(
|
|
24
|
+
pi: Pick<ExtensionAPI, "registerCommand">,
|
|
25
|
+
getController: () => DockController | null,
|
|
26
|
+
): void {
|
|
27
|
+
pi.registerCommand("ps:dock", {
|
|
28
|
+
description: "Expand, collapse, or close the managed process dock.",
|
|
29
|
+
getArgumentCompletions: (prefix: string) => completions(prefix),
|
|
30
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
31
|
+
const controller = getController();
|
|
32
|
+
if (!controller) {
|
|
33
|
+
report(ctx, "Process dock is only available in UI mode.", "warning");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const action = args.trim().split(/\s+/, 1)[0] || "expand";
|
|
38
|
+
if (action === "expand") controller.actions.expand();
|
|
39
|
+
else if (action === "collapse") controller.actions.collapse();
|
|
40
|
+
else if (action === "close") controller.actions.close();
|
|
41
|
+
else {
|
|
42
|
+
report(ctx, "Usage: /ps:dock [expand|collapse|close]", "warning");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
controller.refresh();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function completions(prefix: string): AutocompleteItem[] | null {
|
|
52
|
+
const normalized = prefix.trim().toLowerCase();
|
|
53
|
+
const items = ACTIONS.filter((action) =>
|
|
54
|
+
action.value.startsWith(normalized),
|
|
55
|
+
).map((action) => ({
|
|
56
|
+
value: action.value,
|
|
57
|
+
label: action.value,
|
|
58
|
+
description: action.description,
|
|
59
|
+
}));
|
|
60
|
+
return items.length > 0 ? items : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function report(
|
|
64
|
+
ctx: ExtensionCommandContext,
|
|
65
|
+
message: string,
|
|
66
|
+
level: "info" | "warning",
|
|
67
|
+
): void {
|
|
68
|
+
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
69
|
+
else console.log(message);
|
|
70
|
+
}
|