@sayknow-cli/tui 0.3.7 → 0.3.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/dist/types/animation-scheduler.d.ts +13 -0
- package/dist/types/autocomplete.d.ts +1 -0
- package/dist/types/components/editor.d.ts +11 -2
- package/dist/types/components/loader.d.ts +10 -1
- package/dist/types/components/markdown.d.ts +14 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/tui.d.ts +13 -4
- package/dist/types/utils.d.ts +12 -0
- package/package.json +4 -4
- package/src/animation-scheduler.ts +99 -0
- package/src/autocomplete.ts +119 -96
- package/src/components/editor.ts +283 -146
- package/src/components/loader.ts +36 -37
- package/src/components/markdown.ts +79 -2
- package/src/index.ts +1 -0
- package/src/stdin-buffer.ts +89 -11
- package/src/tui.ts +227 -78
- package/src/utils.ts +77 -11
package/src/components/loader.ts
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
|
+
import { type AnimationRegistration, registerAnimationCallback } from "../animation-scheduler";
|
|
1
2
|
import type { TUI } from "../tui";
|
|
2
3
|
import { sliceByColumn, visibleWidth } from "../utils";
|
|
3
4
|
import { Text } from "./text";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* Loader component that drives display refresh at ~60fps so callers whose
|
|
7
|
-
* message colorizer is time-dependent (e.g. shimmer/KITT) animate smoothly.
|
|
8
|
-
*
|
|
9
|
-
* Two cadences are interleaved on a single timer:
|
|
10
|
-
* - **Recompute tick** (every `RENDER_INTERVAL_MS`) → recomposes the spinner +
|
|
11
|
-
* colorized message every 16ms. A redraw is requested only when that composed
|
|
12
|
-
* text actually changed since the last tick (`#lastDisplayed`), so animated
|
|
13
|
-
* colorizers (shimmer/KITT) and spinner-frame advances still repaint, while
|
|
14
|
-
* static loaders skip the redundant no-op render requests between advances.
|
|
15
|
-
* - **Spinner advance** (every `SPINNER_ADVANCE_MS`) → bumps the spinner
|
|
16
|
-
* frame index. Decoupled from the recompute cadence so the spinner keeps
|
|
17
|
-
* its classic ~12.5fps step pace regardless of shimmer state.
|
|
18
|
-
*
|
|
19
|
-
* The animation timer is `unref`'d so an active loader never keeps the event
|
|
20
|
-
* loop alive on its own.
|
|
21
|
-
*/
|
|
22
|
-
const RENDER_INTERVAL_MS = 16;
|
|
23
6
|
const SPINNER_ADVANCE_MS = 80;
|
|
24
7
|
|
|
8
|
+
export interface LoaderOptions {
|
|
9
|
+
timeDependentColor?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
13
|
+
export const __loaderPerfCounters = {
|
|
14
|
+
liveIntervals: 0,
|
|
15
|
+
startedIntervals: 0,
|
|
16
|
+
reset(): void {
|
|
17
|
+
this.liveIntervals = 0;
|
|
18
|
+
this.startedIntervals = 0;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
25
22
|
export class Loader extends Text {
|
|
26
23
|
#frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
27
24
|
#currentFrame = 0;
|
|
28
|
-
#
|
|
25
|
+
#animation?: AnimationRegistration;
|
|
29
26
|
#ui: TUI | null = null;
|
|
30
27
|
#lastSpinnerTick = 0;
|
|
31
28
|
#lastDisplayed?: string;
|
|
29
|
+
#timeDependentColor: boolean;
|
|
32
30
|
|
|
33
31
|
constructor(
|
|
34
32
|
ui: TUI,
|
|
@@ -36,9 +34,11 @@ export class Loader extends Text {
|
|
|
36
34
|
private messageColorFn: (str: string) => string,
|
|
37
35
|
private message: string = "Loading...",
|
|
38
36
|
spinnerFrames?: string[],
|
|
37
|
+
options: LoaderOptions = {},
|
|
39
38
|
) {
|
|
40
39
|
super("", 1, 0);
|
|
41
40
|
this.#ui = ui;
|
|
41
|
+
this.#timeDependentColor = options.timeDependentColor ?? false;
|
|
42
42
|
if (spinnerFrames && spinnerFrames.length > 0) {
|
|
43
43
|
this.#frames = spinnerFrames;
|
|
44
44
|
}
|
|
@@ -57,24 +57,28 @@ export class Loader extends Text {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
start() {
|
|
60
|
+
if (this.#animation) return;
|
|
60
61
|
this.#lastSpinnerTick = performance.now();
|
|
61
62
|
this.#updateDisplay();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.#lastSpinnerTick
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
__loaderPerfCounters.liveIntervals += 1;
|
|
64
|
+
__loaderPerfCounters.startedIntervals += 1;
|
|
65
|
+
this.#animation = registerAnimationCallback(
|
|
66
|
+
now => {
|
|
67
|
+
if (now - this.#lastSpinnerTick >= SPINNER_ADVANCE_MS) {
|
|
68
|
+
this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
|
|
69
|
+
this.#lastSpinnerTick = now;
|
|
70
|
+
}
|
|
71
|
+
this.#updateDisplay();
|
|
72
|
+
},
|
|
73
|
+
this.#timeDependentColor ? 16 : 80,
|
|
74
|
+
);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
stop() {
|
|
75
|
-
if (this.#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if (this.#animation) {
|
|
79
|
+
this.#animation.unregister();
|
|
80
|
+
__loaderPerfCounters.liveIntervals = Math.max(0, __loaderPerfCounters.liveIntervals - 1);
|
|
81
|
+
this.#animation = undefined;
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -90,11 +94,6 @@ export class Loader extends Text {
|
|
|
90
94
|
#updateDisplay() {
|
|
91
95
|
const frame = this.#frames[this.#currentFrame];
|
|
92
96
|
const next = `${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`;
|
|
93
|
-
// Only touch the component and ask the TUI to repaint when the rendered
|
|
94
|
-
// text actually changed. Time-dependent colorizers (shimmer/KITT) produce
|
|
95
|
-
// new text every tick and still animate; static loaders skip the ~16ms
|
|
96
|
-
// no-op render requests between 80ms spinner advances. Output is unchanged
|
|
97
|
-
// because a suppressed frame would have produced a no-op write anyway.
|
|
98
97
|
if (next === this.#lastDisplayed) return;
|
|
99
98
|
this.#lastDisplayed = next;
|
|
100
99
|
this.setText(next);
|
|
@@ -42,6 +42,13 @@ const RENDER_CACHE_MAX = 256; // sane cap: ~256 distinct message × width combos
|
|
|
42
42
|
const renderCache = new LRUCache<string, { source: string; lines: string[] }>({ max: RENDER_CACHE_MAX });
|
|
43
43
|
const PARSE_CACHE_MAX = 128;
|
|
44
44
|
const parseCache = new LRUCache<string, { source: string; tokens: Token[] }>({ max: PARSE_CACHE_MAX });
|
|
45
|
+
const MARKDOWN_STREAM_THROTTLE_MS = 64;
|
|
46
|
+
let markdownNow = (): number => performance.now();
|
|
47
|
+
|
|
48
|
+
/** Test-only clock seam for streaming throttle tests. */
|
|
49
|
+
export function __setMarkdownNowForTest(now: (() => number) | undefined): void {
|
|
50
|
+
markdownNow = now ?? (() => performance.now());
|
|
51
|
+
}
|
|
45
52
|
|
|
46
53
|
// Per-code-block highlight cache (F3): keyed by theme + lang + code so streaming
|
|
47
54
|
// appends only highlight new/changed blocks instead of re-highlighting the whole
|
|
@@ -61,6 +68,16 @@ export function resetMarkdownHighlightCallCount(): void {
|
|
|
61
68
|
highlightCallCount = 0;
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
72
|
+
export const __markdownPerfCounters = {
|
|
73
|
+
lexerInvocations: 0,
|
|
74
|
+
lexedBytes: 0,
|
|
75
|
+
reset(): void {
|
|
76
|
+
this.lexerInvocations = 0;
|
|
77
|
+
this.lexedBytes = 0;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
64
81
|
// Full-content 64-bit wyhash over every byte (no lossy sampling). Cache hits
|
|
65
82
|
// additionally verify entry.source against the normalized text, so even a
|
|
66
83
|
// hash collision can never return another message's render.
|
|
@@ -178,6 +195,11 @@ export class Markdown implements Component {
|
|
|
178
195
|
#cachedWidth?: number;
|
|
179
196
|
#cachedLines?: string[];
|
|
180
197
|
|
|
198
|
+
#streaming = false;
|
|
199
|
+
#lastFullParseAt = 0;
|
|
200
|
+
#onStaleThrottle?: () => void;
|
|
201
|
+
#staleThrottleTimer?: ReturnType<typeof setTimeout>;
|
|
202
|
+
|
|
181
203
|
constructor(
|
|
182
204
|
text: string,
|
|
183
205
|
paddingX: number,
|
|
@@ -194,11 +216,55 @@ export class Markdown implements Component {
|
|
|
194
216
|
this.#codeBlockIndent = Math.max(0, Math.floor(codeBlockIndent));
|
|
195
217
|
}
|
|
196
218
|
|
|
197
|
-
|
|
219
|
+
setOnStaleThrottle(callback: (() => void) | undefined): void {
|
|
220
|
+
this.#onStaleThrottle = callback;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setText(text: string, options?: { streaming?: boolean }): void {
|
|
224
|
+
if (options?.streaming !== undefined) {
|
|
225
|
+
this.setStreaming(options.streaming);
|
|
226
|
+
}
|
|
198
227
|
this.#text = text;
|
|
228
|
+
if (this.#streaming) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
199
231
|
this.invalidate();
|
|
200
232
|
}
|
|
201
233
|
|
|
234
|
+
setStreaming(streaming: boolean): void {
|
|
235
|
+
if (this.#streaming === streaming) return;
|
|
236
|
+
this.#streaming = streaming;
|
|
237
|
+
if (!streaming) {
|
|
238
|
+
this.#clearStaleThrottleTimer();
|
|
239
|
+
this.#lastFullParseAt = 0;
|
|
240
|
+
this.invalidate();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#clearStaleThrottleTimer(): void {
|
|
245
|
+
if (!this.#staleThrottleTimer) return;
|
|
246
|
+
clearTimeout(this.#staleThrottleTimer);
|
|
247
|
+
this.#staleThrottleTimer = undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#armStaleThrottleTimer(remainingMs: number): void {
|
|
251
|
+
if (!this.#onStaleThrottle || this.#staleThrottleTimer || this.#cachedText === this.#text) return;
|
|
252
|
+
this.#staleThrottleTimer = setTimeout(
|
|
253
|
+
() => {
|
|
254
|
+
this.#staleThrottleTimer = undefined;
|
|
255
|
+
if (this.#streaming && this.#cachedText !== this.#text) {
|
|
256
|
+
this.#onStaleThrottle?.();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
Math.max(0, remainingMs),
|
|
260
|
+
);
|
|
261
|
+
this.#staleThrottleTimer.unref?.();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
dispose(): void {
|
|
265
|
+
this.#clearStaleThrottleTimer();
|
|
266
|
+
}
|
|
267
|
+
|
|
202
268
|
invalidate(): void {
|
|
203
269
|
this.#cachedText = undefined;
|
|
204
270
|
this.#cachedWidth = undefined;
|
|
@@ -256,6 +322,14 @@ export class Markdown implements Component {
|
|
|
256
322
|
// Calculate available width for content (subtract horizontal padding)
|
|
257
323
|
const contentWidth = Math.max(1, width - this.#paddingX * 2);
|
|
258
324
|
|
|
325
|
+
if (this.#streaming && this.#cachedLines && this.#cachedWidth === width && this.#lastFullParseAt > 0) {
|
|
326
|
+
const elapsedMs = markdownNow() - this.#lastFullParseAt;
|
|
327
|
+
if (elapsedMs < MARKDOWN_STREAM_THROTTLE_MS) {
|
|
328
|
+
this.#armStaleThrottleTimer(MARKDOWN_STREAM_THROTTLE_MS - elapsedMs);
|
|
329
|
+
return this.#cachedLines;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
259
333
|
// Don't render anything if there's no actual text
|
|
260
334
|
if (!this.#text || this.#text.trim() === "") {
|
|
261
335
|
const result: string[] = [];
|
|
@@ -268,7 +342,7 @@ export class Markdown implements Component {
|
|
|
268
342
|
|
|
269
343
|
// Replace tabs with 3 spaces for consistent rendering
|
|
270
344
|
const normalizedText = replaceTabs(this.#text);
|
|
271
|
-
|
|
345
|
+
this.#clearStaleThrottleTimer();
|
|
272
346
|
const contentKey = markdownContentKey(normalizedText);
|
|
273
347
|
|
|
274
348
|
// L2: module-level LRU — survives component disposal/recreation across
|
|
@@ -296,6 +370,8 @@ export class Markdown implements Component {
|
|
|
296
370
|
if (cachedParse !== undefined && cachedParse.source === normalizedText) {
|
|
297
371
|
tokens = cachedParse.tokens;
|
|
298
372
|
} else {
|
|
373
|
+
__markdownPerfCounters.lexerInvocations += 1;
|
|
374
|
+
__markdownPerfCounters.lexedBytes += normalizedText.length;
|
|
299
375
|
tokens = markdownParser.lexer(normalizedText);
|
|
300
376
|
parseCache.set(contentKey, { source: normalizedText, tokens });
|
|
301
377
|
}
|
|
@@ -361,6 +437,7 @@ export class Markdown implements Component {
|
|
|
361
437
|
this.#cachedText = this.#text;
|
|
362
438
|
this.#cachedWidth = width;
|
|
363
439
|
this.#cachedLines = result;
|
|
440
|
+
this.#lastFullParseAt = markdownNow();
|
|
364
441
|
|
|
365
442
|
// Update L2 module-level LRU so future instances with the same key skip
|
|
366
443
|
// the marked.lexer + highlightCode (Rust FFI) work entirely.
|
package/src/index.ts
CHANGED
package/src/stdin-buffer.ts
CHANGED
|
@@ -24,6 +24,39 @@ const ESC = "\x1b";
|
|
|
24
24
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
25
25
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
26
26
|
|
|
27
|
+
function isUtf8LeadByte(byte: number): boolean {
|
|
28
|
+
return byte >= 0xc2 && byte <= 0xf4;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function endsWithIncompleteUtf8Sequence(data: Buffer): boolean {
|
|
32
|
+
if (data.length === 0) return false;
|
|
33
|
+
|
|
34
|
+
let index = data.length - 1;
|
|
35
|
+
let continuationCount = 0;
|
|
36
|
+
while (index >= 0) {
|
|
37
|
+
const byte = data[index]!;
|
|
38
|
+
if (byte < 0x80 || byte > 0xbf) break;
|
|
39
|
+
continuationCount++;
|
|
40
|
+
index--;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (index < 0) {
|
|
44
|
+
return continuationCount > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lead = data[index]!;
|
|
48
|
+
let expectedLength = 0;
|
|
49
|
+
if (lead >= 0xc2 && lead <= 0xdf) expectedLength = 2;
|
|
50
|
+
else if (lead >= 0xe0 && lead <= 0xef) expectedLength = 3;
|
|
51
|
+
else if (lead >= 0xf0 && lead <= 0xf4) expectedLength = 4;
|
|
52
|
+
|
|
53
|
+
return expectedLength > 0 && continuationCount + 1 < expectedLength;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function legacyMetaSequence(byte: number): string {
|
|
57
|
+
return `\x1b${String.fromCharCode(byte - 128)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/**
|
|
28
61
|
* Check if a string is a complete escape sequence or needs more data
|
|
29
62
|
*/
|
|
@@ -268,6 +301,8 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
268
301
|
// split across two stdin events) reassemble correctly instead of emitting
|
|
269
302
|
// U+FFFD. Reset on clear()/destroy(); never finalized on normal flush.
|
|
270
303
|
#decoder = new StringDecoder("utf8");
|
|
304
|
+
#decoderHasPendingUtf8 = false;
|
|
305
|
+
#pendingSingleUtf8LeadByte: number | undefined;
|
|
271
306
|
|
|
272
307
|
constructor(options: StdinBufferOptions = {}) {
|
|
273
308
|
super();
|
|
@@ -286,22 +321,51 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
286
321
|
let str: string;
|
|
287
322
|
let decodedFromBuffer = false;
|
|
288
323
|
if (Buffer.isBuffer(data)) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
let bytes = data;
|
|
325
|
+
const hadPendingUtf8 = this.#decoderHasPendingUtf8 || this.#pendingSingleUtf8LeadByte !== undefined;
|
|
326
|
+
|
|
327
|
+
if (this.#pendingSingleUtf8LeadByte !== undefined) {
|
|
328
|
+
const nextByte = data[0];
|
|
329
|
+
if (nextByte !== undefined && nextByte >= 0x80 && nextByte <= 0xbf) {
|
|
330
|
+
bytes = Buffer.concat([Buffer.from([this.#pendingSingleUtf8LeadByte]), data]);
|
|
331
|
+
this.#pendingSingleUtf8LeadByte = undefined;
|
|
332
|
+
} else {
|
|
333
|
+
const pendingMeta = this.#consumePendingSingleUtf8LeadAsMeta();
|
|
334
|
+
if (pendingMeta !== undefined) {
|
|
335
|
+
this.#emitDataSequence(pendingMeta);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (bytes.length === 1 && bytes[0]! > 127 && !this.#decoderHasPendingUtf8) {
|
|
341
|
+
const byte = bytes[0]!;
|
|
342
|
+
if (isUtf8LeadByte(byte)) {
|
|
343
|
+
this.#pendingSingleUtf8LeadByte = byte;
|
|
344
|
+
this.#decoderHasPendingUtf8 = true;
|
|
345
|
+
this.#timeout = setTimeout(() => {
|
|
346
|
+
const sequence = this.#consumePendingSingleUtf8LeadAsMeta();
|
|
347
|
+
if (sequence !== undefined) {
|
|
348
|
+
this.#emitDataSequence(sequence);
|
|
349
|
+
}
|
|
350
|
+
}, this.#timeoutMs);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
str = legacyMetaSequence(byte);
|
|
297
354
|
} else {
|
|
298
355
|
// Decode through the persistent StringDecoder so a multi-byte
|
|
299
356
|
// sequence split across chunks (e.g. a 3-byte Korean syllable)
|
|
300
357
|
// is reassembled instead of emitting U+FFFD.
|
|
301
|
-
str = this.#decoder.write(
|
|
358
|
+
str = this.#decoder.write(bytes);
|
|
302
359
|
decodedFromBuffer = true;
|
|
360
|
+
const allContinuationBytes = bytes.every(byte => byte >= 0x80 && byte <= 0xbf);
|
|
361
|
+
this.#decoderHasPendingUtf8 =
|
|
362
|
+
endsWithIncompleteUtf8Sequence(bytes) && !(hadPendingUtf8 && str.length > 0 && allContinuationBytes);
|
|
303
363
|
}
|
|
304
364
|
} else {
|
|
365
|
+
const pendingMeta = this.#consumePendingSingleUtf8LeadAsMeta();
|
|
366
|
+
if (pendingMeta !== undefined) {
|
|
367
|
+
this.#emitDataSequence(pendingMeta);
|
|
368
|
+
}
|
|
305
369
|
str = data;
|
|
306
370
|
}
|
|
307
371
|
|
|
@@ -348,6 +412,9 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
348
412
|
for (const sequence of result.sequences) {
|
|
349
413
|
this.#emitDataSequence(sequence);
|
|
350
414
|
}
|
|
415
|
+
if (result.remainder.length > 0) {
|
|
416
|
+
this.#emitDataSequence(result.remainder);
|
|
417
|
+
}
|
|
351
418
|
}
|
|
352
419
|
|
|
353
420
|
this.#pendingKittyPrintableCodepoint = undefined;
|
|
@@ -392,6 +459,13 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
392
459
|
}
|
|
393
460
|
}
|
|
394
461
|
|
|
462
|
+
#consumePendingSingleUtf8LeadAsMeta(): string | undefined {
|
|
463
|
+
const byte = this.#pendingSingleUtf8LeadByte;
|
|
464
|
+
if (byte === undefined) return undefined;
|
|
465
|
+
this.#pendingSingleUtf8LeadByte = undefined;
|
|
466
|
+
this.#decoderHasPendingUtf8 = false;
|
|
467
|
+
return legacyMetaSequence(byte);
|
|
468
|
+
}
|
|
395
469
|
#emitDataSequence(sequence: string): void {
|
|
396
470
|
const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
|
|
397
471
|
if (rawCodepoint !== undefined && rawCodepoint === this.#pendingKittyPrintableCodepoint) {
|
|
@@ -409,11 +483,13 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
409
483
|
this.#timeout = undefined;
|
|
410
484
|
}
|
|
411
485
|
|
|
486
|
+
const pendingMeta = this.#consumePendingSingleUtf8LeadAsMeta();
|
|
487
|
+
|
|
412
488
|
if (this.#buffer.length === 0) {
|
|
413
|
-
return [];
|
|
489
|
+
return pendingMeta === undefined ? [] : [pendingMeta];
|
|
414
490
|
}
|
|
415
491
|
|
|
416
|
-
const sequences = [this.#buffer];
|
|
492
|
+
const sequences = pendingMeta === undefined ? [this.#buffer] : [pendingMeta, this.#buffer];
|
|
417
493
|
this.#buffer = "";
|
|
418
494
|
this.#pendingKittyPrintableCodepoint = undefined;
|
|
419
495
|
return sequences;
|
|
@@ -432,6 +508,8 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
432
508
|
// stale partial prefix cannot combine with future input. destroy()
|
|
433
509
|
// resets the decoder by calling clear().
|
|
434
510
|
this.#decoder = new StringDecoder("utf8");
|
|
511
|
+
this.#decoderHasPendingUtf8 = false;
|
|
512
|
+
this.#pendingSingleUtf8LeadByte = undefined;
|
|
435
513
|
}
|
|
436
514
|
|
|
437
515
|
getBuffer(): string {
|