@aliou/pi-processes 0.5.0 → 0.6.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 (47) hide show
  1. package/README.md +83 -13
  2. package/package.json +2 -2
  3. package/src/commands/clear/command.ts +14 -0
  4. package/src/commands/clear/index.ts +1 -0
  5. package/src/commands/completions.ts +38 -0
  6. package/src/commands/dock/command.ts +29 -0
  7. package/src/commands/dock/index.ts +1 -0
  8. package/src/commands/index.ts +26 -327
  9. package/src/commands/kill/command.ts +69 -0
  10. package/src/commands/kill/index.ts +1 -0
  11. package/src/commands/logs/command.ts +47 -0
  12. package/src/commands/logs/index.ts +1 -0
  13. package/src/commands/pick-process.ts +34 -0
  14. package/src/commands/pin/command.ts +33 -0
  15. package/src/commands/pin/index.ts +1 -0
  16. package/src/commands/processes/command.ts +39 -0
  17. package/src/commands/processes/index.ts +1 -0
  18. package/src/commands/settings/apply-setting-change.ts +72 -0
  19. package/src/commands/settings/build-sections.ts +160 -0
  20. package/src/commands/settings/command.ts +20 -0
  21. package/src/commands/settings/index.ts +1 -0
  22. package/src/components/log-dock-component.ts +238 -0
  23. package/src/components/log-file-viewer.ts +317 -0
  24. package/src/components/log-overlay-component.ts +555 -0
  25. package/src/components/processes-component.ts +0 -1
  26. package/src/config.ts +28 -1
  27. package/src/constants/index.ts +1 -0
  28. package/src/constants/types.ts +7 -1
  29. package/src/hooks/index.ts +5 -3
  30. package/src/hooks/process-end.ts +3 -30
  31. package/src/hooks/widget/index.ts +2 -0
  32. package/src/hooks/widget/setup.ts +168 -0
  33. package/src/hooks/{widget.ts → widget/status-widget.ts} +7 -74
  34. package/src/hooks/widget/types.ts +21 -0
  35. package/src/index.ts +8 -3
  36. package/src/manager.ts +61 -9
  37. package/src/tools/actions/index.ts +5 -0
  38. package/src/tools/actions/write.ts +87 -0
  39. package/src/tools/index.ts +82 -14
  40. package/src/utils/command-executor.ts +1 -1
  41. package/src/utils/keybindings.ts +76 -0
  42. package/src/commands/settings-command.ts +0 -179
  43. package/src/components/log-stream-component.ts +0 -149
  44. package/src/test/test-exit-crash.sh +0 -19
  45. package/src/test/test-exit-failure.sh +0 -17
  46. package/src/test/test-exit-success.sh +0 -16
  47. package/src/test/test-output.sh +0 -28
@@ -1,7 +1,4 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
2
  import { MESSAGE_TYPE_PROCESS_UPDATE, type ProcessInfo } from "../constants";
6
3
  import type { ProcessManager } from "../manager";
7
4
  import { formatRuntime } from "../utils";
@@ -17,21 +14,6 @@ interface ProcessUpdateDetails {
17
14
  }
18
15
 
19
16
  export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
