@oh-my-pi/pi-tui 15.11.7 → 15.11.8
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 +11 -1
- package/package.json +3 -3
- package/src/components/markdown.ts +95 -1
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.
|
|
4
|
+
"version": "15.11.8",
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.11.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.11.8",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.11.8",
|
|
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 =
|
|
482
|
+
const tokens = this.#lexTokens(normalizedText);
|
|
389
483
|
|
|
390
484
|
// Convert tokens to styled terminal output
|
|
391
485
|
const renderedLines: string[] = [];
|