@aliou/pi-processes 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) 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 +34 -9
  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 -119
  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 -155
  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-truncate.ts +0 -187
  166. package/src/tools/actions/output.test.ts +0 -392
  167. package/src/tools/actions/output.ts +0 -447
  168. package/src/tools/actions/start.ts +0 -245
  169. package/src/tools/actions/write.ts +0 -113
  170. package/src/tools/index.ts +0 -207
  171. package/src/utils/ansi.test.ts +0 -22
  172. package/src/utils/command-executor.test.ts +0 -48
  173. package/src/utils/keybindings.ts +0 -71
@@ -0,0 +1,166 @@
1
+ import {
2
+ type EventBus,
3
+ type ExtensionAPI,
4
+ type ExtensionCommandContext,
5
+ getSelectListTheme,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ type AutocompleteItem,
9
+ type SelectItem,
10
+ SelectList,
11
+ } from "@earendil-works/pi-tui";
12
+ import { formatProcessSelectionDescription } from "../../shared/ui";
13
+ import { requestProcess, requestProcessList } from "../client";
14
+ import type { DockController } from "../widget/setup";
15
+
16
+ export function registerPinCommand(
17
+ pi: Pick<ExtensionAPI, "registerCommand">,
18
+ events: EventBus,
19
+ getController: () => DockController | null,
20
+ ): void {
21
+ pi.registerCommand("ps:pin", {
22
+ description: "Pin the process dock to a process.",
23
+ getArgumentCompletions: (prefix: string) =>
24
+ completions(
25
+ events,
26
+ prefix,
27
+ getController()?.actions.getFocusedProcessId() ?? null,
28
+ ),
29
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
30
+ const controller = getController();
31
+ if (!controller) {
32
+ report(ctx, "Process dock is only available in UI mode.", "warning");
33
+ return;
34
+ }
35
+
36
+ const rawId = args.trim().split(/\s+/, 1)[0];
37
+ const pickedId = rawId
38
+ ? rawId
39
+ : await pickPinTarget(ctx, events, controller);
40
+ if (!pickedId) return;
41
+ const id = pickedId;
42
+
43
+ if (id === "clear") {
44
+ controller.actions.setFocus(null);
45
+ controller.actions.expand();
46
+ controller.refresh();
47
+ return;
48
+ }
49
+
50
+ const process = requestProcess(events, id);
51
+ if (!process) {
52
+ report(ctx, `Process not found: ${id}`, "warning");
53
+ return;
54
+ }
55
+
56
+ controller.actions.setFocus(process.id);
57
+ controller.actions.expand();
58
+ controller.refresh();
59
+ },
60
+ });
61
+ }
62
+
63
+ async function pickPinTarget(
64
+ ctx: ExtensionCommandContext,
65
+ events: EventBus,
66
+ controller: DockController,
67
+ ): Promise<string | null> {
68
+ if (ctx.mode !== "tui") {
69
+ report(ctx, "Usage: /ps:pin <process-id|clear>", "warning");
70
+ return null;
71
+ }
72
+
73
+ const pinnedProcessId = controller.actions.getFocusedProcessId();
74
+ const processes = requestProcessList(events);
75
+ const items: SelectItem[] = [
76
+ ...(pinnedProcessId
77
+ ? [
78
+ {
79
+ label: "clear",
80
+ value: "clear",
81
+ description: "Unpin the dock and show all running process logs.",
82
+ },
83
+ ]
84
+ : []),
85
+ ...processes.map((process) => ({
86
+ label: `${process.name} (${process.id})`,
87
+ value: process.id,
88
+ description: formatProcessSelectionDescription(
89
+ process,
90
+ process.id === pinnedProcessId ? " — pinned" : "",
91
+ ),
92
+ })),
93
+ ];
94
+
95
+ if (items.length === 0) {
96
+ report(ctx, "No processes to pin.", "warning");
97
+ return null;
98
+ }
99
+
100
+ const result = await ctx.ui.custom<string | null>(
101
+ (tui, theme, _keybindings, done) => {
102
+ const list = new SelectList(
103
+ items,
104
+ Math.min(items.length, 10),
105
+ getSelectListTheme(),
106
+ );
107
+ list.onSelect = (item) => done(String(item.value));
108
+ list.onCancel = () => done(null);
109
+
110
+ return {
111
+ render: (width: number) => [
112
+ theme.fg("muted", "─".repeat(Math.max(0, width))),
113
+ ...list.render(width),
114
+ theme.fg("muted", "─".repeat(Math.max(0, width))),
115
+ ],
116
+ invalidate: () => list.invalidate(),
117
+ handleInput: (data: string) => {
118
+ list.handleInput(data);
119
+ tui.requestRender();
120
+ },
121
+ };
122
+ },
123
+ );
124
+
125
+ return result ?? null;
126
+ }
127
+
128
+ function completions(
129
+ events: EventBus,
130
+ prefix: string,
131
+ pinnedProcessId: string | null,
132
+ ): AutocompleteItem[] | null {
133
+ const normalized = prefix.trim().toLowerCase();
134
+ const clearItems =
135
+ pinnedProcessId && "clear".startsWith(normalized)
136
+ ? [
137
+ {
138
+ value: "clear",
139
+ label: "clear",
140
+ description: "Unpin the dock and show all running process logs.",
141
+ },
142
+ ]
143
+ : [];
144
+ const processItems = requestProcessList(events)
145
+ .filter(
146
+ (process) =>
147
+ process.id.toLowerCase().startsWith(normalized) ||
148
+ process.name.toLowerCase().includes(normalized),
149
+ )
150
+ .map((process) => ({
151
+ value: process.id,
152
+ label: `${process.name} (${process.id})`,
153
+ description: formatProcessSelectionDescription(process),
154
+ }));
155
+ const items = [...clearItems, ...processItems];
156
+ return items.length > 0 ? items : null;
157
+ }
158
+
159
+ function report(
160
+ ctx: ExtensionCommandContext,
161
+ message: string,
162
+ level: "info" | "warning",
163
+ ): void {
164
+ if (ctx.hasUI) ctx.ui.notify(message, level);
165
+ else console.log(message);
166
+ }
@@ -0,0 +1,375 @@
1
+ import { Stack } from "@aliou/pi-utils-ui";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ type Component,
5
+ truncateToWidth,
6
+ visibleWidth,
7
+ } from "@earendil-works/pi-tui";
8
+ import type { ProcessInfo } from "../../../src/types";
9
+ import { LIVE_STATUSES } from "../../../src/types";
10
+ import { stripAnsi } from "../../../src/utils/ansi";
11
+ import { renderProcessTab } from "../../process-tabs";
12
+ import {
13
+ clampNameColumn,
14
+ LineComponent,
15
+ LinesComponent,
16
+ statusDot,
17
+ } from "../../shared/ui";
18
+ import type { ProcessLogLine } from "../client";
19
+ import type { DockState } from "../widget/types";
20
+
21
+ export interface ProcessLogEntry {
22
+ processId: string;
23
+ line: ProcessLogLine;
24
+ }
25
+
26
+ export interface LogDockSnapshot {
27
+ processes: ProcessInfo[];
28
+ pinnedProcess: ProcessInfo | null;
29
+ pinnedLines: ProcessLogLine[];
30
+ processLogStream: ProcessLogEntry[];
31
+ previews: Map<string, ProcessLogLine | null>;
32
+ notifyLines: Set<string>;
33
+ notifyCounts: Map<string, number>;
34
+ state: DockState;
35
+ }
36
+
37
+ export function renderCollapsedDockLine(
38
+ content: string,
39
+ width: number,
40
+ ): string {
41
+ if (width <= 0) return "";
42
+
43
+ const line = truncateToWidth(content, width, "", true);
44
+ return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
45
+ }
46
+
47
+ export function renderLogDock(
48
+ snapshot: LogDockSnapshot,
49
+ theme: Theme,
50
+ width: number,
51
+ logRows: number,
52
+ ): string[] {
53
+ if (snapshot.state.visibility === "closed") return [];
54
+ if (snapshot.processes.length === 0) return [];
55
+
56
+ const body =
57
+ snapshot.state.visibility === "expanded"
58
+ ? buildExpandedBody(snapshot, theme, Math.max(1, logRows))
59
+ : buildCollapsedBody(snapshot, theme);
60
+
61
+ return renderMinimalBox(
62
+ "Processes",
63
+ body,
64
+ theme,
65
+ width,
66
+ snapshot.state.visibility === "expanded",
67
+ );
68
+ }
69
+
70
+ const SIDE_PADDING = 1;
71
+
72
+ /**
73
+ * Render the dock as a minimal panel: a top bar with the title (no side
74
+ * padding), an optional full-width rule between the process strip and the
75
+ * logs, and a single space of side padding for the body lines. No bottom
76
+ * border so the dock opens cleanly into the editor below.
77
+ */
78
+ function renderMinimalBox(
79
+ title: string,
80
+ body: Component,
81
+ theme: Theme,
82
+ width: number,
83
+ withProcessStrip: boolean,
84
+ ): string[] {
85
+ const dim = (text: string) => theme.fg("dim", text);
86
+ const lines: string[] = [];
87
+
88
+ // Top bar: no side padding, title sits after a small "── " flourish.
89
+ const label = `── ${title} `;
90
+ const labelWidth = visibleWidth(label);
91
+ if (labelWidth >= width) {
92
+ lines.push(dim("─".repeat(Math.max(0, width))));
93
+ } else {
94
+ lines.push(dim(label) + dim("─".repeat(width - labelWidth)));
95
+ }
96
+
97
+ const innerWidth = Math.max(0, width - SIDE_PADDING * 2);
98
+ const pad = " ".repeat(SIDE_PADDING);
99
+ const bodyLines = body.render(innerWidth);
100
+
101
+ if (!withProcessStrip) {
102
+ // Collapsed mode: body is just log preview lines.
103
+ for (const line of bodyLines) {
104
+ const log = truncateToWidth(line, innerWidth, "", true);
105
+ lines.push(`${pad}${log}${pad}`);
106
+ }
107
+ return lines;
108
+ }
109
+
110
+ // Expanded mode: first body line is the process strip, the rest are logs.
111
+ if (bodyLines.length > 0) {
112
+ const strip = truncateToWidth(bodyLines[0], innerWidth, "", true);
113
+ lines.push(`${pad}${strip}${pad}`);
114
+ }
115
+
116
+ if (bodyLines.length > 1) {
117
+ lines.push(dim("─".repeat(Math.max(0, width))));
118
+ for (let i = 1; i < bodyLines.length; i++) {
119
+ const log = truncateToWidth(bodyLines[i], innerWidth, "", true);
120
+ lines.push(`${pad}${log}${pad}`);
121
+ }
122
+ }
123
+
124
+ return lines;
125
+ }
126
+
127
+ function buildCollapsedBody(
128
+ snapshot: LogDockSnapshot,
129
+ theme: Theme,
130
+ ): Component {
131
+ // Collapsed dock is intentionally minimal: just the header and a short
132
+ // log preview. No process strip.
133
+ return new LinesComponent((width) =>
134
+ renderCollapsedLogLines(snapshot, theme, width),
135
+ );
136
+ }
137
+
138
+ function buildExpandedBody(
139
+ snapshot: LogDockSnapshot,
140
+ theme: Theme,
141
+ logRows: number,
142
+ ): Component {
143
+ const body = new Stack({ gap: 0 });
144
+ body.addChild(
145
+ new LineComponent((width) => renderProcessStrip(snapshot, theme, width)),
146
+ );
147
+ body.addChild(
148
+ new LinesComponent((width) => {
149
+ const contentLines = snapshot.pinnedProcess
150
+ ? renderPinnedLogLines(snapshot, theme, width, logRows)
151
+ : renderAllRunningLogLines(snapshot, theme, width, logRows);
152
+ return Array.from(
153
+ { length: logRows },
154
+ (_, index) => contentLines[index] ?? "",
155
+ );
156
+ }),
157
+ );
158
+ return body;
159
+ }
160
+
161
+ function renderCollapsedLogLines(
162
+ snapshot: LogDockSnapshot,
163
+ theme: Theme,
164
+ width: number,
165
+ ): string[] {
166
+ const height = 2;
167
+ const lines = snapshot.pinnedProcess
168
+ ? renderCollapsedPinnedLogLines(snapshot, theme, width, height)
169
+ : renderAllRunningLogLines(snapshot, theme, width, height);
170
+
171
+ if (lines.length === 0) return ["", ""];
172
+ return Array.from({ length: height }, (_, index) => lines[index] ?? "");
173
+ }
174
+
175
+ function renderCollapsedPinnedLogLines(
176
+ snapshot: LogDockSnapshot,
177
+ theme: Theme,
178
+ width: number,
179
+ height: number,
180
+ ): string[] {
181
+ if (snapshot.pinnedLines.length > 0) {
182
+ return renderLogLines(snapshot.pinnedLines, snapshot, theme, width, height);
183
+ }
184
+
185
+ const processId = snapshot.pinnedProcess?.id;
186
+ const lines = processId
187
+ ? snapshot.processLogStream
188
+ .filter((entry) => entry.processId === processId)
189
+ .map((entry) => entry.line)
190
+ : [];
191
+ return renderLogLines(lines, snapshot, theme, width, height);
192
+ }
193
+
194
+ function renderProcessStrip(
195
+ snapshot: LogDockSnapshot,
196
+ theme: Theme,
197
+ width: number,
198
+ ): string {
199
+ const liveOrVisible = snapshot.processes.filter(
200
+ (process) =>
201
+ LIVE_STATUSES.has(process.status) ||
202
+ snapshot.pinnedProcess?.id === process.id,
203
+ );
204
+ const finished = snapshot.processes.filter(
205
+ (process) =>
206
+ !LIVE_STATUSES.has(process.status) &&
207
+ snapshot.pinnedProcess?.id !== process.id,
208
+ );
209
+
210
+ // Show live and failed/killed processes individually; collapse only
211
+ // exited-success into a single summary token.
212
+ const exitedSuccess: ProcessInfo[] = [];
213
+ const shown: ProcessInfo[] = [];
214
+ for (const process of finished) {
215
+ if (process.status === "exited" && process.success) {
216
+ exitedSuccess.push(process);
217
+ } else {
218
+ shown.push(process);
219
+ }
220
+ }
221
+
222
+ const ordered = [...liveOrVisible, ...shown];
223
+ const parts = ordered.map((process) =>
224
+ renderProcessToken(process, snapshot, theme),
225
+ );
226
+ if (exitedSuccess.length > 0) {
227
+ const summary = {
228
+ id: "_summary",
229
+ name: "",
230
+ status: "exited",
231
+ success: true,
232
+ exitCode: 0,
233
+ } as ProcessInfo;
234
+ parts.push(
235
+ `${statusDot(summary, false, theme)} ${theme.fg("dim", `${exitedSuccess.length} done`)}`,
236
+ );
237
+ }
238
+
239
+ return truncateToWidth(
240
+ parts.join(" ") || theme.fg("dim", "No running processes"),
241
+ width,
242
+ );
243
+ }
244
+
245
+ function renderPinnedLogLines(
246
+ snapshot: LogDockSnapshot,
247
+ theme: Theme,
248
+ width: number,
249
+ height: number,
250
+ ): string[] {
251
+ if (snapshot.pinnedLines.length === 0) {
252
+ return centerLine(theme.fg("muted", "No output yet"), width, height);
253
+ }
254
+
255
+ return renderLogLines(snapshot.pinnedLines, snapshot, theme, width, height);
256
+ }
257
+
258
+ function renderAllRunningLogLines(
259
+ snapshot: LogDockSnapshot,
260
+ theme: Theme,
261
+ width: number,
262
+ height: number,
263
+ ): string[] {
264
+ const byId = new Map(
265
+ snapshot.processes.map((process) => [process.id, process]),
266
+ );
267
+ const stream = snapshot.processLogStream.filter(({ processId }) => {
268
+ const process = byId.get(processId);
269
+ return process ? LIVE_STATUSES.has(process.status) : false;
270
+ });
271
+
272
+ if (stream.length === 0) {
273
+ return centerLine(theme.fg("muted", "No output yet"), width, height);
274
+ }
275
+
276
+ const activeProcesses = snapshot.processes.filter((process) =>
277
+ LIVE_STATUSES.has(process.status),
278
+ );
279
+ const nameCol = clampNameColumn(activeProcesses);
280
+ const separator = theme.fg("dim", " │ ");
281
+ const sepLen = visibleWidth(separator);
282
+
283
+ return stream.slice(-height).map(({ processId, line }) => {
284
+ const process = byId.get(processId);
285
+ const label = theme.fg("dim", padName(process?.name ?? processId, nameCol));
286
+ const text = renderLogText(
287
+ line,
288
+ snapshot,
289
+ theme,
290
+ Math.max(1, width - nameCol - sepLen),
291
+ );
292
+ return truncateToWidth(`${label}${separator}${text}`, width, "", true);
293
+ });
294
+ }
295
+
296
+ function renderLogLines(
297
+ lines: ProcessLogLine[],
298
+ snapshot: LogDockSnapshot,
299
+ theme: Theme,
300
+ width: number,
301
+ height: number,
302
+ ): string[] {
303
+ const compacted = compactRepeatedLines(lines);
304
+ return compacted
305
+ .slice(-height)
306
+ .map((line) => renderLogText(line, snapshot, theme, width));
307
+ }
308
+
309
+ function renderLogText(
310
+ line: ProcessLogLine,
311
+ snapshot: LogDockSnapshot,
312
+ theme: Theme,
313
+ width: number,
314
+ ): string {
315
+ const text = truncateToWidth(stripAnsi(line.text), width, "", true);
316
+ if (snapshot.notifyLines.has(line.text) || snapshot.notifyLines.has(text)) {
317
+ return truncateToWidth(theme.underline(text), width);
318
+ }
319
+ if (line.type === "stderr") {
320
+ return truncateToWidth(theme.fg("warning", text), width);
321
+ }
322
+ return truncateToWidth(text, width);
323
+ }
324
+
325
+ function renderProcessToken(
326
+ process: ProcessInfo,
327
+ snapshot: LogDockSnapshot,
328
+ theme: Theme,
329
+ ): string {
330
+ const tab = renderProcessTab(
331
+ process,
332
+ snapshot.pinnedProcess?.id === process.id,
333
+ theme,
334
+ );
335
+ const badge = snapshot.notifyCounts.get(process.id)
336
+ ? theme.fg("warning", `▸${snapshot.notifyCounts.get(process.id)}`)
337
+ : "";
338
+ return badge ? `${tab}${badge}` : tab;
339
+ }
340
+
341
+ function compactRepeatedLines(lines: ProcessLogLine[]): ProcessLogLine[] {
342
+ const result: ProcessLogLine[] = [];
343
+ for (let index = 0; index < lines.length; index++) {
344
+ const line = lines[index];
345
+ let repeats = 1;
346
+ while (
347
+ index + repeats < lines.length &&
348
+ lines[index + repeats]?.type === line.type &&
349
+ lines[index + repeats]?.text === line.text
350
+ ) {
351
+ repeats++;
352
+ }
353
+ result.push(line);
354
+ if (repeats >= 3) {
355
+ result.push({ type: line.type, text: `… repeated ${repeats - 1} times` });
356
+ } else {
357
+ for (let repeat = 1; repeat < repeats; repeat++) result.push(line);
358
+ }
359
+ index += repeats - 1;
360
+ }
361
+ return result;
362
+ }
363
+
364
+ function centerLine(content: string, width: number, height: number): string[] {
365
+ const lines = Array.from({ length: height }, () => "");
366
+ const row = Math.max(0, Math.floor((height - 1) / 2));
367
+ const leftPad = Math.max(0, Math.floor((width - visibleWidth(content)) / 2));
368
+ lines[row] = truncateToWidth(`${" ".repeat(leftPad)}${content}`, width);
369
+ return lines;
370
+ }
371
+
372
+ function padName(value: string, width: number): string {
373
+ const name = truncateToWidth(value, width, "", true);
374
+ return `${name}${" ".repeat(Math.max(0, width - visibleWidth(name)))}`;
375
+ }
@@ -0,0 +1,33 @@
1
+ import type { DockActions, DockState, DockVisibility } from "./widget/types";
2
+
3
+ export function createDockState(initial: Partial<DockState> = {}): {
4
+ getState: () => DockState;
5
+ actions: DockActions;
6
+ } {
7
+ const state: DockState = {
8
+ visibility: initial.visibility ?? "closed",
9
+ followEnabled: initial.followEnabled ?? true,
10
+ focusedProcessId: initial.focusedProcessId ?? null,
11
+ };
12
+
13
+ const setVisibility = (visibility: DockVisibility) => {
14
+ state.visibility = visibility;
15
+ };
16
+
17
+ return {
18
+ getState: () => ({ ...state }),
19
+ actions: {
20
+ getFocusedProcessId: () => state.focusedProcessId,
21
+ isFollowEnabled: () => state.followEnabled,
22
+ setFocus: (processId) => {
23
+ state.focusedProcessId = processId;
24
+ if (processId && state.visibility === "closed") {
25
+ state.visibility = "expanded";
26
+ }
27
+ },
28
+ expand: () => setVisibility("expanded"),
29
+ collapse: () => setVisibility("collapsed"),
30
+ close: () => setVisibility("closed"),
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { isWindowsPlatform } from "../../src/utils/platform";
3
+ import { registerDockCommand } from "./commands/dock";
4
+ import { registerPinCommand } from "./commands/pin";
5
+ import { type DockController, setupDockWidgets } from "./widget/setup";
6
+
7
+ export default async function processesDockExtension(
8
+ pi: ExtensionAPI,
9
+ ): Promise<void> {
10
+ // The core extension no-ops on Windows; the dock has no backing manager to
11
+ // read from, so bail silently (the core extension already warned).
12
+ if (isWindowsPlatform()) return;
13
+
14
+ let controller: DockController | null = null;
15
+
16
+ registerDockCommand(pi, () => controller);
17
+ registerPinCommand(pi, pi.events, () => controller);
18
+
19
+ pi.on("session_start", async (_event, ctx) => {
20
+ controller?.dispose();
21
+ controller = setupDockWidgets(ctx, pi.events);
22
+ });
23
+
24
+ pi.on("session_shutdown", () => {
25
+ controller?.dispose();
26
+ controller = null;
27
+ });
28
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ connectToProcessLogs,
3
+ type LogsConnection,
4
+ type ProcessLogLine,
5
+ } from "../processes-logs/logs-client";