panex 0.9.4 → 0.9.6
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 +1 -1
- package/dist/cli.js +175 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -2
- package/dist/index.js +175 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,50 @@ interface PanexConfig {
|
|
|
18
18
|
|
|
19
19
|
declare function createTUI(config: PanexConfig): Promise<void>;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A terminal buffer that properly interprets ANSI escape sequences including:
|
|
23
|
+
* - Cursor movement (\x1b[A up, \x1b[B down, \x1b[C right, \x1b[D left)
|
|
24
|
+
* - Cursor positioning (\x1b[H, \x1b[row;colH)
|
|
25
|
+
* - Line clearing (\x1b[K erase to end, \x1b[2K erase line)
|
|
26
|
+
* - Screen clearing (\x1b[2J clear screen)
|
|
27
|
+
* - Carriage return (\r) for in-place updates like progress bars
|
|
28
|
+
* - ANSI colors and formatting (preserved in output via SerializeAddon)
|
|
29
|
+
*/
|
|
30
|
+
declare class TerminalBuffer {
|
|
31
|
+
private terminal;
|
|
32
|
+
private serializeAddon;
|
|
33
|
+
private rows;
|
|
34
|
+
private cols;
|
|
35
|
+
constructor(cols?: number, rows?: number);
|
|
36
|
+
/**
|
|
37
|
+
* Write data to the terminal buffer.
|
|
38
|
+
* The terminal will interpret all ANSI escape sequences.
|
|
39
|
+
*/
|
|
40
|
+
write(data: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get the current terminal buffer content as an array of lines.
|
|
43
|
+
* Only returns lines that have content (not all 500 rows).
|
|
44
|
+
*/
|
|
45
|
+
getLines(): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Get the terminal content as a single string with newlines.
|
|
48
|
+
* Preserves ANSI color codes and formatting.
|
|
49
|
+
*/
|
|
50
|
+
toString(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Clear the terminal buffer.
|
|
53
|
+
*/
|
|
54
|
+
clear(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Resize the terminal.
|
|
57
|
+
*/
|
|
58
|
+
resize(cols: number, rows: number): void;
|
|
59
|
+
/**
|
|
60
|
+
* Dispose of the terminal to free resources.
|
|
61
|
+
*/
|
|
62
|
+
dispose(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
21
65
|
interface PtyHandle {
|
|
22
66
|
write(data: string): void;
|
|
23
67
|
resize(cols: number, rows: number): void;
|
|
@@ -28,13 +72,12 @@ interface ManagedProcess {
|
|
|
28
72
|
config: ProcessConfig;
|
|
29
73
|
pty: PtyHandle | null;
|
|
30
74
|
status: 'running' | 'stopped' | 'error';
|
|
31
|
-
|
|
75
|
+
terminalBuffer: TerminalBuffer;
|
|
32
76
|
exitCode: number | null;
|
|
33
77
|
}
|
|
34
78
|
declare class ProcessManager extends EventEmitter {
|
|
35
79
|
private procs;
|
|
36
80
|
private processes;
|
|
37
|
-
private maxOutputLines;
|
|
38
81
|
constructor(procs: Record<string, ProcessConfig>);
|
|
39
82
|
startAll(): Promise<void>;
|
|
40
83
|
start(name: string, config: ProcessConfig): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -11,13 +11,89 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|
|
11
11
|
|
|
12
12
|
// src/process-manager.ts
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
|
+
|
|
15
|
+
// src/terminal-buffer.ts
|
|
16
|
+
import { Terminal } from "@xterm/headless";
|
|
17
|
+
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
18
|
+
var TerminalBuffer = class {
|
|
19
|
+
terminal;
|
|
20
|
+
serializeAddon;
|
|
21
|
+
rows;
|
|
22
|
+
cols;
|
|
23
|
+
constructor(cols = 200, rows = 500) {
|
|
24
|
+
this.rows = rows;
|
|
25
|
+
this.cols = cols;
|
|
26
|
+
this.terminal = new Terminal({
|
|
27
|
+
cols,
|
|
28
|
+
rows,
|
|
29
|
+
scrollback: 1e4,
|
|
30
|
+
allowProposedApi: true
|
|
31
|
+
});
|
|
32
|
+
this.serializeAddon = new SerializeAddon();
|
|
33
|
+
this.terminal.loadAddon(this.serializeAddon);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Write data to the terminal buffer.
|
|
37
|
+
* The terminal will interpret all ANSI escape sequences.
|
|
38
|
+
*/
|
|
39
|
+
write(data) {
|
|
40
|
+
this.terminal.write(data);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the current terminal buffer content as an array of lines.
|
|
44
|
+
* Only returns lines that have content (not all 500 rows).
|
|
45
|
+
*/
|
|
46
|
+
getLines() {
|
|
47
|
+
const buffer = this.terminal.buffer.active;
|
|
48
|
+
const lines = [];
|
|
49
|
+
const contentLength = buffer.baseY + buffer.cursorY + 1;
|
|
50
|
+
for (let i = 0; i < contentLength; i++) {
|
|
51
|
+
const line = buffer.getLine(i);
|
|
52
|
+
if (line) {
|
|
53
|
+
lines.push(line.translateToString(true));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
57
|
+
lines.pop();
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the terminal content as a single string with newlines.
|
|
63
|
+
* Preserves ANSI color codes and formatting.
|
|
64
|
+
*/
|
|
65
|
+
toString() {
|
|
66
|
+
return this.serializeAddon.serialize();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Clear the terminal buffer.
|
|
70
|
+
*/
|
|
71
|
+
clear() {
|
|
72
|
+
this.terminal.reset();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resize the terminal.
|
|
76
|
+
*/
|
|
77
|
+
resize(cols, rows) {
|
|
78
|
+
this.cols = cols;
|
|
79
|
+
this.rows = rows;
|
|
80
|
+
this.terminal.resize(cols, rows);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Dispose of the terminal to free resources.
|
|
84
|
+
*/
|
|
85
|
+
dispose() {
|
|
86
|
+
this.terminal.dispose();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/process-manager.ts
|
|
14
91
|
var ProcessManager = class extends EventEmitter {
|
|
15
92
|
constructor(procs) {
|
|
16
93
|
super();
|
|
17
94
|
this.procs = procs;
|
|
18
95
|
}
|
|
19
96
|
processes = /* @__PURE__ */ new Map();
|
|
20
|
-
maxOutputLines = 1e4;
|
|
21
97
|
async startAll() {
|
|
22
98
|
for (const [name, config] of Object.entries(this.procs)) {
|
|
23
99
|
await this.start(name, config);
|
|
@@ -37,7 +113,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
37
113
|
config,
|
|
38
114
|
pty: null,
|
|
39
115
|
status: "running",
|
|
40
|
-
|
|
116
|
+
terminalBuffer: new TerminalBuffer(120, 30),
|
|
41
117
|
exitCode: null
|
|
42
118
|
};
|
|
43
119
|
this.processes.set(name, managed);
|
|
@@ -50,10 +126,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
50
126
|
rows: 30,
|
|
51
127
|
data: (_terminal, data) => {
|
|
52
128
|
const str = new TextDecoder().decode(data);
|
|
53
|
-
managed.
|
|
54
|
-
if (managed.output.length > this.maxOutputLines) {
|
|
55
|
-
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
56
|
-
}
|
|
129
|
+
managed.terminalBuffer.write(str);
|
|
57
130
|
this.emit("output", name, str);
|
|
58
131
|
}
|
|
59
132
|
}
|
|
@@ -75,7 +148,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
75
148
|
this.emit("started", name);
|
|
76
149
|
} catch (error) {
|
|
77
150
|
managed.status = "error";
|
|
78
|
-
managed.
|
|
151
|
+
managed.terminalBuffer.write(`Error starting process: ${error}`);
|
|
79
152
|
managed.exitCode = -1;
|
|
80
153
|
this.emit("error", name, error);
|
|
81
154
|
}
|
|
@@ -86,7 +159,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
86
159
|
if (proc.pty) {
|
|
87
160
|
proc.pty.kill();
|
|
88
161
|
}
|
|
89
|
-
proc.
|
|
162
|
+
proc.terminalBuffer.clear();
|
|
90
163
|
this.start(name, proc.config);
|
|
91
164
|
}
|
|
92
165
|
}
|
|
@@ -130,7 +203,7 @@ var ProcessManager = class extends EventEmitter {
|
|
|
130
203
|
return Array.from(this.processes.keys());
|
|
131
204
|
}
|
|
132
205
|
getOutput(name) {
|
|
133
|
-
return this.processes.get(name)?.
|
|
206
|
+
return this.processes.get(name)?.terminalBuffer.toString() ?? "";
|
|
134
207
|
}
|
|
135
208
|
};
|
|
136
209
|
|
|
@@ -193,7 +266,7 @@ function useFocusMode() {
|
|
|
193
266
|
// src/hooks/useMouseWheel.ts
|
|
194
267
|
import { useEffect as useEffect2, useCallback as useCallback3 } from "react";
|
|
195
268
|
import { useStdin, useStdout } from "ink";
|
|
196
|
-
function useMouseWheel({ enabled = true, onWheel } = {}) {
|
|
269
|
+
function useMouseWheel({ enabled = true, onWheel, onClick } = {}) {
|
|
197
270
|
const { stdin, setRawMode } = useStdin();
|
|
198
271
|
const { stdout } = useStdout();
|
|
199
272
|
const handleData = useCallback3((data) => {
|
|
@@ -206,14 +279,16 @@ function useMouseWheel({ enabled = true, onWheel } = {}) {
|
|
|
206
279
|
const y = parseInt(match[3] ?? "0", 10);
|
|
207
280
|
const isPress = match[4] === "M";
|
|
208
281
|
if (isPress) {
|
|
209
|
-
if (button ===
|
|
282
|
+
if (button === 0) {
|
|
283
|
+
onClick?.({ type: "click", x, y });
|
|
284
|
+
} else if (button === 64) {
|
|
210
285
|
onWheel?.({ type: "wheel-up", x, y });
|
|
211
286
|
} else if (button === 65) {
|
|
212
287
|
onWheel?.({ type: "wheel-down", x, y });
|
|
213
288
|
}
|
|
214
289
|
}
|
|
215
290
|
}
|
|
216
|
-
}, [onWheel]);
|
|
291
|
+
}, [onWheel, onClick]);
|
|
217
292
|
useEffect2(() => {
|
|
218
293
|
if (!enabled || !stdin || !stdout) return;
|
|
219
294
|
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
@@ -231,6 +306,7 @@ import { Box, Text, useStdout as useStdout2 } from "ink";
|
|
|
231
306
|
import { ScrollList } from "ink-scroll-list";
|
|
232
307
|
import { forwardRef, useImperativeHandle, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
233
308
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
309
|
+
var PROCESS_LIST_WIDTH = 20;
|
|
234
310
|
var ProcessList = forwardRef(
|
|
235
311
|
function ProcessList2({ names, selected, getStatus, active, height }, ref) {
|
|
236
312
|
const borderStyle = active ? "double" : "single";
|
|
@@ -254,7 +330,7 @@ var ProcessList = forwardRef(
|
|
|
254
330
|
flexDirection: "column",
|
|
255
331
|
borderStyle,
|
|
256
332
|
borderColor: active ? "blue" : "gray",
|
|
257
|
-
width:
|
|
333
|
+
width: PROCESS_LIST_WIDTH,
|
|
258
334
|
height,
|
|
259
335
|
paddingX: 1,
|
|
260
336
|
children: /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 0, height: height ? height - 2 : void 0, children: /* @__PURE__ */ jsx(
|
|
@@ -273,7 +349,7 @@ var ProcessList = forwardRef(
|
|
|
273
349
|
name,
|
|
274
350
|
" "
|
|
275
351
|
] }),
|
|
276
|
-
/* @__PURE__ */ jsx(Text, { color:
|
|
352
|
+
/* @__PURE__ */ jsx(Text, { color: statusColor, children: statusIcon })
|
|
277
353
|
] }, name);
|
|
278
354
|
})
|
|
279
355
|
}
|
|
@@ -328,6 +404,13 @@ var OutputPanel = forwardRef2(
|
|
|
328
404
|
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
329
405
|
const [contentHeight, setContentHeight] = useState3(0);
|
|
330
406
|
const [viewportHeight, setViewportHeight] = useState3(0);
|
|
407
|
+
const [prevName, setPrevName] = useState3(name);
|
|
408
|
+
if (name !== prevName) {
|
|
409
|
+
setPrevName(name);
|
|
410
|
+
setScrollOffset(0);
|
|
411
|
+
setContentHeight(0);
|
|
412
|
+
setViewportHeight(0);
|
|
413
|
+
}
|
|
331
414
|
useEffect4(() => {
|
|
332
415
|
const handleResize = () => scrollRef.current?.remeasure();
|
|
333
416
|
stdout?.on("resize", handleResize);
|
|
@@ -391,7 +474,8 @@ var OutputPanel = forwardRef2(
|
|
|
391
474
|
onContentHeightChange: handleContentHeightChange,
|
|
392
475
|
onViewportSizeChange: (layout) => setViewportHeight(layout.height),
|
|
393
476
|
children: lines.map((line, i) => /* @__PURE__ */ jsx3(Text3, { wrap: "truncate", children: line }, i))
|
|
394
|
-
}
|
|
477
|
+
},
|
|
478
|
+
name
|
|
395
479
|
) }),
|
|
396
480
|
hasScroll && /* @__PURE__ */ jsx3(
|
|
397
481
|
Scrollbar,
|
|
@@ -416,9 +500,8 @@ function StatusBar({ focusMode, processName, showShiftTabHint = true }) {
|
|
|
416
500
|
const shiftTabHint = showShiftTabHint ? "Shift-Tab/" : "";
|
|
417
501
|
return /* @__PURE__ */ jsx4(Box4, { backgroundColor: "green", width: "100%", children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "black", backgroundColor: "green", children: [
|
|
418
502
|
" ",
|
|
419
|
-
"FOCUS: ",
|
|
420
503
|
processName,
|
|
421
|
-
"
|
|
504
|
+
" | [",
|
|
422
505
|
shiftTabHint,
|
|
423
506
|
"Esc] to exit focus mode",
|
|
424
507
|
" "
|
|
@@ -496,6 +579,8 @@ function App({ config }) {
|
|
|
496
579
|
const outputRef = useRef4(null);
|
|
497
580
|
const processListRef = useRef4(null);
|
|
498
581
|
const [autoScroll, setAutoScroll] = useState4({});
|
|
582
|
+
const scrollPositionsRef = useRef4({});
|
|
583
|
+
const pendingRestoreRef = useRef4(null);
|
|
499
584
|
const {
|
|
500
585
|
names,
|
|
501
586
|
getOutput,
|
|
@@ -538,6 +623,42 @@ function App({ config }) {
|
|
|
538
623
|
const selectedName = names[selected] ?? "";
|
|
539
624
|
const output = selectedName ? getOutput(selectedName) : "";
|
|
540
625
|
const currentAutoScroll = selectedName ? autoScroll[selectedName] ?? true : true;
|
|
626
|
+
const saveCurrentScrollPosition = useCallback5(() => {
|
|
627
|
+
if (selectedName && outputRef.current) {
|
|
628
|
+
scrollPositionsRef.current[selectedName] = outputRef.current.getScrollOffset();
|
|
629
|
+
}
|
|
630
|
+
}, [selectedName]);
|
|
631
|
+
const handleSetSelected = useCallback5((newSelected) => {
|
|
632
|
+
saveCurrentScrollPosition();
|
|
633
|
+
setSelected((prev) => {
|
|
634
|
+
const next = typeof newSelected === "function" ? newSelected(prev) : newSelected;
|
|
635
|
+
if (next !== prev) {
|
|
636
|
+
pendingRestoreRef.current = names[next] ?? null;
|
|
637
|
+
}
|
|
638
|
+
return next;
|
|
639
|
+
});
|
|
640
|
+
}, [saveCurrentScrollPosition, names]);
|
|
641
|
+
useEffect5(() => {
|
|
642
|
+
const restoreName = pendingRestoreRef.current;
|
|
643
|
+
if (restoreName && restoreName === selectedName) {
|
|
644
|
+
pendingRestoreRef.current = null;
|
|
645
|
+
const savedOffset = scrollPositionsRef.current[restoreName];
|
|
646
|
+
if (savedOffset !== void 0 && savedOffset > 0 && !autoScroll[restoreName]) {
|
|
647
|
+
setImmediate(() => {
|
|
648
|
+
if (outputRef.current) {
|
|
649
|
+
const contentHeight = outputRef.current.getContentHeight();
|
|
650
|
+
const viewportHeight = outputRef.current.getViewportHeight();
|
|
651
|
+
if (contentHeight > viewportHeight) {
|
|
652
|
+
const currentOffset = outputRef.current.getScrollOffset();
|
|
653
|
+
if (currentOffset !== savedOffset) {
|
|
654
|
+
outputRef.current.scrollBy(savedOffset - currentOffset);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}, [selectedName, autoScroll]);
|
|
541
662
|
const handleAutoScrollChange = useCallback5((enabled) => {
|
|
542
663
|
if (selectedName) {
|
|
543
664
|
setAutoScroll((prev) => ({ ...prev, [selectedName]: enabled }));
|
|
@@ -552,17 +673,33 @@ function App({ config }) {
|
|
|
552
673
|
}
|
|
553
674
|
}
|
|
554
675
|
}, [selectedName]);
|
|
676
|
+
const handleClick = useCallback5((event) => {
|
|
677
|
+
if (event.x <= PROCESS_LIST_WIDTH) {
|
|
678
|
+
if (focusMode) {
|
|
679
|
+
exitFocus();
|
|
680
|
+
}
|
|
681
|
+
const clickedIndex = event.y - 2;
|
|
682
|
+
if (clickedIndex >= 0 && clickedIndex < names.length) {
|
|
683
|
+
handleSetSelected(clickedIndex);
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
if (!focusMode) {
|
|
687
|
+
enterFocus();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}, [names.length, focusMode, enterFocus, exitFocus, handleSetSelected]);
|
|
555
691
|
useMouseWheel({
|
|
556
692
|
enabled: !showHelp,
|
|
557
693
|
// Disable when help is shown
|
|
558
|
-
onWheel: handleWheel
|
|
694
|
+
onWheel: handleWheel,
|
|
695
|
+
onClick: handleClick
|
|
559
696
|
});
|
|
560
697
|
useInput((input, key) => {
|
|
561
698
|
if (showHelp) {
|
|
562
699
|
setShowHelp(false);
|
|
563
700
|
return;
|
|
564
701
|
}
|
|
565
|
-
if (
|
|
702
|
+
if (key.ctrl && input === "c") {
|
|
566
703
|
killAll();
|
|
567
704
|
setRawMode(false);
|
|
568
705
|
const rows = stdout?.rows ?? 999;
|
|
@@ -571,10 +708,6 @@ function App({ config }) {
|
|
|
571
708
|
exit();
|
|
572
709
|
process.exit(0);
|
|
573
710
|
}
|
|
574
|
-
if (input === "?") {
|
|
575
|
-
setShowHelp(true);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
711
|
if (focusMode) {
|
|
579
712
|
const name = names[selected];
|
|
580
713
|
if (!name) return;
|
|
@@ -607,16 +740,32 @@ function App({ config }) {
|
|
|
607
740
|
return;
|
|
608
741
|
}
|
|
609
742
|
if (input && !key.ctrl && !key.meta) {
|
|
610
|
-
|
|
743
|
+
const filtered = input.replace(/\x1b?\[<\d+;\d+;\d+[Mm]/g, "");
|
|
744
|
+
if (filtered) {
|
|
745
|
+
write(name, filtered);
|
|
746
|
+
}
|
|
611
747
|
}
|
|
612
748
|
return;
|
|
613
749
|
}
|
|
750
|
+
if (input === "q") {
|
|
751
|
+
killAll();
|
|
752
|
+
setRawMode(false);
|
|
753
|
+
const rows = stdout?.rows ?? 999;
|
|
754
|
+
stdout?.write(`\x1B[${rows};1H\x1B[J\x1B[?1000l\x1B[?1006l\x1B[?25h\x1B[0m
|
|
755
|
+
`);
|
|
756
|
+
exit();
|
|
757
|
+
process.exit(0);
|
|
758
|
+
}
|
|
759
|
+
if (input === "?") {
|
|
760
|
+
setShowHelp(true);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
614
763
|
if (key.upArrow || input === "k") {
|
|
615
|
-
|
|
764
|
+
handleSetSelected((s) => Math.max(s - 1, 0));
|
|
616
765
|
return;
|
|
617
766
|
}
|
|
618
767
|
if (key.downArrow || input === "j") {
|
|
619
|
-
|
|
768
|
+
handleSetSelected((s) => Math.min(s + 1, names.length - 1));
|
|
620
769
|
return;
|
|
621
770
|
}
|
|
622
771
|
if (key.return || key.tab) {
|