@oh-my-pi/pi-tui 15.11.7 → 15.12.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/CHANGELOG.md CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.8] - 2026-06-12
6
+
7
+ ### Changed
8
+
9
+ - Markdown rendering during streaming re-lexes only the grown tail instead of the whole buffer on every reveal tick. marked has no resumable lexer, but block tokenization is local across a blank-line boundary with balanced fences, so the largest blank-line-bounded prefix's block tokens are frozen and reused (`lex(prefix) ++ lex(tail)`), with a full-lex fallback for non-append edits, reference-link definitions, and CRLF input. The output is byte-identical to a full lex (covered by a contract test), turning the O(N²) cost of revealing a long single-block message into O(N): a 6,000-grapheme reveal dropped from ~575 ms to ~89 ms of CPU in benchmarks.
10
+
5
11
  ## [15.11.5] - 2026-06-12
12
+
6
13
  ### Added
7
14
 
8
15
  - Added `fuzzyRank` to return sorted matches together with a fuzzy score
@@ -18,6 +25,7 @@
18
25
  - Fixed multi-word searches so `fuzzyMatch` no longer matches when query letters are only scattered across unrelated words
19
26
 
20
27
  ## [15.11.4] - 2026-06-12
28
+
21
29
  ### Added
22
30
 
23
31
  - Added `partialHoldTimeout` to `StdinBufferOptions` to control the maximum extra delay held for unambiguous incomplete escape sequences before they are flushed
@@ -64,6 +72,7 @@
64
72
  - Skipped native syntax highlighting for transient markdown streaming renders, including nested list code blocks, leaving code blocks plain until their content stabilizes to avoid main-thread highlighter spikes.
65
73
 
66
74
  ## [15.11.1] - 2026-06-11
75
+
67
76
  ### Added
68
77
 
69
78
  - Added `TUI.requestComponentRender(component)` to schedule component-scoped renders for self-contained updates
