@gajae-code/tui 0.17.1 → 0.17.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.4] - 2026-09-23
6
+
7
+ ## [0.17.3] - 2026-09-22
8
+
9
+ ### Performance
10
+
11
+ - Avoid unsupported Kitty placement extraction on ordinary non-Kitty input frames.
12
+ - Reuse unchanged editor logical-line layouts, including keyboard shrink/join paths that previously retained deleted-line cache entries.
13
+ - Reuse the byte/line admission decision for exact cached Markdown highlights instead of rescanning unchanged fenced code.
14
+ - Add a native-highlight input-to-synchronized-write benchmark with same-frame visibility checks. Scheduling, preparation, force precedence, and output revisions are unchanged.
15
+
16
+ - Avoid allocating fallback arrays while attributing Kitty placements during frame assembly. Emitted terminal bytes are unchanged.
17
+
18
+ - Hold the raw frame by reference instead of copying every transcript line during frame assembly. Emitted terminal bytes are unchanged.
19
+
20
+ ## [0.17.2] - 2026-09-18
21
+
5
22
  ## [0.17.1] - 2026-09-17
6
23
 
7
24
  ## [0.17.0] - 2026-09-17
@@ -98,6 +98,8 @@ export declare class Editor implements Component, Focusable {
98
98
  invalidate(): void;
99
99
  render(width: number): string[];
100
100
  handleInput(data: string): void;
101
+ /** Test-only seam: retained per-logical-line layout entries. */
102
+ get logicalLayoutCacheSize(): number;
101
103
  /** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
102
104
  get wrappedLineCacheSize(): number;
103
105
  getText(): string;
@@ -21,6 +21,7 @@ export declare function resetMarkdownHighlightCallCount(): void;
21
21
  export declare const __markdownPerfCounters: {
22
22
  lexerInvocations: number;
23
23
  lexedBytes: number;
24
+ highlightAdmissionScans: number;
24
25
  reset(): void;
25
26
  };
26
27
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
@@ -364,6 +364,7 @@ type TuiRenderCounterSnapshot = {
364
364
  widthReflowScanRows: number;
365
365
  widthReflowVisibleWidthCalls: number;
366
366
  kittyPlacementScanRows: number;
367
+ kittyPlacementReferenceRows: number;
367
368
  };
368
369
  /**
369
370
  * TUI - Main class for managing terminal UI with differential rendering
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.17.1",
4
+ "version": "0.17.4",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -36,8 +36,8 @@
36
36
  "fmt": "biome format --write ."
37
37
  },
38
38
  "dependencies": {
39
- "@gajae-code/natives": "0.17.1",
40
- "@gajae-code/utils": "0.17.1",
39
+ "@gajae-code/natives": "0.17.4",
40
+ "@gajae-code/utils": "0.17.4",
41
41
  "lru-cache": "11.3.6",
42
42
  "marked": "18.0.6"
43
43
  },
@@ -443,6 +443,7 @@ export class Editor implements Component, Focusable {
443
443
  #wrappedLineCache: CachedWrappedLine[] = [];
444
444
  #docVersion = 0;
445
445
  #layoutCache: LayoutCache | undefined;
446
+ #logicalLayoutCache: Array<{ text: string; width: number; cursorCol: number | undefined; lines: LayoutLine[] }> = [];
446
447
 
447
448
  // Emacs-style kill ring
448
449
  #killRing = new KillRing();
@@ -754,11 +755,20 @@ export class Editor implements Component, Focusable {
754
755
  }
755
756
 
756
757
  invalidate(): void {
758
+ this.#logicalLayoutCache.length = 0;
757
759
  this.#wrappedLineCache.length = 0;
758
760
  this.#layoutCache = undefined;
759
761
  }
760
762
 
763
+ #trimLogicalLayoutCache(): void {
764
+ // Retain at most one layout per current logical line, never deleted tails.
765
+ if (this.#logicalLayoutCache.length > this.#state.lines.length) {
766
+ this.#logicalLayoutCache.length = this.#state.lines.length;
767
+ }
768
+ }
769
+
761
770
  #bumpDocumentVersion(): void {
771
+ this.#trimLogicalLayoutCache();
762
772
  this.#docVersion += 1;
763
773
  this.#layoutCache = undefined;
764
774
  }
@@ -1163,6 +1173,16 @@ export class Editor implements Component, Focusable {
1163
1173
  }
1164
1174
 
1165
1175
  handleInput(data: string): void {
1176
+ try {
1177
+ this.#handleInput(data);
1178
+ } finally {
1179
+ // Undo snapshots invalidate before mutation; trim after all input paths,
1180
+ // including early returns and nested bracketed-paste handling.
1181
+ this.#trimLogicalLayoutCache();
1182
+ }
1183
+ }
1184
+
1185
+ #handleInput(data: string): void {
1166
1186
  const kb = getKeybindings();
1167
1187
 
1168
1188
  // Handle character jump mode (awaiting next character to jump to)
@@ -1585,6 +1605,11 @@ export class Editor implements Component, Focusable {
1585
1605
  return wrapped;
1586
1606
  }
1587
1607
 
1608
+ /** Test-only seam: retained per-logical-line layout entries. */
1609
+ get logicalLayoutCacheSize(): number {
1610
+ return this.#logicalLayoutCache.length;
1611
+ }
1612
+
1588
1613
  /** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
1589
1614
  get wrappedLineCacheSize(): number {
1590
1615
  return this.#wrappedLineCache.length;
@@ -1624,6 +1649,16 @@ export class Editor implements Component, Focusable {
1624
1649
  }
1625
1650
 
1626
1651
  #layoutLogicalLine(lineIndex: number, contentWidth: number): LayoutLine[] {
1652
+ const text = this.#state.lines[lineIndex] || "";
1653
+ const cursorCol = lineIndex === this.#state.cursorLine ? this.#state.cursorCol : undefined;
1654
+ const cached = this.#logicalLayoutCache[lineIndex];
1655
+ if (cached?.text === text && cached.width === contentWidth && cached.cursorCol === cursorCol) return cached.lines;
1656
+ const lines = this.#buildLogicalLine(lineIndex, contentWidth);
1657
+ this.#logicalLayoutCache[lineIndex] = { text, width: contentWidth, cursorCol, lines };
1658
+ return lines;
1659
+ }
1660
+
1661
+ #buildLogicalLine(lineIndex: number, contentWidth: number): LayoutLine[] {
1627
1662
  __editorPerfCounters.layoutLogicalLinesProcessed += 1;
1628
1663
  const line = this.#state.lines[lineIndex] || "";
1629
1664
  const isCurrentLine = lineIndex === this.#state.cursorLine;
@@ -1722,6 +1757,7 @@ export class Editor implements Component, Focusable {
1722
1757
  }
1723
1758
 
1724
1759
  #layoutText(contentWidth: number): LayoutLine[] {
1760
+ this.#trimLogicalLayoutCache();
1725
1761
  __editorPerfCounters.layoutTextInvocations += 1;
1726
1762
  const key = this.#makeLayoutCacheKey(contentWidth);
1727
1763
  const cached = this.#layoutCache;
@@ -214,9 +214,11 @@ export function resetMarkdownHighlightCallCount(): void {
214
214
  export const __markdownPerfCounters = {
215
215
  lexerInvocations: 0,
216
216
  lexedBytes: 0,
217
+ highlightAdmissionScans: 0,
217
218
  reset(): void {
218
219
  this.lexerInvocations = 0;
219
220
  this.lexedBytes = 0;
221
+ this.highlightAdmissionScans = 0;
220
222
  },
221
223
  };
222
224
 
@@ -452,6 +454,7 @@ export class Markdown implements Component {
452
454
  }
453
455
 
454
456
  #exceedsHighlightCap(code: string): boolean {
457
+ __markdownPerfCounters.highlightAdmissionScans += 1;
455
458
  // UTF-8 requires at least one byte per UTF-16 code unit, including lone
456
459
  // surrogates. Reject obviously oversized input without scanning or hashing it.
457
460
  if (code.length > MAX_HIGHLIGHT_BYTES) return true;
@@ -467,10 +470,13 @@ export class Markdown implements Component {
467
470
 
468
471
  #highlightCodeBlock(code: string, lang: string): string[] | null {
469
472
  if (!this.#theme.highlightCode) return null;
470
- if (this.#exceedsHighlightCap(code)) return null;
473
+ // Avoid building/hashing keys for obviously oversized input. A cache hit
474
+ // already passed the immutable byte/line caps; only misses need a scan.
475
+ if (code.length > MAX_HIGHLIGHT_BYTES) return null;
471
476
  const key = `${objectId(this.#theme)}\x00${lang}\x00${code}`;
472
477
  const cached = highlightCache.get(key);
473
478
  if (cached?.lang === lang && cached.code === code) return cached.lines;
479
+ if (this.#exceedsHighlightCap(code)) return null;
474
480
  highlightCallCount += 1;
475
481
  const result = this.#theme.highlightCode(code, lang || undefined);
476
482
  highlightCache.set(key, { lang, code, lines: result });
package/src/tui.ts CHANGED
@@ -821,6 +821,7 @@ type TuiRenderCounterSnapshot = {
821
821
  widthReflowScanRows: number;
822
822
  widthReflowVisibleWidthCalls: number;
823
823
  kittyPlacementScanRows: number;
824
+ kittyPlacementReferenceRows: number;
824
825
  };
825
826
  type RenderCommitWaiter = {
826
827
  resolve: (committed: boolean) => void;
@@ -1187,6 +1188,7 @@ export class TUI extends Container {
1187
1188
  widthReflowScanRows: 0,
1188
1189
  widthReflowVisibleWidthCalls: 0,
1189
1190
  kittyPlacementScanRows: 0,
1191
+ kittyPlacementReferenceRows: 0,
1190
1192
  };
1191
1193
 
1192
1194
  static resetRenderCountersForTest(): void {
@@ -1197,6 +1199,7 @@ export class TUI extends Container {
1197
1199
  widthReflowScanRows: 0,
1198
1200
  widthReflowVisibleWidthCalls: 0,
1199
1201
  kittyPlacementScanRows: 0,
1202
+ kittyPlacementReferenceRows: 0,
1200
1203
  };
1201
1204
  }
1202
1205
 
@@ -4624,7 +4627,12 @@ export class TUI extends Container {
4624
4627
  const safeLines = reuseCached ? cached.safeLines : rendered.lines.map(stripTerminalEraseControls);
4625
4628
  const kittyPlacements = reuseCached
4626
4629
  ? cached.kittyPlacements
4627
- : rendered.lines.map(line => [...extractKittyPlacementReferences(line)]);
4630
+ : TERMINAL.imageProtocol === ImageProtocol.Kitty
4631
+ ? rendered.lines.map(line => {
4632
+ TUI.#renderCounters.kittyPlacementReferenceRows++;
4633
+ return [...extractKittyPlacementReferences(line)];
4634
+ })
4635
+ : [];
4628
4636
  if (!reuseCached && componentRevision !== undefined && source !== null) {
4629
4637
  this.#viewportAnchorRenderCache = {
4630
4638
  component: child,
@@ -4644,8 +4652,11 @@ export class TUI extends Container {
4644
4652
  }
4645
4653
  const owner: KittyPlacementOwner = hasStickySuffix && childIndex >= pinnedChildIndex ? "suffix" : "transcript";
4646
4654
  for (let lineIndex = 0; lineIndex < rendered.lines.length; lineIndex++) {
4647
- for (const placement of kittyPlacements[lineIndex] ?? []) {
4648
- placementOwners.set(this.#kittyPlacementKey(placement), owner);
4655
+ const placements = kittyPlacements[lineIndex];
4656
+ if (placements !== undefined) {
4657
+ for (const placement of placements) {
4658
+ placementOwners.set(this.#kittyPlacementKey(placement), owner);
4659
+ }
4649
4660
  }
4650
4661
  renderedLines.push(safeLines[lineIndex] ?? rendered.lines[lineIndex]!);
4651
4662
  }
@@ -4705,7 +4716,8 @@ export class TUI extends Container {
4705
4716
  // re-normalized and the diff starts at the window. Output is byte-identical to the
4706
4717
  // full path (reused entries are deterministic normalizations of identical raw lines).
4707
4718
  const VIEWPORT_NORMALIZE_OVERSCAN = 8;
4708
- const rawLines = newLines.slice();
4719
+ // Reassigned below, never mutated in place -- a reference is the snapshot.
4720
+ const rawLines = newLines;
4709
4721
  const total = rawLines.length;
4710
4722
  let diffStart = 0;
4711
4723
  let usedWindowNormalize = false;