@prometheus-ai/tui 0.5.3 → 0.5.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/autocomplete.d.ts +3 -1
- package/dist/types/components/box.d.ts +1 -1
- package/dist/types/components/editor.d.ts +35 -2
- package/dist/types/components/image.d.ts +22 -3
- package/dist/types/components/input.d.ts +6 -1
- package/dist/types/components/loader.d.ts +9 -2
- package/dist/types/components/markdown.d.ts +3 -1
- package/dist/types/components/scroll-view.d.ts +23 -1
- package/dist/types/components/select-list.d.ts +19 -1
- package/dist/types/components/settings-list.d.ts +87 -7
- package/dist/types/components/spacer.d.ts +1 -1
- package/dist/types/components/tab-bar.d.ts +37 -4
- package/dist/types/components/text.d.ts +2 -2
- package/dist/types/components/truncated-text.d.ts +1 -1
- package/dist/types/fuzzy.d.ts +10 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/keybindings.d.ts +5 -3
- package/dist/types/keys.d.ts +1 -1
- package/dist/types/kill-ring.d.ts +0 -7
- package/dist/types/kitty-graphics.d.ts +16 -31
- package/dist/types/loop-watchdog.d.ts +39 -0
- package/dist/types/mouse.d.ts +41 -0
- package/dist/types/stdin-buffer.d.ts +17 -0
- package/dist/types/terminal-capabilities.d.ts +74 -18
- package/dist/types/terminal.d.ts +34 -36
- package/dist/types/tui.d.ts +191 -79
- package/dist/types/utils.d.ts +5 -2
- package/package.json +4 -4
- package/src/autocomplete.ts +79 -65
- package/src/components/box.ts +43 -63
- package/src/components/editor.ts +471 -136
- package/src/components/image.ts +85 -9
- package/src/components/input.ts +12 -3
- package/src/components/loader.ts +35 -21
- package/src/components/markdown.ts +174 -53
- package/src/components/scroll-view.ts +63 -2
- package/src/components/select-list.ts +233 -38
- package/src/components/settings-list.ts +626 -64
- package/src/components/spacer.ts +9 -5
- package/src/components/tab-bar.ts +153 -28
- package/src/components/text.ts +6 -2
- package/src/components/truncated-text.ts +10 -2
- package/src/fuzzy.ts +214 -59
- package/src/index.ts +3 -1
- package/src/keybindings.ts +72 -14
- package/src/keys.ts +1 -1
- package/src/kill-ring.ts +5 -0
- package/src/kitty-graphics.ts +2 -101
- package/src/loop-watchdog.ts +106 -0
- package/src/mouse.ts +55 -0
- package/src/stdin-buffer.ts +291 -81
- package/src/terminal-capabilities.ts +206 -168
- package/src/terminal.ts +367 -110
- package/src/tui.ts +2102 -1729
- package/src/utils.ts +92 -60
package/src/terminal.ts
CHANGED
|
@@ -2,14 +2,118 @@ import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { $env, isBunTestRuntime, logger } from "@prometheus-ai/utils";
|
|
4
4
|
import { setKittyProtocolActive } from "./keys";
|
|
5
|
-
import { encodeKittyTempFileProbe, getKittyGraphics, kittyTempFileAllowed, setKittyGraphics } from "./kitty-graphics";
|
|
6
5
|
import { StdinBuffer } from "./stdin-buffer";
|
|
7
|
-
import {
|
|
6
|
+
import { NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL } from "./terminal-capabilities";
|
|
8
7
|
|
|
9
8
|
const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
|
|
10
9
|
const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
|
|
11
10
|
const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
|
12
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Maximum encoded UTF-8 bytes per `process.stdout.write` call on Windows.
|
|
14
|
+
*
|
|
15
|
+
* Windows ConPTY ties viewport tracking to per-`WriteFile` boundaries: when a
|
|
16
|
+
* single write exceeds ~32-64 KB, the pseudo-console stops following the
|
|
17
|
+
* cursor and the host UI's viewport stays parked at whatever scroll position
|
|
18
|
+
* the write started from. The visible symptom is that a full-paint of a long
|
|
19
|
+
* session (resume, history rebuild, large permission dialog) shows only the
|
|
20
|
+
* first ~30 lines until any focus event forces the host to re-query the
|
|
21
|
+
* cursor. The data is delivered correctly — it's purely a viewport-sync bug.
|
|
22
|
+
*
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_CONPTY_WRITE_CHUNK_BYTES = 16 * 1024;
|
|
41
|
+
|
|
42
|
+
/**
|
|
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.
|
|
58
|
+
*
|
|
59
|
+
* Exported for unit testing of the chunking contract; `#safeWrite` is the
|
|
60
|
+
* sole production caller.
|
|
61
|
+
*/
|
|
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];
|
|
65
|
+
const chunks: string[] = [];
|
|
66
|
+
const len = data.length;
|
|
67
|
+
let pos = 0;
|
|
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) {
|
|
110
|
+
chunks.push(data.slice(pos));
|
|
111
|
+
pos = len;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return chunks;
|
|
115
|
+
}
|
|
116
|
+
|
|
13
117
|
/**
|
|
14
118
|
* Minimal terminal interface for TUI
|
|
15
119
|
*/
|
|
@@ -18,9 +122,113 @@ const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
|
|
|
18
122
|
let activeTerminal: ProcessTerminal | null = null;
|
|
19
123
|
// Track if a terminal was ever started (for emergency restore logic)
|
|
20
124
|
let terminalEverStarted = false;
|
|
125
|
+
// Whether the alternate screen buffer is currently active (mirrors the TUI's
|
|
126
|
+
// overlay enter/leave writes). Consulted by emergencyTerminalRestore: DECRST
|
|
127
|
+
// 1049 must never be written blindly, because Windows' shared VT dispatcher
|
|
128
|
+
// (conhost and Windows Terminal both use AdaptDispatch) executes an
|
|
129
|
+
// unconditional cursor restore on it — with no prior DECSC save the cursor
|
|
130
|
+
// jumps to the viewport home, dropping the parent shell prompt on top of the
|
|
131
|
+
// dead frame after exit.
|
|
132
|
+
let altScreenActive = false;
|
|
133
|
+
|
|
134
|
+
/** Record alternate-screen state (called by the TUI on `?1049h`/`?1049l` writes). */
|
|
135
|
+
export function setAltScreenActive(active: boolean): void {
|
|
136
|
+
altScreenActive = active;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const stdoutErrorHandlers = new Set<(err: Error) => void>();
|
|
140
|
+
let stdoutErrorListenerInstalled = false;
|
|
141
|
+
|
|
142
|
+
function onStdoutError(err: Error): void {
|
|
143
|
+
for (const handler of stdoutErrorHandlers) handler(err);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function registerStdoutErrorHandler(handler: (err: Error) => void): () => void {
|
|
147
|
+
stdoutErrorHandlers.add(handler);
|
|
148
|
+
if (!stdoutErrorListenerInstalled) {
|
|
149
|
+
process.stdout.on("error", onStdoutError);
|
|
150
|
+
stdoutErrorListenerInstalled = true;
|
|
151
|
+
}
|
|
152
|
+
return () => {
|
|
153
|
+
stdoutErrorHandlers.delete(handler);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
21
156
|
|
|
22
157
|
const STD_INPUT_HANDLE = -10;
|
|
23
158
|
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
159
|
+
/** UTF-8 codepage id for SetConsoleCP/SetConsoleOutputCP. */
|
|
160
|
+
const CP_UTF8 = 65001;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Lazily-initialized closure re-asserting the UTF-8 console codepage, or
|
|
164
|
+
* `null` when unavailable (non-win32, FFI failure, console detached).
|
|
165
|
+
*/
|
|
166
|
+
let consoleCodepageGuard: (() => void) | null | undefined;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Re-assert the UTF-8 console codepage before writing (win32 only).
|
|
170
|
+
*
|
|
171
|
+
* Bun sets both console codepages to UTF-8 (65001) at startup, and
|
|
172
|
+
* `process.stdout.write(string)` hands UTF-8 bytes to `WriteFile`, which
|
|
173
|
+
* conhost translates using the *current* console output codepage. Child
|
|
174
|
+
* processes spawned by tools (bash commands, MCP/LSP servers, eval kernels)
|
|
175
|
+
* share this console, and some flip the codepage behind our back: PHP >=7.1
|
|
176
|
+
* CLI issues the equivalent of `chcp` whenever `internal_encoding` mismatches
|
|
177
|
+
* the console codepage (php.net request #73716) and skips the restore when
|
|
178
|
+
* killed — and two PHP processes in a pipeline race their restores. Once the
|
|
179
|
+
* codepage falls back to an OEM page (437/850), every non-ASCII glyph the TUI
|
|
180
|
+
* paints is mis-translated: box-drawing borders degrade into `Γöé`/`ΓöÇ`
|
|
181
|
+
* mojibake on the next full repaint (most visibly ctrl+o expand, which
|
|
182
|
+
* rewrites every row).
|
|
183
|
+
*
|
|
184
|
+
* `GetConsoleOutputCP` is one cheap console call per `#safeWrite`; the setter
|
|
185
|
+
* only runs after a foreign flip. A reading of 0 means "no console" — leave
|
|
186
|
+
* that alone. Guarding the write chokepoint (rather than per-spawn cleanup)
|
|
187
|
+
* covers every console-sharing child and long-running processes that flip
|
|
188
|
+
* the codepage mid-session.
|
|
189
|
+
*/
|
|
190
|
+
function ensureWindowsConsoleUtf8(): void {
|
|
191
|
+
if (consoleCodepageGuard === undefined) consoleCodepageGuard = createConsoleCodepageGuard();
|
|
192
|
+
consoleCodepageGuard?.();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let lastWarnedCodepage = 0;
|
|
196
|
+
|
|
197
|
+
function createConsoleCodepageGuard(): (() => void) | null {
|
|
198
|
+
if (process.platform !== "win32") return null;
|
|
199
|
+
try {
|
|
200
|
+
const kernel32 = dlopen("kernel32.dll", {
|
|
201
|
+
GetConsoleOutputCP: { args: [], returns: FFIType.u32 },
|
|
202
|
+
SetConsoleOutputCP: { args: [FFIType.u32], returns: FFIType.bool },
|
|
203
|
+
GetConsoleCP: { args: [], returns: FFIType.u32 },
|
|
204
|
+
SetConsoleCP: { args: [FFIType.u32], returns: FFIType.bool },
|
|
205
|
+
});
|
|
206
|
+
return () => {
|
|
207
|
+
try {
|
|
208
|
+
const outCp = kernel32.symbols.GetConsoleOutputCP();
|
|
209
|
+
if (outCp !== 0 && outCp !== CP_UTF8) {
|
|
210
|
+
kernel32.symbols.SetConsoleOutputCP(CP_UTF8);
|
|
211
|
+
if (outCp !== lastWarnedCodepage) {
|
|
212
|
+
lastWarnedCodepage = outCp;
|
|
213
|
+
logger.warn("console output codepage changed by a child process; restoring UTF-8", {
|
|
214
|
+
codepage: outCp,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const inCp = kernel32.symbols.GetConsoleCP();
|
|
219
|
+
if (inCp !== 0 && inCp !== CP_UTF8) {
|
|
220
|
+
kernel32.symbols.SetConsoleCP(CP_UTF8);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Console APIs failed (console detached mid-session); disable the guard.
|
|
224
|
+
consoleCodepageGuard = null;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
// bun:ffi unavailable; rendering proceeds without the guard.
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
24
232
|
/**
|
|
25
233
|
* Emergency terminal restore - call this from signal/crash handlers
|
|
26
234
|
* Resets terminal state without requiring access to the ProcessTerminal instance
|
|
@@ -30,6 +238,16 @@ export function emergencyTerminalRestore(): void {
|
|
|
30
238
|
const terminal = activeTerminal;
|
|
31
239
|
if (terminal) {
|
|
32
240
|
terminal.stop();
|
|
241
|
+
// stop() never touches the alternate screen — the TUI owns that
|
|
242
|
+
// state and exits it on the normal shutdown path. Only crash paths
|
|
243
|
+
// with a fullscreen overlay still hold the alt buffer here. The
|
|
244
|
+
// leave sequence is gated on the tracked state because it is NOT a
|
|
245
|
+
// universally safe no-op: Windows' VT dispatcher homes the cursor
|
|
246
|
+
// on DECRST 1049 even when the alt buffer is inactive.
|
|
247
|
+
if (altScreenActive) {
|
|
248
|
+
terminal.write("\x1b[?1049l");
|
|
249
|
+
altScreenActive = false;
|
|
250
|
+
}
|
|
33
251
|
terminal.showCursor();
|
|
34
252
|
} else if (terminalEverStarted) {
|
|
35
253
|
// Blind restore only if we know a terminal was started but lost track of it
|
|
@@ -40,10 +258,18 @@ export function emergencyTerminalRestore(): void {
|
|
|
40
258
|
"\x1b[?2004l" + // Disable bracketed paste
|
|
41
259
|
"\x1b[?2031l" + // Disable Mode 2031 appearance notifications
|
|
42
260
|
"\x1b[?2048l" + // Disable in-band resize notifications
|
|
261
|
+
"\x1b[?5522l" + // Disable enhanced paste notifications
|
|
43
262
|
"\x1b[<u" + // Pop kitty keyboard protocol
|
|
44
263
|
"\x1b[>4;0m" + // Disable modifyOtherKeys fallback
|
|
264
|
+
"\x1b[?1006l\x1b[?1003l\x1b[?1000l" + // Disable mouse tracking (fullscreen overlays)
|
|
265
|
+
// Leave the alternate screen only when a fullscreen overlay
|
|
266
|
+
// actually holds it — on Windows, DECRST 1049 on the main
|
|
267
|
+
// buffer homes the cursor (unconditional CursorRestoreState
|
|
268
|
+
// with no prior save), corrupting the shell handoff on exit.
|
|
269
|
+
(altScreenActive ? "\x1b[?1049l" : "") +
|
|
45
270
|
"\x1b[?25h", // Show cursor
|
|
46
271
|
);
|
|
272
|
+
altScreenActive = false;
|
|
47
273
|
if (process.stdin.setRawMode) {
|
|
48
274
|
process.stdin.setRawMode(false);
|
|
49
275
|
}
|
|
@@ -79,6 +305,11 @@ export interface Terminal {
|
|
|
79
305
|
// Whether Kitty keyboard protocol is active
|
|
80
306
|
get kittyProtocolActive(): boolean;
|
|
81
307
|
|
|
308
|
+
// The exact kitty keyboard push sequence in effect ("\x1b[>1u" or "\x1b[>7u"),
|
|
309
|
+
// or null when the protocol is not active. Kitty keyboard flags are per-screen,
|
|
310
|
+
// so the TUI re-pushes this after entering the alternate screen.
|
|
311
|
+
get kittyEnableSequence(): string | null;
|
|
312
|
+
|
|
82
313
|
// Cursor positioning (relative to current position)
|
|
83
314
|
moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
|
|
84
315
|
|
|
@@ -97,44 +328,6 @@ export interface Terminal {
|
|
|
97
328
|
// Progress indicator (OSC 9;4)
|
|
98
329
|
setProgress(active: boolean): void;
|
|
99
330
|
|
|
100
|
-
/**
|
|
101
|
-
* Returns whether the native terminal viewport is at the scrollback tail when
|
|
102
|
-
* the host exposes that state. `undefined` means the terminal cannot report it.
|
|
103
|
-
*
|
|
104
|
-
* `ProcessTerminal` deliberately does not implement this — no real terminal
|
|
105
|
-
* can answer it truthfully:
|
|
106
|
-
*
|
|
107
|
-
* - POSIX terminals expose no scrollback-position API at all.
|
|
108
|
-
* - Every modern Windows terminal host (Windows Terminal, VS Code, Tabby,
|
|
109
|
-
* Hyper, Alacritty, WezTerm, JetBrains, …) fronts console apps through
|
|
110
|
-
* ConPTY, where kernel32's `GetConsoleScreenBufferInfo` describes the
|
|
111
|
-
* pseudo-console buffer. That buffer is pinned to the visible grid —
|
|
112
|
-
* scrollback lives in the host UI, invisible to console APIs
|
|
113
|
-
* (microsoft/terminal#10191) — so a probe reads "at bottom" no matter
|
|
114
|
-
* where the user scrolled. Trusting it let streaming-time rebuilds emit
|
|
115
|
-
* `\x1b[3J` and yank scrolled readers: #1635 (Windows Terminal), #1746
|
|
116
|
-
* (Tabby and other ConPTY hosts). No env var distinguishes these hosts
|
|
117
|
-
* (Tabby sets none), so trust cannot be conditional on the environment.
|
|
118
|
-
* - Legacy conhost (the only non-ConPTY host) keeps a real scrollback
|
|
119
|
-
* buffer, but its window follows the output cursor: a probe comparing
|
|
120
|
-
* `srWindow.Bottom` against `dwSize.Y - 1` reads "scrolled up" for a user
|
|
121
|
-
* following live output until all ~9001 buffer rows fill, permanently
|
|
122
|
-
* blocking checkpoint scrollback reconciliation.
|
|
123
|
-
*
|
|
124
|
-
* The renderer treats a missing implementation / `undefined` as "unknown":
|
|
125
|
-
* live mutations defer destructive rebuilds and reconcile native scrollback
|
|
126
|
-
* at explicit checkpoints (prompt submit), where the user's keystroke has
|
|
127
|
-
* already pinned the host viewport to the bottom. Only test terminals
|
|
128
|
-
* (xterm.js-backed) implement this with a real answer.
|
|
129
|
-
*/
|
|
130
|
-
isNativeViewportAtBottom?(): boolean | undefined;
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Override the global terminal-profile ED3 risk decision for custom/test
|
|
134
|
-
* terminals. `undefined` falls back to the resolved `TERMINAL` profile.
|
|
135
|
-
*/
|
|
136
|
-
hasEagerEraseScrollbackRisk?(): boolean | undefined;
|
|
137
|
-
|
|
138
331
|
/**
|
|
139
332
|
* Register a callback for terminal appearance (dark/light) changes.
|
|
140
333
|
* Detection uses OSC 11 background color query with Mode 2031 as a change trigger.
|
|
@@ -150,7 +343,17 @@ export interface Terminal {
|
|
|
150
343
|
onPrivateModeReport?(callback: (mode: number, supported: boolean) => void): void;
|
|
151
344
|
}
|
|
152
345
|
|
|
153
|
-
|
|
346
|
+
/**
|
|
347
|
+
* True when stdout flows through a ConPTY pseudo-console (native win32, or
|
|
348
|
+
* Linux running under WSL where stdout still crosses into ConPTY at the
|
|
349
|
+
* `wslhost` boundary). ConPTY hosts share the per-WriteFile viewport-tracking
|
|
350
|
+
* quirks documented above and on {@link MAX_CONPTY_WRITE_CHUNK_BYTES}, so both
|
|
351
|
+
* `#safeWrite` and the renderer's post-big-paint settle gate hang off this
|
|
352
|
+
* single predicate.
|
|
353
|
+
*/
|
|
354
|
+
export function isConPTYHosted(): boolean {
|
|
355
|
+
if (process.platform === "win32") return true;
|
|
356
|
+
// WSL: stdout still crosses into ConPTY at the `wslhost` boundary.
|
|
154
357
|
return process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
|
|
155
358
|
}
|
|
156
359
|
|
|
@@ -159,11 +362,9 @@ type Da1SentinelOwner =
|
|
|
159
362
|
| { kind: "keyboard" }
|
|
160
363
|
| { kind: "osc11" }
|
|
161
364
|
| { kind: "privateMode"; mode: number }
|
|
162
|
-
| { kind: "kittyGraphicsProbe"; id: number }
|
|
163
365
|
| { kind: "osc99Probe"; id: string };
|
|
164
366
|
|
|
165
367
|
let nextOsc99ProbeId = 1;
|
|
166
|
-
let nextKittyGraphicsProbeId = 1;
|
|
167
368
|
|
|
168
369
|
function parseOsc99KeyValues(section: string): Map<string, string> {
|
|
169
370
|
const values = new Map<string, string>();
|
|
@@ -183,12 +384,18 @@ export class ProcessTerminal implements Terminal {
|
|
|
183
384
|
#resizeHandler?: () => void;
|
|
184
385
|
#stdoutResizeListener?: () => void;
|
|
185
386
|
#kittyProtocolActive = false;
|
|
387
|
+
#kittyEnableSeq: string | null = null;
|
|
186
388
|
#modifyOtherKeysActive = false;
|
|
187
389
|
#modifyOtherKeysTimeout?: Timer;
|
|
188
390
|
#stdinBuffer?: StdinBuffer;
|
|
189
391
|
#stdinDataHandler?: (data: string) => void;
|
|
190
392
|
#dead = false;
|
|
191
393
|
#writeLogPath = $env.PROMETHEUS_TUI_WRITE_LOG || "";
|
|
394
|
+
#stdoutErrorCleanup?: () => void;
|
|
395
|
+
#stdoutErrorHandler = (err: Error) => {
|
|
396
|
+
this.#markTerminalWriteFailed(err);
|
|
397
|
+
};
|
|
398
|
+
|
|
192
399
|
#windowsVTInputRestore?: () => void;
|
|
193
400
|
#appearanceCallbacks: Array<(appearance: TerminalAppearance) => void> = [];
|
|
194
401
|
#appearance: TerminalAppearance | undefined;
|
|
@@ -198,8 +405,6 @@ export class ProcessTerminal implements Terminal {
|
|
|
198
405
|
#osc99PendingId: string | undefined;
|
|
199
406
|
#osc99ResponseBuffer = "";
|
|
200
407
|
#osc99Capabilities = new Map<string, string>();
|
|
201
|
-
#kittyGraphicsPendingId: number | undefined;
|
|
202
|
-
#kittyGraphicsProbeCleanup: (() => void) | undefined;
|
|
203
408
|
#privateCsiResponseBuffer = "";
|
|
204
409
|
#da1SentinelOwners: Da1SentinelOwner[] = [];
|
|
205
410
|
/** Resolved DECRQM support per private mode (mode → supported). */
|
|
@@ -207,6 +412,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
207
412
|
#privateModeCallbacks: Array<(mode: number, supported: boolean) => void> = [];
|
|
208
413
|
/** Whether DEC 2048 in-band resize notifications are currently enabled. */
|
|
209
414
|
#inBandResizeActive = false;
|
|
415
|
+
/** Reassembly buffer for a DEC 2048 in-band resize report split across stdin reads. */
|
|
416
|
+
#inBandResizeBuffer = "";
|
|
210
417
|
#reportedColumns?: number;
|
|
211
418
|
#reportedRows?: number;
|
|
212
419
|
#osc11PollTimer?: Timer;
|
|
@@ -217,6 +424,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
217
424
|
return this.#kittyProtocolActive;
|
|
218
425
|
}
|
|
219
426
|
|
|
427
|
+
get kittyEnableSequence(): string | null {
|
|
428
|
+
return this.#kittyProtocolActive ? this.#kittyEnableSeq : null;
|
|
429
|
+
}
|
|
430
|
+
|
|
220
431
|
get appearance(): TerminalAppearance | undefined {
|
|
221
432
|
return this.#appearance;
|
|
222
433
|
}
|
|
@@ -285,11 +496,6 @@ export class ProcessTerminal implements Terminal {
|
|
|
285
496
|
// without leaking probe bytes to application input.
|
|
286
497
|
this.#queryOsc99Support();
|
|
287
498
|
|
|
288
|
-
// Probe Kitty temp-file (`t=t`) graphics transmission support. Rides the
|
|
289
|
-
// same DA1 sentinel FIFO; promotes the transmission medium to temp-file
|
|
290
|
-
// only on an explicit `OK`, so unsupported terminals stay on direct base64.
|
|
291
|
-
this.#queryKittyGraphicsTempFile();
|
|
292
|
-
|
|
293
499
|
// Subscribe to Mode 2031 appearance change notifications.
|
|
294
500
|
// When the terminal reports a change, we re-query OSC 11 to get the
|
|
295
501
|
// actual background color (following Neovim convention) with 100ms debounce.
|
|
@@ -304,7 +510,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
304
510
|
// Windows Terminal under WSL has been observed to close the hosting tab
|
|
305
511
|
// after repeated OSC 11/DA1 probes. Keep the initial/event-driven probes,
|
|
306
512
|
// but avoid background polling there.
|
|
307
|
-
|
|
513
|
+
const isWSL = process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
|
|
514
|
+
if (!isWSL) {
|
|
308
515
|
this.#startOsc11Poll();
|
|
309
516
|
}
|
|
310
517
|
|
|
@@ -379,7 +586,12 @@ export class ProcessTerminal implements Terminal {
|
|
|
379
586
|
* to handle the case where the response arrives split across multiple events.
|
|
380
587
|
*/
|
|
381
588
|
#setupStdinBuffer(): void {
|
|
382
|
-
|
|
589
|
+
// 50ms balances two failure modes: a bare ESC keypress on legacy
|
|
590
|
+
// terminals waits this long before it is delivered, while a CSI key
|
|
591
|
+
// escape split across stdin reads (laggy ssh/tmux links) leaks as
|
|
592
|
+
// literal typed text if the flush fires between the fragments. 10ms
|
|
593
|
+
// proved too tight for split escapes (#1238 covered only probe replies).
|
|
594
|
+
this.#stdinBuffer = new StdinBuffer({ timeout: 50 });
|
|
383
595
|
|
|
384
596
|
// Kitty protocol response pattern: \x1b[?<flags>u
|
|
385
597
|
const kittyResponsePattern = /^\x1b\[\?(\d+)u$/;
|
|
@@ -445,6 +657,46 @@ export class ProcessTerminal implements Terminal {
|
|
|
445
657
|
}
|
|
446
658
|
}
|
|
447
659
|
|
|
660
|
+
// In-band resize report (DEC 2048) split across stdin reads. The report
|
|
661
|
+
// is `\x1b[48;rows;cols;yPx;xPx t`; when the StdinBuffer flush timeout
|
|
662
|
+
// elapses mid-sequence — common during a rapid resize that keeps the
|
|
663
|
+
// event loop busy — the `\x1b[48;…` prefix arrives as one event and the
|
|
664
|
+
// tail (`…;xPx t`) arrives as bare character events that would otherwise
|
|
665
|
+
// leak into the prompt as literal keystrokes. Reassemble until the
|
|
666
|
+
// terminator, then fall through to the resize handler below. A
|
|
667
|
+
// reassembled sequence that turns out not to be a resize report (e.g. a
|
|
668
|
+
// split kitty `\x1b[48;…u` for a digit key) is forwarded to the input
|
|
669
|
+
// handler rather than dropped.
|
|
670
|
+
const inBandResizePartialPattern = /^\x1b\[4[\d;]*$/;
|
|
671
|
+
const isInBandResizePartial = this.#inBandResizeActive && inBandResizePartialPattern.test(sequence);
|
|
672
|
+
if (this.#inBandResizeBuffer && sequence.startsWith("\x1b")) {
|
|
673
|
+
// A new escape interrupted the partial; the stale partial is
|
|
674
|
+
// unrecoverable. If the new escape is itself an in-band prefix,
|
|
675
|
+
// restart reassembly with it; otherwise let it flow through below.
|
|
676
|
+
this.#inBandResizeBuffer = isInBandResizePartial ? sequence : "";
|
|
677
|
+
if (isInBandResizePartial) return;
|
|
678
|
+
} else if (this.#inBandResizeBuffer || isInBandResizePartial) {
|
|
679
|
+
this.#inBandResizeBuffer += sequence;
|
|
680
|
+
if (this.#inBandResizeBuffer.length > 256) {
|
|
681
|
+
this.#inBandResizeBuffer = "";
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const lastCode = this.#inBandResizeBuffer.charCodeAt(this.#inBandResizeBuffer.length - 1);
|
|
685
|
+
if (lastCode >= 0x40 && lastCode <= 0x7e) {
|
|
686
|
+
// Terminator arrived: let the resize handler below claim it, or
|
|
687
|
+
// fall through to the input handler if it is not a resize report.
|
|
688
|
+
sequence = this.#inBandResizeBuffer;
|
|
689
|
+
this.#inBandResizeBuffer = "";
|
|
690
|
+
} else if (!inBandResizePartialPattern.test(this.#inBandResizeBuffer)) {
|
|
691
|
+
// Diverged from a valid in-band prefix — drop the garbled report.
|
|
692
|
+
this.#inBandResizeBuffer = "";
|
|
693
|
+
return;
|
|
694
|
+
} else {
|
|
695
|
+
// Still accumulating the report.
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
448
700
|
// In-band resize report (DEC mode 2048). Unsolicited and not tied to a
|
|
449
701
|
// sentinel: update reported geometry + cell size, then drive the resize
|
|
450
702
|
// handler so the renderer reflows.
|
|
@@ -508,19 +760,25 @@ export class ProcessTerminal implements Terminal {
|
|
|
508
760
|
this.#resolveOsc99Support(owner.id, false);
|
|
509
761
|
break;
|
|
510
762
|
}
|
|
511
|
-
case "kittyGraphicsProbe":
|
|
512
|
-
this.#resolveKittyGraphicsTempFile(owner.id, false);
|
|
513
|
-
break;
|
|
514
763
|
}
|
|
515
764
|
return;
|
|
516
765
|
}
|
|
517
766
|
|
|
518
767
|
const match = sequence.match(kittyResponsePattern);
|
|
519
|
-
if (match
|
|
768
|
+
if (match) {
|
|
520
769
|
if (this.#modifyOtherKeysTimeout) {
|
|
521
770
|
clearTimeout(this.#modifyOtherKeysTimeout);
|
|
522
771
|
this.#modifyOtherKeysTimeout = undefined;
|
|
523
772
|
}
|
|
773
|
+
// A DA1 sentinel that beat the kitty reply may have already
|
|
774
|
+
// engaged the modifyOtherKeys fallback (terminals such as
|
|
775
|
+
// Superset/xterm-on-Electron answer DA1 before `\x1b[?u`).
|
|
776
|
+
// Kitty is strictly preferred — undo the fallback so the two
|
|
777
|
+
// modes do not stack. See #2042.
|
|
778
|
+
if (this.#modifyOtherKeysActive) {
|
|
779
|
+
this.#safeWrite("\x1b[>4;0m");
|
|
780
|
+
this.#modifyOtherKeysActive = false;
|
|
781
|
+
}
|
|
524
782
|
// Any reply to `\x1b[?u` means the terminal speaks the kitty keyboard
|
|
525
783
|
// protocol. The reported flag value is the *current* stack-top — fresh
|
|
526
784
|
// terminals report 0 — so support is implied by the reply itself, not by
|
|
@@ -532,11 +790,13 @@ export class ProcessTerminal implements Terminal {
|
|
|
532
790
|
if (reportedFlags >= 3) {
|
|
533
791
|
// Already enriched (Ghostty/foot may keep flags from a parent app).
|
|
534
792
|
// Push level-2 to lock in event reporting.
|
|
535
|
-
this.#
|
|
793
|
+
this.#kittyEnableSeq = "\x1b[>7u";
|
|
794
|
+
this.#safeWrite(this.#kittyEnableSeq);
|
|
536
795
|
} else {
|
|
537
796
|
// Level 1 (disambiguate escape codes) — enough for Shift+Enter
|
|
538
797
|
// without the modifyOtherKeys fallback that caused regression #3259.
|
|
539
|
-
this.#
|
|
798
|
+
this.#kittyEnableSeq = "\x1b[>1u";
|
|
799
|
+
this.#safeWrite(this.#kittyEnableSeq);
|
|
540
800
|
}
|
|
541
801
|
return;
|
|
542
802
|
}
|
|
@@ -576,21 +836,6 @@ export class ProcessTerminal implements Terminal {
|
|
|
576
836
|
}
|
|
577
837
|
}
|
|
578
838
|
|
|
579
|
-
// Kitty graphics temp-file probe reply: ESC _ G i=<id>;OK ESC \. The
|
|
580
|
-
// owner remains in the FIFO and is drained by its DA1 sentinel (no-op
|
|
581
|
-
// once resolved here).
|
|
582
|
-
if (this.#kittyGraphicsPendingId !== undefined && sequence.startsWith("\x1b_G")) {
|
|
583
|
-
const graphicsMatch = sequence.match(/^\x1b_G([^;]*);([\s\S]*?)\x1b\\$/u);
|
|
584
|
-
if (graphicsMatch) {
|
|
585
|
-
const idMatch = graphicsMatch[1]!.match(/(?:^|,)i=(\d+)(?:,|$)/);
|
|
586
|
-
const replyId = idMatch ? parseInt(idMatch[1]!, 10) : undefined;
|
|
587
|
-
if (replyId === this.#kittyGraphicsPendingId) {
|
|
588
|
-
this.#resolveKittyGraphicsTempFile(replyId, graphicsMatch[2]!.trim() === "OK");
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
839
|
// Mode 2031 change notification: re-query OSC 11 with 100ms debounce
|
|
595
840
|
// (Neovim convention — coalesces rapid notifications during transitions)
|
|
596
841
|
const appearanceMatch = sequence.match(appearanceDsrPattern);
|
|
@@ -686,37 +931,6 @@ export class ProcessTerminal implements Terminal {
|
|
|
686
931
|
setOsc99Supported(supported);
|
|
687
932
|
}
|
|
688
933
|
|
|
689
|
-
#shouldQueryKittyGraphicsTempFile(): boolean {
|
|
690
|
-
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return false;
|
|
691
|
-
// Honor the remote/explicit env gate, and skip when temp-file is already on.
|
|
692
|
-
if (!kittyTempFileAllowed() || getKittyGraphics().transmissionMedium === "temp-file") return false;
|
|
693
|
-
return !isBunTestRuntime() || $env.PROMETHEUS_TUI_KITTY_GRAPHICS_PROBE === "1";
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
#queryKittyGraphicsTempFile(): void {
|
|
697
|
-
this.#clearKittyGraphicsProbe();
|
|
698
|
-
if (this.#dead || !this.#shouldQueryKittyGraphicsTempFile()) return;
|
|
699
|
-
|
|
700
|
-
const id = nextKittyGraphicsProbeId++;
|
|
701
|
-
const probe = encodeKittyTempFileProbe(id);
|
|
702
|
-
if (!probe) return;
|
|
703
|
-
this.#kittyGraphicsPendingId = id;
|
|
704
|
-
this.#kittyGraphicsProbeCleanup = probe.cleanup;
|
|
705
|
-
this.#da1SentinelOwners.push({ kind: "kittyGraphicsProbe", id });
|
|
706
|
-
this.#safeWrite(`${probe.sequence}\x1b[c`);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
#resolveKittyGraphicsTempFile(id: number, supported: boolean): void {
|
|
710
|
-
if (this.#kittyGraphicsPendingId !== id) return;
|
|
711
|
-
if (supported) setKittyGraphics({ transmissionMedium: "temp-file" });
|
|
712
|
-
this.#clearKittyGraphicsProbe();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
#clearKittyGraphicsProbe(): void {
|
|
716
|
-
this.#kittyGraphicsPendingId = undefined;
|
|
717
|
-
this.#kittyGraphicsProbeCleanup?.();
|
|
718
|
-
this.#kittyGraphicsProbeCleanup = undefined;
|
|
719
|
-
}
|
|
720
934
|
/**
|
|
721
935
|
* Parse an OSC 11 background color response and compute BT.601 luminance.
|
|
722
936
|
* Handles 1-, 2-, 3-, and 4-digit XParseColor hex components.
|
|
@@ -744,6 +958,9 @@ export class ProcessTerminal implements Terminal {
|
|
|
744
958
|
/**
|
|
745
959
|
* Start periodic OSC 11 re-queries for terminals without Mode 2031 (Warp, Alacritty, WezTerm).
|
|
746
960
|
* Self-disables once Mode 2031 fires (push-based is better than polling).
|
|
961
|
+
* The interval is deliberately long: each poll's OSC 11 + DA1 write clears
|
|
962
|
+
* an active text selection on several terminals, so polling exists only to
|
|
963
|
+
* eventually notice a rare OS theme switch, not to track it promptly.
|
|
747
964
|
*/
|
|
748
965
|
#startOsc11Poll(): void {
|
|
749
966
|
this.#stopOsc11Poll();
|
|
@@ -753,7 +970,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
753
970
|
return;
|
|
754
971
|
}
|
|
755
972
|
this.#queryBackgroundColor();
|
|
756
|
-
},
|
|
973
|
+
}, 30_000);
|
|
757
974
|
this.#osc11PollTimer.unref();
|
|
758
975
|
}
|
|
759
976
|
|
|
@@ -943,6 +1160,12 @@ export class ProcessTerminal implements Terminal {
|
|
|
943
1160
|
|
|
944
1161
|
// Disable bracketed paste mode
|
|
945
1162
|
this.#safeWrite("\x1b[?2004l");
|
|
1163
|
+
this.#safeWrite("\x1b[?5522l");
|
|
1164
|
+
|
|
1165
|
+
// Disable mouse tracking (enabled only by fullscreen overlays; safe
|
|
1166
|
+
// no-ops otherwise). Covers crash paths that reach stop() without the
|
|
1167
|
+
// TUI's own overlay teardown running.
|
|
1168
|
+
this.#safeWrite("\x1b[?1006l\x1b[?1003l\x1b[?1000l");
|
|
946
1169
|
|
|
947
1170
|
// Disable Mode 2031 appearance change notifications
|
|
948
1171
|
this.#safeWrite("\x1b[?2031l");
|
|
@@ -965,8 +1188,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
965
1188
|
this.#osc99ResponseBuffer = "";
|
|
966
1189
|
this.#osc99Capabilities.clear();
|
|
967
1190
|
setOsc99Supported(false);
|
|
968
|
-
this.#clearKittyGraphicsProbe();
|
|
969
1191
|
this.#privateCsiResponseBuffer = "";
|
|
1192
|
+
this.#inBandResizeBuffer = "";
|
|
970
1193
|
this.#da1SentinelOwners.length = 0;
|
|
971
1194
|
this.#privateModeCallbacks = [];
|
|
972
1195
|
this.#privateModeSupport.clear();
|
|
@@ -1017,6 +1240,18 @@ export class ProcessTerminal implements Terminal {
|
|
|
1017
1240
|
if (process.stdin.setRawMode) {
|
|
1018
1241
|
process.stdin.setRawMode(this.#wasRaw);
|
|
1019
1242
|
}
|
|
1243
|
+
this.#stdoutErrorCleanup?.();
|
|
1244
|
+
this.#stdoutErrorCleanup = undefined;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
#ensureStdoutErrorHandler(): void {
|
|
1248
|
+
this.#stdoutErrorCleanup ??= registerStdoutErrorHandler(this.#stdoutErrorHandler);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
#markTerminalWriteFailed(err: unknown): void {
|
|
1252
|
+
if (this.#dead) return;
|
|
1253
|
+
this.#dead = true;
|
|
1254
|
+
logger.warn("terminal write failed; disabling terminal rendering", { err });
|
|
1020
1255
|
}
|
|
1021
1256
|
|
|
1022
1257
|
write(data: string): void {
|
|
@@ -1035,12 +1270,34 @@ export class ProcessTerminal implements Terminal {
|
|
|
1035
1270
|
// Skip control sequences when stdout isn't a TTY (piped output, tests, log
|
|
1036
1271
|
// files). They serve no purpose there and would surface as visible noise.
|
|
1037
1272
|
if (!process.stdout.isTTY) return;
|
|
1273
|
+
this.#ensureStdoutErrorHandler();
|
|
1274
|
+
// A console-sharing child process may have flipped the console codepage
|
|
1275
|
+
// away from UTF-8; repair it before any bytes hit WriteFile so no frame
|
|
1276
|
+
// is ever translated through an OEM codepage. See ensureWindowsConsoleUtf8.
|
|
1277
|
+
if (process.platform === "win32") ensureWindowsConsoleUtf8();
|
|
1038
1278
|
try {
|
|
1039
|
-
|
|
1279
|
+
// Windows ConPTY drops viewport tracking when a single write exceeds
|
|
1280
|
+
// ~32-64 KB: the host UI's scroll position stays parked at wherever
|
|
1281
|
+
// the write began, even though every byte landed in scrollback. Split
|
|
1282
|
+
// large paints into newline-aligned chunks so each underlying
|
|
1283
|
+
// `WriteFile` stays well below the threshold. The gate also covers
|
|
1284
|
+
// WSL — `process.platform === "linux"` there, but stdout still
|
|
1285
|
+
// crosses into ConPTY at the `wslhost` boundary, so the same per-
|
|
1286
|
+
// WriteFile cap applies. Non-ConPTY PTYs keep the single-write fast
|
|
1287
|
+
// path. The cap is on encoded UTF-8 bytes, not JS code units, because
|
|
1288
|
+
// `process.stdout.write(string)` UTF-8-encodes before `WriteFile`,
|
|
1289
|
+
// and a code-unit cap would let CJK transcript rows expand past the
|
|
1290
|
+
// threshold. See #2034 and #2095.
|
|
1291
|
+
if (isConPTYHosted() && Buffer.byteLength(data, "utf8") > MAX_CONPTY_WRITE_CHUNK_BYTES) {
|
|
1292
|
+
for (const chunk of chunkForConPTY(data, MAX_CONPTY_WRITE_CHUNK_BYTES)) {
|
|
1293
|
+
if (this.#dead) break;
|
|
1294
|
+
process.stdout.write(chunk);
|
|
1295
|
+
}
|
|
1296
|
+
} else {
|
|
1297
|
+
process.stdout.write(data);
|
|
1298
|
+
}
|
|
1040
1299
|
} catch (err) {
|
|
1041
|
-
|
|
1042
|
-
this.#dead = true;
|
|
1043
|
-
logger.warn("terminal is dead - no recovery possible", { error: err, data });
|
|
1300
|
+
this.#markTerminalWriteFailed(err);
|
|
1044
1301
|
}
|
|
1045
1302
|
}
|
|
1046
1303
|
|