pomitu 1.4.0 → 1.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.
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ If you want to contribute to Pomitu or run it in development mode:
|
|
|
98
98
|
Dev installation test:
|
|
99
99
|
|
|
100
100
|
```sh
|
|
101
|
-
npm run build && npm pack && npm install -g pomitu-1.
|
|
101
|
+
npm run build && npm pack && npm install -g pomitu-1.6.0.tgz && rm pomitu-1.6.0.tgz
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
### Linting
|
package/dist/commands/start.js
CHANGED
|
@@ -47,6 +47,9 @@ export const start = new Command('start')
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
if (runInteractive && anyTuiActive) {
|
|
51
|
+
console.log('A TUI session is already running in another terminal — not opening a new one');
|
|
52
|
+
}
|
|
50
53
|
if (runInteractive && !anyTuiActive) {
|
|
51
54
|
render(React.createElement(ProcessTUI, {
|
|
52
55
|
configPath: name,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in replacement for ink-select-input's SelectInput that preserves scroll
|
|
3
|
+
* position when items change, instead of resetting to the top.
|
|
4
|
+
*/
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
7
|
+
import { Box, useInput, useStdin } from 'ink';
|
|
8
|
+
import { Indicator, Item as ItemComponent } from 'ink-select-input';
|
|
9
|
+
function rotateArray(arr, k) {
|
|
10
|
+
if (arr.length === 0)
|
|
11
|
+
return arr;
|
|
12
|
+
const n = arr.length;
|
|
13
|
+
const steps = ((k % n) + n) % n;
|
|
14
|
+
if (steps === 0)
|
|
15
|
+
return [...arr];
|
|
16
|
+
return [...arr.slice(-steps), ...arr.slice(0, -steps)];
|
|
17
|
+
}
|
|
18
|
+
export function PersistentSelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent: IndicatorComp = Indicator, itemComponent: ItemComp = ItemComponent, limit: customLimit, onSelect, onHighlight, headingPredicate, }) {
|
|
19
|
+
const hasLimit = typeof customLimit === 'number' && items.length > customLimit;
|
|
20
|
+
const limit = hasLimit ? Math.min(customLimit, items.length) : items.length;
|
|
21
|
+
const lastIndex = limit - 1;
|
|
22
|
+
const [rotateIndex, setRotateIndex] = useState(initialIndex > lastIndex ? lastIndex - initialIndex : 0);
|
|
23
|
+
const [selectedIndex, setSelectedIndex] = useState(initialIndex ? (initialIndex > lastIndex ? lastIndex : initialIndex) : 0);
|
|
24
|
+
const previousItems = useRef(items);
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const { internal_eventEmitter } = useStdin();
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isFocused || items.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
const handleHomeEnd = (data) => {
|
|
31
|
+
const str = data.toString();
|
|
32
|
+
const isHome = str === '\x1b[H' || str === '\x1bOH' || str === '\x1b[1~' || str === '\x1b[7~';
|
|
33
|
+
const isEnd = str === '\x1b[F' || str === '\x1bOF' || str === '\x1b[4~' || str === '\x1b[8~';
|
|
34
|
+
if (isHome) {
|
|
35
|
+
setSelectedIndex(0);
|
|
36
|
+
setRotateIndex(0);
|
|
37
|
+
if (typeof onHighlight === 'function' && items[0])
|
|
38
|
+
onHighlight(items[0]);
|
|
39
|
+
}
|
|
40
|
+
if (isEnd) {
|
|
41
|
+
if (hasLimit) {
|
|
42
|
+
const newRotateIndex = -(items.length - limit);
|
|
43
|
+
setRotateIndex(newRotateIndex);
|
|
44
|
+
setSelectedIndex(limit - 1);
|
|
45
|
+
const sliced = rotateArray(items, newRotateIndex).slice(0, limit);
|
|
46
|
+
if (typeof onHighlight === 'function' && sliced[limit - 1])
|
|
47
|
+
onHighlight(sliced[limit - 1]);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const lastIdx = items.length - 1;
|
|
51
|
+
setSelectedIndex(lastIdx);
|
|
52
|
+
if (typeof onHighlight === 'function' && items[lastIdx])
|
|
53
|
+
onHighlight(items[lastIdx]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
internal_eventEmitter?.on('input', handleHomeEnd);
|
|
58
|
+
return () => {
|
|
59
|
+
internal_eventEmitter?.removeListener('input', handleHomeEnd);
|
|
60
|
+
};
|
|
61
|
+
}, [isFocused, items, hasLimit, limit, onHighlight, internal_eventEmitter]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isDeepStrictEqual(previousItems.current.map(item => item.value), items.map(item => item.value))) {
|
|
64
|
+
const newHasLimit = typeof customLimit === 'number' && items.length > customLimit;
|
|
65
|
+
if (newHasLimit) {
|
|
66
|
+
// Clamp scroll and cursor to new valid bounds
|
|
67
|
+
const maxScroll = items.length - customLimit;
|
|
68
|
+
setRotateIndex(prev => Math.max(-maxScroll, Math.min(0, prev)));
|
|
69
|
+
setSelectedIndex(prev => Math.min(prev, customLimit - 1));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// No scrolling needed anymore
|
|
73
|
+
setRotateIndex(0);
|
|
74
|
+
setSelectedIndex(prev => Math.min(prev, Math.max(0, items.length - 1)));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
previousItems.current = items;
|
|
78
|
+
}, [items, customLimit]);
|
|
79
|
+
useInput(useCallback((input, key) => {
|
|
80
|
+
if (input === 'k' || key.upArrow) {
|
|
81
|
+
const atVisualTop = selectedIndex === 0;
|
|
82
|
+
if (hasLimit) {
|
|
83
|
+
if (atVisualTop) {
|
|
84
|
+
if (rotateIndex < 0) {
|
|
85
|
+
// Scroll window up
|
|
86
|
+
const nextRotateIndex = rotateIndex + 1;
|
|
87
|
+
setRotateIndex(nextRotateIndex);
|
|
88
|
+
const sliced = rotateArray(items, nextRotateIndex).slice(0, limit);
|
|
89
|
+
if (typeof onHighlight === 'function' && sliced[0])
|
|
90
|
+
onHighlight(sliced[0]);
|
|
91
|
+
}
|
|
92
|
+
// else: already at absolute top, do nothing
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const nextSelectedIndex = selectedIndex - 1;
|
|
96
|
+
setSelectedIndex(nextSelectedIndex);
|
|
97
|
+
const sliced = rotateArray(items, rotateIndex).slice(0, limit);
|
|
98
|
+
if (typeof onHighlight === 'function' && sliced[nextSelectedIndex])
|
|
99
|
+
onHighlight(sliced[nextSelectedIndex]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// No limit: wrap around
|
|
104
|
+
const nextSelectedIndex = atVisualTop ? items.length - 1 : selectedIndex - 1;
|
|
105
|
+
setSelectedIndex(nextSelectedIndex);
|
|
106
|
+
if (typeof onHighlight === 'function' && items[nextSelectedIndex])
|
|
107
|
+
onHighlight(items[nextSelectedIndex]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (input === 'j' || key.downArrow) {
|
|
111
|
+
const atVisualBottom = selectedIndex === (hasLimit ? limit : items.length) - 1;
|
|
112
|
+
if (hasLimit) {
|
|
113
|
+
const atAbsBottom = rotateIndex <= -(items.length - limit);
|
|
114
|
+
if (atVisualBottom) {
|
|
115
|
+
if (!atAbsBottom) {
|
|
116
|
+
// Scroll window down
|
|
117
|
+
const nextRotateIndex = rotateIndex - 1;
|
|
118
|
+
setRotateIndex(nextRotateIndex);
|
|
119
|
+
const sliced = rotateArray(items, nextRotateIndex).slice(0, limit);
|
|
120
|
+
if (typeof onHighlight === 'function' && sliced[limit - 1])
|
|
121
|
+
onHighlight(sliced[limit - 1]);
|
|
122
|
+
}
|
|
123
|
+
// else: already at absolute bottom, do nothing
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const nextSelectedIndex = selectedIndex + 1;
|
|
127
|
+
setSelectedIndex(nextSelectedIndex);
|
|
128
|
+
const sliced = rotateArray(items, rotateIndex).slice(0, limit);
|
|
129
|
+
if (typeof onHighlight === 'function' && sliced[nextSelectedIndex])
|
|
130
|
+
onHighlight(sliced[nextSelectedIndex]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// No limit: wrap around
|
|
135
|
+
const nextSelectedIndex = atVisualBottom ? 0 : selectedIndex + 1;
|
|
136
|
+
setSelectedIndex(nextSelectedIndex);
|
|
137
|
+
if (typeof onHighlight === 'function' && items[nextSelectedIndex])
|
|
138
|
+
onHighlight(items[nextSelectedIndex]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (key.pageUp || key.pageDown) {
|
|
142
|
+
const absIndex = hasLimit ? -rotateIndex + selectedIndex : selectedIndex;
|
|
143
|
+
let targetAbs = -1;
|
|
144
|
+
if (headingPredicate) {
|
|
145
|
+
if (key.pageUp) {
|
|
146
|
+
for (let i = absIndex - 1; i >= 0; i--) {
|
|
147
|
+
if (items[i] && headingPredicate(items[i])) {
|
|
148
|
+
targetAbs = i;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
for (let i = absIndex + 1; i < items.length; i++) {
|
|
155
|
+
if (items[i] && headingPredicate(items[i])) {
|
|
156
|
+
targetAbs = i;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
targetAbs = key.pageUp
|
|
164
|
+
? Math.max(absIndex - limit, 0)
|
|
165
|
+
: Math.min(absIndex + limit, items.length - 1);
|
|
166
|
+
}
|
|
167
|
+
if (targetAbs < 0)
|
|
168
|
+
return;
|
|
169
|
+
if (hasLimit) {
|
|
170
|
+
const windowStart = -rotateIndex;
|
|
171
|
+
let newWindowStart;
|
|
172
|
+
if (targetAbs < windowStart) {
|
|
173
|
+
newWindowStart = targetAbs;
|
|
174
|
+
}
|
|
175
|
+
else if (targetAbs >= windowStart + limit) {
|
|
176
|
+
newWindowStart = Math.min(targetAbs, items.length - limit);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
newWindowStart = windowStart;
|
|
180
|
+
}
|
|
181
|
+
const newRotateIndex = -newWindowStart;
|
|
182
|
+
const newSelectedIndex = targetAbs - newWindowStart;
|
|
183
|
+
setRotateIndex(newRotateIndex);
|
|
184
|
+
setSelectedIndex(newSelectedIndex);
|
|
185
|
+
const sliced = rotateArray(items, newRotateIndex).slice(0, limit);
|
|
186
|
+
if (typeof onHighlight === 'function' && sliced[newSelectedIndex])
|
|
187
|
+
onHighlight(sliced[newSelectedIndex]);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
setSelectedIndex(targetAbs);
|
|
191
|
+
if (typeof onHighlight === 'function' && items[targetAbs])
|
|
192
|
+
onHighlight(items[targetAbs]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (/^[1-9]$/.test(input)) {
|
|
196
|
+
const targetIndex = Number.parseInt(input, 10) - 1;
|
|
197
|
+
const visibleItems = hasLimit
|
|
198
|
+
? rotateArray(items, rotateIndex).slice(0, limit)
|
|
199
|
+
: items;
|
|
200
|
+
if (targetIndex >= 0 && targetIndex < visibleItems.length) {
|
|
201
|
+
const selectedItem = visibleItems[targetIndex];
|
|
202
|
+
if (selectedItem) {
|
|
203
|
+
onSelect?.(selectedItem);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (key.return) {
|
|
208
|
+
const slicedItems = hasLimit
|
|
209
|
+
? rotateArray(items, rotateIndex).slice(0, limit)
|
|
210
|
+
: items;
|
|
211
|
+
if (typeof onSelect === 'function') {
|
|
212
|
+
onSelect(slicedItems[selectedIndex]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, [hasLimit, limit, rotateIndex, selectedIndex, items, onSelect, onHighlight, headingPredicate]), { isActive: isFocused });
|
|
216
|
+
const slicedItems = hasLimit
|
|
217
|
+
? rotateArray(items, rotateIndex).slice(0, limit)
|
|
218
|
+
: items;
|
|
219
|
+
return (React.createElement(Box, { flexDirection: "column" }, slicedItems.map((item, index) => {
|
|
220
|
+
const isSelected = index === selectedIndex;
|
|
221
|
+
return (React.createElement(Box, { key: item.key ?? String(item.value) },
|
|
222
|
+
React.createElement(IndicatorComp, { isSelected: isSelected }),
|
|
223
|
+
React.createElement(ItemComp, { isSelected: isSelected, label: item.label })));
|
|
224
|
+
})));
|
|
225
|
+
}
|
|
@@ -2,8 +2,8 @@ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
|
|
2
2
|
import readline from 'node:readline';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import open from 'open';
|
|
5
|
-
import { Box, Text, useApp, useStdin } from 'ink';
|
|
6
|
-
import
|
|
5
|
+
import { Box, Text, useApp, useStdin, useInput } from 'ink';
|
|
6
|
+
import { PersistentSelectInput } from './PersistentSelectInput.js';
|
|
7
7
|
import { ProcessManager, ConfigManager } from '../services/index.js';
|
|
8
8
|
import { getFileNameFriendlyName, getProcessLogOutFilePath, getProcessLogErrorFilePath, getPomituSignalsDirectory } from '../helpers.js';
|
|
9
9
|
import chokidar from 'chokidar';
|
|
@@ -18,8 +18,15 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
18
18
|
const [messageColor, setMessageColor] = useState('green');
|
|
19
19
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
20
20
|
const [isReloading, setIsReloading] = useState(false);
|
|
21
|
-
const [
|
|
21
|
+
const [searchActive, setSearchActive] = useState(false);
|
|
22
22
|
const [searchQuery, setSearchQuery] = useState('');
|
|
23
|
+
// ESC via Ink's own pipeline — no readline disambiguation delay
|
|
24
|
+
useInput(useCallback((_input, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
setSearchActive(false);
|
|
27
|
+
setSearchQuery('');
|
|
28
|
+
}
|
|
29
|
+
}, []), { isActive: searchActive });
|
|
23
30
|
const previousRawModeRef = useRef(false);
|
|
24
31
|
const rawModeCapturedRef = useRef(false);
|
|
25
32
|
const appsRef = useRef([]);
|
|
@@ -148,6 +155,13 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
148
155
|
appsRef.current = apps;
|
|
149
156
|
apps.forEach(app => writeTuiPresence(app.name));
|
|
150
157
|
}, [apps]);
|
|
158
|
+
// Heartbeat: refresh TUI presence file mtimes so isTuiActive can detect stale files
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const interval = setInterval(() => {
|
|
161
|
+
appsRef.current.forEach(app => writeTuiPresence(app.name));
|
|
162
|
+
}, 10000);
|
|
163
|
+
return () => clearInterval(interval);
|
|
164
|
+
}, []);
|
|
151
165
|
// Set up chokidar watcher for IPC signals (mounted once; uses refs for latest state)
|
|
152
166
|
useEffect(() => {
|
|
153
167
|
const signalsDir = getPomituSignalsDirectory();
|
|
@@ -186,44 +200,39 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
186
200
|
setRawMode(true);
|
|
187
201
|
}
|
|
188
202
|
const handleKeypress = (str, key) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
203
|
+
if (key?.ctrl && key.name === 'c') {
|
|
204
|
+
cleanExit();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (searchActive) {
|
|
196
208
|
if (key?.name === 'backspace') {
|
|
197
209
|
setSearchQuery(prev => prev.slice(0, -1));
|
|
198
210
|
return;
|
|
199
211
|
}
|
|
200
|
-
if (key?.name
|
|
201
|
-
setSearchMode(false);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
212
|
+
if (key?.name !== 'return' && str && str.length === 1 && !key?.ctrl && !key?.meta) {
|
|
205
213
|
setSearchQuery(prev => prev + str);
|
|
206
|
-
return;
|
|
207
214
|
}
|
|
215
|
+
// ESC is handled by useInput (Ink pipeline); arrow keys, Enter, and other
|
|
216
|
+
// navigation are handled by PersistentSelectInput via Ink's input pipeline
|
|
208
217
|
return;
|
|
209
218
|
}
|
|
210
|
-
// Normal mode
|
|
211
219
|
if (str === 'q') {
|
|
212
220
|
cleanExit();
|
|
213
221
|
}
|
|
214
222
|
if (str === 'r') {
|
|
215
223
|
reloadConfig();
|
|
216
224
|
}
|
|
217
|
-
if (str === '
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
if (str === 'e') {
|
|
226
|
+
open(configPath).catch(error => {
|
|
227
|
+
setMessage(`Failed to open config: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
+
setMessageColor('red');
|
|
229
|
+
setTimeout(() => setMessage(''), 3000);
|
|
230
|
+
});
|
|
220
231
|
}
|
|
221
|
-
if (
|
|
232
|
+
if (str === '/') {
|
|
233
|
+
setSearchActive(true);
|
|
222
234
|
setSearchQuery('');
|
|
223
235
|
}
|
|
224
|
-
if (key?.ctrl && key.name === 'c') {
|
|
225
|
-
cleanExit();
|
|
226
|
-
}
|
|
227
236
|
};
|
|
228
237
|
// Use prependListener to capture keys before other handlers
|
|
229
238
|
stdin.prependListener('keypress', handleKeypress);
|
|
@@ -233,7 +242,7 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
233
242
|
setRawMode(previousRawModeRef.current);
|
|
234
243
|
}
|
|
235
244
|
};
|
|
236
|
-
}, [stdin, setRawMode, cleanExit, reloadConfig,
|
|
245
|
+
}, [stdin, setRawMode, cleanExit, reloadConfig, searchActive]);
|
|
237
246
|
// Handle Ctrl+C signal directly - use prependListener to be first
|
|
238
247
|
useEffect(() => {
|
|
239
248
|
const handleSigInt = () => {
|
|
@@ -305,6 +314,20 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
305
314
|
setIsProcessing(false);
|
|
306
315
|
return;
|
|
307
316
|
}
|
|
317
|
+
else if (action === 'opencwd') {
|
|
318
|
+
try {
|
|
319
|
+
await open(app.cwd);
|
|
320
|
+
setMessage('Opening working directory...');
|
|
321
|
+
setMessageColor('green');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
setMessage(`Failed to open directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
setMessageColor('red');
|
|
326
|
+
}
|
|
327
|
+
setTimeout(() => setMessage(''), 3000);
|
|
328
|
+
setIsProcessing(false);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
308
331
|
setProcesses(computeStatuses());
|
|
309
332
|
}
|
|
310
333
|
catch (error) {
|
|
@@ -318,9 +341,9 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
318
341
|
// Clear message after 3 seconds
|
|
319
342
|
setTimeout(() => setMessage(''), 3000);
|
|
320
343
|
}, [apps, clearLogs, computeStatuses, processManager, isProcessing, isReloading]);
|
|
321
|
-
const
|
|
344
|
+
const buildMenuItems = useCallback((statuses) => {
|
|
322
345
|
const items = [];
|
|
323
|
-
|
|
346
|
+
statuses.forEach(proc => {
|
|
324
347
|
const statusLabel = proc.isRunning
|
|
325
348
|
? `🟢 Running (PID: ${proc.pid})`
|
|
326
349
|
: '🔴 Stopped';
|
|
@@ -342,9 +365,13 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
342
365
|
value: `viewout:${proc.name}`
|
|
343
366
|
});
|
|
344
367
|
items.push({
|
|
345
|
-
label: '
|
|
368
|
+
label: ' ├─ View Error Log',
|
|
346
369
|
value: `viewerr:${proc.name}`
|
|
347
370
|
});
|
|
371
|
+
items.push({
|
|
372
|
+
label: ' └─ Open Working Directory',
|
|
373
|
+
value: `opencwd:${proc.name}`
|
|
374
|
+
});
|
|
348
375
|
}
|
|
349
376
|
else {
|
|
350
377
|
items.push({
|
|
@@ -356,21 +383,25 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
356
383
|
value: `viewout:${proc.name}`
|
|
357
384
|
});
|
|
358
385
|
items.push({
|
|
359
|
-
label: '
|
|
386
|
+
label: ' ├─ View Error Log',
|
|
360
387
|
value: `viewerr:${proc.name}`
|
|
361
388
|
});
|
|
389
|
+
items.push({
|
|
390
|
+
label: ' └─ Open Working Directory',
|
|
391
|
+
value: `opencwd:${proc.name}`
|
|
392
|
+
});
|
|
362
393
|
}
|
|
363
394
|
});
|
|
364
395
|
return items;
|
|
365
|
-
}, [
|
|
396
|
+
}, []);
|
|
397
|
+
const allItems = useMemo(() => buildMenuItems(processes), [buildMenuItems, processes]);
|
|
366
398
|
const items = useMemo(() => {
|
|
367
|
-
const allItems = getMenuItems();
|
|
368
399
|
if (!searchQuery)
|
|
369
400
|
return allItems;
|
|
370
401
|
const query = searchQuery.toLowerCase();
|
|
371
402
|
return allItems.filter(item => item.label.toLowerCase().includes(query) ||
|
|
372
403
|
item.value.toLowerCase().includes(query));
|
|
373
|
-
}, [
|
|
404
|
+
}, [allItems, searchQuery]);
|
|
374
405
|
const handleMenuSelect = useCallback((item) => {
|
|
375
406
|
if (item.value === 'separator' || item.value.startsWith('info:')) {
|
|
376
407
|
// Informational rows are read-only
|
|
@@ -392,23 +423,25 @@ export function ProcessTUI({ configPath, clearLogs }) {
|
|
|
392
423
|
React.createElement(Text, { bold: true, color: "cyan" }, "Pomitu Process Manager - Interactive Mode")),
|
|
393
424
|
processes.length > 0 ? (React.createElement(React.Fragment, null,
|
|
394
425
|
React.createElement(Box, { marginBottom: 1 },
|
|
395
|
-
React.createElement(Text, { dimColor: true }, "Use arrow keys to navigate, Enter to select, '/' to search, 'r' to reload, 'q' or Ctrl+C to quit")),
|
|
396
|
-
|
|
426
|
+
React.createElement(Text, { dimColor: true }, "Use arrow keys to navigate, Enter to select, '/' to search, 'r' to reload, 'e' to edit config, 'q' or Ctrl+C to quit")),
|
|
427
|
+
searchActive && (React.createElement(Box, { marginBottom: 1 },
|
|
397
428
|
React.createElement(Text, { color: "yellow" },
|
|
398
429
|
"Search: ",
|
|
399
430
|
searchQuery),
|
|
400
|
-
React.createElement(Text, { dimColor: true }, " (ESC to
|
|
401
|
-
searchQuery &&
|
|
402
|
-
React.createElement(Text, {
|
|
403
|
-
"
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
431
|
+
React.createElement(Text, { dimColor: true }, " (ESC to clear)"))),
|
|
432
|
+
searchActive && searchQuery && items.length < allItems.length && (React.createElement(Box, { marginBottom: 1 },
|
|
433
|
+
React.createElement(Text, { dimColor: true },
|
|
434
|
+
"Showing ",
|
|
435
|
+
items.length,
|
|
436
|
+
" of ",
|
|
437
|
+
allItems.length,
|
|
438
|
+
" items"))),
|
|
439
|
+
!searchActive && items.length > 15 && (React.createElement(Box, { marginBottom: 1 },
|
|
407
440
|
React.createElement(Text, { dimColor: true },
|
|
408
441
|
"Showing 15 of ",
|
|
409
442
|
items.length,
|
|
410
443
|
" items - scroll with \u2191\u2193 arrows"))),
|
|
411
|
-
React.createElement(
|
|
444
|
+
React.createElement(PersistentSelectInput, { items: items, onSelect: handleMenuSelect, isFocused: !isProcessing && !isReloading, limit: 15, headingPredicate: item => item.value.startsWith('info:') }))) : (React.createElement(Text, null, "Loading processes...")),
|
|
412
445
|
React.createElement(Box, { marginTop: 1 },
|
|
413
446
|
React.createElement(Text, { color: notificationColor ?? 'gray' }, notificationText ?? ' '))));
|
|
414
447
|
}
|
|
@@ -13,14 +13,19 @@ export function clearTuiPresence(appName) {
|
|
|
13
13
|
fs.unlinkSync(tuiPidPath);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
const TUI_HEARTBEAT_MAX_AGE_MS = 30000;
|
|
16
17
|
export function isTuiActive(appName) {
|
|
17
18
|
const tuiPidPath = getTuiPidPath(getFileNameFriendlyName(appName));
|
|
18
19
|
if (!fs.existsSync(tuiPidPath)) {
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
21
22
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const stat = fs.statSync(tuiPidPath);
|
|
24
|
+
if (Date.now() - stat.mtimeMs > TUI_HEARTBEAT_MAX_AGE_MS) {
|
|
25
|
+
fs.unlinkSync(tuiPidPath);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
24
29
|
}
|
|
25
30
|
catch {
|
|
26
31
|
return false;
|