@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.
Files changed (171) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +47 -20
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +499 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -106
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -148
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output.ts +0 -269
  166. package/src/tools/actions/start.ts +0 -245
  167. package/src/tools/actions/write.ts +0 -113
  168. package/src/tools/index.ts +0 -207
  169. package/src/utils/ansi.test.ts +0 -22
  170. package/src/utils/command-executor.test.ts +0 -48
  171. 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,3 @@
1
+ export type { MessageKey } from "./messages";
2
+ export type { Translator } from "./translator";
3
+ export { createTranslator, t } from "./translator";
@@ -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
+ }