@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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background command blocker.
|
|
3
|
+
*
|
|
4
|
+
* Listens for bash tool calls and blocks commands that contain
|
|
5
|
+
* shell background patterns (trailing &, nohup, disown, setsid).
|
|
6
|
+
*
|
|
7
|
+
* Uses @aliou/sh to parse commands into an AST and walk all
|
|
8
|
+
* SimpleCommand nodes, checking for:
|
|
9
|
+
* - Statement.background (trailing &)
|
|
10
|
+
* - Command name matching nohup/disown/setsid
|
|
11
|
+
*
|
|
12
|
+
* Registered only when config.interception.blockBackgroundCommands is enabled.
|
|
13
|
+
* Returns a blocking reason that tells the agent to use the process tool instead.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Command,
|
|
18
|
+
Program,
|
|
19
|
+
SimpleCommand,
|
|
20
|
+
Statement,
|
|
21
|
+
Word,
|
|
22
|
+
} from "@aliou/sh";
|
|
23
|
+
import { parse } from "@aliou/sh";
|
|
24
|
+
import type {
|
|
25
|
+
BashToolCallEvent,
|
|
26
|
+
ExtensionAPI,
|
|
27
|
+
ToolCallEventResult,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
|
+
|
|
30
|
+
import { t } from "../i18n";
|
|
31
|
+
|
|
32
|
+
const BACKGROUND_KEYWORDS = new Set(["nohup", "disown", "setsid"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract the literal command name from a Word node.
|
|
36
|
+
* Returns the value of the first Literal part, or undefined if
|
|
37
|
+
* the word starts with a non-literal (e.g., variable expansion).
|
|
38
|
+
*/
|
|
39
|
+
function getCommandName(word: Word): string | undefined {
|
|
40
|
+
const firstPart = word.parts?.[0];
|
|
41
|
+
if (firstPart?.type === "Literal") {
|
|
42
|
+
return firstPart.value;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a SimpleCommand's name matches a background keyword.
|
|
49
|
+
*/
|
|
50
|
+
function isBackgroundSimpleCommand(cmd: SimpleCommand): boolean {
|
|
51
|
+
const firstName = cmd.words?.[0] ? getCommandName(cmd.words[0]) : undefined;
|
|
52
|
+
return firstName !== undefined && BACKGROUND_KEYWORDS.has(firstName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recursively walk a Command AST, returning true if any branch
|
|
57
|
+
* contains a background pattern (trailing & or background keyword).
|
|
58
|
+
*/
|
|
59
|
+
function walkCommand(command: Command, isBg: { value: boolean }): void {
|
|
60
|
+
switch (command.type) {
|
|
61
|
+
case "SimpleCommand":
|
|
62
|
+
if (isBackgroundSimpleCommand(command)) {
|
|
63
|
+
isBg.value = true;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case "Pipeline":
|
|
68
|
+
for (const stmt of command.commands ?? []) {
|
|
69
|
+
walkStatement(stmt, isBg);
|
|
70
|
+
if (isBg.value) return;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case "Logical":
|
|
75
|
+
walkStatement(command.left, isBg);
|
|
76
|
+
if (isBg.value) return;
|
|
77
|
+
walkStatement(command.right, isBg);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case "Subshell":
|
|
81
|
+
case "Block":
|
|
82
|
+
for (const stmt of command.body ?? []) {
|
|
83
|
+
walkStatement(stmt, isBg);
|
|
84
|
+
if (isBg.value) return;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case "IfClause":
|
|
89
|
+
for (const stmt of command.cond ?? []) {
|
|
90
|
+
walkStatement(stmt, isBg);
|
|
91
|
+
if (isBg.value) return;
|
|
92
|
+
}
|
|
93
|
+
for (const stmt of command.then ?? []) {
|
|
94
|
+
walkStatement(stmt, isBg);
|
|
95
|
+
if (isBg.value) return;
|
|
96
|
+
}
|
|
97
|
+
for (const stmt of command.else ?? []) {
|
|
98
|
+
walkStatement(stmt, isBg);
|
|
99
|
+
if (isBg.value) return;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "WhileClause":
|
|
104
|
+
case "ForClause":
|
|
105
|
+
case "SelectClause":
|
|
106
|
+
case "CStyleLoop":
|
|
107
|
+
for (const stmt of command.body ?? []) {
|
|
108
|
+
walkStatement(stmt, isBg);
|
|
109
|
+
if (isBg.value) return;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "FunctionDecl":
|
|
114
|
+
for (const stmt of command.body ?? []) {
|
|
115
|
+
walkStatement(stmt, isBg);
|
|
116
|
+
if (isBg.value) return;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "CaseClause":
|
|
121
|
+
for (const item of command.items ?? []) {
|
|
122
|
+
for (const stmt of item.body ?? []) {
|
|
123
|
+
walkStatement(stmt, isBg);
|
|
124
|
+
if (isBg.value) return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
// TimeClause, TestClause, ArithCmd, CoprocClause, DeclClause, LetClause:
|
|
131
|
+
// these don't carry SimpleCommands that are background indicators.
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function walkStatement(stmt: Statement, isBg: { value: boolean }): void {
|
|
137
|
+
if (stmt.background) {
|
|
138
|
+
isBg.value = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (stmt.command) {
|
|
142
|
+
walkCommand(stmt.command, isBg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a command string contains background execution patterns.
|
|
148
|
+
* Returns true if the command should be blocked.
|
|
149
|
+
*
|
|
150
|
+
* Uses @aliou/sh to parse the command into an AST. If the command
|
|
151
|
+
* cannot be parsed (syntax error), falls back to a simple regex
|
|
152
|
+
* heuristic for trailing &.
|
|
153
|
+
*/
|
|
154
|
+
export function isBackgroundCommand(command: string): boolean {
|
|
155
|
+
let program: Program;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = parse(command);
|
|
159
|
+
program = result.ast;
|
|
160
|
+
} catch {
|
|
161
|
+
// Fallback: if the command can't be parsed (syntax error),
|
|
162
|
+
// use a simple regex heuristic for trailing &.
|
|
163
|
+
return /\s*&\s*$/.test(command);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const isBg = { value: false };
|
|
167
|
+
|
|
168
|
+
for (const stmt of program.body ?? []) {
|
|
169
|
+
walkStatement(stmt, isBg);
|
|
170
|
+
if (isBg.value) return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register the background command blocker on the tool_call event.
|
|
178
|
+
*
|
|
179
|
+
* The blocker only activates when the provided isEnabled callback returns true.
|
|
180
|
+
* pi.on() does not return a disposer, so cleanup is handled by session_shutdown.
|
|
181
|
+
*/
|
|
182
|
+
export function registerBackgroundBlocker(
|
|
183
|
+
pi: ExtensionAPI,
|
|
184
|
+
isEnabled: () => boolean,
|
|
185
|
+
): void {
|
|
186
|
+
pi.on("tool_call", (event) => {
|
|
187
|
+
if (!isEnabled()) return;
|
|
188
|
+
|
|
189
|
+
if (event.toolName !== "bash") return;
|
|
190
|
+
|
|
191
|
+
const bashEvent = event as BashToolCallEvent;
|
|
192
|
+
const command = bashEvent.input.command;
|
|
193
|
+
|
|
194
|
+
if (isBackgroundCommand(command)) {
|
|
195
|
+
return {
|
|
196
|
+
block: true,
|
|
197
|
+
reason: t("process.blocker.background_command"),
|
|
198
|
+
} satisfies ToolCallEventResult;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { ProcessManager } from "../../../src/manager";
|
|
4
|
+
import type { NotificationRegistry } from "../notifications/registry";
|
|
5
|
+
|
|
6
|
+
interface NotificationService {
|
|
7
|
+
dispose(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Disposer = () => void;
|
|
11
|
+
|
|
12
|
+
interface CleanupHookDeps {
|
|
13
|
+
manager: ProcessManager;
|
|
14
|
+
notifications: NotificationRegistry;
|
|
15
|
+
notificationService: NotificationService;
|
|
16
|
+
disposers?: Disposer[];
|
|
17
|
+
disposeOverlays?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerCleanupHook(
|
|
21
|
+
pi: ExtensionAPI,
|
|
22
|
+
deps: CleanupHookDeps,
|
|
23
|
+
): void {
|
|
24
|
+
let shuttingDown = false;
|
|
25
|
+
|
|
26
|
+
pi.on("session_shutdown", async () => {
|
|
27
|
+
if (shuttingDown) return;
|
|
28
|
+
shuttingDown = true;
|
|
29
|
+
|
|
30
|
+
// Close any open overview panels first so they stop listening to events
|
|
31
|
+
// before the manager and protocol handlers are torn down.
|
|
32
|
+
deps.disposeOverlays?.();
|
|
33
|
+
|
|
34
|
+
for (const dispose of deps.disposers ?? []) {
|
|
35
|
+
dispose();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
deps.notificationService.dispose();
|
|
39
|
+
deps.notifications.clear();
|
|
40
|
+
deps.manager.killAll();
|
|
41
|
+
deps.manager.cleanup();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { ProcessManager } from "../../../src/manager";
|
|
4
|
+
import { CHANNELS } from "../../../src/protocol";
|
|
5
|
+
|
|
6
|
+
export function registerEventBridge(
|
|
7
|
+
events: EventBus,
|
|
8
|
+
manager: ProcessManager,
|
|
9
|
+
): () => void {
|
|
10
|
+
return manager.onEvent((event) => {
|
|
11
|
+
switch (event.type) {
|
|
12
|
+
case "process_started":
|
|
13
|
+
events.emit(CHANNELS.STARTED, event.info);
|
|
14
|
+
events.emit(CHANNELS.CHANGED, { reason: "started" });
|
|
15
|
+
break;
|
|
16
|
+
case "process_ended":
|
|
17
|
+
events.emit(CHANNELS.ENDED, event.info);
|
|
18
|
+
events.emit(CHANNELS.CHANGED, { reason: "ended" });
|
|
19
|
+
break;
|
|
20
|
+
case "process_output_changed":
|
|
21
|
+
events.emit(CHANNELS.OUTPUT_CHANGED, {
|
|
22
|
+
id: event.id,
|
|
23
|
+
appendedText: event.appendedText,
|
|
24
|
+
droppedLines: event.droppedLines,
|
|
25
|
+
});
|
|
26
|
+
break;
|
|
27
|
+
case "processes_changed":
|
|
28
|
+
events.emit(CHANNELS.CHANGED, { reason: "cleared" });
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type MessageKey =
|
|
2
|
+
| "process.list.empty"
|
|
3
|
+
| "process.list.summary"
|
|
4
|
+
| "process.stop.not_found"
|
|
5
|
+
| "process.stop.timeout"
|
|
6
|
+
| "process.status.running"
|
|
7
|
+
| "process.status.terminating"
|
|
8
|
+
| "process.status.terminate_timeout"
|
|
9
|
+
| "process.status.exited"
|
|
10
|
+
| "process.status.killed"
|
|
11
|
+
| "process.status.failed"
|
|
12
|
+
| "process.blocker.background_command";
|
|
13
|
+
|
|
14
|
+
export const ENGLISH: Record<MessageKey, string> = {
|
|
15
|
+
"process.list.empty": "No running processes",
|
|
16
|
+
"process.list.summary": "{count} process{count_plural}",
|
|
17
|
+
"process.stop.not_found": "Process not found: {id}",
|
|
18
|
+
"process.stop.timeout": "Process kill timed out: {id}",
|
|
19
|
+
"process.status.running": "running",
|
|
20
|
+
"process.status.terminating": "terminating",
|
|
21
|
+
"process.status.terminate_timeout": "terminate timeout",
|
|
22
|
+
"process.status.exited": "exited ({exitCode})",
|
|
23
|
+
"process.status.killed": "killed",
|
|
24
|
+
"process.status.failed": "failed ({errorMessage})",
|
|
25
|
+
"process.blocker.background_command":
|
|
26
|
+
"Shell background patterns (&, nohup, disown, setsid) are not allowed. Use the process tool to manage background commands.",
|
|
27
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MessageKey } from "./messages";
|
|
2
|
+
import { ENGLISH } from "./messages";
|
|
3
|
+
|
|
4
|
+
export type { MessageKey } from "./messages";
|
|
5
|
+
|
|
6
|
+
export type Translator = (
|
|
7
|
+
key: MessageKey,
|
|
8
|
+
params?: Record<string, unknown>,
|
|
9
|
+
) => string;
|
|
10
|
+
|
|
11
|
+
function pluralize(count: number): string {
|
|
12
|
+
return count === 1 ? "" : "es";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatTemplate(
|
|
16
|
+
template: string,
|
|
17
|
+
params: Record<string, unknown>,
|
|
18
|
+
): string {
|
|
19
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
20
|
+
if (key === "count_plural") {
|
|
21
|
+
const count = Number(params.count) || 0;
|
|
22
|
+
return pluralize(count);
|
|
23
|
+
}
|
|
24
|
+
return String(params[key] ?? match);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a translator with optional overrides.
|
|
30
|
+
*
|
|
31
|
+
* Overrides map message keys to custom strings. Unoverridden keys
|
|
32
|
+
* fall back to English defaults.
|
|
33
|
+
*/
|
|
34
|
+
export function createTranslator(
|
|
35
|
+
overrides?: Partial<Record<MessageKey, string>>,
|
|
36
|
+
): Translator {
|
|
37
|
+
const strings = { ...ENGLISH, ...overrides };
|
|
38
|
+
|
|
39
|
+
return (key, params) => {
|
|
40
|
+
const template = strings[key] ?? ENGLISH[key] ?? key;
|
|
41
|
+
if (!params) return template;
|
|
42
|
+
return formatTemplate(template, params);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default translator with English fallbacks. No overrides.
|
|
48
|
+
*/
|
|
49
|
+
export const t: Translator = createTranslator();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getManager } from "../../src/get-manager";
|
|
3
|
+
import { isWindowsPlatform } from "../../src/utils/platform";
|
|
4
|
+
import { registerClearCommand } from "./commands/clear";
|
|
5
|
+
import { registerKillCommand } from "./commands/kill";
|
|
6
|
+
import { registerOverviewCommand } from "./commands/overview";
|
|
7
|
+
import { configLoader, drainImportMessages, loadProcessConfig } from "./config";
|
|
8
|
+
import { registerCommandHandlers } from "./handlers/commands";
|
|
9
|
+
import { registerNotificationDelivery } from "./handlers/notifications";
|
|
10
|
+
import { registerRequestHandlers } from "./handlers/requests";
|
|
11
|
+
import { registerLogSubscriptions } from "./handlers/subscriptions";
|
|
12
|
+
import { registerBackgroundBlocker } from "./hooks/background-blocker";
|
|
13
|
+
import { registerCleanupHook } from "./hooks/cleanup";
|
|
14
|
+
import { registerEventBridge } from "./hooks/event-bridge";
|
|
15
|
+
import { registerProcessNotificationRenderer } from "./message-renderer";
|
|
16
|
+
import {
|
|
17
|
+
createNotificationRegistry,
|
|
18
|
+
createNotificationService,
|
|
19
|
+
} from "./notifications/service";
|
|
20
|
+
import { registerProcessSettings } from "./settings";
|
|
21
|
+
import { registerProcessTool } from "./tools";
|
|
22
|
+
|
|
23
|
+
export default async function processesExtension(
|
|
24
|
+
pi: ExtensionAPI,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
if (isWindowsPlatform()) {
|
|
27
|
+
// POSIX-only: the manager relies on detached process groups and
|
|
28
|
+
// negative-pid group kill (src/utils/process-group.ts), neither of
|
|
29
|
+
// which exists on Windows. Bail before touching the config or spawning.
|
|
30
|
+
pi.on("session_start", (_event, ctx) => {
|
|
31
|
+
if (!ctx.hasUI) return;
|
|
32
|
+
ctx.ui.notify(
|
|
33
|
+
"The pi-processes extension is not available on Windows.",
|
|
34
|
+
"warning",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await loadProcessConfig();
|
|
41
|
+
registerMigrationMessageNotifications(pi);
|
|
42
|
+
|
|
43
|
+
const manager = getManager({
|
|
44
|
+
getConfiguredShellPath: () => configLoader.getConfig().execution.shellPath,
|
|
45
|
+
});
|
|
46
|
+
const notifications = createNotificationRegistry();
|
|
47
|
+
const notificationService = createNotificationService({
|
|
48
|
+
events: pi.events,
|
|
49
|
+
manager,
|
|
50
|
+
registry: notifications,
|
|
51
|
+
getProcess: (id) => manager.get(id),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const getConfig = () => configLoader.getConfig();
|
|
55
|
+
|
|
56
|
+
const openOverlays = new Set<{ dispose: () => void }>();
|
|
57
|
+
const registerOverlay = (overlay: { dispose: () => void }) => {
|
|
58
|
+
openOverlays.add(overlay);
|
|
59
|
+
return () => openOverlays.delete(overlay);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const disposers = [
|
|
63
|
+
registerEventBridge(pi.events, manager),
|
|
64
|
+
registerRequestHandlers(pi.events, manager, getConfig),
|
|
65
|
+
registerCommandHandlers(pi.events, manager, notifications),
|
|
66
|
+
registerLogSubscriptions(pi.events, manager),
|
|
67
|
+
registerNotificationDelivery(pi.events, pi),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
registerBackgroundBlocker(
|
|
71
|
+
pi,
|
|
72
|
+
() => getConfig().interception.blockBackgroundCommands,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
registerProcessNotificationRenderer(pi);
|
|
76
|
+
registerProcessTool(pi, manager, notifications);
|
|
77
|
+
registerProcessSettings(pi);
|
|
78
|
+
registerOverviewCommand(pi, { events: pi.events, registerOverlay });
|
|
79
|
+
registerKillCommand(pi);
|
|
80
|
+
registerClearCommand(pi);
|
|
81
|
+
|
|
82
|
+
registerCleanupHook(pi, {
|
|
83
|
+
manager,
|
|
84
|
+
notifications,
|
|
85
|
+
notificationService,
|
|
86
|
+
disposers,
|
|
87
|
+
disposeOverlays: () => {
|
|
88
|
+
for (const overlay of [...openOverlays]) overlay.dispose();
|
|
89
|
+
openOverlays.clear();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function registerMigrationMessageNotifications(pi: ExtensionAPI): void {
|
|
95
|
+
pi.on("session_start", (_event, ctx) => {
|
|
96
|
+
const messages = [
|
|
97
|
+
...drainImportMessages(),
|
|
98
|
+
...configLoader.drainMessages(),
|
|
99
|
+
];
|
|
100
|
+
if (messages.length === 0) return;
|
|
101
|
+
|
|
102
|
+
const formattedMessages = messages.map((m) => `- ${m}`);
|
|
103
|
+
|
|
104
|
+
const message = `[processes]\n${formattedMessages.join("\n")}`;
|
|
105
|
+
if (ctx.hasUI) {
|
|
106
|
+
ctx.ui.notify(message, "info");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
MessageRenderOptions,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type Component, Container, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
import { MESSAGE_TYPE_PROCESS_NOTIFICATION } from "./constants";
|
|
9
|
+
import type { ProcessNotificationDetails } from "./notifications/types";
|
|
10
|
+
import { truncateToWidth } from "./utils/truncate";
|
|
11
|
+
|
|
12
|
+
export function registerProcessNotificationRenderer(pi: ExtensionAPI): void {
|
|
13
|
+
pi.registerMessageRenderer<ProcessNotificationDetails>(
|
|
14
|
+
MESSAGE_TYPE_PROCESS_NOTIFICATION,
|
|
15
|
+
renderProcessNotificationMessage,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderProcessNotificationMessage(
|
|
20
|
+
message: { details?: ProcessNotificationDetails },
|
|
21
|
+
options: MessageRenderOptions,
|
|
22
|
+
theme: Theme,
|
|
23
|
+
): Component | undefined {
|
|
24
|
+
const details = message.details;
|
|
25
|
+
if (!details) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const container = new Container();
|
|
30
|
+
container.addChild(new Text(formatSummary(details, theme), 0, 0));
|
|
31
|
+
|
|
32
|
+
if (options.expanded) {
|
|
33
|
+
container.addChild(new Text(formatDetailLine(details, theme), 0, 1));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return container;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatSummary(
|
|
40
|
+
details: ProcessNotificationDetails,
|
|
41
|
+
theme: Theme,
|
|
42
|
+
): string {
|
|
43
|
+
const prefix = theme.fg("accent", "[process]");
|
|
44
|
+
const name = theme.fg("muted", `"${details.processName}"`);
|
|
45
|
+
|
|
46
|
+
if (details.logMatch) {
|
|
47
|
+
const line = truncateToWidth(details.logMatch.line, 160, "…");
|
|
48
|
+
return `${prefix} ${name} matched ${details.logMatch.stream}: ${line}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (details.kind === "killed" && details.signal) {
|
|
52
|
+
const number =
|
|
53
|
+
details.signal.number === null ? "" : ` (${details.signal.number})`;
|
|
54
|
+
return `${prefix} ${name} killed by ${details.signal.name}${number}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const severity = colorKind(details.kind, theme, summaryVerb(details));
|
|
58
|
+
return `${prefix} ${name} ${severity}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatDetailLine(
|
|
62
|
+
details: ProcessNotificationDetails,
|
|
63
|
+
theme: Theme,
|
|
64
|
+
): string {
|
|
65
|
+
const parts = [
|
|
66
|
+
`id: ${details.processId}`,
|
|
67
|
+
`attention: ${details.attention}`,
|
|
68
|
+
details.status ? `status: ${details.status}` : null,
|
|
69
|
+
details.exitCode === undefined || details.exitCode === null
|
|
70
|
+
? null
|
|
71
|
+
: `exit: ${details.exitCode}`,
|
|
72
|
+
details.endReason ? `reason: ${details.endReason}` : null,
|
|
73
|
+
].filter(Boolean);
|
|
74
|
+
|
|
75
|
+
return theme.fg("muted", parts.join(" "));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function summaryVerb(details: ProcessNotificationDetails): string {
|
|
79
|
+
if (details.summary) {
|
|
80
|
+
return details.summary;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (details.kind) {
|
|
84
|
+
case "success":
|
|
85
|
+
return "succeeded";
|
|
86
|
+
case "failure":
|
|
87
|
+
case "crash":
|
|
88
|
+
return details.exitCode === null || details.exitCode === undefined
|
|
89
|
+
? "failed"
|
|
90
|
+
: `failed exit(${details.exitCode})`;
|
|
91
|
+
case "killed":
|
|
92
|
+
return "killed";
|
|
93
|
+
case "log_match":
|
|
94
|
+
return "matched log output";
|
|
95
|
+
case "log_match_suppressed":
|
|
96
|
+
return "suppressed repeated log matches";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function colorKind(
|
|
101
|
+
kind: ProcessNotificationDetails["kind"],
|
|
102
|
+
theme: Theme,
|
|
103
|
+
text: string,
|
|
104
|
+
): string {
|
|
105
|
+
switch (kind) {
|
|
106
|
+
case "success":
|
|
107
|
+
return theme.fg("success", text);
|
|
108
|
+
case "killed":
|
|
109
|
+
return theme.fg("warning", text);
|
|
110
|
+
case "failure":
|
|
111
|
+
case "crash":
|
|
112
|
+
return theme.fg("error", text);
|
|
113
|
+
case "log_match":
|
|
114
|
+
case "log_match_suppressed":
|
|
115
|
+
return theme.fg("accent", text);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { MESSAGE_TYPE_PROCESS_NOTIFICATION } from "./constants";
|
|
4
|
+
import { buildProcessNotificationContent } from "./notifications/render-content";
|
|
5
|
+
import type {
|
|
6
|
+
Attention,
|
|
7
|
+
ProcessNotificationDetails,
|
|
8
|
+
} from "./notifications/types";
|
|
9
|
+
|
|
10
|
+
export interface ProcessNotificationSendOptions {
|
|
11
|
+
triggerTurn: boolean;
|
|
12
|
+
deliverAs: "steer" | "followUp" | "nextTurn";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Maps a notification attention level to Pi send-message options. */
|
|
16
|
+
export function attentionToSendOptions(
|
|
17
|
+
attention: Attention,
|
|
18
|
+
): ProcessNotificationSendOptions {
|
|
19
|
+
switch (attention) {
|
|
20
|
+
case "turn":
|
|
21
|
+
return { triggerTurn: true, deliverAs: "steer" };
|
|
22
|
+
case "context":
|
|
23
|
+
return { triggerTurn: false, deliverAs: "steer" };
|
|
24
|
+
case "ignore":
|
|
25
|
+
return { triggerTurn: false, deliverAs: "steer" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sendProcessNotificationMessage(
|
|
30
|
+
pi: ExtensionAPI,
|
|
31
|
+
details: ProcessNotificationDetails,
|
|
32
|
+
options: ProcessNotificationSendOptions,
|
|
33
|
+
): void {
|
|
34
|
+
pi.sendMessage<ProcessNotificationDetails>(
|
|
35
|
+
{
|
|
36
|
+
customType: MESSAGE_TYPE_PROCESS_NOTIFICATION,
|
|
37
|
+
content: buildProcessNotificationContent(details),
|
|
38
|
+
display: true,
|
|
39
|
+
details,
|
|
40
|
+
},
|
|
41
|
+
options,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProcessInfo } from "../../../src/types";
|
|
2
|
+
import type { ProcessNotificationKind } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Classify a process end into a notification kind.
|
|
6
|
+
*
|
|
7
|
+
* A kill that times out is never classified here: `terminate_timeout` never
|
|
8
|
+
* reaches `process_ended` (the kill path sets `endTime` and transitions without
|
|
9
|
+
* emitting, and the close handler bails on the `endTime` guard). The stop tool
|
|
10
|
+
* carries the timeout via `KillResult.reason: "timeout"` instead.
|
|
11
|
+
*/
|
|
12
|
+
export function classifyProcessEnd(info: ProcessInfo): ProcessNotificationKind {
|
|
13
|
+
if (info.status === "killed") return "killed";
|
|
14
|
+
if (info.success === true) return "success";
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
info.endReason === "spawn_error" ||
|
|
18
|
+
info.endReason === "missing_pid" ||
|
|
19
|
+
info.endReason === "lost"
|
|
20
|
+
) {
|
|
21
|
+
return "crash";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (info.exitCode !== null && info.exitCode !== 0) return "crash";
|
|
25
|
+
|
|
26
|
+
return "failure";
|
|
27
|
+
}
|