@@ -79,6 +88,7 @@
79
88
  - Fixed `ProcessTerminal` treating asynchronous stdout `EIO` errors as uncaught exceptions: stdout `error` events now mark the terminal dead, disable future renders, and keep the active session process alive ([#2284](https://github.com/can1357/oh-my-pi/issues/2284)).
80
89
 
81
90
  ## [15.11.0] - 2026-06-10
91
+
82
92
  ### Added
83
93
 
84
94
  - Added support for asynchronous `onSubmit` handlers by allowing the callback to return a `Promise<void>`
@@ -1354,4 +1364,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
1354
1364
 
1355
1365
  ### Fixed
1356
1366
 
1357
- - **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
1367
+ - **Readline-style Ctrl+W**: Now skips trailing whitespace before deleting the preceding word, matching standard readline behavior. ([#306](https://github.com/badlogic/pi-mono/pull/306) by [@kim0](https://github.com/kim0))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "15.11.7",
4
+ "version": "15.12.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "15.11.7",
41
- "@oh-my-pi/pi-utils": "15.11.7",
40
+ "@oh-my-pi/pi-natives": "15.12.0",
41
+ "@oh-my-pi/pi-utils": "15.12.0",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.4"
44
44
  },
@@ -61,6 +61,13 @@ const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
61
61
  const EMPTY_RENDER_LINES: readonly string[] = [];
62
62
  const renderCache = new LRUCache<string, readonly string[]>({ max: RENDER_CACHE_MAX });
63
63
 
64
+ // A reference-link definition (`[label]: dest`) resolves across the whole
65
+ // document, so a split lex cannot reproduce it — disable the streaming fast path
66
+ // when one is present (rare in streamed output). The label may contain
67
+ // backslash-escaped characters (`[a\]b]: x`), so escapes are matched explicitly;
68
+ // over-matching is safe (it only costs the fast path), under-matching is not.
69
+ const HAS_REF_DEF = /^ {0,3}\[(?:\\.|[^\]\\])+\]:/m;
70
+
64
71
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
65
72
  export function clearRenderCache(): void {
66
73
  renderCache.clear();
@@ -297,6 +304,16 @@ export class Markdown implements Component {
297
304
  #cachedLines?: readonly string[];
298
305
  #transientRenderCache = false;
299
306
 
307
+ // Streaming-lex cache: the largest blank-line-bounded prefix of #text whose
308
+ // block tokens are frozen, plus those tokens. marked has no resumable lexer,
309
+ // but block tokenization is local across a "\n\n" boundary with balanced
310
+ // fences, so lex(prefix) ++ lex(tail) === lex(prefix+tail). On append-only
311
+ // growth (the streaming path) this re-lexes only the grown tail instead of the
312
+ // whole buffer, turning O(N^2) reveal cost into O(N). Width/theme do not affect
313
+ // tokenization, so this cache is independent of the render caches above.
314
+ #streamPrefixText?: string;
315
+ #streamPrefixTokens?: Token[];
316
+
300
317
  constructor(
301
318
  text: string,
302
319
  paddingX: number,
@@ -315,6 +332,13 @@ export class Markdown implements Component {
315
332
 
316
333
  setText(text: string): void {
317
334
  this.#text = text;
335
+ if (!text.trim()) {
336
+ // Blank replacement: render() early-returns before #lexTokens can see
337
+ // the non-append edit, so drop the frozen stream state here or it
338
+ // outlives the content it indexed.
339
+ this.#streamPrefixText = undefined;
340
+ this.#streamPrefixTokens = undefined;
341
+ }
318
342
  this.invalidate();
319
343
  }
320
344
 
@@ -334,6 +358,76 @@ export class Markdown implements Component {
334
358
  this.invalidate();
335
359
  }
336
360
 
361
+ // Lex `text` into block tokens, reusing the frozen stable prefix when the text
362
+ // only grew (the streaming path). Falls back to a full lex whenever the prefix
363
+ // is no longer a prefix (non-append edit), the text carries reference-link
364
+ // definitions, or it contains CR (marked normalizes CRLF, which would desync
365
+ // raw-span offsets). Every fallback is correctness-preserving — only speed
366
+ // differs; the render loop sees the identical token list either way.
367
+ #lexTokens(text: string): Token[] {
368
+ const canStream = !HAS_REF_DEF.test(text) && !text.includes("\r");
369
+ const prefix = this.#streamPrefixText;
370
+ const prefixTokens = this.#streamPrefixTokens;
371
+ if (
372
+ canStream &&
373
+ prefix !== undefined &&
374
+ prefixTokens !== undefined &&
375
+ text.length > prefix.length &&
376
+ text.startsWith(prefix)
377
+ ) {
378
+ const tailTokens = markdownParser.lexer(text.slice(prefix.length));
379
+ const tokens = [...prefixTokens, ...tailTokens];
380
+ this.#freezeStablePrefix(text, tokens);
381
+ return tokens;
382
+ }
383
+ const tokens = markdownParser.lexer(text);
384
+ if (canStream) {
385
+ this.#freezeStablePrefix(text, tokens);
386
+ } else {
387
+ this.#streamPrefixText = undefined;
388
+ this.#streamPrefixTokens = undefined;
389
+ }
390
+ return tokens;
391
+ }
392
+
393
+ // Freeze the largest run of leading blocks that end on a hard "\n\n" boundary
394
+ // (complete and immutable under append-only growth) so the next streaming
395
+ // render re-lexes only the unfrozen tail. Caller guarantees no CR / no
396
+ // reference definitions, so each token's `raw` is a verbatim slice of `text`
397
+ // and the summed offsets address `text` exactly.
398
+ #freezeStablePrefix(text: string, tokens: Token[]): void {
399
+ let pos = 0;
400
+ let frozenEnd = 0;
401
+ let frozenCount = 0;
402
+ for (let i = 0; i < tokens.length; i++) {
403
+ const raw = tokens[i].raw;
404
+ const end = pos + raw.length;
405
+ // A `space` token ending in "\n\n" closes the preceding block, but a
406
+ // `list` before it can still be extended by a following same-marker
407
+ // item across the blank line (CommonMark loose-list continuation),
408
+ // which marked merges into one renumbered loose list. Freezing across
409
+ // such a cut would keep the lists separate. Never freeze right after a
410
+ // list — it stays in the re-lexed tail.
411
+ if (raw.endsWith("\n\n") && tokens[i - 1]?.type !== "list") {
412
+ frozenEnd = end;
413
+ frozenCount = i + 1;
414
+ }
415
+ pos = end;
416
+ }
417
+ // Freeze only when the tail begins with real block content. If the next
418
+ // char is whitespace (an extra blank line, or an indented continuation),
419
+ // the block separator straddles the cut and lex(prefix)++lex(tail) would
420
+ // desync from a full lex — e.g. a fence followed by "\n\n\n- list". When
421
+ // frozenEnd is at end-of-text the next char is unknown, so defer.
422
+ if (frozenCount > 0 && frozenEnd < text.length) {
423
+ const next = text.charCodeAt(frozenEnd);
424
+ if (next !== 0x20 /* space */ && next !== 0x0a /* \n */) {
425
+ this.#streamPrefixText = text.slice(0, frozenEnd);
426
+ this.#streamPrefixTokens = tokens.slice(0, frozenCount);
427
+ }
428
+ }
429
+ }
430
+
337
431
  render(width: number): readonly string[] {
338
432
  // L1: per-instance cache — fastest path for repeated renders of the same
339
433
  // instance at the same width (e.g. resize debounce, repeated redraws).
@@ -385,7 +479,7 @@ export class Markdown implements Component {
385
479
  }
386
480
 
387
481
  // Parse markdown to HTML-like tokens
388
- const tokens = markdownParser.lexer(normalizedText);
482
+ const tokens = this.#lexTokens(normalizedText);
389
483
 
390
484
  // Convert tokens to styled terminal output
391
485
  const renderedLines: string[] = [];