@code-yeongyu/senpi-tui 2026.7.31 → 2026.8.3
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 +77 -14
- package/dist/TuiAltScreen.d.ts +83 -0
- package/dist/TuiAltScreen.d.ts.map +1 -0
- package/dist/TuiAltScreen.js +687 -0
- package/dist/TuiAltScreen.js.map +1 -0
- package/dist/TuiMainScreen.d.ts +5 -0
- package/dist/TuiMainScreen.d.ts.map +1 -0
- package/dist/TuiMainScreen.js +5 -0
- package/dist/TuiMainScreen.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +36 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +4 -4
- package/dist/components/image.js.map +1 -1
- package/dist/components/markdown.d.ts +2 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +3 -2
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/scroll-view.d.ts +51 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +163 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +3 -6
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +109 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +21 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +30 -0
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +18 -0
- package/dist/keybindings.js.map +1 -1
- package/dist/layout-node.d.ts +40 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +315 -0
- package/dist/layout.js.map +1 -0
- package/dist/mux.js +1 -1
- package/dist/mux.js.map +1 -1
- package/dist/terminal-image.d.ts +10 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +61 -3
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal-text.d.ts +1 -0
- package/dist/terminal-text.d.ts.map +1 -1
- package/dist/terminal-text.js +12 -2
- package/dist/terminal-text.js.map +1 -1
- package/dist/tui.d.ts +37 -16
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +89 -21
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +95 -4
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
package/dist/tui.js
CHANGED
|
@@ -22,20 +22,17 @@ function parseKittyImageHeader(line) {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
const ids = [];
|
|
24
24
|
let rows = 1;
|
|
25
|
-
const
|
|
26
|
-
for (const param of params.split(",")) {
|
|
25
|
+
for (const param of line.slice(paramsStart, paramsEnd).split(",")) {
|
|
27
26
|
const [key, value] = param.split("=", 2);
|
|
28
27
|
if (value === undefined)
|
|
29
28
|
continue;
|
|
30
29
|
const numberValue = Number(value);
|
|
31
30
|
if (!Number.isInteger(numberValue) || numberValue <= 0 || numberValue > 0xffffffff)
|
|
32
31
|
continue;
|
|
33
|
-
if (key === "i")
|
|
32
|
+
if (key === "i")
|
|
34
33
|
ids.push(numberValue);
|
|
35
|
-
|
|
36
|
-
else if (key === "r") {
|
|
34
|
+
else if (key === "r")
|
|
37
35
|
rows = numberValue;
|
|
38
|
-
}
|
|
39
36
|
}
|
|
40
37
|
return { ids, rows };
|
|
41
38
|
}
|
|
@@ -45,6 +42,9 @@ function extractKittyImageIds(line) {
|
|
|
45
42
|
function extractKittyImageRows(line) {
|
|
46
43
|
return parseKittyImageHeader(line)?.rows ?? 1;
|
|
47
44
|
}
|
|
45
|
+
function isTermuxSession() {
|
|
46
|
+
return Boolean(process.env.TERMUX_VERSION);
|
|
47
|
+
}
|
|
48
48
|
/** Type guard to check if a component implements Focusable */
|
|
49
49
|
export function isFocusable(component) {
|
|
50
50
|
return component !== null && "focused" in component;
|
|
@@ -186,9 +186,6 @@ function parseSizeValue(value, referenceSize) {
|
|
|
186
186
|
}
|
|
187
187
|
return undefined;
|
|
188
188
|
}
|
|
189
|
-
function isTermuxSession() {
|
|
190
|
-
return Boolean(process.env.TERMUX_VERSION);
|
|
191
|
-
}
|
|
192
189
|
/**
|
|
193
190
|
* Container - a component that contains other components
|
|
194
191
|
*/
|
|
@@ -258,15 +255,51 @@ export class Container {
|
|
|
258
255
|
/**
|
|
259
256
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
260
257
|
*/
|
|
261
|
-
|
|
258
|
+
const SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";
|
|
259
|
+
/** Composite overlay content into a terminal line at a fixed column. */
|
|
260
|
+
export function compositeTuiLine(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
|
|
261
|
+
if (isImageLine(baseLine) && visibleWidth(baseLine) === 0)
|
|
262
|
+
return baseLine;
|
|
263
|
+
const placeholderIndex = baseLine.indexOf("\u{10eeee}");
|
|
264
|
+
const protocolEnd = placeholderIndex === -1 ? -1 : baseLine.lastIndexOf("\x1b\\", placeholderIndex);
|
|
265
|
+
const protocolPrefix = protocolEnd === -1 ? "" : baseLine.slice(0, protocolEnd + 2);
|
|
266
|
+
const afterStart = startCol + overlayWidth;
|
|
267
|
+
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
268
|
+
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
269
|
+
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
270
|
+
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
271
|
+
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
272
|
+
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
273
|
+
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
274
|
+
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
275
|
+
const result = base.before +
|
|
276
|
+
" ".repeat(beforePad) +
|
|
277
|
+
SEGMENT_RESET +
|
|
278
|
+
overlay.text +
|
|
279
|
+
" ".repeat(overlayPad) +
|
|
280
|
+
SEGMENT_RESET +
|
|
281
|
+
base.after +
|
|
282
|
+
" ".repeat(afterPad);
|
|
283
|
+
const composited = visibleWidth(result) <= totalWidth ? result : sliceByColumn(result, 0, totalWidth, true);
|
|
284
|
+
return protocolPrefix + composited;
|
|
285
|
+
}
|
|
286
|
+
export const VIEWPORT_TUI = Symbol.for("@earendil-works/pi-tui/viewport");
|
|
287
|
+
export function isViewportTUI(tui) {
|
|
288
|
+
return tui[VIEWPORT_TUI] === true;
|
|
289
|
+
}
|
|
290
|
+
export class TuiBase extends Container {
|
|
262
291
|
/** Minimum interval between scheduled renders. Default preserves the historic ~60fps cap. */
|
|
263
292
|
#minRenderIntervalMs;
|
|
264
293
|
#lastCursorVisibility;
|
|
294
|
+
get hasOverlayEntries() {
|
|
295
|
+
return this.overlayStack.length > 0;
|
|
296
|
+
}
|
|
265
297
|
#muxDetector;
|
|
266
|
-
constructor(terminal, options) {
|
|
298
|
+
constructor(terminal, options, logDirectory) {
|
|
267
299
|
super();
|
|
268
300
|
this.previousLines = [];
|
|
269
301
|
this.previousRawLines = [];
|
|
302
|
+
this.normalizeMemo = new Map();
|
|
270
303
|
this.previousKittyImageIds = new Set();
|
|
271
304
|
this.previousWidth = 0;
|
|
272
305
|
this.previousHeight = 0;
|
|
@@ -299,10 +332,16 @@ export class TUI extends Container {
|
|
|
299
332
|
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
300
333
|
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
301
334
|
this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
|
|
335
|
+
this.logDirectory = logDirectory ?? path.join(os.homedir(), ".senpi", "agent");
|
|
302
336
|
if (normalizedOptions.showHardwareCursor !== undefined) {
|
|
303
337
|
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
304
338
|
}
|
|
305
339
|
}
|
|
340
|
+
resetRenderState() { }
|
|
341
|
+
beforeTerminalStart() { }
|
|
342
|
+
afterTerminalStart() { }
|
|
343
|
+
beforeTerminalStop() { }
|
|
344
|
+
afterTerminalStop() { }
|
|
306
345
|
get fullRedraws() {
|
|
307
346
|
return this.fullRedrawCount;
|
|
308
347
|
}
|
|
@@ -432,8 +471,11 @@ export class TUI extends Container {
|
|
|
432
471
|
}
|
|
433
472
|
}
|
|
434
473
|
}
|
|
474
|
+
getMountedRoots() {
|
|
475
|
+
return this.children;
|
|
476
|
+
}
|
|
435
477
|
isComponentMounted(component) {
|
|
436
|
-
return this.
|
|
478
|
+
return this.getMountedRoots().some((child) => this.containsComponent(child, component));
|
|
437
479
|
}
|
|
438
480
|
containsComponent(root, target) {
|
|
439
481
|
if (root === target)
|
|
@@ -595,7 +637,9 @@ export class TUI extends Container {
|
|
|
595
637
|
this.renderRequested = false;
|
|
596
638
|
this.inputRenderPending = false;
|
|
597
639
|
this.#lastCursorVisibility = undefined;
|
|
598
|
-
this.
|
|
640
|
+
this.beforeTerminalStart();
|
|
641
|
+
this.terminal.start((data) => this.handleTerminalInput(data), () => this.requestRender());
|
|
642
|
+
this.afterTerminalStart();
|
|
599
643
|
if (process.env.TMUX)
|
|
600
644
|
this.terminal.write(ENABLE_FOCUS_REPORTING);
|
|
601
645
|
this.#setCursorVisibility(false);
|
|
@@ -649,6 +693,7 @@ export class TUI extends Container {
|
|
|
649
693
|
if (this.terminalColorSchemeNotificationsEnabled) {
|
|
650
694
|
this.terminal.write("\x1b[?2031l");
|
|
651
695
|
}
|
|
696
|
+
this.beforeTerminalStop();
|
|
652
697
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
|
|
653
698
|
if (this.previousLines.length > 0) {
|
|
654
699
|
// Overwrite the inverted cursor with a normal space to clear the artifact
|
|
@@ -668,6 +713,8 @@ export class TUI extends Container {
|
|
|
668
713
|
if (process.env.TMUX)
|
|
669
714
|
this.terminal.write(DISABLE_FOCUS_REPORTING);
|
|
670
715
|
this.terminal.stop();
|
|
716
|
+
this.afterTerminalStop();
|
|
717
|
+
this.resetRenderState();
|
|
671
718
|
this.#lastCursorVisibility = undefined;
|
|
672
719
|
this.previousLines = [];
|
|
673
720
|
this.previousRawLines = [];
|
|
@@ -693,6 +740,7 @@ export class TUI extends Container {
|
|
|
693
740
|
requestRender(force = false, source = "unknown") {
|
|
694
741
|
if (force) {
|
|
695
742
|
this.inputRenderPending = false;
|
|
743
|
+
this.resetRenderState();
|
|
696
744
|
this.previousLines = [];
|
|
697
745
|
this.previousRawLines = [];
|
|
698
746
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
@@ -772,7 +820,7 @@ export class TUI extends Container {
|
|
|
772
820
|
this.lastRenderAt = performance.now();
|
|
773
821
|
this.doRender();
|
|
774
822
|
}
|
|
775
|
-
|
|
823
|
+
handleTerminalInput(data) {
|
|
776
824
|
const focus = consumeTmuxFocusEvent(data);
|
|
777
825
|
if (focus.event !== null) {
|
|
778
826
|
resetCapabilitiesCache();
|
|
@@ -1098,21 +1146,37 @@ export class TUI extends Container {
|
|
|
1098
1146
|
if (isImageLine(line)) {
|
|
1099
1147
|
return { line, normalized: false };
|
|
1100
1148
|
}
|
|
1101
|
-
|
|
1149
|
+
const cached = this.normalizeMemo.get(line);
|
|
1150
|
+
if (cached !== undefined) {
|
|
1151
|
+
return { line: cached, normalized: false };
|
|
1152
|
+
}
|
|
1153
|
+
const normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1154
|
+
this.normalizeMemo.set(line, normalized);
|
|
1155
|
+
return { line: normalized, normalized: true };
|
|
1102
1156
|
}
|
|
1103
|
-
applyLineResets(lines
|
|
1157
|
+
applyLineResets(lines) {
|
|
1158
|
+
return this.applyLineResetResult(lines).lines;
|
|
1159
|
+
}
|
|
1160
|
+
applyLineResetResult(lines, mode = "full") {
|
|
1161
|
+
const previousMemo = this.normalizeMemo;
|
|
1162
|
+
const nextMemo = new Map();
|
|
1104
1163
|
const normalizedLines = [];
|
|
1105
1164
|
let normalizedCount = 0;
|
|
1106
1165
|
for (let i = 0; i < lines.length; i++) {
|
|
1107
1166
|
const line = lines[i];
|
|
1108
1167
|
if (isImageLine(line)) {
|
|
1109
1168
|
normalizedLines.push(line);
|
|
1169
|
+
continue;
|
|
1110
1170
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1171
|
+
let normalized = nextMemo.get(line) ?? previousMemo.get(line);
|
|
1172
|
+
if (normalized === undefined) {
|
|
1173
|
+
normalized = normalizeTerminalOutput(line) + TUI.SEGMENT_RESET;
|
|
1113
1174
|
normalizedCount += 1;
|
|
1114
1175
|
}
|
|
1176
|
+
nextMemo.set(line, normalized);
|
|
1177
|
+
normalizedLines.push(normalized);
|
|
1115
1178
|
}
|
|
1179
|
+
this.normalizeMemo = nextMemo;
|
|
1116
1180
|
recordViewportRenderStats(normalizedCount, mode);
|
|
1117
1181
|
return {
|
|
1118
1182
|
lines: normalizedLines,
|
|
@@ -1127,7 +1191,7 @@ export class TUI extends Container {
|
|
|
1127
1191
|
this.previousLines.length === 0 ||
|
|
1128
1192
|
this.previousLines.length !== rawLines.length ||
|
|
1129
1193
|
this.previousRawLines.length !== rawLines.length) {
|
|
1130
|
-
return this.
|
|
1194
|
+
return this.applyLineResetResult(rawLines);
|
|
1131
1195
|
}
|
|
1132
1196
|
const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
|
|
1133
1197
|
const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
|
|
@@ -1140,7 +1204,7 @@ export class TUI extends Container {
|
|
|
1140
1204
|
firstRawChanged = i;
|
|
1141
1205
|
}
|
|
1142
1206
|
if (i < windowStart || i >= windowEnd) {
|
|
1143
|
-
return this.
|
|
1207
|
+
return this.applyLineResetResult(rawLines, "escaped");
|
|
1144
1208
|
}
|
|
1145
1209
|
}
|
|
1146
1210
|
const lines = this.previousLines.slice();
|
|
@@ -1531,8 +1595,9 @@ export class TUI extends Container {
|
|
|
1531
1595
|
const logRedraw = (reason) => {
|
|
1532
1596
|
if (!debugRedraw)
|
|
1533
1597
|
return;
|
|
1534
|
-
const logPath = path.join(
|
|
1598
|
+
const logPath = path.join(this.logDirectory, "pi-debug.log");
|
|
1535
1599
|
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
|
|
1600
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1536
1601
|
fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
|
|
1537
1602
|
chmodDiagnosticLogBestEffort(logPath);
|
|
1538
1603
|
};
|
|
@@ -1982,4 +2047,7 @@ export class TUI extends Container {
|
|
|
1982
2047
|
});
|
|
1983
2048
|
}
|
|
1984
2049
|
}
|
|
2050
|
+
/** Legacy main-screen renderer export. */
|
|
2051
|
+
export class TUI extends TuiBase {
|
|
2052
|
+
}
|
|
1985
2053
|
//# sourceMappingURL=tui.js.map
|