@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.
- package/CONTRIBUTING.md +40 -42
- package/README.md +54 -84
- package/extensions/process-tabs.ts +26 -0
- package/extensions/processes/client.ts +194 -0
- package/extensions/processes/commands/clear.ts +29 -0
- package/extensions/processes/commands/kill.ts +152 -0
- package/extensions/processes/commands/overview.ts +102 -0
- package/extensions/processes/components/overview-component.ts +729 -0
- package/extensions/processes/components/overview-panel.ts +177 -0
- package/extensions/processes/config/defaults.ts +28 -0
- package/extensions/processes/config/index.ts +13 -0
- package/extensions/processes/config/loader.ts +62 -0
- package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
- package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
- package/extensions/processes/config/migrations/index.ts +25 -0
- package/extensions/processes/config/schema.ts +9 -0
- package/extensions/processes/config/types.ts +72 -0
- package/extensions/processes/constants.ts +1 -0
- package/extensions/processes/handlers/commands.ts +113 -0
- package/extensions/processes/handlers/kill-process.ts +31 -0
- package/extensions/processes/handlers/notifications.ts +147 -0
- package/extensions/processes/handlers/requests.ts +114 -0
- package/extensions/processes/handlers/subscriptions.ts +144 -0
- package/extensions/processes/hooks/background-blocker.ts +201 -0
- package/extensions/processes/hooks/cleanup.ts +43 -0
- package/extensions/processes/hooks/event-bridge.ts +32 -0
- package/extensions/processes/i18n/index.ts +3 -0
- package/extensions/processes/i18n/messages.ts +27 -0
- package/extensions/processes/i18n/translator.ts +49 -0
- package/extensions/processes/index.ts +109 -0
- package/extensions/processes/message-renderer.ts +117 -0
- package/extensions/processes/notification-sender.ts +43 -0
- package/extensions/processes/notifications/classify.ts +27 -0
- package/extensions/processes/notifications/log-matchers.ts +146 -0
- package/extensions/processes/notifications/registry.ts +225 -0
- package/extensions/processes/notifications/render-content.ts +122 -0
- package/extensions/processes/notifications/service.ts +336 -0
- package/extensions/processes/notifications/types.ts +11 -0
- package/extensions/processes/settings/apply-setting-change.ts +30 -0
- package/extensions/processes/settings/build-sections.ts +420 -0
- package/extensions/processes/settings/index.ts +42 -0
- package/extensions/processes/tools/clear/index.ts +21 -0
- package/extensions/processes/tools/clear/render.ts +40 -0
- package/extensions/processes/tools/components/index.ts +4 -0
- package/extensions/processes/tools/components/process-action-header.ts +31 -0
- package/extensions/processes/tools/components/process-action-title.ts +17 -0
- package/extensions/processes/tools/components/tool-layout.ts +70 -0
- package/extensions/processes/tools/components/watch.ts +53 -0
- package/extensions/processes/tools/index.ts +265 -0
- package/extensions/processes/tools/list/index.ts +157 -0
- package/extensions/processes/tools/list/render.ts +211 -0
- package/extensions/processes/tools/notify.ts +191 -0
- package/extensions/processes/tools/output/index.ts +307 -0
- package/extensions/processes/tools/output/render.ts +229 -0
- package/extensions/processes/tools/schema.ts +263 -0
- package/extensions/processes/tools/start/index.ts +64 -0
- package/extensions/processes/tools/start/render.ts +67 -0
- package/extensions/processes/tools/stop/index.ts +35 -0
- package/extensions/processes/tools/stop/render.ts +43 -0
- package/extensions/processes/tools/update/index.ts +320 -0
- package/extensions/processes/tools/update/render.ts +251 -0
- package/extensions/processes/tools/utils.ts +112 -0
- package/extensions/processes/tools/watch-format.ts +14 -0
- package/extensions/processes/tools/write/index.ts +75 -0
- package/extensions/processes/tools/write/render.ts +68 -0
- package/extensions/processes/utils/truncate.ts +285 -0
- package/extensions/processes-dock/client.ts +60 -0
- package/extensions/processes-dock/commands/dock.ts +70 -0
- package/extensions/processes-dock/commands/pin.ts +166 -0
- package/extensions/processes-dock/components/log-dock-component.ts +375 -0
- package/extensions/processes-dock/dock-state.ts +33 -0
- package/extensions/processes-dock/index.ts +28 -0
- package/extensions/processes-dock/logs-client.ts +5 -0
- package/extensions/processes-dock/widget/setup.ts +520 -0
- package/extensions/processes-dock/widget/status.ts +149 -0
- package/extensions/processes-dock/widget/types.ts +16 -0
- package/extensions/processes-logs/client.ts +51 -0
- package/extensions/processes-logs/commands/logs.ts +108 -0
- package/extensions/processes-logs/completions.ts +31 -0
- package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
- package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
- package/extensions/processes-logs/index.ts +27 -0
- package/extensions/processes-logs/logs-client.ts +101 -0
- package/extensions/shared/line-buffer.ts +35 -0
- package/extensions/shared/output-payload.ts +28 -0
- package/extensions/shared/ui.ts +146 -0
- package/package.json +47 -20
- package/schema.json +134 -0
- package/skills/pi-processes/SKILL.md +496 -25
- package/src/get-manager.ts +15 -0
- package/src/manager/index.ts +182 -0
- package/src/manager/internal-types.ts +130 -0
- package/src/manager/limits.ts +15 -0
- package/src/manager/process-log-store.ts +422 -0
- package/src/manager/process-output.ts +345 -0
- package/src/manager/process-registry.ts +78 -0
- package/src/manager/process-runtime-controller.ts +499 -0
- package/src/protocol/broadcasts.ts +14 -0
- package/src/protocol/channels.ts +30 -0
- package/src/protocol/commands.ts +24 -0
- package/src/protocol/index.ts +34 -0
- package/src/protocol/logs.ts +23 -0
- package/src/protocol/notifications.ts +48 -0
- package/src/protocol/requests.ts +76 -0
- package/src/types.ts +66 -0
- package/src/utils/ansi.ts +2 -4
- package/src/utils/format.ts +24 -32
- package/src/utils/index.ts +5 -1
- package/src/utils/is-record.ts +3 -0
- package/src/utils/match-line.ts +32 -0
- package/src/utils/platform.ts +13 -0
- package/src/utils/shorten-path.ts +11 -0
- package/src/utils/signals.ts +22 -0
- package/src/commands/clear/command.ts +0 -14
- package/src/commands/clear/index.ts +0 -1
- package/src/commands/completions.ts +0 -38
- package/src/commands/dock/command.ts +0 -29
- package/src/commands/dock/index.ts +0 -1
- package/src/commands/index.ts +0 -33
- package/src/commands/kill/command.ts +0 -69
- package/src/commands/kill/index.ts +0 -1
- package/src/commands/logs/command.ts +0 -47
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/pick-process.ts +0 -34
- package/src/commands/pin/command.ts +0 -34
- package/src/commands/pin/index.ts +0 -1
- package/src/commands/processes/command.ts +0 -39
- package/src/commands/processes/index.ts +0 -1
- package/src/commands/settings/apply-setting-change.ts +0 -72
- package/src/commands/settings/build-sections.ts +0 -160
- package/src/commands/settings/command.ts +0 -20
- package/src/commands/settings/index.ts +0 -1
- package/src/components/log-dock-component.test.ts +0 -29
- package/src/components/log-dock-component.ts +0 -236
- package/src/components/log-file-viewer.ts +0 -317
- package/src/components/log-overlay-component.ts +0 -546
- package/src/components/panel-helpers.ts +0 -37
- package/src/components/process-picker-component.ts +0 -166
- package/src/components/processes-component.ts +0 -485
- package/src/components/status-format.ts +0 -38
- package/src/config.ts +0 -107
- package/src/constants/index.ts +0 -16
- package/src/constants/types.ts +0 -106
- package/src/hooks/background-blocker.ts +0 -66
- package/src/hooks/cleanup.ts +0 -10
- package/src/hooks/index.ts +0 -32
- package/src/hooks/message-renderer.ts +0 -115
- package/src/hooks/process-end.ts +0 -69
- package/src/hooks/process-watch.ts +0 -85
- package/src/hooks/utils.ts +0 -23
- package/src/hooks/widget/index.ts +0 -2
- package/src/hooks/widget/setup.ts +0 -167
- package/src/hooks/widget/status-widget.ts +0 -105
- package/src/hooks/widget/types.ts +0 -20
- package/src/index.ts +0 -35
- package/src/manager.test.ts +0 -331
- package/src/manager.ts +0 -758
- package/src/tools/actions/clear.ts +0 -20
- package/src/tools/actions/debug.ts +0 -148
- package/src/tools/actions/index.ts +0 -171
- package/src/tools/actions/kill.ts +0 -89
- package/src/tools/actions/list.ts +0 -188
- package/src/tools/actions/logs.ts +0 -117
- package/src/tools/actions/output.ts +0 -269
- package/src/tools/actions/start.ts +0 -245
- package/src/tools/actions/write.ts +0 -113
- package/src/tools/index.ts +0 -207
- package/src/utils/ansi.test.ts +0 -22
- package/src/utils/command-executor.test.ts +0 -48
- package/src/utils/keybindings.ts +0 -71
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LogOverlayComponent - tabbed log viewer as a floating overlay.
|
|
3
|
-
*
|
|
4
|
-
* Layout (CHROME_LINES = 7, logRows computed from terminal height):
|
|
5
|
-
*
|
|
6
|
-
* ╭──────────── Process Logs ─────────────╮
|
|
7
|
-
* │ [● backend] ✓ frontend ✗ worker │
|
|
8
|
-
* ├───────────────────────────────────────┤
|
|
9
|
-
* │ log line 1 │
|
|
10
|
-
* │ ... │
|
|
11
|
-
* ├───────────────────────────────────────┤
|
|
12
|
-
* │ /query 1/4 42% L50/120│
|
|
13
|
-
* │ ←/→ tab g/G j/k / n/N s f q │
|
|
14
|
-
* ╰───────────────────────────────────────╯
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
type Component,
|
|
20
|
-
Input,
|
|
21
|
-
matchesKey,
|
|
22
|
-
type TUI,
|
|
23
|
-
truncateToWidth,
|
|
24
|
-
visibleWidth,
|
|
25
|
-
} from "@earendil-works/pi-tui";
|
|
26
|
-
import { LIVE_STATUSES, type ProcessInfo } from "../constants";
|
|
27
|
-
import type { ProcessManager } from "../manager";
|
|
28
|
-
import { LogFileViewer } from "./log-file-viewer";
|
|
29
|
-
import { statusIcon } from "./status-format";
|
|
30
|
-
|
|
31
|
-
// Lines that aren't log content: top border + tabs + divider + divider + status + footer + bottom border
|
|
32
|
-
const CHROME_LINES = 7;
|
|
33
|
-
const MIN_LOG_ROWS = 5;
|
|
34
|
-
const OVERLAY_FRACTION = 0.8;
|
|
35
|
-
const MAX_TAB_NAME = 12;
|
|
36
|
-
|
|
37
|
-
type OverlayMode = "normal" | "search-typing" | "search-active";
|
|
38
|
-
|
|
39
|
-
interface LogOverlayOptions {
|
|
40
|
-
tui: TUI;
|
|
41
|
-
theme: Theme;
|
|
42
|
-
manager: ProcessManager;
|
|
43
|
-
/** Pre-select this process on open. If absent, uses first in list. */
|
|
44
|
-
initialProcessId?: string;
|
|
45
|
-
done: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class LogOverlayComponent implements Component {
|
|
49
|
-
private tui: TUI;
|
|
50
|
-
private theme: Theme;
|
|
51
|
-
private manager: ProcessManager;
|
|
52
|
-
private done: () => void;
|
|
53
|
-
|
|
54
|
-
private processes: ProcessInfo[] = [];
|
|
55
|
-
private tabIndex = 0;
|
|
56
|
-
private tabViewOffset = 0;
|
|
57
|
-
|
|
58
|
-
/** One LogFileViewer per process id, lazy-created on first visit. */
|
|
59
|
-
private viewers: Map<string, LogFileViewer> = new Map();
|
|
60
|
-
|
|
61
|
-
private mode: OverlayMode = "normal";
|
|
62
|
-
private searchInput: Input = new Input();
|
|
63
|
-
|
|
64
|
-
private unsubscribeManager: (() => void) | null = null;
|
|
65
|
-
|
|
66
|
-
constructor(opts: LogOverlayOptions) {
|
|
67
|
-
this.tui = opts.tui;
|
|
68
|
-
this.theme = opts.theme;
|
|
69
|
-
this.manager = opts.manager;
|
|
70
|
-
this.done = opts.done;
|
|
71
|
-
|
|
72
|
-
this.processes = this.sortProcesses(this.manager.list());
|
|
73
|
-
|
|
74
|
-
if (opts.initialProcessId) {
|
|
75
|
-
const idx = this.processes.findIndex(
|
|
76
|
-
(p) => p.id === opts.initialProcessId,
|
|
77
|
-
);
|
|
78
|
-
if (idx >= 0) this.tabIndex = idx;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
this.unsubscribeManager = this.manager.onEvent(() => {
|
|
82
|
-
const next = this.manager.list();
|
|
83
|
-
// Auto-close when all processes have been cleared.
|
|
84
|
-
if (next.length === 0) {
|
|
85
|
-
this.close();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
this.processes = this.sortProcesses(next);
|
|
89
|
-
this.tabIndex = Math.min(this.tabIndex, this.processes.length - 1);
|
|
90
|
-
this.tui.requestRender();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
this.searchInput.onSubmit = (query) => {
|
|
94
|
-
const trimmed = query.trim();
|
|
95
|
-
if (trimmed) {
|
|
96
|
-
this.currentViewer()?.setSearch(trimmed);
|
|
97
|
-
this.mode = "search-active";
|
|
98
|
-
} else {
|
|
99
|
-
this.currentViewer()?.clearSearch();
|
|
100
|
-
this.mode = "normal";
|
|
101
|
-
}
|
|
102
|
-
this.tui.requestRender();
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
this.searchInput.onEscape = () => {
|
|
106
|
-
this.mode = "normal";
|
|
107
|
-
this.searchInput.setValue("");
|
|
108
|
-
this.tui.requestRender();
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Sorting
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
private sortProcesses(list: ProcessInfo[]): ProcessInfo[] {
|
|
117
|
-
const isLive = (p: ProcessInfo) => LIVE_STATUSES.has(p.status);
|
|
118
|
-
return [...list].sort((a, b) => {
|
|
119
|
-
const aLive = isLive(a) ? 1 : 0;
|
|
120
|
-
const bLive = isLive(b) ? 1 : 0;
|
|
121
|
-
if (bLive !== aLive) return bLive - aLive; // live first
|
|
122
|
-
return b.startTime - a.startTime; // most recent first within each group
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Viewer lifecycle
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
private getViewer(proc: ProcessInfo): LogFileViewer | null {
|
|
131
|
-
let viewer = this.viewers.get(proc.id);
|
|
132
|
-
if (!viewer) {
|
|
133
|
-
const logFiles = this.manager.getLogFiles(proc.id);
|
|
134
|
-
if (!logFiles) return null;
|
|
135
|
-
viewer = new LogFileViewer({
|
|
136
|
-
filePath: logFiles.combinedFile,
|
|
137
|
-
format: "combined",
|
|
138
|
-
theme: this.theme,
|
|
139
|
-
follow: false,
|
|
140
|
-
});
|
|
141
|
-
this.viewers.set(proc.id, viewer);
|
|
142
|
-
}
|
|
143
|
-
return viewer;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private currentProcess(): ProcessInfo | null {
|
|
147
|
-
return this.processes[this.tabIndex] ?? null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private currentViewer(): LogFileViewer | null {
|
|
151
|
-
const proc = this.currentProcess();
|
|
152
|
-
if (!proc) return null;
|
|
153
|
-
return this.getViewer(proc) ?? null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Cleanup
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
private close(): void {
|
|
161
|
-
this.unsubscribeManager?.();
|
|
162
|
-
this.unsubscribeManager = null;
|
|
163
|
-
this.done();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Input
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
handleInput(data: string): boolean {
|
|
171
|
-
if (this.mode === "search-typing")
|
|
172
|
-
return this.handleSearchTypingInput(data);
|
|
173
|
-
if (this.mode === "search-active")
|
|
174
|
-
return this.handleSearchActiveInput(data);
|
|
175
|
-
return this.handleNormalInput(data);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private handleNormalInput(data: string): boolean {
|
|
179
|
-
const viewer = this.currentViewer();
|
|
180
|
-
|
|
181
|
-
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
182
|
-
this.close();
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (data === "\t") {
|
|
187
|
-
this.nextTab();
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
if (matchesKey(data, "shift+tab")) {
|
|
191
|
-
this.prevTab();
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (!viewer) return true;
|
|
196
|
-
|
|
197
|
-
if (data === "g") {
|
|
198
|
-
viewer.scrollToTop();
|
|
199
|
-
this.tui.requestRender();
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
if (data === "G") {
|
|
203
|
-
viewer.scrollToBottom();
|
|
204
|
-
this.tui.requestRender();
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
if (matchesKey(data, "down") || data === "j") {
|
|
208
|
-
viewer.scrollBy(1);
|
|
209
|
-
this.tui.requestRender();
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
if (matchesKey(data, "up") || data === "k") {
|
|
213
|
-
viewer.scrollBy(-1);
|
|
214
|
-
this.tui.requestRender();
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
if (data === "f") {
|
|
218
|
-
viewer.toggleFollow();
|
|
219
|
-
this.tui.requestRender();
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
if (data === "s") {
|
|
223
|
-
viewer.cycleStreamFilter();
|
|
224
|
-
this.tui.requestRender();
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (data === "/") {
|
|
229
|
-
this.searchInput.setValue("");
|
|
230
|
-
this.mode = "search-typing";
|
|
231
|
-
this.tui.requestRender();
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private handleSearchTypingInput(data: string): boolean {
|
|
238
|
-
// Delegate all editing to the Input component.
|
|
239
|
-
// onSubmit / onEscape are wired in the constructor and fire synchronously.
|
|
240
|
-
this.searchInput.handleInput(data);
|
|
241
|
-
this.tui.requestRender();
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private handleSearchActiveInput(data: string): boolean {
|
|
246
|
-
if (matchesKey(data, "escape")) {
|
|
247
|
-
this.currentViewer()?.clearSearch();
|
|
248
|
-
this.mode = "normal";
|
|
249
|
-
this.searchInput.setValue("");
|
|
250
|
-
this.tui.requestRender();
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
if (data === "n") {
|
|
254
|
-
this.currentViewer()?.nextMatch();
|
|
255
|
-
this.tui.requestRender();
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
if (data === "N") {
|
|
259
|
-
this.currentViewer()?.prevMatch();
|
|
260
|
-
this.tui.requestRender();
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
if (data === "/") {
|
|
264
|
-
// Re-open typing with current query pre-filled.
|
|
265
|
-
const current = this.currentViewer()?.getSearchInfo()?.query ?? "";
|
|
266
|
-
this.searchInput.setValue(current);
|
|
267
|
-
this.mode = "search-typing";
|
|
268
|
-
this.tui.requestRender();
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
// All other keys: normal navigation (j/k, g/G, f, s, Tab, q, etc.)
|
|
272
|
-
return this.handleNormalInput(data);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private prevTab(): void {
|
|
276
|
-
if (this.processes.length === 0) return;
|
|
277
|
-
this.tabIndex =
|
|
278
|
-
(this.tabIndex - 1 + this.processes.length) % this.processes.length;
|
|
279
|
-
this.ensureTabVisible();
|
|
280
|
-
this.tui.requestRender();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private nextTab(): void {
|
|
284
|
-
if (this.processes.length === 0) return;
|
|
285
|
-
this.tabIndex = (this.tabIndex + 1) % this.processes.length;
|
|
286
|
-
this.ensureTabVisible();
|
|
287
|
-
this.tui.requestRender();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private ensureTabVisible(): void {
|
|
291
|
-
if (this.tabIndex < this.tabViewOffset) {
|
|
292
|
-
this.tabViewOffset = this.tabIndex;
|
|
293
|
-
}
|
|
294
|
-
this.tabViewOffset = Math.max(
|
|
295
|
-
0,
|
|
296
|
-
Math.min(this.tabViewOffset, this.tabIndex),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Rendering
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
render(width: number): string[] {
|
|
305
|
-
const totalRows = this.tui.terminal.rows ?? 24;
|
|
306
|
-
const logRows = Math.max(
|
|
307
|
-
MIN_LOG_ROWS,
|
|
308
|
-
Math.floor(totalRows * OVERLAY_FRACTION) - CHROME_LINES,
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const theme = this.theme;
|
|
312
|
-
// innerWidth = space available for content inside "│ " and " │"
|
|
313
|
-
const innerWidth = width - 4;
|
|
314
|
-
const border = (s: string) => theme.fg("dim", s);
|
|
315
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
316
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
317
|
-
|
|
318
|
-
// Pad content to exactly innerWidth visible chars, then wrap in borders.
|
|
319
|
-
const pad = (s: string): string => {
|
|
320
|
-
const w = visibleWidth(s);
|
|
321
|
-
if (w > innerWidth) return truncateToWidth(s, innerWidth);
|
|
322
|
-
return s + " ".repeat(innerWidth - w);
|
|
323
|
-
};
|
|
324
|
-
const row = (content: string): string =>
|
|
325
|
-
`${border("│ ")}${pad(content)}${border(" │")}`;
|
|
326
|
-
const divider = (): string => border(`├${"─".repeat(width - 2)}┤`);
|
|
327
|
-
|
|
328
|
-
const lines: string[] = [];
|
|
329
|
-
|
|
330
|
-
// ── Top border with centered title ──────────────────────────────────────
|
|
331
|
-
const title = " Process Logs ";
|
|
332
|
-
const titleW = visibleWidth(title);
|
|
333
|
-
const sideTotal = Math.max(0, width - 2 - titleW);
|
|
334
|
-
const leftDash = Math.floor(sideTotal / 2);
|
|
335
|
-
const rightDash = sideTotal - leftDash;
|
|
336
|
-
lines.push(
|
|
337
|
-
border(`╭${"─".repeat(leftDash)}`) +
|
|
338
|
-
accent(title) +
|
|
339
|
-
border(`${"─".repeat(rightDash)}╮`),
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// ── Tab bar ─────────────────────────────────────────────────────────────
|
|
343
|
-
lines.push(row(this.renderTabBar(innerWidth)));
|
|
344
|
-
|
|
345
|
-
// ── Divider ─────────────────────────────────────────────────────────────
|
|
346
|
-
lines.push(divider());
|
|
347
|
-
|
|
348
|
-
// ── Log content ─────────────────────────────────────────────────────────
|
|
349
|
-
const viewer = this.currentViewer();
|
|
350
|
-
if (!viewer || this.processes.length === 0) {
|
|
351
|
-
for (let i = 0; i < logRows; i++) {
|
|
352
|
-
lines.push(
|
|
353
|
-
row(i === Math.floor(logRows / 2) ? dim("No processes") : ""),
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
} else {
|
|
357
|
-
const contentLines = viewer.renderLines(innerWidth, logRows);
|
|
358
|
-
// Overlay "following" indicator at bottom-right of the content area.
|
|
359
|
-
if (viewer.isFollowing()) {
|
|
360
|
-
const indicator = theme.fg("accent", "following");
|
|
361
|
-
const indicatorW = visibleWidth(indicator);
|
|
362
|
-
const targetIdx = logRows - 1;
|
|
363
|
-
const line = contentLines[targetIdx] ?? "";
|
|
364
|
-
const truncated = truncateToWidth(line, innerWidth - indicatorW);
|
|
365
|
-
const truncW = visibleWidth(truncated);
|
|
366
|
-
contentLines[targetIdx] =
|
|
367
|
-
truncated +
|
|
368
|
-
" ".repeat(Math.max(0, innerWidth - truncW - indicatorW)) +
|
|
369
|
-
indicator;
|
|
370
|
-
}
|
|
371
|
-
for (let i = 0; i < logRows; i++) {
|
|
372
|
-
lines.push(row(contentLines[i] ?? ""));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// ── Divider ─────────────────────────────────────────────────────────────
|
|
377
|
-
lines.push(divider());
|
|
378
|
-
|
|
379
|
-
// ── Status bar ──────────────────────────────────────────────────────────
|
|
380
|
-
const statusContent = this.renderStatusContent(innerWidth, viewer);
|
|
381
|
-
lines.push(row(statusContent));
|
|
382
|
-
|
|
383
|
-
// ── Footer / keybindings ────────────────────────────────────────────────
|
|
384
|
-
lines.push(row(this.renderFooterContent(innerWidth)));
|
|
385
|
-
|
|
386
|
-
// ── Bottom border ───────────────────────────────────────────────────────
|
|
387
|
-
lines.push(border(`╰${"─".repeat(width - 2)}╯`));
|
|
388
|
-
|
|
389
|
-
return lines;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private renderTabBar(innerWidth: number): string {
|
|
393
|
-
if (this.processes.length === 0) {
|
|
394
|
-
return this.theme.fg("dim", "No processes");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const theme = this.theme;
|
|
398
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
399
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
400
|
-
const success = (s: string) => theme.fg("success", s);
|
|
401
|
-
const warning = (s: string) => theme.fg("warning", s);
|
|
402
|
-
const error = (s: string) => theme.fg("error", s);
|
|
403
|
-
|
|
404
|
-
const coloredIcon = (proc: ProcessInfo): string => {
|
|
405
|
-
const icon = statusIcon(proc.status, proc.success);
|
|
406
|
-
switch (proc.status) {
|
|
407
|
-
case "running":
|
|
408
|
-
return success(icon);
|
|
409
|
-
case "terminating":
|
|
410
|
-
case "terminate_timeout":
|
|
411
|
-
return warning(icon);
|
|
412
|
-
case "killed":
|
|
413
|
-
return error(icon);
|
|
414
|
-
case "exited":
|
|
415
|
-
return proc.success ? dim(icon) : error(icon);
|
|
416
|
-
default:
|
|
417
|
-
return dim(icon);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// Overflow indicators reserve 2 chars each.
|
|
422
|
-
const OVERFLOW_W = 2;
|
|
423
|
-
const SEP = " ";
|
|
424
|
-
const SEP_W = 2;
|
|
425
|
-
|
|
426
|
-
const hasLeft = this.tabViewOffset > 0;
|
|
427
|
-
let usedWidth = hasLeft ? OVERFLOW_W : 0;
|
|
428
|
-
const tabStrings: string[] = [];
|
|
429
|
-
let lastVisible = this.tabViewOffset - 1;
|
|
430
|
-
|
|
431
|
-
for (let i = this.tabViewOffset; i < this.processes.length; i++) {
|
|
432
|
-
const proc = this.processes[i];
|
|
433
|
-
if (!proc) continue;
|
|
434
|
-
const isActive = i === this.tabIndex;
|
|
435
|
-
|
|
436
|
-
const namePlain = proc.name.slice(0, MAX_TAB_NAME);
|
|
437
|
-
// Visible width of this tab: "icon name" plus brackets if active.
|
|
438
|
-
// Active: "[icon name]" = 1 + 1(icon) + 1(space) + nameLen + 1 = nameLen + 4
|
|
439
|
-
// Inactive: " icon name " = 1 + 1(icon) + 1(space) + nameLen + 1 = nameLen + 4 (same)
|
|
440
|
-
const tabW = 1 + 1 + 1 + namePlain.length + 1; // bracket + icon + space + name + bracket
|
|
441
|
-
const needed = tabStrings.length > 0 ? SEP_W + tabW : tabW;
|
|
442
|
-
const rightReserve = i < this.processes.length - 1 ? OVERFLOW_W : 0;
|
|
443
|
-
|
|
444
|
-
if (usedWidth + needed + rightReserve > innerWidth) break;
|
|
445
|
-
|
|
446
|
-
usedWidth += needed;
|
|
447
|
-
lastVisible = i;
|
|
448
|
-
|
|
449
|
-
const icon = coloredIcon(proc);
|
|
450
|
-
const name = isActive ? accent(namePlain) : dim(namePlain);
|
|
451
|
-
if (isActive) {
|
|
452
|
-
tabStrings.push(`${accent("[")}${icon} ${name}${accent("]")}`);
|
|
453
|
-
} else {
|
|
454
|
-
tabStrings.push(`${dim(" ")}${icon} ${name}${dim(" ")}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const hasRight = lastVisible < this.processes.length - 1;
|
|
459
|
-
const left = hasLeft ? dim("← ") : "";
|
|
460
|
-
const right = hasRight ? dim(" →") : "";
|
|
461
|
-
|
|
462
|
-
return left + tabStrings.join(SEP) + right;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private renderStatusContent(
|
|
466
|
-
innerWidth: number,
|
|
467
|
-
viewer: LogFileViewer | null,
|
|
468
|
-
): string {
|
|
469
|
-
const theme = this.theme;
|
|
470
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
471
|
-
|
|
472
|
-
if (this.mode === "search-typing") {
|
|
473
|
-
// Input renders as "> <text>" — replace "> " with "/" for search prompt.
|
|
474
|
-
// Reserve innerWidth - 1 chars for Input so the "/" prefix fits.
|
|
475
|
-
const inputWidth = Math.max(1, innerWidth - 1);
|
|
476
|
-
const rendered = this.searchInput.render(inputWidth);
|
|
477
|
-
const inputLine = rendered[0] ?? "";
|
|
478
|
-
// Input always prefixes with "> " (2 plain chars, no ANSI before them).
|
|
479
|
-
const withSlash = dim("/") + inputLine.slice(2);
|
|
480
|
-
const w = visibleWidth(withSlash);
|
|
481
|
-
if (w >= innerWidth) return truncateToWidth(withSlash, innerWidth);
|
|
482
|
-
return withSlash + " ".repeat(Math.max(0, innerWidth - w));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (!viewer) return "";
|
|
486
|
-
// LogFileViewer.renderStatusBar() returns a string padded to given width.
|
|
487
|
-
return viewer.renderStatusBar(innerWidth);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private renderFooterContent(innerWidth: number): string {
|
|
491
|
-
const theme = this.theme;
|
|
492
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
493
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
494
|
-
|
|
495
|
-
if (this.mode === "search-typing") {
|
|
496
|
-
const hint = `${dim("enter")} apply ${dim("esc")} cancel ${dim("ctrl+u")} clear`;
|
|
497
|
-
const w = visibleWidth(hint);
|
|
498
|
-
if (w >= innerWidth) return truncateToWidth(hint, innerWidth);
|
|
499
|
-
return hint + " ".repeat(innerWidth - w);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (this.mode === "search-active") {
|
|
503
|
-
const hint =
|
|
504
|
-
`${dim("n")} next ` +
|
|
505
|
-
`${dim("N")} prev ` +
|
|
506
|
-
`${dim("/")} edit search ` +
|
|
507
|
-
`${dim("esc")} clear ` +
|
|
508
|
-
`${dim("j/k")} scroll ` +
|
|
509
|
-
`${dim("f")} follow ` +
|
|
510
|
-
`${dim("q")} quit`;
|
|
511
|
-
const w = visibleWidth(hint);
|
|
512
|
-
if (w >= innerWidth) return truncateToWidth(hint, innerWidth);
|
|
513
|
-
return hint + " ".repeat(innerWidth - w);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const viewer = this.currentViewer();
|
|
517
|
-
const streamFilter = viewer?.getStreamFilter() ?? "combined";
|
|
518
|
-
// Show stdout+stderr with only the active stream(s) highlighted.
|
|
519
|
-
const stdoutPart =
|
|
520
|
-
streamFilter === "combined" || streamFilter === "stdout"
|
|
521
|
-
? accent("stdout")
|
|
522
|
-
: dim("stdout");
|
|
523
|
-
const stderrPart =
|
|
524
|
-
streamFilter === "combined" || streamFilter === "stderr"
|
|
525
|
-
? accent("stderr")
|
|
526
|
-
: dim("stderr");
|
|
527
|
-
const streamIndicator = `${dim("s:")}${stdoutPart}${dim("+")}${stderrPart}`;
|
|
528
|
-
|
|
529
|
-
const footer =
|
|
530
|
-
`${dim("tab/shift+tab")} switch ` +
|
|
531
|
-
`${dim("g/G")} top/bot ` +
|
|
532
|
-
`${dim("j/k")} scroll ` +
|
|
533
|
-
`${dim("/")} search ` +
|
|
534
|
-
streamIndicator +
|
|
535
|
-
` ${dim("f")} follow ` +
|
|
536
|
-
`${dim("q")} quit`;
|
|
537
|
-
|
|
538
|
-
const w = visibleWidth(footer);
|
|
539
|
-
if (w >= innerWidth) return truncateToWidth(footer, innerWidth);
|
|
540
|
-
return footer + " ".repeat(innerWidth - w);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
invalidate(): void {
|
|
544
|
-
// No local cache.
|
|
545
|
-
}
|
|
546
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
|
|
4
|
-
export function createPanelPadder(width: number): (content: string) => string {
|
|
5
|
-
const innerWidth = Math.max(0, width - 2);
|
|
6
|
-
|
|
7
|
-
return (content: string): string => {
|
|
8
|
-
const truncated = truncateToWidth(content, innerWidth, "", true);
|
|
9
|
-
const padding = " ".repeat(
|
|
10
|
-
Math.max(0, innerWidth - visibleWidth(truncated)),
|
|
11
|
-
);
|
|
12
|
-
return `│${truncated}${padding}│`;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function renderPanelRule(width: number, theme: Theme): string {
|
|
17
|
-
if (width <= 0) return "";
|
|
18
|
-
if (width === 1) return theme.fg("dim", "─");
|
|
19
|
-
|
|
20
|
-
return theme.fg("dim", `├${"─".repeat(Math.max(0, width - 2))}┤`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function renderPanelTitleLine(
|
|
24
|
-
title: string,
|
|
25
|
-
width: number,
|
|
26
|
-
theme: Theme,
|
|
27
|
-
): string {
|
|
28
|
-
if (width <= 0) return "";
|
|
29
|
-
if (width === 1) return theme.fg("dim", "─");
|
|
30
|
-
|
|
31
|
-
const innerWidth = Math.max(0, width - 2);
|
|
32
|
-
const label = ` ${title} `;
|
|
33
|
-
const truncated = truncateToWidth(label, innerWidth, "", true);
|
|
34
|
-
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
35
|
-
|
|
36
|
-
return theme.fg("dim", `┌${truncated}${"─".repeat(remaining)}┐`);
|
|
37
|
-
}
|