@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.
Files changed (74) hide show
  1. package/README.md +77 -14
  2. package/dist/TuiAltScreen.d.ts +83 -0
  3. package/dist/TuiAltScreen.d.ts.map +1 -0
  4. package/dist/TuiAltScreen.js +687 -0
  5. package/dist/TuiAltScreen.js.map +1 -0
  6. package/dist/TuiMainScreen.d.ts +5 -0
  7. package/dist/TuiMainScreen.d.ts.map +1 -0
  8. package/dist/TuiMainScreen.js +5 -0
  9. package/dist/TuiMainScreen.js.map +1 -0
  10. package/dist/components/alt-screen-flash.d.ts +13 -0
  11. package/dist/components/alt-screen-flash.d.ts.map +1 -0
  12. package/dist/components/alt-screen-flash.js +36 -0
  13. package/dist/components/alt-screen-flash.js.map +1 -0
  14. package/dist/components/h-stack.d.ts +7 -0
  15. package/dist/components/h-stack.d.ts.map +1 -0
  16. package/dist/components/h-stack.js +43 -0
  17. package/dist/components/h-stack.js.map +1 -0
  18. package/dist/components/image.d.ts.map +1 -1
  19. package/dist/components/image.js +4 -4
  20. package/dist/components/image.js.map +1 -1
  21. package/dist/components/markdown.d.ts +2 -0
  22. package/dist/components/markdown.d.ts.map +1 -1
  23. package/dist/components/markdown.js +3 -2
  24. package/dist/components/markdown.js.map +1 -1
  25. package/dist/components/scroll-view.d.ts +51 -0
  26. package/dist/components/scroll-view.d.ts.map +1 -0
  27. package/dist/components/scroll-view.js +163 -0
  28. package/dist/components/scroll-view.js.map +1 -0
  29. package/dist/components/settings-list.d.ts.map +1 -1
  30. package/dist/components/settings-list.js +3 -6
  31. package/dist/components/settings-list.js.map +1 -1
  32. package/dist/components/stack.d.ts +32 -0
  33. package/dist/components/stack.d.ts.map +1 -0
  34. package/dist/components/stack.js +109 -0
  35. package/dist/components/stack.js.map +1 -0
  36. package/dist/components/v-stack.d.ts +8 -0
  37. package/dist/components/v-stack.d.ts.map +1 -0
  38. package/dist/components/v-stack.js +21 -0
  39. package/dist/components/v-stack.js.map +1 -0
  40. package/dist/index.d.ts +10 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +10 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/keybindings.d.ts +30 -0
  45. package/dist/keybindings.d.ts.map +1 -1
  46. package/dist/keybindings.js +18 -0
  47. package/dist/keybindings.js.map +1 -1
  48. package/dist/layout-node.d.ts +40 -0
  49. package/dist/layout-node.d.ts.map +1 -0
  50. package/dist/layout-node.js +6 -0
  51. package/dist/layout-node.js.map +1 -0
  52. package/dist/layout.d.ts +40 -0
  53. package/dist/layout.d.ts.map +1 -0
  54. package/dist/layout.js +315 -0
  55. package/dist/layout.js.map +1 -0
  56. package/dist/mux.js +1 -1
  57. package/dist/mux.js.map +1 -1
  58. package/dist/terminal-image.d.ts +10 -0
  59. package/dist/terminal-image.d.ts.map +1 -1
  60. package/dist/terminal-image.js +61 -3
  61. package/dist/terminal-image.js.map +1 -1
  62. package/dist/terminal-text.d.ts +1 -0
  63. package/dist/terminal-text.d.ts.map +1 -1
  64. package/dist/terminal-text.js +12 -2
  65. package/dist/terminal-text.js.map +1 -1
  66. package/dist/tui.d.ts +37 -16
  67. package/dist/tui.d.ts.map +1 -1
  68. package/dist/tui.js +89 -21
  69. package/dist/tui.js.map +1 -1
  70. package/dist/utils.d.ts +10 -0
  71. package/dist/utils.d.ts.map +1 -1
  72. package/dist/utils.js +95 -4
  73. package/dist/utils.js.map +1 -1
  74. 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 params = line.slice(paramsStart, paramsEnd);
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
- export class TUI extends Container {
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.children.some((child) => this.containsComponent(child, component));
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.terminal.start((data) => this.handleInput(data), () => this.requestRender());
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
- handleInput(data) {
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
- return { line: normalizeTerminalOutput(line) + TUI.SEGMENT_RESET, normalized: true };
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, mode = "full") {
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
- else {
1112
- normalizedLines.push(normalizeTerminalOutput(line) + TUI.SEGMENT_RESET);
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.applyLineResets(rawLines);
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.applyLineResets(rawLines, "escaped");
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(os.homedir(), ".senpi", "agent", "senpi-debug.log");
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