@aliou/pi-processes 0.4.0 → 0.4.3

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/commands/index.ts CHANGED
@@ -48,15 +48,10 @@ function allProcessCompletions(manager: ProcessManager) {
48
48
  };
49
49
  }
50
50
 
51
- export interface ProcessCommands {
52
- /** Start streaming logs for a process via the widget. Needs a UI context. */
53
- streamProcess: (processId: string, ui: ExtensionCommandContext["ui"]) => void;
54
- }
55
-
56
51
  export function setupProcessesCommands(
57
52
  pi: ExtensionAPI,
58
53
  manager: ProcessManager,
59
- ): ProcessCommands {
54
+ ): void {
60
55
  let streamingProcessId: string | null = null;
61
56
 
62
57
  // ── /process:list ──────────────────────────────────────────────────
@@ -304,12 +299,6 @@ export function setupProcessesCommands(
304
299
  { placement: "aboveEditor" },
305
300
  );
306
301
  }
307
-
308
- return {
309
- streamProcess: (processId: string, ui: ExtensionCommandContext["ui"]) => {
310
- startStreaming(ui, manager, processId);
311
- },
312
- };
313
302
  }
314
303
 
315
304
  async function pickProcess(
@@ -1,3 +1,8 @@
1
+ import {
2
+ createPanelPadder,
3
+ renderPanelRule,
4
+ renderPanelTitleLine,
5
+ } from "@aliou/pi-utils-ui";
1
6
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
7
  import {
3
8
  type Component,
@@ -62,26 +67,26 @@ export class LogStreamComponent implements Component {
62
67
 
63
68
  const theme = this.theme;
64
69
  const dim = (s: string) => theme.fg("dim", s);
65
- const accent = (s: string) => theme.fg("accent", s);
66
70
  const warning = (s: string) => theme.fg("warning", s);
67
- const border = (s: string) => theme.fg("dim", s);
68
71
  const innerWidth = width - 2;
69
72
 
73
+ const basePadLine = createPanelPadder(width);
70
74
  const padLine = (content: string): string => {
71
75
  const contentWidth = visibleWidth(content);
72
- if (contentWidth > innerWidth) {
73
- return ` ${truncateToWidth(content, innerWidth)} `;
74
- }
75
- return ` ${content}${" ".repeat(Math.max(0, innerWidth - contentWidth))} `;
76
+ return basePadLine(
77
+ contentWidth > innerWidth
78
+ ? truncateToWidth(content, innerWidth)
79
+ : content,
80
+ );
76
81
  };
77
82
 
78
83
  const lines: string[] = [];
79
84
  const proc = this.manager.get(this.processId);
80
85
 
81
86
  if (!proc) {
82
- lines.push(border("─".repeat(width)));
87
+ lines.push(renderPanelRule(width, theme));
83
88
  lines.push(padLine(warning("Process not found")));
84
- lines.push(border("─".repeat(width)));
89
+ lines.push(renderPanelRule(width, theme));
85
90
  this.cachedLines = lines;
86
91
  this.cachedWidth = width;
87
92
  return this.cachedLines;
@@ -90,15 +95,12 @@ export class LogStreamComponent implements Component {
90
95
  // Header
91
96
  const icon = statusIcon(proc.status, proc.success);
92
97
  const label = statusLabel(proc);
93
- const headerText = ` ${accent(proc.name)} ${dim(`(${proc.id})`)} ${icon} ${dim(label)} `;
94
- const headerLen = visibleWidth(headerText);
95
- const borderLen = Math.max(0, width - headerLen);
96
- const leftBorder = Math.floor(borderLen / 2);
97
- const rightBorder = borderLen - leftBorder;
98
98
  lines.push(
99
- border("─".repeat(leftBorder)) +
100
- headerText +
101
- border("─".repeat(rightBorder)),
99
+ renderPanelTitleLine(
100
+ `Process: ${proc.name} (${proc.id}) ${icon} ${label}`,
101
+ width,
102
+ theme,
103
+ ),
102
104
  );
103
105
 
104
106
  // Log lines (interleaved stdout + stderr in temporal order).
@@ -127,9 +129,9 @@ export class LogStreamComponent implements Component {
127
129
  }
128
130
 
129
131
  // Footer hint
130
- lines.push(border("─".repeat(width)));
132
+ lines.push(renderPanelRule(width, theme));
131
133
  lines.push(padLine(dim("Run /process:stream to dismiss")));
132
- lines.push(border("─".repeat(width)));
134
+ lines.push(renderPanelRule(width, theme));
133
135
 
134
136
  this.cachedLines = lines;
135
137
  this.cachedWidth = width;
@@ -1,5 +1,10 @@
1
+ import {
2
+ createPanelPadder,
3
+ renderPanelRule,
4
+ renderPanelTitleLine,
5
+ } from "@aliou/pi-utils-ui";
1
6
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { type Component, matchesKey, visibleWidth } from "@mariozechner/pi-tui";
7
+ import { type Component, matchesKey } from "@mariozechner/pi-tui";
3
8
  import type { ProcessInfo } from "../constants";
4
9
  import type { ProcessManager } from "../manager";
5
10
  import { statusIcon, statusLabel } from "./status-format";
@@ -105,29 +110,13 @@ export class ProcessPickerComponent implements Component {
105
110
  const theme = this.theme;
106
111
  const dim = (s: string) => theme.fg("dim", s);
107
112
  const accent = (s: string) => theme.fg("accent", s);
108
- const bold = (s: string) => theme.bold(s);
109
- const border = (s: string) => theme.fg("dim", s);
110
- const innerWidth = width - 2;
111
113
 
112
- const padLine = (content: string): string => {
113
- const len = visibleWidth(content);
114
- return ` ${content}${" ".repeat(Math.max(0, innerWidth - len))} `;
115
- };
114
+ const padLine = createPanelPadder(width);
116
115
 
117
116
  const lines: string[] = [];
118
117
  const processes = this.getProcesses();
119
118
 
120
- // Header
121
- const titleText = ` ${this.title} `;
122
- const titleLen = titleText.length;
123
- const borderLen = Math.max(0, width - titleLen);
124
- const leftBorder = Math.floor(borderLen / 2);
125
- const rightBorder = borderLen - leftBorder;
126
- lines.push(
127
- border("─".repeat(leftBorder)) +
128
- accent(bold(titleText)) +
129
- border("─".repeat(rightBorder)),
130
- );
119
+ lines.push(renderPanelTitleLine(this.title, width, theme));
131
120
 
132
121
  if (processes.length === 0) {
133
122
  lines.push(padLine(""));
@@ -151,13 +140,13 @@ export class ProcessPickerComponent implements Component {
151
140
  }
152
141
 
153
142
  // Footer
154
- lines.push(border("─".repeat(width)));
143
+ lines.push(renderPanelRule(width, theme));
155
144
  lines.push(
156
145
  padLine(
157
146
  `${dim("j/k")} select ${dim("enter")} confirm ${dim("q")} cancel`,
158
147
  ),
159
148
  );
160
- lines.push(border("─".repeat(width)));
149
+ lines.push(renderPanelRule(width, theme));
161
150
 
162
151
  this.cachedLines = lines;
163
152
  this.cachedWidth = width;
@@ -1,3 +1,8 @@
1
+ import {
2
+ createPanelPadder,
3
+ renderPanelRule,
4
+ renderPanelTitleLine,
5
+ } from "@aliou/pi-utils-ui";
1
6
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
7
  import { type Component, matchesKey, visibleWidth } from "@mariozechner/pi-tui";
3
8
  import { configLoader } from "../config";
@@ -204,28 +209,14 @@ export class ProcessesComponent implements Component {
204
209
  const dim = (s: string) => theme.fg("dim", s);
205
210
  const accent = (s: string) => theme.fg("accent", s);
206
211
  const warning = (s: string) => theme.fg("warning", s);
207
- const bold = (s: string) => theme.bold(s);
208
- const border = (s: string) => theme.fg("dim", s);
209
212
 
210
213
  const lines: string[] = [];
211
214
  const processes = this.manager.list();
212
215
  const innerWidth = width - 2;
213
216
 
214
- const padLine = (content: string): string => {
215
- const len = visibleWidth(content);
216
- return ` ${content}${" ".repeat(Math.max(0, innerWidth - len))} `;
217
- };
218
-
219
- const title = " Background Processes ";
220
- const titleLen = title.length;
221
- const borderLen = Math.max(0, width - titleLen);
222
- const leftBorder = Math.floor(borderLen / 2);
223
- const rightBorder = borderLen - leftBorder;
224
- lines.push(
225
- border("─".repeat(leftBorder)) +
226
- accent(bold(title)) +
227
- border("─".repeat(rightBorder)),
228
- );
217
+ const padLine = createPanelPadder(width);
218
+
219
+ lines.push(renderPanelTitleLine("Background Processes", width, theme));
229
220
 
230
221
  if (processes.length === 0) {
231
222
  lines.push(padLine(""));
@@ -270,7 +261,7 @@ export class ProcessesComponent implements Component {
270
261
  dim("Size".padStart(sizeWidth)) +
271
262
  (hasProcessScroll ? dim(headerSuffixText) : "");
272
263
  lines.push(padLine(header));
273
- lines.push(border("─".repeat(width)));
264
+ lines.push(renderPanelRule(width, theme));
274
265
 
275
266
  const visibleProcessCount = Math.min(
276
267
  maxVisibleProcesses,
@@ -321,7 +312,7 @@ export class ProcessesComponent implements Component {
321
312
  const output = this.manager.getOutput(selected.id, maxPreviewLines * 2);
322
313
  const sizes = this.manager.getFileSize(selected.id);
323
314
 
324
- lines.push(border("─".repeat(width)));
315
+ lines.push(renderPanelRule(width, theme));
325
316
 
326
317
  const logTitlePlain = `Output: ${selected.name} (${selected.id})`;
327
318
  const sizeInfoPlain = sizes
@@ -394,7 +385,7 @@ export class ProcessesComponent implements Component {
394
385
  }
395
386
  }
396
387
 
397
- lines.push(border("─".repeat(width)));
388
+ lines.push(renderPanelRule(width, theme));
398
389
 
399
390
  const footerLeft =
400
391
  `${dim("enter")} stream ` +
@@ -421,7 +412,7 @@ export class ProcessesComponent implements Component {
421
412
  const footer = footerLeft + " ".repeat(footerGap) + footerRight;
422
413
 
423
414
  lines.push(padLine(footer));
424
- lines.push(border("─".repeat(width)));
415
+ lines.push(renderPanelRule(width, theme));
425
416
 
426
417
  this.cachedLines = lines;
427
418
  this.cachedWidth = width;
package/hooks/widget.ts CHANGED
@@ -126,8 +126,9 @@ export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
126
126
  }
127
127
 
128
128
  pi.on("session_start", async (_event, ctx) => {
129
+ // Startup defer: capture context only. First render happens on process
130
+ // manager events or explicit settings updates.
129
131
  latestContext = ctx;
130
- updateWidget();
131
132
  });
132
133
 
133
134
  pi.on("session_switch", async (_event, ctx) => {
package/index.ts CHANGED
@@ -19,8 +19,8 @@ export default async function (pi: ExtensionAPI) {
19
19
  const manager = new ProcessManager();
20
20
 
21
21
  const { update: updateWidget } = setupProcessesHooks(pi, manager);
22
- const commands = setupProcessesCommands(pi, manager);
23
- setupProcessesTools(pi, manager, commands);
22
+ setupProcessesCommands(pi, manager);
23
+ setupProcessesTools(pi, manager);
24
24
  registerProcessesSettings(pi, () => {
25
25
  updateWidget();
26
26
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-processes",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "keywords": [
@@ -34,7 +34,8 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@sinclair/typebox": "^0.34.41",
37
- "@aliou/pi-utils-settings": "0.2.1"
37
+ "@aliou/pi-utils-settings": "0.4.0",
38
+ "@aliou/pi-utils-ui": "^0.1.0"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@mariozechner/pi-coding-agent": ">=0.51.0"
package/tools/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ToolBody, ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
1
2
  import { StringEnum } from "@mariozechner/pi-ai";
2
3
  import type {
3
4
  AgentToolResult,
@@ -57,13 +58,7 @@ const ProcessesParams = Type.Object({
57
58
 
58
59
  type ProcessesParamsType = Static<typeof ProcessesParams>;
59
60
 
60
- import type { ProcessCommands } from "../commands";
61
-
62
- export function setupProcessesTools(
63
- pi: ExtensionAPI,
64
- manager: ProcessManager,
65
- commands: ProcessCommands,
66
- ) {
61
+ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
67
62
  pi.registerTool<typeof ProcessesParams, ProcessesDetails>({
68
63
  name: "process",
69
64
  label: "Process",
@@ -85,50 +80,56 @@ Note: User always sees process updates in the UI. The notify flags control wheth
85
80
  parameters: ProcessesParams,
86
81
 
87
82
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
88
- const result = await executeAction(params, manager, ctx);
89
- // Auto-stream logs when a process is started successfully.
90
- if (
91
- params.action === "start" &&
92
- result.details?.success &&
93
- result.details.process &&
94
- ctx.hasUI
95
- ) {
96
- commands.streamProcess(result.details.process.id, ctx.ui);
97
- }
98
- return result;
83
+ return executeAction(params, manager, ctx);
99
84
  },
100
85
 
101
- renderCall(args: ProcessesParamsType, theme: Theme): Text {
102
- let text = theme.fg("toolTitle", theme.bold("Process "));
103
- text += theme.fg("accent", args.action);
86
+ renderCall(args: ProcessesParamsType, theme: Theme) {
87
+ const longArgs: Array<{ label?: string; value: string }> = [];
88
+ const optionArgs: Array<{ label: string; value: string }> = [];
89
+ let mainArg: string | undefined;
104
90
 
105
- switch (args.action) {
106
- case "start": {
107
- if (args.name) {
108
- text += ` ${theme.fg("accent", `"${args.name}"`)}`;
109
- }
110
- if (args.command) {
111
- text += `\n${theme.fg("muted", `$ ${args.command}`)}`;
112
- }
113
- return new Text(text, 0, 0);
91
+ if (args.action === "start") {
92
+ if (args.name) {
93
+ mainArg = `"${args.name}"`;
114
94
  }
115
- case "output":
116
- case "kill":
117
- case "logs":
118
- if (args.id) {
119
- text += ` ${theme.fg("muted", args.id)}`;
95
+
96
+ if (args.command) {
97
+ if (!mainArg && args.command.length <= 60) {
98
+ mainArg = args.command;
99
+ } else if (args.command.length <= 60) {
100
+ optionArgs.push({ label: "command", value: args.command });
101
+ } else {
102
+ longArgs.push({ label: "command", value: args.command });
120
103
  }
121
- break;
104
+ }
105
+ }
106
+
107
+ if (
108
+ (args.action === "output" ||
109
+ args.action === "kill" ||
110
+ args.action === "logs") &&
111
+ args.id
112
+ ) {
113
+ mainArg = args.id;
122
114
  }
123
115
 
124
- return new Text(text, 0, 0);
116
+ return new ToolCallHeader(
117
+ {
118
+ toolName: "Process",
119
+ action: args.action,
120
+ mainArg,
121
+ optionArgs,
122
+ longArgs,
123
+ },
124
+ theme,
125
+ );
125
126
  },
126
127
 
127
128
  renderResult(
128
129
  result: AgentToolResult<ProcessesDetails>,
129
- _options: ToolRenderResultOptions,
130
+ options: ToolRenderResultOptions,
130
131
  theme: Theme,
131
- ): Text {
132
+ ) {
132
133
  const { details } = result;
133
134
 
134
135
  if (!details) {
@@ -140,34 +141,32 @@ Note: User always sees process updates in the UI. The notify flags control wheth
140
141
  );
141
142
  }
142
143
 
143
- if (!details.success) {
144
- return new Text(theme.fg("error", details.message), 0, 0);
145
- }
146
-
147
- // For start action
148
- if (details.action === "start" && details.process) {
149
- const p = details.process;
150
- return new Text(
151
- theme.fg("success", "\u2713 Started ") +
152
- theme.fg("accent", `"${p.name}"`) +
153
- ` (${p.id}, PID: ${p.pid})`,
154
- 0,
155
- 0,
156
- );
157
- }
158
-
159
- // For output action
160
- if (details.action === "output" && details.output) {
161
- const lines: string[] = [];
162
- lines.push(theme.fg("muted", details.message));
144
+ const fields: Array<
145
+ { label: string; value: string; showCollapsed?: boolean } | Text
146
+ > = [];
163
147
 
148
+ if (!details.success) {
149
+ fields.push({
150
+ label: "Error",
151
+ value: theme.fg("error", details.message),
152
+ showCollapsed: true,
153
+ });
154
+ } else if (details.action === "start" && details.process) {
155
+ const process = details.process;
156
+ fields.push({
157
+ label: "Status",
158
+ value:
159
+ theme.fg("success", "Started") +
160
+ ` ${theme.fg("accent", `"${process.name}"`)} (${process.id}, PID: ${process.pid})`,
161
+ showCollapsed: true,
162
+ });
163
+ } else if (details.action === "output" && details.output) {
164
+ const lines: string[] = [theme.fg("muted", details.message)];
164
165
  let hadAnsi = false;
165
166
 
166
167
  if (details.output.stdout.length > 0) {
167
- lines.push("");
168
- lines.push(theme.fg("accent", "stdout:"));
169
- const stdoutLines = details.output.stdout.slice(-20);
170
- for (const line of stdoutLines) {
168
+ lines.push("", theme.fg("accent", "stdout:"));
169
+ for (const line of details.output.stdout.slice(-20)) {
171
170
  if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
172
171
  lines.push(stripAnsi(line));
173
172
  }
@@ -182,10 +181,8 @@ Note: User always sees process updates in the UI. The notify flags control wheth
182
181
  }
183
182
 
184
183
  if (details.output.stderr.length > 0) {
185
- lines.push("");
186
- lines.push(theme.fg("warning", "stderr:"));
187
- const stderrLines = details.output.stderr.slice(-10);
188
- for (const line of stderrLines) {
184
+ lines.push("", theme.fg("warning", "stderr:"));
185
+ for (const line of details.output.stderr.slice(-10)) {
189
186
  if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
190
187
  lines.push(theme.fg("warning", stripAnsi(line)));
191
188
  }
@@ -200,28 +197,25 @@ Note: User always sees process updates in the UI. The notify flags control wheth
200
197
  }
201
198
 
202
199
  if (hadAnsi) {
203
- lines.push("");
204
200
  lines.push(
201
+ "",
205
202
  theme.fg("muted", "ANSI escape codes were stripped from output"),
206
203
  );
207
204
  }
208
205
 
209
- return new Text(lines.join("\n"), 0, 0);
210
- }
211
-
212
- // For list action
213
- if (
206
+ fields.push(new Text(lines.join("\n"), 0, 0));
207
+ } else if (
214
208
  details.action === "list" &&
215
209
  details.processes &&
216
210
  details.processes.length > 0
217
211
  ) {
218
- const lines: string[] = [];
219
- lines.push(
212
+ const lines: string[] = [
220
213
  theme.fg("success", `${details.processes.length} process(es):`),
221
- );
222
- for (const p of details.processes) {
214
+ ];
215
+
216
+ for (const process of details.processes) {
223
217
  let status: string;
224
- switch (p.status) {
218
+ switch (process.status) {
225
219
  case "running":
226
220
  status = theme.fg("accent", "running");
227
221
  break;
@@ -235,43 +229,52 @@ Note: User always sees process updates in the UI. The notify flags control wheth
235
229
  status = theme.fg("warning", "killed");
236
230
  break;
237
231
  case "exited":
238
- status = p.success
232
+ status = process.success
239
233
  ? theme.fg("success", "exit(0)")
240
- : theme.fg("error", `exit(${p.exitCode ?? "?"})`);
234
+ : theme.fg("error", `exit(${process.exitCode ?? "?"})`);
241
235
  break;
242
236
  default:
243
- status = theme.fg("muted", p.status);
237
+ status = theme.fg("muted", process.status);
244
238
  }
239
+
245
240
  lines.push(
246
- ` ${p.id} ${theme.fg("accent", `"${p.name}"`)}: ${truncateCmd(p.command)} [${status}] ${formatRuntime(p.startTime, p.endTime)}`,
241
+ ` ${process.id} ${theme.fg("accent", `"${process.name}"`)}: ${truncateCmd(process.command)} [${status}] ${formatRuntime(process.startTime, process.endTime)}`,
247
242
  );
248
243
  }
249
- return new Text(lines.join("\n"), 0, 0);
250
- }
251
244
 
252
- // For logs action
253
- if (details.action === "logs" && details.logFiles) {
254
- const lines: string[] = [];
255
- lines.push(theme.fg("success", "Log files:"));
256
- lines.push(
257
- ` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
245
+ fields.push(new Text(lines.join("\n"), 0, 0));
246
+ } else if (details.action === "logs" && details.logFiles) {
247
+ fields.push(
248
+ new Text(
249
+ [
250
+ theme.fg("success", "Log files:"),
251
+ ` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
252
+ ` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
253
+ ].join("\n"),
254
+ 0,
255
+ 0,
256
+ ),
258
257
  );
259
- lines.push(
260
- ` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
261
- );
262
- return new Text(lines.join("\n"), 0, 0);
258
+ } else {
259
+ fields.push({
260
+ label: "Result",
261
+ value: details.message,
262
+ showCollapsed: true,
263
+ });
263
264
  }
264
265
 
265
- // For clear action
266
- if (details.action === "clear") {
267
- return new Text(
268
- theme.fg("success", "\u2713 ") + theme.fg("muted", details.message),
269
- 0,
270
- 0,
271
- );
272
- }
266
+ const footer = new ToolFooter(theme, {
267
+ items: [
268
+ { label: "action", value: details.action, tone: "accent" },
269
+ {
270
+ label: "status",
271
+ value: details.success ? "ok" : "error",
272
+ tone: details.success ? "success" : "error",
273
+ },
274
+ ],
275
+ });
273
276
 
274
- return new Text(details.message, 0, 0);
277
+ return new ToolBody({ fields, footer }, options, theme);
275
278
  },
276
279
  });
277
280
  }