@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
@@ -1,39 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { ProcessesComponent } from "../../components/processes-component";
3
- import type { DockActions } from "../../hooks/widget";
4
- import type { ProcessManager } from "../../manager";
5
-
6
- export function registerPsCommand(
7
- pi: ExtensionAPI,
8
- manager: ProcessManager,
9
- dockActions: DockActions,
10
- ): void {
11
- pi.registerCommand("ps", {
12
- description: "View and manage background processes",
13
- handler: async (_args, ctx) => {
14
- if (!ctx.hasUI) {
15
- return;
16
- }
17
-
18
- const result = await ctx.ui.custom<string | null>(
19
- (tui, theme, _keybindings, done) => {
20
- return new ProcessesComponent(
21
- tui,
22
- theme,
23
- (processId?: string) => {
24
- if (processId) {
25
- dockActions.setFocus(processId);
26
- }
27
- done(processId ?? null);
28
- },
29
- manager,
30
- );
31
- },
32
- );
33
-
34
- if (result === undefined) {
35
- return;
36
- }
37
- },
38
- });
39
- }
@@ -1 +0,0 @@
1
- export { registerPsCommand } from "./command";
@@ -1,72 +0,0 @@
1
- import type { ProcessesConfig } from "../../config";
2
-
3
- export function applySettingChange(
4
- id: string,
5
- newValue: string,
6
- config: ProcessesConfig,
7
- ): ProcessesConfig | null {
8
- const updated = structuredClone(config);
9
-
10
- if (id === "interception.blockBackgroundCommands") {
11
- if (!updated.interception) updated.interception = {};
12
- updated.interception.blockBackgroundCommands = newValue === "on";
13
- return updated;
14
- }
15
- if (id === "widget.showStatusWidget") {
16
- if (!updated.widget) updated.widget = {};
17
- updated.widget.showStatusWidget = newValue === "on";
18
- return updated;
19
- }
20
- if (id === "widget.dockDefaultState") {
21
- if (!updated.widget) updated.widget = {};
22
- updated.widget.dockDefaultState =
23
- newValue === "hidden" ? "hidden" : "collapsed";
24
- return updated;
25
- }
26
- if (id === "widget.dockHeight") {
27
- if (!updated.widget) updated.widget = {};
28
- updated.widget.dockHeight = Number.parseInt(newValue, 10);
29
- return updated;
30
- }
31
- if (id === "follow.enabledByDefault") {
32
- if (!updated.follow) updated.follow = {};
33
- updated.follow.enabledByDefault = newValue === "on";
34
- return updated;
35
- }
36
- if (id === "follow.autoHideOnFinish") {
37
- if (!updated.follow) updated.follow = {};
38
- updated.follow.autoHideOnFinish = newValue === "on";
39
- return updated;
40
- }
41
- if (id === "execution.shellPath") {
42
- if (!updated.execution) updated.execution = {};
43
- updated.execution.shellPath = newValue === "auto" ? undefined : newValue;
44
- return updated;
45
- }
46
-
47
- const num = Number.parseInt(newValue, 10);
48
- if (Number.isNaN(num)) return null;
49
-
50
- switch (id) {
51
- case "processList.maxVisibleProcesses":
52
- if (!updated.processList) updated.processList = {};
53
- updated.processList.maxVisibleProcesses = num;
54
- break;
55
- case "processList.maxPreviewLines":
56
- if (!updated.processList) updated.processList = {};
57
- updated.processList.maxPreviewLines = num;
58
- break;
59
- case "output.defaultTailLines":
60
- if (!updated.output) updated.output = {};
61
- updated.output.defaultTailLines = num;
62
- break;
63
- case "output.maxOutputLines":
64
- if (!updated.output) updated.output = {};
65
- updated.output.maxOutputLines = num;
66
- break;
67
- default:
68
- return null;
69
- }
70
-
71
- return updated;
72
- }
@@ -1,160 +0,0 @@
1
- import type { SettingsSection } from "@aliou/pi-utils-settings";
2
- import type { ProcessesConfig, ResolvedProcessesConfig } from "../../config";
3
-
4
- export function buildSettingsSections(
5
- tabConfig: ProcessesConfig | null,
6
- resolved: ResolvedProcessesConfig,
7
- ): SettingsSection[] {
8
- return [
9
- {
10
- label: "Process List",
11
- items: [
12
- {
13
- id: "processList.maxVisibleProcesses",
14
- label: "Max visible processes",
15
- description:
16
- "Maximum processes shown in the /ps list before scrolling",
17
- currentValue: String(
18
- tabConfig?.processList?.maxVisibleProcesses ??
19
- resolved.processList.maxVisibleProcesses,
20
- ),
21
- values: ["4", "6", "8", "12", "16"],
22
- },
23
- {
24
- id: "processList.maxPreviewLines",
25
- label: "Max preview lines",
26
- description: "Log preview lines shown below the selected process",
27
- currentValue: String(
28
- tabConfig?.processList?.maxPreviewLines ??
29
- resolved.processList.maxPreviewLines,
30
- ),
31
- values: ["6", "8", "12", "16", "24"],
32
- },
33
- ],
34
- },
35
- {
36
- label: "Output Limits",
37
- items: [
38
- {
39
- id: "output.defaultTailLines",
40
- label: "Default tail lines",
41
- description: "Number of tail lines returned to the agent by default",
42
- currentValue: String(
43
- tabConfig?.output?.defaultTailLines ??
44
- resolved.output.defaultTailLines,
45
- ),
46
- values: ["50", "100", "200", "500"],
47
- },
48
- {
49
- id: "output.maxOutputLines",
50
- label: "Max output lines",
51
- description: "Hard cap on output lines returned to the agent",
52
- currentValue: String(
53
- tabConfig?.output?.maxOutputLines ?? resolved.output.maxOutputLines,
54
- ),
55
- values: ["100", "200", "500", "1000"],
56
- },
57
- ],
58
- },
59
- {
60
- label: "Execution",
61
- items: [
62
- {
63
- id: "execution.shellPath",
64
- label: "Shell path",
65
- description: "Absolute shell path override used to execute commands",
66
- currentValue:
67
- tabConfig?.execution?.shellPath ??
68
- resolved.execution.shellPath ??
69
- "auto",
70
- values: [
71
- "auto",
72
- "/run/current-system/sw/bin/bash",
73
- "/bin/bash",
74
- "/usr/bin/bash",
75
- "/usr/local/bin/bash",
76
- ],
77
- },
78
- ],
79
- },
80
- {
81
- label: "Interception",
82
- items: [
83
- {
84
- id: "interception.blockBackgroundCommands",
85
- label: "Block background commands",
86
- description:
87
- "Block bash background commands (&, nohup, disown, setsid) and guide the model to use the process tool",
88
- currentValue:
89
- (tabConfig?.interception?.blockBackgroundCommands ??
90
- resolved.interception.blockBackgroundCommands)
91
- ? "on"
92
- : "off",
93
- values: ["on", "off"],
94
- },
95
- ],
96
- },
97
- {
98
- label: "Widget",
99
- items: [
100
- {
101
- id: "widget.showStatusWidget",
102
- label: "Show status widget",
103
- description: "Show process status widget below the editor",
104
- currentValue:
105
- (tabConfig?.widget?.showStatusWidget ??
106
- resolved.widget.showStatusWidget)
107
- ? "on"
108
- : "off",
109
- values: ["on", "off"],
110
- },
111
- {
112
- id: "widget.dockDefaultState",
113
- label: "Dock default state",
114
- description:
115
- "Default visibility state of the log dock when follow mode is on",
116
- currentValue:
117
- tabConfig?.widget?.dockDefaultState ??
118
- resolved.widget.dockDefaultState,
119
- values: ["hidden", "collapsed"],
120
- },
121
- {
122
- id: "widget.dockHeight",
123
- label: "Dock height",
124
- description: "Height of the log dock in lines when open",
125
- currentValue: String(
126
- tabConfig?.widget?.dockHeight ?? resolved.widget.dockHeight,
127
- ),
128
- values: ["8", "10", "12", "16", "20"],
129
- },
130
- ],
131
- },
132
- {
133
- label: "Follow Mode",
134
- items: [
135
- {
136
- id: "follow.enabledByDefault",
137
- label: "Enable by default",
138
- description: "Automatically show logs when a process starts",
139
- currentValue:
140
- (tabConfig?.follow?.enabledByDefault ??
141
- resolved.follow.enabledByDefault)
142
- ? "on"
143
- : "off",
144
- values: ["on", "off"],
145
- },
146
- {
147
- id: "follow.autoHideOnFinish",
148
- label: "Auto-hide on finish",
149
- description: "Hide dock when all processes finish",
150
- currentValue:
151
- (tabConfig?.follow?.autoHideOnFinish ??
152
- resolved.follow.autoHideOnFinish)
153
- ? "on"
154
- : "off",
155
- values: ["on", "off"],
156
- },
157
- ],
158
- },
159
- ];
160
- }
@@ -1,20 +0,0 @@
1
- import { registerSettingsCommand } from "@aliou/pi-utils-settings";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import type { ProcessesConfig, ResolvedProcessesConfig } from "../../config";
4
- import { configLoader } from "../../config";
5
- import { applySettingChange } from "./apply-setting-change";
6
- import { buildSettingsSections } from "./build-sections";
7
-
8
- export function registerProcessesSettings(
9
- pi: ExtensionAPI,
10
- onSave?: () => void,
11
- ): void {
12
- registerSettingsCommand<ProcessesConfig, ResolvedProcessesConfig>(pi, {
13
- commandName: "ps:settings",
14
- title: "Processes Settings",
15
- configStore: configLoader,
16
- buildSections: buildSettingsSections,
17
- onSettingChange: applySettingChange,
18
- onSave,
19
- });
20
- }
@@ -1 +0,0 @@
1
- export { registerProcessesSettings } from "./command";
@@ -1,29 +0,0 @@
1
- import { visibleWidth } from "@earendil-works/pi-tui";
2
- import { describe, expect, it } from "vitest";
3
- import { renderCollapsedDockLine } from "./log-dock-component";
4
-
5
- const LONG_LOG_LINE =
6
- "build-step - 3f7a9c2 pending ↻ https://example.com/pipelines/project/service/123456/workflows/abcdef12-3456-7890-abcd-ef1234567890/jobs/integration-test?token=very-long-unbroken-log-segment-and-query-string";
7
-
8
- describe("renderCollapsedDockLine", () => {
9
- it("leaves a spare terminal column for long log lines", () => {
10
- for (const width of [1, 2, 40, 80, 120]) {
11
- const rendered = renderCollapsedDockLine(LONG_LOG_LINE, width);
12
-
13
- expect(visibleWidth(rendered)).toBe(width - 1);
14
- }
15
- });
16
-
17
- it("leaves a spare terminal column for ansi-styled log lines", () => {
18
- const rendered = renderCollapsedDockLine(
19
- `\u001b[2m${LONG_LOG_LINE}\u001b[22m`,
20
- 80,
21
- );
22
-
23
- expect(visibleWidth(rendered)).toBe(79);
24
- });
25
-
26
- it("renders nothing for zero width", () => {
27
- expect(renderCollapsedDockLine(LONG_LOG_LINE, 0)).toBe("");
28
- });
29
- });
@@ -1,236 +0,0 @@
1
- /**
2
- * Log Dock Component - shows process logs in the bottom dock.
3
- *
4
- * Collapsed view: one-line summary (running procs) + last log line.
5
- * Open view: LogFileViewer for the focused process (or first running), follow mode on.
6
- */
7
-
8
- import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
9
- import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
- import { LIVE_STATUSES } from "../constants";
12
- import type { ProcessManager } from "../manager";
13
- import { stripAnsi } from "../utils";
14
- import { LogFileViewer } from "./log-file-viewer";
15
- import {
16
- createPanelPadder,
17
- renderPanelRule,
18
- renderPanelTitleLine,
19
- } from "./panel-helpers";
20
-
21
- const PROCESS_COLORS: ThemeColor[] = [
22
- "accent",
23
- "warning",
24
- "success",
25
- "error",
26
- "accent",
27
- "dim",
28
- "accent",
29
- "warning",
30
- ];
31
-
32
- const COLLAPSED_DOCK_RIGHT_MARGIN = 1;
33
-
34
- function getCollapsedDockLineWidth(width: number): number {
35
- return Math.max(0, width - COLLAPSED_DOCK_RIGHT_MARGIN);
36
- }
37
-
38
- export function renderCollapsedDockLine(
39
- content: string,
40
- width: number,
41
- ): string {
42
- const lineWidth = getCollapsedDockLineWidth(width);
43
- if (lineWidth === 0) return "";
44
-
45
- const innerWidth = Math.max(0, lineWidth - 2);
46
- const line = truncateToWidth(content, innerWidth, "", true);
47
- return ` ${line}${" ".repeat(Math.max(0, lineWidth - 1 - visibleWidth(line)))}`;
48
- }
49
-
50
- interface LogDockOptions {
51
- manager: ProcessManager;
52
- theme: Theme;
53
- tui: { requestRender: () => void };
54
- mode: "collapsed" | "open";
55
- focusedProcessId: string | null;
56
- dockHeight?: number;
57
- }
58
-
59
- export class LogDockComponent implements Component {
60
- private manager: ProcessManager;
61
- private theme: Theme;
62
- private tui: { requestRender: () => void };
63
- private dockHeight: number;
64
- private mode: "collapsed" | "open";
65
- private focusedProcessId: string | null;
66
-
67
- private unsubscribeManager: (() => void) | null = null;
68
-
69
- /** One viewer per process, lazily created, follow:true. */
70
- private viewers: Map<string, LogFileViewer> = new Map();
71
-
72
- private processColors: Map<string, ThemeColor> = new Map();
73
- private colorCounter = 0;
74
-
75
- constructor(options: LogDockOptions) {
76
- this.manager = options.manager;
77
- this.theme = options.theme;
78
- this.tui = options.tui;
79
- this.dockHeight = options.dockHeight ?? 12;
80
- this.mode = options.mode;
81
- this.focusedProcessId = options.focusedProcessId;
82
-
83
- this.unsubscribeManager = this.manager.onEvent(() => {
84
- this.tui.requestRender();
85
- });
86
- }
87
-
88
- update(opts: {
89
- mode: "collapsed" | "open";
90
- focusedProcessId: string | null;
91
- dockHeight: number;
92
- }): void {
93
- this.mode = opts.mode;
94
- this.focusedProcessId = opts.focusedProcessId;
95
- this.dockHeight = opts.dockHeight;
96
- this.tui.requestRender();
97
- }
98
-
99
- handleInput(_data: string): boolean {
100
- return false;
101
- }
102
-
103
- invalidate(): void {
104
- // No local cache; always renders fresh.
105
- }
106
-
107
- private getProcessColor(processId: string): ThemeColor {
108
- const existing = this.processColors.get(processId);
109
- if (existing) return existing;
110
- const color = PROCESS_COLORS[this.colorCounter % PROCESS_COLORS.length];
111
- this.colorCounter++;
112
- this.processColors.set(processId, color);
113
- return color;
114
- }
115
-
116
- private getViewer(processId: string, combinedFile: string): LogFileViewer {
117
- let viewer = this.viewers.get(processId);
118
- if (!viewer) {
119
- viewer = new LogFileViewer({
120
- filePath: combinedFile,
121
- format: "combined",
122
- theme: this.theme,
123
- follow: true,
124
- });
125
- this.viewers.set(processId, viewer);
126
- }
127
- return viewer;
128
- }
129
-
130
- render(width: number): string[] {
131
- if (this.mode === "collapsed") return this.renderCollapsed(width);
132
- return this.renderOpen(width);
133
- }
134
-
135
- private renderCollapsed(width: number): string[] {
136
- const theme = this.theme;
137
- const dim = (s: string) => theme.fg("dim", s);
138
- const fg = (color: ThemeColor, s: string) => theme.fg(color, s);
139
-
140
- const processes = this.manager.list();
141
- const lineWidth = getCollapsedDockLineWidth(width);
142
- const padLine = (content: string) =>
143
- renderCollapsedDockLine(content, width);
144
-
145
- if (processes.length === 0) {
146
- return [renderPanelRule(lineWidth, theme), padLine(dim("No processes"))];
147
- }
148
-
149
- const running = processes.filter((p) => LIVE_STATUSES.has(p.status));
150
- const finished = processes.filter((p) => !LIVE_STATUSES.has(p.status));
151
-
152
- const parts: string[] = [];
153
- for (const proc of running) {
154
- const color = this.getProcessColor(proc.id);
155
- parts.push(`${fg(color, "●")} ${proc.name}`);
156
- }
157
- if (finished.length > 0) {
158
- parts.push(dim(`+${finished.length} finished`));
159
- }
160
-
161
- const firstLine = parts.join(" | ");
162
- const lines = [renderPanelRule(lineWidth, theme), padLine(firstLine)];
163
-
164
- if (running.length > 0) {
165
- const lastLogs = this.manager.getCombinedOutput(running[0].id, 1);
166
- if (lastLogs && lastLogs.length > 0) {
167
- const lastLog = stripAnsi(lastLogs[lastLogs.length - 1].text);
168
- lines.push(padLine(dim(lastLog)));
169
- }
170
- }
171
-
172
- return lines;
173
- }
174
-
175
- private renderOpen(width: number): string[] {
176
- const theme = this.theme;
177
- const dim = (s: string) => theme.fg("dim", s);
178
-
179
- const innerWidth = width - 2;
180
- const basePadLine = createPanelPadder(width);
181
- const padLine = (content: string): string =>
182
- basePadLine(
183
- visibleWidth(content) > innerWidth
184
- ? truncateToWidth(content, innerWidth, "", true)
185
- : content,
186
- );
187
-
188
- const processes = this.manager.list();
189
- const running = processes.filter((p) => LIVE_STATUSES.has(p.status));
190
-
191
- const targetProc =
192
- (this.focusedProcessId
193
- ? processes.find((p) => p.id === this.focusedProcessId)
194
- : null) ??
195
- running[0] ??
196
- processes[0] ??
197
- null;
198
-
199
- if (!targetProc) {
200
- return [
201
- renderPanelTitleLine("Process Logs", width, theme),
202
- padLine(dim("No processes")),
203
- padLine(dim("Run a command to start")),
204
- ];
205
- }
206
-
207
- const logFiles = this.manager.getLogFiles(targetProc.id);
208
- if (!logFiles) {
209
- return [
210
- renderPanelTitleLine("Process Logs", width, theme),
211
- padLine(dim("Log files unavailable")),
212
- ];
213
- }
214
-
215
- const viewer = this.getViewer(targetProc.id, logFiles.combinedFile);
216
-
217
- const logRows = Math.max(1, this.dockHeight - 2);
218
-
219
- const title = `${targetProc.name} ${dim(`(${targetProc.id})`)}`;
220
- const lines: string[] = [];
221
- lines.push(renderPanelTitleLine(title, width, theme));
222
-
223
- const contentLines = viewer.renderLines(innerWidth, logRows);
224
- for (let i = 0; i < logRows; i++) {
225
- lines.push(padLine(contentLines[i] ?? ""));
226
- }
227
-
228
- return lines.slice(0, this.dockHeight);
229
- }
230
-
231
- dispose(): void {
232
- this.unsubscribeManager?.();
233
- this.viewers.clear();
234
- this.processColors.clear();
235
- }
236
- }