@aliou/pi-processes 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +47 -20
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +499 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -106
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -148
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output.ts +0 -269
  166. package/src/tools/actions/start.ts +0 -245
  167. package/src/tools/actions/write.ts +0 -113
  168. package/src/tools/index.ts +0 -207
  169. package/src/utils/ansi.test.ts +0 -22
  170. package/src/utils/command-executor.test.ts +0 -48
  171. package/src/utils/keybindings.ts +0 -71
@@ -0,0 +1,51 @@
1
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ CHANNELS,
4
+ type ProcessProtocolConfig,
5
+ type RequestConfigPayload,
6
+ type RequestGetPayload,
7
+ type RequestListPayload,
8
+ } from "../../src/protocol";
9
+ import type { ProcessInfo } from "../../src/types";
10
+
11
+ export function requestProcessList(events: EventBus): ProcessInfo[] {
12
+ let processes: ProcessInfo[] = [];
13
+ const payload: RequestListPayload = {
14
+ reply: (result) => {
15
+ processes = result;
16
+ },
17
+ };
18
+ events.emit(CHANNELS.REQUEST_LIST, payload);
19
+ return processes;
20
+ }
21
+
22
+ export function requestProcess(
23
+ events: EventBus,
24
+ id: string,
25
+ ): ProcessInfo | null {
26
+ let process: ProcessInfo | null = null;
27
+ const payload: RequestGetPayload = {
28
+ id,
29
+ reply: (result) => {
30
+ process = result;
31
+ },
32
+ };
33
+ events.emit(CHANNELS.REQUEST_GET, payload);
34
+ return process;
35
+ }
36
+
37
+ export function requestConfig(events: EventBus): ProcessProtocolConfig {
38
+ let config: ProcessProtocolConfig | null = null;
39
+ const payload: RequestConfigPayload = {
40
+ reply: (result) => {
41
+ config = result;
42
+ },
43
+ };
44
+ events.emit(CHANNELS.REQUEST_CONFIG, payload);
45
+
46
+ if (!config) {
47
+ throw new Error("processes core extension did not reply to config request");
48
+ }
49
+
50
+ return config;
51
+ }
@@ -0,0 +1,108 @@
1
+ import type {
2
+ EventBus,
3
+ ExtensionAPI,
4
+ ExtensionCommandContext,
5
+ Theme,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { ProcessProtocolConfig } from "../../../src/protocol";
8
+ import type { ProcessInfo } from "../../../src/types";
9
+ import { requestConfig, requestProcess, requestProcessList } from "../client";
10
+ import { allProcessCompletions } from "../completions";
11
+ import { LogOverlayComponent } from "../components/log-overlay-component";
12
+
13
+ export interface OpenLogsOptions {
14
+ events: EventBus;
15
+ registerOverlay: (overlay: { dispose: () => void }) => () => void;
16
+ }
17
+
18
+ export function registerLogsCommand(
19
+ pi: Pick<ExtensionAPI, "registerCommand">,
20
+ options: OpenLogsOptions,
21
+ ): void {
22
+ const command = {
23
+ description: "Open managed process logs.",
24
+ getArgumentCompletions: allProcessCompletions(options.events),
25
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
26
+ await openLogs(args, ctx, options);
27
+ },
28
+ };
29
+
30
+ pi.registerCommand("ps:logs", command);
31
+ }
32
+
33
+ async function openLogs(
34
+ args: string,
35
+ ctx: ExtensionCommandContext,
36
+ options: OpenLogsOptions,
37
+ ): Promise<void> {
38
+ const requestedId = args.trim().split(/\s+/, 1)[0] || undefined;
39
+ const initialProcessId = requestedId
40
+ ? requestProcess(options.events, requestedId)?.id
41
+ : undefined;
42
+
43
+ if (requestedId && !initialProcessId) {
44
+ const message = `Process not found: ${requestedId}`;
45
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
46
+ else console.log(message);
47
+ return;
48
+ }
49
+
50
+ if (ctx.mode !== "tui") {
51
+ const processes = requestProcessList(options.events);
52
+ const message = formatPlainProcessList(processes);
53
+ if (ctx.hasUI) ctx.ui.notify(message, "info");
54
+ else console.log(message);
55
+ return;
56
+ }
57
+
58
+ let config: ProcessProtocolConfig;
59
+ try {
60
+ config = requestConfig(options.events);
61
+ } catch (error) {
62
+ ctx.ui.notify(String(error), "error");
63
+ return;
64
+ }
65
+
66
+ const result = await ctx.ui.custom<"closed">(
67
+ (tui, theme: Theme, _keybindings, done) => {
68
+ let unregister: () => void = () => undefined;
69
+ const overlay = new LogOverlayComponent({
70
+ events: options.events,
71
+ tui,
72
+ theme,
73
+ initialProcessId,
74
+ config,
75
+ onClose: () => {
76
+ unregister();
77
+ done("closed");
78
+ },
79
+ });
80
+ unregister = options.registerOverlay(overlay);
81
+ return overlay;
82
+ },
83
+ {
84
+ overlay: true,
85
+ overlayOptions: {
86
+ anchor: "center",
87
+ width: "90%",
88
+ maxHeight: "90%",
89
+ margin: 2,
90
+ },
91
+ },
92
+ );
93
+
94
+ if (result === undefined) {
95
+ const processes = requestProcessList(options.events);
96
+ ctx.ui.notify(formatPlainProcessList(processes), "info");
97
+ }
98
+ }
99
+
100
+ function formatPlainProcessList(processes: ProcessInfo[]): string {
101
+ if (processes.length === 0) return "No managed processes.";
102
+ return processes
103
+ .map(
104
+ (process) =>
105
+ `${process.id}\t${process.name}\t${process.status}\t${process.command}`,
106
+ )
107
+ .join("\n");
108
+ }
@@ -0,0 +1,31 @@
1
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
2
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
+ import { formatProcessSelectionDescription } from "../shared/ui";
4
+ import { requestProcessList } from "./client";
5
+
6
+ export function allProcessCompletions(
7
+ events: EventBus,
8
+ ): (prefix: string) => AutocompleteItem[] | null {
9
+ return (prefix) => buildCompletions(events, prefix);
10
+ }
11
+
12
+ function buildCompletions(
13
+ events: EventBus,
14
+ prefix: string,
15
+ ): AutocompleteItem[] | null {
16
+ const processes = requestProcessList(events);
17
+ const normalizedPrefix = prefix.trim().toLowerCase();
18
+ const items = processes
19
+ .filter(
20
+ (process) =>
21
+ process.id.toLowerCase().startsWith(normalizedPrefix) ||
22
+ process.name.toLowerCase().includes(normalizedPrefix),
23
+ )
24
+ .map((process) => ({
25
+ value: process.id,
26
+ label: `${process.name} (${process.id})`,
27
+ description: formatProcessSelectionDescription(process),
28
+ }));
29
+
30
+ return items.length > 0 ? items : null;
31
+ }
@@ -0,0 +1,274 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { trimToBudget } from "../../shared/line-buffer";
4
+ import type { ProcessLogLine } from "../logs-client";
5
+
6
+ export type StreamFilter = "both" | "stdout" | "stderr";
7
+
8
+ interface LogFileViewerOptions {
9
+ followEnabled: boolean;
10
+ maxBufferLines: number;
11
+ maxBufferBytes?: number;
12
+ }
13
+
14
+ export class LogFileViewer {
15
+ private lines: ProcessLogLine[];
16
+ private anchorEnd: number | null = null;
17
+ private follow: boolean;
18
+ private streamFilter: StreamFilter = "both";
19
+ private searchQuery = "";
20
+ private searchMatches: number[] = [];
21
+ private searchCurrentMatch = -1;
22
+ private centerTarget: number | null = null;
23
+ private readonly notifyLines = new Set<string>();
24
+
25
+ constructor(
26
+ initialLines: ProcessLogLine[],
27
+ private readonly theme: Theme,
28
+ private readonly options: LogFileViewerOptions,
29
+ ) {
30
+ this.follow = options.followEnabled;
31
+ this.lines = trimToBudget(
32
+ initialLines,
33
+ options.maxBufferLines,
34
+ options.maxBufferBytes ?? Number.MAX_SAFE_INTEGER,
35
+ );
36
+ this.refreshMatches();
37
+ }
38
+
39
+ appendLines(lines: ProcessLogLine[]): void {
40
+ if (lines.length === 0) return;
41
+ this.lines.push(...lines);
42
+ this.lines = trimToBudget(
43
+ this.lines,
44
+ this.options.maxBufferLines,
45
+ this.options.maxBufferBytes ?? Number.MAX_SAFE_INTEGER,
46
+ );
47
+ this.refreshMatches();
48
+ if (this.follow) this.anchorEnd = null;
49
+ }
50
+
51
+ scrollBy(delta: number): void {
52
+ const visible = this.visibleLines();
53
+ this.follow = false;
54
+ this.anchorEnd ??= visible.length;
55
+ this.anchorEnd = Math.max(
56
+ 0,
57
+ Math.min(visible.length, this.anchorEnd - delta),
58
+ );
59
+ }
60
+
61
+ scrollToTop(): void {
62
+ this.follow = false;
63
+ this.anchorEnd = 0;
64
+ }
65
+
66
+ scrollToBottom(): void {
67
+ this.follow = false;
68
+ this.anchorEnd = this.visibleLines().length;
69
+ }
70
+
71
+ toggleFollow(): boolean {
72
+ this.follow = !this.follow;
73
+ this.anchorEnd = this.follow ? null : this.visibleLines().length;
74
+ return this.follow;
75
+ }
76
+
77
+ isFollowing(): boolean {
78
+ return this.follow;
79
+ }
80
+
81
+ cycleStreamFilter(): StreamFilter {
82
+ this.streamFilter =
83
+ this.streamFilter === "both"
84
+ ? "stdout"
85
+ : this.streamFilter === "stdout"
86
+ ? "stderr"
87
+ : "both";
88
+ this.anchorEnd = this.visibleLines().length;
89
+ this.refreshMatches();
90
+ return this.streamFilter;
91
+ }
92
+
93
+ getStreamFilter(): StreamFilter {
94
+ return this.streamFilter;
95
+ }
96
+
97
+ setSearch(query: string): void {
98
+ this.follow = false;
99
+ this.anchorEnd ??= this.visibleLines().length;
100
+ this.searchQuery = query;
101
+ this.refreshMatches();
102
+ if (this.searchMatches.length > 0) {
103
+ this.searchCurrentMatch = this.searchMatches.length - 1;
104
+ this.jumpToVisibleIndex(this.searchMatches[this.searchCurrentMatch] ?? 0);
105
+ } else {
106
+ this.searchCurrentMatch = -1;
107
+ }
108
+ }
109
+
110
+ clearSearch(): void {
111
+ this.searchQuery = "";
112
+ this.searchMatches = [];
113
+ this.searchCurrentMatch = -1;
114
+ }
115
+
116
+ /**
117
+ * Records a notify log-match marker. Lines whose text equals the matched
118
+ * line are highlighted distinctly from manual search matches and with lower
119
+ * priority (search current match > search match > notify match > stream).
120
+ */
121
+ addNotifyMatch(match: { line: string }): void {
122
+ if (match.line) this.notifyLines.add(match.line);
123
+ }
124
+
125
+ clearNotifyMatches(): void {
126
+ this.notifyLines.clear();
127
+ }
128
+
129
+ getNotifyMatchCount(): number {
130
+ return this.notifyLines.size;
131
+ }
132
+
133
+ nextMatch(): void {
134
+ if (this.searchMatches.length === 0) return;
135
+ this.searchCurrentMatch =
136
+ (this.searchCurrentMatch + 1) % this.searchMatches.length;
137
+ this.jumpToVisibleIndex(this.searchMatches[this.searchCurrentMatch] ?? 0);
138
+ }
139
+
140
+ previousMatch(): void {
141
+ if (this.searchMatches.length === 0) return;
142
+ this.searchCurrentMatch =
143
+ (this.searchCurrentMatch - 1 + this.searchMatches.length) %
144
+ this.searchMatches.length;
145
+ this.jumpToVisibleIndex(this.searchMatches[this.searchCurrentMatch] ?? 0);
146
+ }
147
+
148
+ getSearchInfo(): { query: string; current: number; total: number } | null {
149
+ if (!this.searchQuery) return null;
150
+ return {
151
+ query: this.searchQuery,
152
+ current: this.searchCurrentMatch + 1,
153
+ total: this.searchMatches.length,
154
+ };
155
+ }
156
+
157
+ render(width: number, height: number): string[] {
158
+ const visible = this.visibleLines();
159
+ if (visible.length === 0) {
160
+ return this.renderEmpty(width, height);
161
+ }
162
+
163
+ if (this.centerTarget !== null) {
164
+ const half = Math.floor(height / 2);
165
+ this.anchorEnd = Math.min(visible.length, this.centerTarget + half + 1);
166
+ this.centerTarget = null;
167
+ }
168
+
169
+ const rawEnd = this.follow
170
+ ? visible.length
171
+ : (this.anchorEnd ?? visible.length);
172
+ const end = Math.min(
173
+ visible.length,
174
+ Math.max(Math.min(height, visible.length), rawEnd),
175
+ );
176
+ const start = Math.max(0, end - height);
177
+ const matchSet = new Set(this.searchMatches);
178
+ const currentMatchIndex =
179
+ this.searchCurrentMatch >= 0
180
+ ? this.searchMatches[this.searchCurrentMatch]
181
+ : undefined;
182
+
183
+ const rendered = visible.slice(start, end).map((line, index) => {
184
+ const visibleIndex = start + index;
185
+ const text = truncateToWidth(line.text, width, "", true);
186
+ if (visibleIndex === currentMatchIndex) {
187
+ return truncateToWidth(
188
+ this.theme.bold(this.theme.inverse(text)),
189
+ width,
190
+ );
191
+ }
192
+ if (matchSet.has(visibleIndex)) {
193
+ return truncateToWidth(this.theme.fg("warning", text), width);
194
+ }
195
+ if (this.notifyLines.has(line.text)) {
196
+ return truncateToWidth(this.theme.underline(text), width);
197
+ }
198
+ if (line.type === "stderr") {
199
+ return truncateToWidth(this.theme.fg("warning", text), width);
200
+ }
201
+ return truncateToWidth(text, width);
202
+ });
203
+
204
+ while (rendered.length < height) rendered.unshift("");
205
+ return rendered.slice(-height);
206
+ }
207
+
208
+ getStatusParts(): { left: string[]; right: string[] } {
209
+ const dim = (value: string) => this.theme.fg("dim", value);
210
+ const accent = (value: string) => this.theme.fg("accent", value);
211
+ const error = (value: string) => this.theme.fg("error", value);
212
+ const visible = this.visibleLines();
213
+ const total = visible.length;
214
+
215
+ const left: string[] = [];
216
+ const search = this.getSearchInfo();
217
+ if (search) {
218
+ left.push(
219
+ search.total === 0
220
+ ? error(`no matches: "${search.query}"`)
221
+ : `${dim("/")}${search.query} ${dim(`${search.current}/${search.total}`)}`,
222
+ );
223
+ }
224
+
225
+ const right: string[] = [];
226
+ if (this.follow) {
227
+ right.push(accent("following"));
228
+ } else if (total === 0) {
229
+ right.push(dim("empty"));
230
+ } else {
231
+ const end = Math.min(total, Math.max(0, this.anchorEnd ?? total));
232
+ const pct = Math.round((end / total) * 100);
233
+ right.push(dim(`${pct}% L${end}/${total}`));
234
+ }
235
+ if (this.streamFilter !== "both") right.push(dim(`[${this.streamFilter}]`));
236
+
237
+ return { left, right };
238
+ }
239
+
240
+ private renderEmpty(width: number, height: number): string[] {
241
+ const title = this.theme.fg("muted", "No output yet");
242
+ const top = Math.max(0, Math.floor((height - 1) / 2));
243
+ const leftPad = Math.max(0, Math.floor((width - visibleWidth(title)) / 2));
244
+ const lines = Array.from({ length: height }, () => "");
245
+ lines[top] = truncateToWidth(`${" ".repeat(leftPad)}${title}`, width);
246
+ return lines;
247
+ }
248
+
249
+ private visibleLines(): ProcessLogLine[] {
250
+ if (this.streamFilter === "both") return this.lines;
251
+ return this.lines.filter((line) => line.type === this.streamFilter);
252
+ }
253
+
254
+ private refreshMatches(): void {
255
+ if (!this.searchQuery) {
256
+ this.searchMatches = [];
257
+ this.searchCurrentMatch = -1;
258
+ return;
259
+ }
260
+
261
+ const query = this.searchQuery.toLowerCase();
262
+ this.searchMatches = this.visibleLines().flatMap((line, index) =>
263
+ line.text.toLowerCase().includes(query) ? [index] : [],
264
+ );
265
+ if (this.searchCurrentMatch >= this.searchMatches.length) {
266
+ this.searchCurrentMatch = Math.max(0, this.searchMatches.length - 1);
267
+ }
268
+ }
269
+
270
+ private jumpToVisibleIndex(index: number): void {
271
+ this.follow = false;
272
+ this.centerTarget = index;
273
+ }
274
+ }