@oh-my-pi/pi-tui 15.10.2 → 15.10.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 +17 -1
- package/dist/types/terminal.d.ts +25 -9
- package/dist/types/tui.d.ts +1 -1
- package/package.json +3 -3
- package/src/terminal.ts +142 -33
- package/src/tui.ts +219 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.4] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Windows ConPTY session-resume painting the transcript with the last several rows truncated below the viewport until Alt+Tab forced a host repaint. After `sessionReplace`/`historyRebuild`/`overlayRebuild` paints that scroll-push content into native scrollback, the renderer now arms a 150 ms ConPTY settle window that coalesces spinner/blink-driven `requestRender(false)` calls into a single trailing render — Windows Terminal's viewport-follow logic no longer falls further behind the cursor on every tick of the post-paint storm. The arm also reclaims any render request queued *during* the in-flight composition (notably `ImageBudget.endPass()` calling `requestRender()` synchronously when a frame trips the live-graphics cap): without that, the queued request sat on the standard 30 Hz throttle and fired at ~33 ms — well inside the 150 ms quiet window — defeating the coalescing. Bumped the ConPTY per-`WriteFile` chunk cap from 8 KiB to 16 KiB so a multi-megabyte resume paint emits half as many writes (still well under the ~32 KiB threshold from #2034 that the original cap defends against), and made the cap measure encoded UTF-8 bytes instead of JS code units so a CJK-heavy transcript can't silently inflate a 16-KiB-of-code-units chunk into ~48 KiB of `WriteFile` traffic and reintroduce the #2034 viewport bug ([#2095](https://github.com/can1357/oh-my-pi/issues/2095)).
|
|
10
|
+
|
|
11
|
+
## [15.10.3] - 2026-06-08
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed DEC 2048 in-band resize reports (`CSI 48;rows;cols;hpx;wpx t`) leaking into the focused editor as literal text during a rapid resize. When the window is resized quickly the event loop stays busy long enough for the `StdinBuffer` flush timeout to fire mid-report; the `\x1b[48;…` prefix was emitted as one event and the tail (e.g. `8;125;1156;1125t`) arrived as bare printable characters that the editor inserted. `ProcessTerminal` now reassembles a split in-band report (including a split at the bare `\x1b[4` type field) until its terminator and then drives the resize. A reassembled sequence that turns out not to be a resize report — such as a split kitty key like `\x1b[48;5u` (codepoint 48 = `0`) — is forwarded to the input handler as a single escape sequence rather than dropped or leaked.
|
|
16
|
+
- Coalesced terminal-multiplexer SIGWINCH events into a single forced render once the pane stops resizing so closing/dragging a tmux/screen/zellij split no longer flashes the viewport blank before the new geometry repaints ([#2088](https://github.com/can1357/oh-my-pi/issues/2088)).
|
|
17
|
+
|
|
5
18
|
## [15.10.2] - 2026-06-08
|
|
19
|
+
|
|
6
20
|
### Added
|
|
7
21
|
|
|
8
22
|
- Added exported `canonicalKeyId` and `addKeyAliases` keybinding helpers so consumers can share the same canonical shortcut matching semantics as `KeybindingsManager`.
|
|
@@ -19,6 +33,7 @@
|
|
|
19
33
|
- Fixed Windows ConPTY hosts (Windows Terminal, Tabby, Hyper, VS Code) parking the viewport at the top of a full paint after a `/resume` or any long-session repaint. `ProcessTerminal#safeWrite` now splits oversized writes into ≤ 8 KiB pieces at line boundaries on `win32` and inside WSL (where stdout still crosses ConPTY at the `wslhost` boundary) so each underlying `WriteFile` stays below the ~32 KiB threshold where ConPTY stops tracking the cursor; the data was always delivered, but the host UI's scroll position would not follow until any focus event forced a re-query. ([#2034](https://github.com/can1357/oh-my-pi/issues/2034))
|
|
20
34
|
|
|
21
35
|
## [15.10.1] - 2026-06-07
|
|
36
|
+
|
|
22
37
|
### Breaking Changes
|
|
23
38
|
|
|
24
39
|
- Removed Kitty temp-file image transmission, its startup support probe, the `PI_KITTY_IMAGE_TRANSMISSION` override, and the temp-file helper exports. Kitty/Ghostty image payloads now stay on in-band base64 before placeholder/direct placement, avoiding blank first renders from temp-file load races.
|
|
@@ -85,6 +100,7 @@
|
|
|
85
100
|
- Fixed DECCARA background-fill optimization running when synchronized output is disabled, which could expose default-background gaps during rapidly updating tool-use panels ([#2000](https://github.com/can1357/oh-my-pi/issues/2000)).
|
|
86
101
|
|
|
87
102
|
## [15.9.67] - 2026-06-06
|
|
103
|
+
|
|
88
104
|
### Added
|
|
89
105
|
|
|
90
106
|
- Added `setPaddingX` to `Box` so horizontal padding can be updated programmatically after creation
|
|
@@ -1173,4 +1189,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
|
|
|
1173
1189
|
|
|
1174
1190
|
### Fixed
|
|
1175
1191
|
|
|
1176
|
-
- **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))
|
|
1192
|
+
- **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/dist/types/terminal.d.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Split `data` into chunks
|
|
3
|
-
* boundary (`\n`) as the cut point so
|
|
4
|
-
* `\n`) stay intact. The TUI's
|
|
5
|
-
* (`buffer += "\r\n"` between rows),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* Split `data` into chunks whose encoded UTF-8 byte length is no greater than
|
|
3
|
+
* `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
|
|
4
|
+
* escape sequences (which never contain `\n`) stay intact. The TUI's
|
|
5
|
+
* full-paint buffers are line-structured (`buffer += "\r\n"` between rows),
|
|
6
|
+
* so a newline almost always exists within the window. The fallback for a
|
|
7
|
+
* buffer with no newline in range is a hard cut at the last UTF-8 code-point
|
|
8
|
+
* boundary that still fits — the ConPTY viewport bug from a single oversized
|
|
9
|
+
* write is strictly worse than a one-frame escape-sequence glitch on a
|
|
10
|
+
* buffer the renderer effectively never produces.
|
|
11
|
+
*
|
|
12
|
+
* UTF-16 code units are walked manually rather than measuring with
|
|
13
|
+
* `Buffer.byteLength` per slice candidate: each code unit's UTF-8 width is
|
|
14
|
+
* known from its value (BMP `<0x80` → 1, `<0x800` → 2, surrogate pair → 4
|
|
15
|
+
* bytes across two units, other BMP → 3), and surrogate pairs are kept
|
|
16
|
+
* together so the chunker never splits a non-BMP character.
|
|
10
17
|
*
|
|
11
18
|
* Exported for unit testing of the chunking contract; `#safeWrite` is the
|
|
12
19
|
* sole production caller.
|
|
13
20
|
*/
|
|
14
|
-
export declare function chunkForConPTY(data: string,
|
|
21
|
+
export declare function chunkForConPTY(data: string, maxChunkBytes?: number): string[];
|
|
15
22
|
/**
|
|
16
23
|
* Emergency terminal restore - call this from signal/crash handlers
|
|
17
24
|
* Resets terminal state without requiring access to the ProcessTerminal instance
|
|
@@ -91,6 +98,15 @@ export interface Terminal {
|
|
|
91
98
|
*/
|
|
92
99
|
onPrivateModeReport?(callback: (mode: number, supported: boolean) => void): void;
|
|
93
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* True when stdout flows through a ConPTY pseudo-console (native win32, or
|
|
103
|
+
* Linux running under WSL where stdout still crosses into ConPTY at the
|
|
104
|
+
* `wslhost` boundary). ConPTY hosts share the per-WriteFile viewport-tracking
|
|
105
|
+
* quirks documented above and on {@link MAX_CONPTY_WRITE_CHUNK_BYTES}, so both
|
|
106
|
+
* `#safeWrite` and the renderer's post-big-paint settle gate hang off this
|
|
107
|
+
* single predicate.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isConPTYHosted(): boolean;
|
|
94
110
|
/**
|
|
95
111
|
* Real terminal using process.stdin/stdout
|
|
96
112
|
*/
|
package/dist/types/tui.d.ts
CHANGED
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.10.
|
|
4
|
+
"version": "15.10.4",
|
|
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.10.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.10.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.10.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.10.4",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
package/src/terminal.ts
CHANGED
|
@@ -10,7 +10,7 @@ const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
|
|
10
10
|
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Maximum bytes per `process.stdout.write` call on Windows.
|
|
13
|
+
* Maximum encoded UTF-8 bytes per `process.stdout.write` call on Windows.
|
|
14
14
|
*
|
|
15
15
|
* Windows ConPTY ties viewport tracking to per-`WriteFile` boundaries: when a
|
|
16
16
|
* single write exceeds ~32-64 KB, the pseudo-console stops following the
|
|
@@ -20,43 +20,96 @@ const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
|
|
20
20
|
* first ~30 lines until any focus event forces the host to re-query the
|
|
21
21
|
* cursor. The data is delivered correctly — it's purely a viewport-sync bug.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The cap is on **encoded UTF-8 bytes**, not JS code units, because
|
|
24
|
+
* `process.stdout.write(string)` UTF-8-encodes before handing off to
|
|
25
|
+
* `WriteFile`. A pure-CJK transcript row encodes to ~3 bytes per BMP code
|
|
26
|
+
* unit, so a code-unit-based cap of 16 KiB could land at ~48 KiB of actual
|
|
27
|
+
* `WriteFile` traffic and reintroduce the #2034 parked-viewport bug for
|
|
28
|
+
* non-ASCII content.
|
|
29
|
+
*
|
|
30
|
+
* 16 KiB is half the smallest observed Windows Terminal threshold (32 KiB),
|
|
31
|
+
* which keeps the per-write parked-viewport bug fixed by #2034 while halving
|
|
32
|
+
* the WriteFile count on multi-megabyte paints (a 3 MB session resume splits
|
|
33
|
+
* into ~192 chunks instead of ~384). Fewer WriteFiles means fewer chances for
|
|
34
|
+
* WT's viewport-following logic to lose track of the cursor during the burst,
|
|
35
|
+
* which mitigates the residual mid-paint drift the original 8 KiB cap left
|
|
36
|
+
* behind (#2095). Still well clear of the threshold so the other ConPTY hosts
|
|
37
|
+
* (Tabby, Hyper, VS Code) — where the exact limit is undocumented — keep
|
|
38
|
+
* their safety margin.
|
|
27
39
|
*/
|
|
28
|
-
const
|
|
40
|
+
const MAX_CONPTY_WRITE_CHUNK_BYTES = 16 * 1024;
|
|
29
41
|
|
|
30
42
|
/**
|
|
31
|
-
* Split `data` into chunks
|
|
32
|
-
* boundary (`\n`) as the cut point so
|
|
33
|
-
* `\n`) stay intact. The TUI's
|
|
34
|
-
* (`buffer += "\r\n"` between rows),
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
43
|
+
* Split `data` into chunks whose encoded UTF-8 byte length is no greater than
|
|
44
|
+
* `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
|
|
45
|
+
* escape sequences (which never contain `\n`) stay intact. The TUI's
|
|
46
|
+
* full-paint buffers are line-structured (`buffer += "\r\n"` between rows),
|
|
47
|
+
* so a newline almost always exists within the window. The fallback for a
|
|
48
|
+
* buffer with no newline in range is a hard cut at the last UTF-8 code-point
|
|
49
|
+
* boundary that still fits — the ConPTY viewport bug from a single oversized
|
|
50
|
+
* write is strictly worse than a one-frame escape-sequence glitch on a
|
|
51
|
+
* buffer the renderer effectively never produces.
|
|
52
|
+
*
|
|
53
|
+
* UTF-16 code units are walked manually rather than measuring with
|
|
54
|
+
* `Buffer.byteLength` per slice candidate: each code unit's UTF-8 width is
|
|
55
|
+
* known from its value (BMP `<0x80` → 1, `<0x800` → 2, surrogate pair → 4
|
|
56
|
+
* bytes across two units, other BMP → 3), and surrogate pairs are kept
|
|
57
|
+
* together so the chunker never splits a non-BMP character.
|
|
39
58
|
*
|
|
40
59
|
* Exported for unit testing of the chunking contract; `#safeWrite` is the
|
|
41
60
|
* sole production caller.
|
|
42
61
|
*/
|
|
43
|
-
export function chunkForConPTY(data: string,
|
|
44
|
-
|
|
62
|
+
export function chunkForConPTY(data: string, maxChunkBytes: number = MAX_CONPTY_WRITE_CHUNK_BYTES): string[] {
|
|
63
|
+
// Fast path: whole buffer fits in one write.
|
|
64
|
+
if (Buffer.byteLength(data, "utf8") <= maxChunkBytes) return [data];
|
|
45
65
|
const chunks: string[] = [];
|
|
66
|
+
const len = data.length;
|
|
46
67
|
let pos = 0;
|
|
47
|
-
while (pos <
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
while (pos < len) {
|
|
69
|
+
let bytes = 0;
|
|
70
|
+
// Index just past the most recent `\n` we've consumed inside [pos, i):
|
|
71
|
+
// the natural cut point that leaves escape sequences intact.
|
|
72
|
+
let lastNewlineEnd = -1;
|
|
73
|
+
let i = pos;
|
|
74
|
+
while (i < len) {
|
|
75
|
+
const cu = data.charCodeAt(i);
|
|
76
|
+
let cuLen = 1;
|
|
77
|
+
let cuBytes: number;
|
|
78
|
+
if (cu < 0x80) {
|
|
79
|
+
cuBytes = 1;
|
|
80
|
+
} else if (cu < 0x800) {
|
|
81
|
+
cuBytes = 2;
|
|
82
|
+
} else if (cu >= 0xd800 && cu < 0xdc00) {
|
|
83
|
+
// High surrogate: pair with the following low surrogate (4 bytes
|
|
84
|
+
// across two code units); an unpaired surrogate UTF-8-encodes as
|
|
85
|
+
// the 3-byte U+FFFD replacement character.
|
|
86
|
+
const next = i + 1 < len ? data.charCodeAt(i + 1) : 0;
|
|
87
|
+
if (next >= 0xdc00 && next < 0xe000) {
|
|
88
|
+
cuBytes = 4;
|
|
89
|
+
cuLen = 2;
|
|
90
|
+
} else {
|
|
91
|
+
cuBytes = 3;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// BMP non-surrogate or unpaired low surrogate → 3 bytes.
|
|
95
|
+
cuBytes = 3;
|
|
96
|
+
}
|
|
97
|
+
if (bytes + cuBytes > maxChunkBytes && i > pos) {
|
|
98
|
+
// Would overflow the cap. Cut at the last newline if we found one,
|
|
99
|
+
// otherwise hard-cut at the current code-point boundary.
|
|
100
|
+
const cut = lastNewlineEnd > pos ? lastNewlineEnd : i;
|
|
101
|
+
chunks.push(data.slice(pos, cut));
|
|
102
|
+
pos = cut;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
bytes += cuBytes;
|
|
106
|
+
i += cuLen;
|
|
107
|
+
if (cu === 0x0a) lastNewlineEnd = i;
|
|
108
|
+
}
|
|
109
|
+
if (i >= len) {
|
|
50
110
|
chunks.push(data.slice(pos));
|
|
51
|
-
|
|
111
|
+
pos = len;
|
|
52
112
|
}
|
|
53
|
-
const windowEnd = pos + maxChunkSize;
|
|
54
|
-
// Prefer the last newline inside the window so escape sequences stay
|
|
55
|
-
// intact within their chunk; hard-cut at `windowEnd` otherwise.
|
|
56
|
-
const nl = data.lastIndexOf("\n", windowEnd - 1);
|
|
57
|
-
const cut = nl >= pos ? nl + 1 : windowEnd;
|
|
58
|
-
chunks.push(data.slice(pos, cut));
|
|
59
|
-
pos = cut;
|
|
60
113
|
}
|
|
61
114
|
return chunks;
|
|
62
115
|
}
|
|
@@ -202,7 +255,17 @@ export interface Terminal {
|
|
|
202
255
|
onPrivateModeReport?(callback: (mode: number, supported: boolean) => void): void;
|
|
203
256
|
}
|
|
204
257
|
|
|
205
|
-
|
|
258
|
+
/**
|
|
259
|
+
* True when stdout flows through a ConPTY pseudo-console (native win32, or
|
|
260
|
+
* Linux running under WSL where stdout still crosses into ConPTY at the
|
|
261
|
+
* `wslhost` boundary). ConPTY hosts share the per-WriteFile viewport-tracking
|
|
262
|
+
* quirks documented above and on {@link MAX_CONPTY_WRITE_CHUNK_BYTES}, so both
|
|
263
|
+
* `#safeWrite` and the renderer's post-big-paint settle gate hang off this
|
|
264
|
+
* single predicate.
|
|
265
|
+
*/
|
|
266
|
+
export function isConPTYHosted(): boolean {
|
|
267
|
+
if (process.platform === "win32") return true;
|
|
268
|
+
// WSL: stdout still crosses into ConPTY at the `wslhost` boundary.
|
|
206
269
|
return process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
|
|
207
270
|
}
|
|
208
271
|
|
|
@@ -255,6 +318,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
255
318
|
#privateModeCallbacks: Array<(mode: number, supported: boolean) => void> = [];
|
|
256
319
|
/** Whether DEC 2048 in-band resize notifications are currently enabled. */
|
|
257
320
|
#inBandResizeActive = false;
|
|
321
|
+
/** Reassembly buffer for a DEC 2048 in-band resize report split across stdin reads. */
|
|
322
|
+
#inBandResizeBuffer = "";
|
|
258
323
|
#reportedColumns?: number;
|
|
259
324
|
#reportedRows?: number;
|
|
260
325
|
#osc11PollTimer?: Timer;
|
|
@@ -347,7 +412,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
347
412
|
// Windows Terminal under WSL has been observed to close the hosting tab
|
|
348
413
|
// after repeated OSC 11/DA1 probes. Keep the initial/event-driven probes,
|
|
349
414
|
// but avoid background polling there.
|
|
350
|
-
|
|
415
|
+
const isWSL = process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
|
|
416
|
+
if (!isWSL) {
|
|
351
417
|
this.#startOsc11Poll();
|
|
352
418
|
}
|
|
353
419
|
|
|
@@ -488,6 +554,46 @@ export class ProcessTerminal implements Terminal {
|
|
|
488
554
|
}
|
|
489
555
|
}
|
|
490
556
|
|
|
557
|
+
// In-band resize report (DEC 2048) split across stdin reads. The report
|
|
558
|
+
// is `\x1b[48;rows;cols;yPx;xPx t`; when the StdinBuffer flush timeout
|
|
559
|
+
// elapses mid-sequence — common during a rapid resize that keeps the
|
|
560
|
+
// event loop busy — the `\x1b[48;…` prefix arrives as one event and the
|
|
561
|
+
// tail (`…;xPx t`) arrives as bare character events that would otherwise
|
|
562
|
+
// leak into the prompt as literal keystrokes. Reassemble until the
|
|
563
|
+
// terminator, then fall through to the resize handler below. A
|
|
564
|
+
// reassembled sequence that turns out not to be a resize report (e.g. a
|
|
565
|
+
// split kitty `\x1b[48;…u` for a digit key) is forwarded to the input
|
|
566
|
+
// handler rather than dropped.
|
|
567
|
+
const inBandResizePartialPattern = /^\x1b\[4[\d;]*$/;
|
|
568
|
+
const isInBandResizePartial = this.#inBandResizeActive && inBandResizePartialPattern.test(sequence);
|
|
569
|
+
if (this.#inBandResizeBuffer && sequence.startsWith("\x1b")) {
|
|
570
|
+
// A new escape interrupted the partial; the stale partial is
|
|
571
|
+
// unrecoverable. If the new escape is itself an in-band prefix,
|
|
572
|
+
// restart reassembly with it; otherwise let it flow through below.
|
|
573
|
+
this.#inBandResizeBuffer = isInBandResizePartial ? sequence : "";
|
|
574
|
+
if (isInBandResizePartial) return;
|
|
575
|
+
} else if (this.#inBandResizeBuffer || isInBandResizePartial) {
|
|
576
|
+
this.#inBandResizeBuffer += sequence;
|
|
577
|
+
if (this.#inBandResizeBuffer.length > 256) {
|
|
578
|
+
this.#inBandResizeBuffer = "";
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const lastCode = this.#inBandResizeBuffer.charCodeAt(this.#inBandResizeBuffer.length - 1);
|
|
582
|
+
if (lastCode >= 0x40 && lastCode <= 0x7e) {
|
|
583
|
+
// Terminator arrived: let the resize handler below claim it, or
|
|
584
|
+
// fall through to the input handler if it is not a resize report.
|
|
585
|
+
sequence = this.#inBandResizeBuffer;
|
|
586
|
+
this.#inBandResizeBuffer = "";
|
|
587
|
+
} else if (!inBandResizePartialPattern.test(this.#inBandResizeBuffer)) {
|
|
588
|
+
// Diverged from a valid in-band prefix — drop the garbled report.
|
|
589
|
+
this.#inBandResizeBuffer = "";
|
|
590
|
+
return;
|
|
591
|
+
} else {
|
|
592
|
+
// Still accumulating the report.
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
491
597
|
// In-band resize report (DEC mode 2048). Unsolicited and not tied to a
|
|
492
598
|
// sentinel: update reported geometry + cell size, then drive the resize
|
|
493
599
|
// handler so the renderer reflows.
|
|
@@ -970,6 +1076,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
970
1076
|
this.#osc99Capabilities.clear();
|
|
971
1077
|
setOsc99Supported(false);
|
|
972
1078
|
this.#privateCsiResponseBuffer = "";
|
|
1079
|
+
this.#inBandResizeBuffer = "";
|
|
973
1080
|
this.#da1SentinelOwners.length = 0;
|
|
974
1081
|
this.#privateModeCallbacks = [];
|
|
975
1082
|
this.#privateModeSupport.clear();
|
|
@@ -1047,10 +1154,12 @@ export class ProcessTerminal implements Terminal {
|
|
|
1047
1154
|
// WSL — `process.platform === "linux"` there, but stdout still
|
|
1048
1155
|
// crosses into ConPTY at the `wslhost` boundary, so the same per-
|
|
1049
1156
|
// WriteFile cap applies. Non-ConPTY PTYs keep the single-write fast
|
|
1050
|
-
// path.
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1157
|
+
// path. The cap is on encoded UTF-8 bytes, not JS code units, because
|
|
1158
|
+
// `process.stdout.write(string)` UTF-8-encodes before `WriteFile`,
|
|
1159
|
+
// and a code-unit cap would let CJK transcript rows expand past the
|
|
1160
|
+
// threshold. See #2034 and #2095.
|
|
1161
|
+
if (isConPTYHosted() && Buffer.byteLength(data, "utf8") > MAX_CONPTY_WRITE_CHUNK_BYTES) {
|
|
1162
|
+
for (const chunk of chunkForConPTY(data, MAX_CONPTY_WRITE_CHUNK_BYTES)) {
|
|
1054
1163
|
process.stdout.write(chunk);
|
|
1055
1164
|
}
|
|
1056
1165
|
} else {
|
package/src/tui.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { $flag, getDebugLogPath } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import { DEFAULT_MAX_INLINE_IMAGES, ImageBudget } from "./components/image";
|
|
18
18
|
import { planDeccaraFills } from "./deccara";
|
|
19
19
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
20
|
-
import type
|
|
20
|
+
import { isConPTYHosted, type Terminal } from "./terminal";
|
|
21
21
|
import {
|
|
22
22
|
encodeKittyDeleteImage,
|
|
23
23
|
ImageProtocol,
|
|
@@ -74,11 +74,13 @@ const CURSOR_BEGIN = `${HIDE_CURSOR}${SYNC_OUTPUT_BEGIN}`;
|
|
|
74
74
|
const CURSOR_BEGIN_NO_SYNC = HIDE_CURSOR;
|
|
75
75
|
const CURSOR_END = SYNC_OUTPUT_END;
|
|
76
76
|
const CURSOR_END_NO_SYNC = "";
|
|
77
|
-
// Mouse reporting
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// Mouse reporting, enabled only for the lifetime of a fullscreen overlay so the
|
|
78
|
+
// rest of the app keeps the terminal's native text selection. 1000h = button
|
|
79
|
+
// click tracking, 1003h = any-motion tracking so overlays can light up hover
|
|
80
|
+
// targets (the pointer moving with no button held), 1006h = SGR extended
|
|
81
|
+
// coordinates so columns/rows past 223 are reported.
|
|
82
|
+
const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
83
|
+
const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
82
84
|
|
|
83
85
|
type InputListenerResult = { consume?: boolean; data?: string } | undefined;
|
|
84
86
|
type InputListener = (data: string) => InputListenerResult;
|
|
@@ -482,6 +484,36 @@ export class TUI extends Container {
|
|
|
482
484
|
#renderScheduler: RenderScheduler;
|
|
483
485
|
#lastRenderAt = 0;
|
|
484
486
|
static readonly #MIN_RENDER_INTERVAL_MS = 1000 / 30;
|
|
487
|
+
// Pane-reflow settle window for tmux/screen/zellij. The host process gets
|
|
488
|
+
// SIGWINCH (and `process.stdout` already reports the new geometry) before
|
|
489
|
+
// the multiplexer finishes repainting the pane at the new size, and
|
|
490
|
+
// drag-resize/pane-close animations fire several events in flight. A forced
|
|
491
|
+
// render on each SIGWINCH races those mid-reflow paints — the multiplexer's
|
|
492
|
+
// catch-up paint then partially overwrites the TUI output, which the user
|
|
493
|
+
// sees as a viewport flash or blank screen before the next throttled frame
|
|
494
|
+
// arrives (issue #2088). Coalescing every SIGWINCH inside this window into
|
|
495
|
+
// a single forced render lets the multiplexer settle first.
|
|
496
|
+
static readonly #MULTIPLEXER_RESIZE_DEBOUNCE_MS = 50;
|
|
497
|
+
// Post-paint settle window for ConPTY hosts. The `sessionReplace` /
|
|
498
|
+
// `historyRebuild` / `overlayRebuild` intents drive `#emitFullPaint` over
|
|
499
|
+
// a transcript that overflows the viewport, scroll-pushing everything past
|
|
500
|
+
// the last `height` rows into native scrollback. Windows Terminal's
|
|
501
|
+
// viewport-follow logic gets lossy during that burst: spinner/blink-driven
|
|
502
|
+
// `requestRender(false)` calls firing inside the window each produce another
|
|
503
|
+
// diff write, and the WT host processes them faster than its viewport
|
|
504
|
+
// tracker can keep up — the visible tail ends up parked a few rows above
|
|
505
|
+
// the actual last row until any focus event (Alt+Tab) forces a host repaint.
|
|
506
|
+
// Coalescing every non-forced render inside this window into a single
|
|
507
|
+
// trailing render lets the host fully settle the big paint before any
|
|
508
|
+
// follow-up writes touch the buffer. The first-ever `initial` paint is
|
|
509
|
+
// deliberately exempt: nothing has been on screen yet, so no drift can
|
|
510
|
+
// have accumulated, and tests that start the TUI over an over-tall
|
|
511
|
+
// component depend on the next paint firing without delay. Only armed on
|
|
512
|
+
// ConPTY hosts (`isConPTYHosted()`); other terminals do not exhibit the
|
|
513
|
+
// drift and would just see an unnecessary post-paint latency. See #2095.
|
|
514
|
+
static readonly #CONPTY_POST_FULL_PAINT_SETTLE_MS = 150;
|
|
515
|
+
#postFullPaintSettleUntilMs = 0;
|
|
516
|
+
#postFullPaintSettleTimer: RenderTimer | undefined;
|
|
485
517
|
#cursorRow = 0; // Logical cursor row (end of rendered content)
|
|
486
518
|
#hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
|
487
519
|
#hardwareCursorState: HardwareCursorState | null = null;
|
|
@@ -549,6 +581,13 @@ export class TUI extends Container {
|
|
|
549
581
|
// between the viewport and scrollback, so the previous frame no longer
|
|
550
582
|
// describes the screen. Tracking only the dimension delta misses this.
|
|
551
583
|
#resizeEventPending = false;
|
|
584
|
+
// Active multiplexer SIGWINCH debounce. Reset on each event so the timer
|
|
585
|
+
// only fires once the pane stops resizing. Forced renders (resetDisplay,
|
|
586
|
+
// finishSixelProbe, …) issued during the settle window route through the
|
|
587
|
+
// same timer; their `clearScrollback` intent is OR'd into the deferred
|
|
588
|
+
// flag below so the settled paint still honours every caller's request.
|
|
589
|
+
#multiplexerResizeTimer: RenderTimer | undefined;
|
|
590
|
+
#deferredForcedClearScrollback = false;
|
|
552
591
|
#stopped = false;
|
|
553
592
|
|
|
554
593
|
// Transient alternate-screen state for a fullscreen overlay. While active, the
|
|
@@ -858,12 +897,29 @@ export class TUI extends Container {
|
|
|
858
897
|
this.terminal.start(
|
|
859
898
|
data => this.#handleInput(data),
|
|
860
899
|
() => {
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
900
|
+
// Real terminals deliver SIGWINCH (and the equivalent ConPTY
|
|
901
|
+
// notification) atomically with the new `process.stdout` geometry, so
|
|
902
|
+
// a forced render must fire immediately: it clears and replays at the
|
|
903
|
+
// fresh size before the terminal's reflow settles into a state a
|
|
904
|
+
// throttled frame would race. Multiplexer panes (tmux/screen/zellij)
|
|
905
|
+
// do not give that guarantee. The host receives SIGWINCH while the
|
|
906
|
+
// multiplexer is still mid-reflow — it has not finished repainting
|
|
907
|
+
// the pane buffer at the new size — and a drag-resize or pane-close
|
|
908
|
+
// animation fires several events in flight. Forcing a render on each
|
|
909
|
+
// event races those mid-reflow paints: the multiplexer's catch-up
|
|
910
|
+
// paint then partially overwrites the TUI output, which the user sees
|
|
911
|
+
// as a viewport flash or blank screen before the next throttled
|
|
912
|
+
// frame arrives (issue #2088). `#armMultiplexerResizeTimer` coalesces
|
|
913
|
+
// SIGWINCHes (and any forced repaints arriving during the settle
|
|
914
|
+
// window) into a single render once the pane is quiet —
|
|
915
|
+
// `#resizeEventPending` is set first so the eventual render still
|
|
916
|
+
// classifies as a resize.
|
|
865
917
|
this.#resizeEventPending = true;
|
|
866
|
-
|
|
918
|
+
if (!isMultiplexerSession()) {
|
|
919
|
+
this.requestRender(true);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
this.#armMultiplexerResizeTimer(false);
|
|
867
923
|
},
|
|
868
924
|
);
|
|
869
925
|
for (const listener of this.#startListeners) {
|
|
@@ -1066,6 +1122,12 @@ export class TUI extends Container {
|
|
|
1066
1122
|
this.#renderTimer.cancel();
|
|
1067
1123
|
this.#renderTimer = undefined;
|
|
1068
1124
|
}
|
|
1125
|
+
if (this.#multiplexerResizeTimer) {
|
|
1126
|
+
this.#multiplexerResizeTimer.cancel();
|
|
1127
|
+
this.#multiplexerResizeTimer = undefined;
|
|
1128
|
+
}
|
|
1129
|
+
this.#clearPostFullPaintSettle();
|
|
1130
|
+
this.#deferredForcedClearScrollback = false;
|
|
1069
1131
|
// Place the parent shell on the first line after the rendered content. When
|
|
1070
1132
|
// that line is still inside the viewport, moving there and writing `\r` is
|
|
1071
1133
|
// enough; emitting `\r\n` would create an extra blank row. If the content
|
|
@@ -1140,6 +1202,15 @@ export class TUI extends Container {
|
|
|
1140
1202
|
resetDisplay(): void {
|
|
1141
1203
|
if (this.#stopped) return;
|
|
1142
1204
|
this.invalidate();
|
|
1205
|
+
// A reset that lands inside a tmux/screen/zellij resize burst would
|
|
1206
|
+
// paint mid-reflow and re-introduce the flash race (issue #2088).
|
|
1207
|
+
// Fold it into the in-flight debounce instead; the settled paint runs
|
|
1208
|
+
// the same `#prepareForcedRender(!isMultiplexerSession())` path via
|
|
1209
|
+
// `requestRender(true)`, so the clear-scrollback intent is preserved.
|
|
1210
|
+
if (this.#multiplexerResizeTimer) {
|
|
1211
|
+
this.#armMultiplexerResizeTimer(!isMultiplexerSession());
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1143
1214
|
this.#prepareForcedRender(!isMultiplexerSession());
|
|
1144
1215
|
this.#resizeEventPending = true;
|
|
1145
1216
|
this.#renderRequested = false;
|
|
@@ -1151,6 +1222,23 @@ export class TUI extends Container {
|
|
|
1151
1222
|
const allowUnknownViewportMutation = options?.allowUnknownViewportMutation === true;
|
|
1152
1223
|
this.#allowUnknownViewportMutationOnNextRender ||= allowUnknownViewportMutation;
|
|
1153
1224
|
if (force) {
|
|
1225
|
+
// Forced repaints landing inside the multiplexer resize debounce
|
|
1226
|
+
// (e.g. `#finishSixelProbe`, image-budget eviction, a programmatic
|
|
1227
|
+
// `requestRender(true)`) would paint into a still-reflowing pane
|
|
1228
|
+
// and reintroduce the flash race. Fold them into the in-flight
|
|
1229
|
+
// debounce while preserving the caller's `clearScrollback` intent
|
|
1230
|
+
// for the settled paint. The timer's own callback clears
|
|
1231
|
+
// `#multiplexerResizeTimer` before re-entering `requestRender(true)`,
|
|
1232
|
+
// so this guard only catches external callers — the deferred render
|
|
1233
|
+
// itself proceeds straight to `#prepareForcedRender`.
|
|
1234
|
+
if (this.#multiplexerResizeTimer) {
|
|
1235
|
+
this.#armMultiplexerResizeTimer(options?.clearScrollback === true);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
// A forced render preempts the post-full-paint ConPTY settle: it owns
|
|
1239
|
+
// the next paint and is going to redraw the buffer anyway, so the
|
|
1240
|
+
// trailing coalesced render queued by the settle would only race it.
|
|
1241
|
+
this.#clearPostFullPaintSettle();
|
|
1154
1242
|
this.#prepareForcedRender(options?.clearScrollback === true);
|
|
1155
1243
|
this.#renderRequested = true;
|
|
1156
1244
|
this.#renderScheduler.scheduleImmediate(() => {
|
|
@@ -1163,11 +1251,121 @@ export class TUI extends Container {
|
|
|
1163
1251
|
});
|
|
1164
1252
|
return;
|
|
1165
1253
|
}
|
|
1254
|
+
// Coalesce non-forced renders inside the post-full-paint ConPTY settle
|
|
1255
|
+
// window into one trailing render. Spinner/blink/streaming components
|
|
1256
|
+
// otherwise fire `requestRender(false)` at 30 Hz while the host is still
|
|
1257
|
+
// catching up with the previous big paint, and each follow-up viewport
|
|
1258
|
+
// repaint nudges Windows Terminal's viewport tracker further off the
|
|
1259
|
+
// last row (see #2095).
|
|
1260
|
+
if (this.#postFullPaintSettleUntilMs > 0) {
|
|
1261
|
+
const now = this.#renderScheduler.now();
|
|
1262
|
+
if (now < this.#postFullPaintSettleUntilMs) {
|
|
1263
|
+
if (this.#postFullPaintSettleTimer === undefined) {
|
|
1264
|
+
this.#postFullPaintSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1265
|
+
this.#postFullPaintSettleTimer = undefined;
|
|
1266
|
+
this.#postFullPaintSettleUntilMs = 0;
|
|
1267
|
+
if (this.#stopped) return;
|
|
1268
|
+
this.requestRender(false);
|
|
1269
|
+
}, this.#postFullPaintSettleUntilMs - now);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
this.#postFullPaintSettleUntilMs = 0;
|
|
1274
|
+
}
|
|
1166
1275
|
if (this.#renderRequested) return;
|
|
1167
1276
|
this.#renderRequested = true;
|
|
1168
1277
|
this.#renderScheduler.scheduleImmediate(() => this.#scheduleRender());
|
|
1169
1278
|
}
|
|
1170
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Arm or extend the multiplexer-resize debounce so a single forced render
|
|
1282
|
+
* fires once the pane is quiet. Called by the SIGWINCH callback on every
|
|
1283
|
+
* resize event, and by `requestRender(true)` / `resetDisplay()` when they
|
|
1284
|
+
* land inside an in-flight settle window. Each call cancels the prior
|
|
1285
|
+
* timer, supersedes any queued throttled render (otherwise it would race
|
|
1286
|
+
* tmux's mid-reflow paint), and OR's the caller's `clearScrollback`
|
|
1287
|
+
* intent into `#deferredForcedClearScrollback` — the timer's callback
|
|
1288
|
+
* consumes that flag exactly once when it re-enters `requestRender(true)`.
|
|
1289
|
+
*/
|
|
1290
|
+
#armMultiplexerResizeTimer(clearScrollback: boolean): void {
|
|
1291
|
+
this.#deferredForcedClearScrollback ||= clearScrollback;
|
|
1292
|
+
if (this.#renderTimer) {
|
|
1293
|
+
this.#renderTimer.cancel();
|
|
1294
|
+
this.#renderTimer = undefined;
|
|
1295
|
+
}
|
|
1296
|
+
this.#renderRequested = false;
|
|
1297
|
+
if (this.#multiplexerResizeTimer) {
|
|
1298
|
+
this.#multiplexerResizeTimer.cancel();
|
|
1299
|
+
}
|
|
1300
|
+
this.#multiplexerResizeTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1301
|
+
this.#multiplexerResizeTimer = undefined;
|
|
1302
|
+
if (this.#stopped) {
|
|
1303
|
+
this.#deferredForcedClearScrollback = false;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const deferredClearScrollback = this.#deferredForcedClearScrollback;
|
|
1307
|
+
this.#deferredForcedClearScrollback = false;
|
|
1308
|
+
this.requestRender(true, { clearScrollback: deferredClearScrollback });
|
|
1309
|
+
}, TUI.#MULTIPLEXER_RESIZE_DEBOUNCE_MS);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Arm the post-full-paint settle window after an `#emitFullPaint` that
|
|
1314
|
+
* pushed content into native scrollback on a ConPTY host. Idempotent inside
|
|
1315
|
+
* the window: a later overflowing paint extends `until` to the later
|
|
1316
|
+
* deadline so back-to-back big paints do not double-fire the trailing
|
|
1317
|
+
* coalesced render, and the existing deferred timer is rescheduled to the
|
|
1318
|
+
* later deadline.
|
|
1319
|
+
*
|
|
1320
|
+
* Mid-composition callers (most notably `ImageBudget.endPass()`, which can
|
|
1321
|
+
* call `requestRender()` from inside the in-flight paint when a new image
|
|
1322
|
+
* trips the budget) queue their render *before* the settle exists, so they
|
|
1323
|
+
* fall through the gate and set `#renderRequested` / `#renderTimer` on the
|
|
1324
|
+
* 30 Hz throttle. Without absorbing those, the throttled follow-up fires
|
|
1325
|
+
* inside the 150 ms quiet window and reintroduces the cascade the settle
|
|
1326
|
+
* was meant to stop. Cancel both, then eagerly arm the trailing settle
|
|
1327
|
+
* timer so the in-flight request still rides one coalesced render at the
|
|
1328
|
+
* end of the window. See #2095.
|
|
1329
|
+
*/
|
|
1330
|
+
#armPostFullPaintSettle(): void {
|
|
1331
|
+
if (!isConPTYHosted()) return;
|
|
1332
|
+
const until = this.#renderScheduler.now() + TUI.#CONPTY_POST_FULL_PAINT_SETTLE_MS;
|
|
1333
|
+
if (until <= this.#postFullPaintSettleUntilMs) return;
|
|
1334
|
+
this.#postFullPaintSettleUntilMs = until;
|
|
1335
|
+
const hadPendingRender = this.#renderRequested || this.#renderTimer !== undefined;
|
|
1336
|
+
// Reclaim any render that was queued during the in-flight composition:
|
|
1337
|
+
// `#renderRequested` was set before the settle existed and would
|
|
1338
|
+
// otherwise fire on the standard throttle inside the window.
|
|
1339
|
+
this.#renderRequested = false;
|
|
1340
|
+
if (this.#renderTimer) {
|
|
1341
|
+
this.#renderTimer.cancel();
|
|
1342
|
+
this.#renderTimer = undefined;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.#postFullPaintSettleTimer) {
|
|
1345
|
+
this.#postFullPaintSettleTimer.cancel();
|
|
1346
|
+
this.#postFullPaintSettleTimer = undefined;
|
|
1347
|
+
}
|
|
1348
|
+
if (hadPendingRender) {
|
|
1349
|
+
// Replay the absorbed request via the trailing settle timer so the
|
|
1350
|
+
// caller's render still happens — just deferred to the end of the
|
|
1351
|
+
// window. Subsequent `requestRender(false)` calls during the
|
|
1352
|
+
// settle see this timer and fold into it (existing gate at L1263).
|
|
1353
|
+
this.#postFullPaintSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1354
|
+
this.#postFullPaintSettleTimer = undefined;
|
|
1355
|
+
this.#postFullPaintSettleUntilMs = 0;
|
|
1356
|
+
if (this.#stopped) return;
|
|
1357
|
+
this.requestRender(false);
|
|
1358
|
+
}, TUI.#CONPTY_POST_FULL_PAINT_SETTLE_MS);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
#clearPostFullPaintSettle(): void {
|
|
1363
|
+
if (this.#postFullPaintSettleTimer) {
|
|
1364
|
+
this.#postFullPaintSettleTimer.cancel();
|
|
1365
|
+
this.#postFullPaintSettleTimer = undefined;
|
|
1366
|
+
}
|
|
1367
|
+
this.#postFullPaintSettleUntilMs = 0;
|
|
1368
|
+
}
|
|
1171
1369
|
#prepareForcedRender(clearScrollback: boolean): void {
|
|
1172
1370
|
const geometryChanged =
|
|
1173
1371
|
(this.#previousWidth > 0 && this.#previousWidth !== this.terminal.columns) ||
|
|
@@ -1191,6 +1389,13 @@ export class TUI extends Container {
|
|
|
1191
1389
|
if (this.#stopped || this.#renderTimer || !this.#renderRequested) {
|
|
1192
1390
|
return;
|
|
1193
1391
|
}
|
|
1392
|
+
// Defer any new throttled render scheduled inside the multiplexer
|
|
1393
|
+
// resize settle window: it would race tmux's mid-reflow pane repaint.
|
|
1394
|
+
// `#renderRequested` stays set so the eventual forced render — armed
|
|
1395
|
+
// by the SIGWINCH callback — picks up the latest component state.
|
|
1396
|
+
if (this.#multiplexerResizeTimer) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1194
1399
|
const elapsed = this.#renderScheduler.now() - this.#lastRenderAt;
|
|
1195
1400
|
const delay = Math.max(0, TUI.#MIN_RENDER_INTERVAL_MS - elapsed);
|
|
1196
1401
|
this.#renderTimer = this.#renderScheduler.scheduleRender(() => {
|
|
@@ -1826,6 +2031,7 @@ export class TUI extends Container {
|
|
|
1826
2031
|
clearViewport: true,
|
|
1827
2032
|
clearScrollback: !isMultiplexerSession(),
|
|
1828
2033
|
});
|
|
2034
|
+
if (lines.length > height) this.#armPostFullPaintSettle();
|
|
1829
2035
|
this.#hasEverRendered = true;
|
|
1830
2036
|
return;
|
|
1831
2037
|
case "historyRebuild":
|
|
@@ -1834,6 +2040,7 @@ export class TUI extends Container {
|
|
|
1834
2040
|
clearViewport: true,
|
|
1835
2041
|
clearScrollback: !isMultiplexerSession(),
|
|
1836
2042
|
});
|
|
2043
|
+
if (lines.length > height) this.#armPostFullPaintSettle();
|
|
1837
2044
|
return;
|
|
1838
2045
|
case "overlayRebuild":
|
|
1839
2046
|
this.#clearNativeScrollbackDirty();
|
|
@@ -1844,6 +2051,7 @@ export class TUI extends Container {
|
|
|
1844
2051
|
clearScrollback: !isMultiplexerSession(),
|
|
1845
2052
|
});
|
|
1846
2053
|
this.#emitViewportRepaint(lines, width, height, cursorPos);
|
|
2054
|
+
if (baseLines.length > height) this.#armPostFullPaintSettle();
|
|
1847
2055
|
return;
|
|
1848
2056
|
case "liveRegionPinned":
|
|
1849
2057
|
this.#emitLiveRegionPinnedRepaint(
|