20
- let latestContext: ExtensionContext | null = null;
21
-
22
- // Capture context from session events
23
- pi.on("session_start", async (_event, ctx) => {
24
- latestContext = ctx;
25
- });
26
-
27
- pi.on("turn_start", async (_event, ctx) => {
28
- latestContext = ctx;
29
- });
30
-
31
- pi.on("turn_end", async (_event, ctx) => {
32
- latestContext = ctx;
33
- });
34
-
35
17
  manager.onEvent((event) => {
36
18
  if (event.type !== "process_ended") return;
37
19
 
@@ -47,27 +29,18 @@ export function setupProcessEndHook(pi: ExtensionAPI, manager: ProcessManager) {
47
29
 
48
30
  const runtime = formatRuntime(info.startTime, info.endTime);
49
31
 
50
- // Build notification message
32
+ // Build message
51
33
  let message: string;
52
- let level: "info" | "error" | "warning";
53
34
 
54
35
  if (info.status === "killed") {
55
36
  message = `Process '${info.name}' was terminated (${runtime})`;
56
- level = "warning";
57
37
  } else if (info.success) {
58
38
  message = `Process '${info.name}' completed successfully (${runtime})`;
59
- level = "info";
60
39
  } else {
61
40
  message = `Process '${info.name}' crashed with exit code ${info.exitCode ?? "?"} (${runtime})`;
62
- level = "error";
63
- }
64
-
65
- // Always notify user via UI
66
- if (latestContext?.hasUI) {
67
- latestContext.ui.notify(message, level);
68
41
  }
69
42
 
70
- // Always send the message so it appears in the conversation history.
43
+ // Send the message to the conversation - displayed via custom renderer in UI
71
44
  // Only trigger an agent turn when the notification preferences say so.
72
45
  const details: ProcessUpdateDetails = {
73
46
  processId: info.id,
@@ -0,0 +1,2 @@
1
+ export { setupProcessWidget } from "./setup";
2
+ export type { DockActions } from "./types";
@@ -0,0 +1,168 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import { LogDockComponent } from "../../components/log-dock-component";
6
+ import { configLoader, type ResolvedProcessesConfig } from "../../config";
7
+ import { LIVE_STATUSES } from "../../constants";
8
+ import type { ProcessManager } from "../../manager";
9
+ import { renderStatusWidget } from "./status-widget";
10
+ import {
11
+ type DockActions,
12
+ type DockState,
13
+ LOG_DOCK_WIDGET_ID,
14
+ STATUS_WIDGET_ID,
15
+ } from "./types";
16
+
17
+ export function setupProcessWidget(
18
+ pi: ExtensionAPI,
19
+ manager: ProcessManager,
20
+ config: ResolvedProcessesConfig,
21
+ ) {
22
+ let latestContext: ExtensionContext | null = null;
23
+ let logDockComponent: LogDockComponent | null = null;
24
+ let logDockComponentTui: { requestRender(): void } | null = null;
25
+
26
+ const dockState: DockState = {
27
+ visibility: "hidden",
28
+ followEnabled: config.follow.enabledByDefault,
29
+ focusedProcessId: null,
30
+ };
31
+
32
+ function updateWidget() {
33
+ if (!latestContext?.hasUI) return;
34
+
35
+ if (!configLoader.getConfig().widget.showStatusWidget) {
36
+ latestContext.ui.setWidget(STATUS_WIDGET_ID, undefined);
37
+ } else {
38
+ const processes = manager.list();
39
+ const maxWidth = process.stdout.columns || 120;
40
+ const lines = renderStatusWidget(
41
+ processes,
42
+ latestContext.ui.theme,
43
+ maxWidth,
44
+ );
45
+
46
+ if (lines.length === 0) {
47
+ latestContext.ui.setWidget(STATUS_WIDGET_ID, undefined);
48
+ } else {
49
+ latestContext.ui.setWidget(STATUS_WIDGET_ID, lines, {
50
+ placement: "belowEditor",
51
+ });
52
+ }
53
+ }
54
+
55
+ if (dockState.visibility === "hidden") {
56
+ latestContext.ui.setWidget(LOG_DOCK_WIDGET_ID, undefined);
57
+ if (logDockComponent) {
58
+ logDockComponent.dispose();
59
+ logDockComponent = null;
60
+ logDockComponentTui = null;
61
+ }
62
+ return;
63
+ }
64
+
65
+ const mode = dockState.visibility as "collapsed" | "open";
66
+ const height = mode === "collapsed" ? 3 : config.widget.dockHeight;
67
+
68
+ if (logDockComponent && logDockComponentTui) {
69
+ logDockComponent.update({
70
+ mode,
71
+ focusedProcessId: dockState.focusedProcessId,
72
+ dockHeight: height,
73
+ });
74
+ } else {
75
+ const ctx = latestContext;
76
+ ctx.ui.setWidget(
77
+ LOG_DOCK_WIDGET_ID,
78
+ (tui: { requestRender(): void }, theme: typeof ctx.ui.theme) => {
79
+ logDockComponent = new LogDockComponent({
80
+ manager,
81
+ tui,
82
+ theme,
83
+ mode,
84
+ focusedProcessId: dockState.focusedProcessId,
85
+ dockHeight: height,
86
+ });
87
+ logDockComponentTui = tui;
88
+ return logDockComponent;
89
+ },
90
+ { placement: "aboveEditor" },
91
+ );
92
+ }
93
+ }
94
+
95
+ const dockActions: DockActions = {
96
+ getFocusedProcessId: () => dockState.focusedProcessId,
97
+ isFollowEnabled: () => dockState.followEnabled,
98
+ setFocus(id) {
99
+ dockState.focusedProcessId = id;
100
+ if (id && dockState.visibility === "hidden")
101
+ dockState.visibility = "open";
102
+ updateWidget();
103
+ },
104
+ expand() {
105
+ dockState.visibility = "open";
106
+ updateWidget();
107
+ },
108
+ collapse() {
109
+ dockState.visibility = "collapsed";
110
+ updateWidget();
111
+ },
112
+ hide() {
113
+ dockState.visibility = "hidden";
114
+ updateWidget();
115
+ },
116
+ toggle() {
117
+ if (dockState.visibility === "hidden") dockState.visibility = "collapsed";
118
+ else if (dockState.visibility === "collapsed")
119
+ dockState.visibility = "open";
120
+ else dockState.visibility = "collapsed";
121
+ updateWidget();
122
+ },
123
+ toggleFollow() {
124
+ dockState.followEnabled = !dockState.followEnabled;
125
+ updateWidget();
126
+ },
127
+ };
128
+
129
+ manager.onEvent((event) => {
130
+ if (event.type === "process_started") {
131
+ if (dockState.followEnabled && dockState.visibility === "hidden") {
132
+ dockState.visibility = "collapsed";
133
+ }
134
+ }
135
+
136
+ if (event.type === "process_ended") {
137
+ if (dockState.focusedProcessId === event.info.id) {
138
+ dockState.focusedProcessId = null;
139
+ }
140
+ const running = manager.list().filter((p) => LIVE_STATUSES.has(p.status));
141
+ if (
142
+ running.length === 0 &&
143
+ config.follow.autoHideOnFinish &&
144
+ dockState.followEnabled
145
+ ) {
146
+ dockState.visibility = "hidden";
147
+ }
148
+ }
149
+
150
+ updateWidget();
151
+ });
152
+
153
+ pi.on("session_start", async (_event, ctx) => {
154
+ latestContext = ctx;
155
+ });
156
+
157
+ pi.on("session_switch", async (_event, ctx) => {
158
+ if (logDockComponent) {
159
+ logDockComponent.dispose();
160
+ logDockComponent = null;
161
+ logDockComponentTui = null;
162
+ }
163
+ latestContext = ctx;
164
+ updateWidget();
165
+ });
166
+
167
+ return { update: updateWidget, dockActions };
168
+ }
@@ -1,13 +1,6 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
5
2
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
6
- import { configLoader } from "../config";
7
- import type { ProcessInfo } from "../constants";
8
- import type { ProcessManager } from "../manager";
9
-
10
- const WIDGET_ID = "processes-status";
3
+ import type { ProcessInfo } from "../../constants";
11
4
 
12
5
  function formatProcessStatus(
13
6
  proc: ProcessInfo,
@@ -35,14 +28,12 @@ function formatProcessStatus(
35
28
  }
36
29
  }
37
30
 
38
- function renderWidget(
31
+ export function renderStatusWidget(
39
32
  processes: ProcessInfo[],
40
33
  theme: ExtensionContext["ui"]["theme"],
41
34
  maxWidth?: number,
42
35
  ): string[] {
43
- if (processes.length === 0) {
44
- return [];
45
- }
36
+ if (processes.length === 0) return [];
46
37
 
47
38
  const aliveish = processes.filter(
48
39
  (p) =>
@@ -76,13 +67,9 @@ function renderWidget(
76
67
  const formatted = formatProcessStatus(proc, theme);
77
68
  const formattedLen = visibleWidth(formatted);
78
69
  const remaining = allProcs.length - includedCount - 1;
79
-
80
- // Check if adding this part would exceed the width
81
70
  const needed =
82
71
  includedCount > 0 ? separatorLen + formattedLen : formattedLen;
83
72
 
84
- // If there are more processes after this one, reserve space for the
85
- // overflow suffix (separator + "+N more") so the final line fits.
86
73
  let reservedForSuffix = 0;
87
74
  if (remaining > 0) {
88
75
  const suffixText = `+${remaining} more`;
@@ -93,11 +80,8 @@ function renderWidget(
93
80
  currentLen + needed + reservedForSuffix > effectiveMax &&
94
81
  includedCount > 0
95
82
  ) {
96
- // Show how many are hidden
97
83
  const hiddenCount = allProcs.length - includedCount;
98
- if (hiddenCount > 0) {
99
- parts.push(theme.fg("dim", `+${hiddenCount} more`));
100
- }
84
+ if (hiddenCount > 0) parts.push(theme.fg("dim", `+${hiddenCount} more`));
101
85
  break;
102
86
  }
103
87
 
@@ -106,18 +90,11 @@ function renderWidget(
106
90
  includedCount++;
107
91
  }
108
92
 
109
- // Edge case: the very first element was too wide and the loop's overflow
110
- // check was skipped (it only fires when includedCount > 0). The suffix
111
- // reservation already shrinks the budget, but a single process that fills
112
- // the whole line still slips through. Show it truncated rather than nothing.
113
93
  if (includedCount === 0 && allProcs.length > 0) {
114
- const formatted = formatProcessStatus(allProcs[0], theme);
115
- parts.push(formatted);
94
+ parts.push(formatProcessStatus(allProcs[0], theme));
116
95
  }
117
96
 
118
- if (parts.length === 0) {
119
- return [];
120
- }
97
+ if (parts.length === 0) return [];
121
98
 
122
99
  const line = prefix + parts.join(separator);
123
100
  return [
@@ -126,47 +103,3 @@ function renderWidget(
126
103
  : line,
127
104
  ];
128
105
  }
129
-
130
- export function setupProcessWidget(pi: ExtensionAPI, manager: ProcessManager) {
131
- let latestContext: ExtensionContext | null = null;
132
-
133
- function updateWidget() {
134
- if (!latestContext?.hasUI) return;
135
-
136
- if (!configLoader.getConfig().widget.showStatusWidget) {
137
- latestContext.ui.setWidget(WIDGET_ID, undefined);
138
- return;
139
- }
140
-
141
- const processes = manager.list();
142
- const maxWidth = process.stdout.columns || 120;
143
- const lines = renderWidget(processes, latestContext.ui.theme, maxWidth);
144
-
145
- if (lines.length === 0) {
146
- latestContext.ui.setWidget(WIDGET_ID, undefined);
147
- } else {
148
- latestContext.ui.setWidget(WIDGET_ID, lines, {
149
- placement: "belowEditor",
150
- });
151
- }
152
- }
153
-
154
- pi.on("session_start", async (_event, ctx) => {
155
- // Startup defer: capture context only. First render happens on process
156
- // manager events or explicit settings updates.
157
- latestContext = ctx;
158
- });
159
-
160
- pi.on("session_switch", async (_event, ctx) => {
161
- latestContext = ctx;
162
- updateWidget();
163
- });
164
-
165
- manager.onEvent(() => {
166
- updateWidget();
167
- });
168
-
169
- return {
170
- update: updateWidget,
171
- };
172
- }
@@ -0,0 +1,21 @@
1
+ export type DockVisibility = "hidden" | "collapsed" | "open";
2
+
3
+ export interface DockState {
4
+ visibility: DockVisibility;
5
+ followEnabled: boolean;
6
+ focusedProcessId: string | null;
7
+ }
8
+
9
+ export interface DockActions {
10
+ getFocusedProcessId(): string | null;
11
+ isFollowEnabled(): boolean;
12
+ setFocus(id: string | null): void;
13
+ expand(): void;
14
+ collapse(): void;
15
+ hide(): void;
16
+ toggle(): void;
17
+ toggleFollow(): void;
18
+ }
19
+
20
+ export const STATUS_WIDGET_ID = "processes-status";
21
+ export const LOG_DOCK_WIDGET_ID = "processes-dock";
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { setupProcessesCommands } from "./commands";
3
- import { registerProcessesSettings } from "./commands/settings-command";
3
+ import { registerProcessesSettings } from "./commands/settings";
4
4
  import { configLoader } from "./config";
5
5
  import { setupProcessesHooks } from "./hooks";
6
6
  import { ProcessManager } from "./manager";
@@ -21,8 +21,13 @@ export default async function (pi: ExtensionAPI) {
21
21
  });
22
22
 
23
23
  const config = configLoader.getConfig();
24
- const { update: updateWidget } = setupProcessesHooks(pi, manager, config);
25
- setupProcessesCommands(pi, manager);
24
+
25
+ const { update: updateWidget, dockActions } = setupProcessesHooks(
26
+ pi,
27
+ manager,
28
+ config,
29
+ );
30
+ setupProcessesCommands(pi, manager, dockActions);
26
31
  setupProcessesTools(pi, manager);
27
32
  registerProcessesSettings(pi, () => {
28
33
  updateWidget();
package/src/manager.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import type { Writable } from "node:stream";
12
13
 
13
14
  import {
14
15
  type KillResult,
@@ -17,12 +18,15 @@ import {
17
18
  type ProcessInfo,
18
19
  type ProcessStatus,
19
20
  type StartOptions,
21
+ type WriteResult,
20
22
  } from "./constants";
21
23
  import { isProcessGroupAlive, killProcessGroup } from "./utils";
22
24
  import { spawnCommand } from "./utils/command-executor";
23
25
 
24
26
  interface ManagedProcess extends ProcessInfo {
25
27
  process: ChildProcess;
28
+ stdin: Writable | null;
29
+ stdinClosed: boolean;
26
30
  lastSignalSent: NodeJS.Signals | null;
27
31
  combinedFile: string;
28
32
  }
@@ -57,15 +61,8 @@ export class ProcessManager {
57
61
 
58
62
  private transition(managed: ManagedProcess, next: ProcessStatus): void {
59
63
  if (managed.status === next) return;
60
- const prev = managed.status;
61
64
  managed.status = next;
62
65
 
63
- this.emit({
64
- type: "process_status_changed",
65
- info: this.toProcessInfo(managed),
66
- prev,
67
- });
68
-
69
66
  if (next === "exited" || next === "killed") {
70
67
  this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
71
68
  }
@@ -159,6 +156,8 @@ export class ProcessManager {
159
156
  alertOnFailure: options?.alertOnFailure ?? true,
160
157
  alertOnKill: options?.alertOnKill ?? false,
161
158
  process: child,
159
+ stdin: child.stdin,
160
+ stdinClosed: false,
162
161
  lastSignalSent: null,
163
162
  };
164
163
 
@@ -319,12 +318,15 @@ export class ProcessManager {
319
318
  }
320
319
  }
321
320
 
322
- getLogFiles(id: string): { stdoutFile: string; stderrFile: string } | null {
321
+ getLogFiles(
322
+ id: string,
323
+ ): { stdoutFile: string; stderrFile: string; combinedFile: string } | null {
323
324
  const managed = this.processes.get(id);
324
325
  if (!managed) return null;
325
326
  return {
326
327
  stdoutFile: managed.stdoutFile,
327
328
  stderrFile: managed.stderrFile,
329
+ combinedFile: managed.combinedFile,
328
330
  };
329
331
  }
330
332
 
@@ -407,6 +409,50 @@ export class ProcessManager {
407
409
  return { ok: true, info: this.toProcessInfo(managed) };
408
410
  }
409
411
 
412
+ writeToStdin(
413
+ id: string,
414
+ data: string,
415
+ opts?: { end?: boolean },
416
+ ): WriteResult {
417
+ const managed = this.processes.get(id);
418
+ if (!managed) {
419
+ return {
420
+ ok: false,
421
+ reason: "not_found",
422
+ };
423
+ }
424
+
425
+ if (!LIVE_STATUSES.has(managed.status)) {
426
+ return {
427
+ ok: false,
428
+ reason: "process_exited",
429
+ };
430
+ }
431
+
432
+ if (managed.stdinClosed || !managed.stdin) {
433
+ return {
434
+ ok: false,
435
+ reason: "stdin_closed",
436
+ };
437
+ }
438
+
439
+ try {
440
+ managed.stdin.write(data);
441
+
442
+ if (opts?.end) {
443
+ managed.stdin.end();
444
+ managed.stdinClosed = true;
445
+ }
446
+
447
+ return { ok: true };
448
+ } catch {
449
+ return {
450
+ ok: false,
451
+ reason: "write_error",
452
+ };
453
+ }
454
+ }
455
+
410
456
  clearFinished(): number {
411
457
  let cleared = 0;
412
458
  for (const [id, managed] of this.processes) {
@@ -519,4 +565,10 @@ export class ProcessManager {
519
565
  }
520
566
  }
521
567
 
522
- export type { ProcessInfo, ProcessStatus, ManagerEvent, KillResult };
568
+ export type {
569
+ ProcessInfo,
570
+ ProcessStatus,
571
+ ManagerEvent,
572
+ KillResult,
573
+ WriteResult,
574
+ };
@@ -7,12 +7,15 @@ import { executeList } from "./list";
7
7
  import { executeLogs } from "./logs";
8
8
  import { executeOutput } from "./output";
9
9
  import { executeStart } from "./start";
10
+ import { executeWrite } from "./write";
10
11
 
11
12
  interface ActionParams {
12
13
  action: string;
13
14
  command?: string;
14
15
  name?: string;
15
16
  id?: string;
17
+ input?: string;
18
+ end?: boolean;
16
19
  alertOnSuccess?: boolean;
17
20
  alertOnFailure?: boolean;
18
21
  alertOnKill?: boolean;
@@ -36,6 +39,8 @@ export async function executeAction(
36
39
  return executeKill(params, manager);
37
40
  case "clear":
38
41
  return executeClear(manager);
42
+ case "write":
43
+ return executeWrite(params, manager);
39
44
  default:
40
45
  return {
41
46
  content: [{ type: "text", text: `Unknown action: ${params.action}` }],
@@ -0,0 +1,87 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+
4
+ interface WriteParams {
5
+ id?: string;
6
+ input?: string;
7
+ end?: boolean;
8
+ }
9
+
10
+ export function executeWrite(
11
+ params: WriteParams,
12
+ manager: ProcessManager,
13
+ ): ExecuteResult {
14
+ const { id, input, end } = params;
15
+
16
+ if (!id) {
17
+ return {
18
+ content: [{ type: "text", text: "Missing required parameter: id" }],
19
+ details: {
20
+ action: "write",
21
+ success: false,
22
+ message: "Missing required parameter: id",
23
+ },
24
+ };
25
+ }
26
+
27
+ if (input === undefined) {
28
+ return {
29
+ content: [{ type: "text", text: "Missing required parameter: input" }],
30
+ details: {
31
+ action: "write",
32
+ success: false,
33
+ message: "Missing required parameter: input",
34
+ },
35
+ };
36
+ }
37
+
38
+ const process = manager.find(id);
39
+ if (!process) {
40
+ return {
41
+ content: [{ type: "text", text: `Process not found: ${id}` }],
42
+ details: {
43
+ action: "write",
44
+ success: false,
45
+ message: `Process not found: ${id}`,
46
+ },
47
+ };
48
+ }
49
+
50
+ const result = manager.writeToStdin(process.id, input, { end });
51
+
52
+ if (!result.ok) {
53
+ const messages: Record<string, string> = {
54
+ not_found: `Process not found: ${process.id}`,
55
+ process_exited: `Process has already exited: ${process.id}`,
56
+ stdin_closed: `Stdin already closed for process: ${process.id}`,
57
+ write_error: `Failed to write to stdin for process: ${process.id}`,
58
+ };
59
+
60
+ const message =
61
+ messages[result.reason] || `Unknown error: ${result.reason}`;
62
+
63
+ return {
64
+ content: [{ type: "text", text: message }],
65
+ details: {
66
+ action: "write",
67
+ success: false,
68
+ message,
69
+ },
70
+ };
71
+ }
72
+
73
+ const suffix = end ? " (stdin closed)" : "";
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `Wrote ${input.length} bytes to ${process.id}${suffix}`,
79
+ },
80
+ ],
81
+ details: {
82
+ action: "write",
83
+ success: true,
84
+ message: `Wrote ${input.length} bytes to process stdin${suffix}`,
85
+ },
86
+ };
87
+ }