@oh-my-pi/pi-tui 15.11.1 → 15.11.2
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 +7 -0
- package/dist/types/components/markdown.d.ts +2 -4
- package/dist/types/terminal.d.ts +2 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +13 -6
- package/src/terminal.ts +28 -5
- package/src/tui.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.11.2] - 2026-06-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Ctrl+C/exit corrupting the parent shell on Windows: `emergencyTerminalRestore()` wrote `\x1b[?1049l` (leave alternate screen) unconditionally on every exit path, and conhost/Windows Terminal execute an unconditional cursor restore for it even when the alt buffer was never entered — with no prior save the cursor jumped to the viewport home, so the shell prompt landed on top of the dead frame. The leave sequence is now gated on tracked alt-screen state (set/cleared by the TUI's fullscreen-overlay enter/leave and stop paths).
|
|
10
|
+
- Skipped native syntax highlighting for transient markdown streaming renders, including nested list code blocks, leaving code blocks plain until their content stabilizes to avoid main-thread highlighter spikes.
|
|
11
|
+
|
|
5
12
|
## [15.11.1] - 2026-06-11
|
|
6
13
|
### Added
|
|
7
14
|
|
|
@@ -49,13 +49,11 @@ export interface MarkdownTheme {
|
|
|
49
49
|
}
|
|
50
50
|
export declare class Markdown implements Component {
|
|
51
51
|
#private;
|
|
52
|
-
/** When true, skip the module-level LRU (lookup and insert) for this instance's
|
|
53
|
-
* renders. Set for in-flight streaming partials whose text changes every frame —
|
|
54
|
-
* caching those churns the LRU with near-duplicate full-message snapshots. */
|
|
55
|
-
transientRenderCache: boolean;
|
|
56
52
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
|
|
57
53
|
setText(text: string): void;
|
|
58
54
|
invalidate(): void;
|
|
55
|
+
get transientRenderCache(): boolean;
|
|
56
|
+
set transientRenderCache(value: boolean);
|
|
59
57
|
render(width: number): readonly string[];
|
|
60
58
|
}
|
|
61
59
|
/**
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* sole production caller.
|
|
20
20
|
*/
|
|
21
21
|
export declare function chunkForConPTY(data: string, maxChunkBytes?: number): string[];
|
|
22
|
+
/** Record alternate-screen state (called by the TUI on `?1049h`/`?1049l` writes). */
|
|
23
|
+
export declare function setAltScreenActive(active: boolean): void;
|
|
22
24
|
/**
|
|
23
25
|
* Emergency terminal restore - call this from signal/crash handlers
|
|
24
26
|
* Resets terminal state without requiring access to the ProcessTerminal instance
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.11.
|
|
4
|
+
"version": "15.11.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.11.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.11.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.11.2",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.11.2",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.4"
|
|
44
44
|
},
|
|
@@ -295,10 +295,7 @@ export class Markdown implements Component {
|
|
|
295
295
|
#cachedText?: string;
|
|
296
296
|
#cachedWidth?: number;
|
|
297
297
|
#cachedLines?: readonly string[];
|
|
298
|
-
|
|
299
|
-
* renders. Set for in-flight streaming partials whose text changes every frame —
|
|
300
|
-
* caching those churns the LRU with near-duplicate full-message snapshots. */
|
|
301
|
-
transientRenderCache = false;
|
|
298
|
+
#transientRenderCache = false;
|
|
302
299
|
|
|
303
300
|
constructor(
|
|
304
301
|
text: string,
|
|
@@ -326,6 +323,16 @@ export class Markdown implements Component {
|
|
|
326
323
|
this.#cachedWidth = undefined;
|
|
327
324
|
this.#cachedLines = undefined;
|
|
328
325
|
}
|
|
326
|
+
get transientRenderCache(): boolean {
|
|
327
|
+
return this.#transientRenderCache;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
set transientRenderCache(value: boolean) {
|
|
331
|
+
const next = value === true;
|
|
332
|
+
if (this.#transientRenderCache === next) return;
|
|
333
|
+
this.#transientRenderCache = next;
|
|
334
|
+
this.invalidate();
|
|
335
|
+
}
|
|
329
336
|
|
|
330
337
|
render(width: number): readonly string[] {
|
|
331
338
|
// L1: per-instance cache — fastest path for repeated renders of the same
|
|
@@ -618,7 +625,7 @@ export class Markdown implements Component {
|
|
|
618
625
|
|
|
619
626
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
620
627
|
lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
621
|
-
if (this.#theme.highlightCode) {
|
|
628
|
+
if (this.#theme.highlightCode && !this.transientRenderCache) {
|
|
622
629
|
const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
|
|
623
630
|
for (const hlLine of highlightedLines) {
|
|
624
631
|
lines.push(`${codeIndent}${hlLine}`);
|
|
@@ -909,7 +916,7 @@ export class Markdown implements Component {
|
|
|
909
916
|
// Code block in list item
|
|
910
917
|
const codeIndent = padding(this.#codeBlockIndent);
|
|
911
918
|
lines.push({ text: this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`), nested: false });
|
|
912
|
-
if (this.#theme.highlightCode) {
|
|
919
|
+
if (this.#theme.highlightCode && !this.transientRenderCache) {
|
|
913
920
|
const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
|
|
914
921
|
for (const hlLine of highlightedLines) {
|
|
915
922
|
lines.push({ text: `${codeIndent}${hlLine}`, nested: false });
|
package/src/terminal.ts
CHANGED
|
@@ -122,6 +122,19 @@ export function chunkForConPTY(data: string, maxChunkBytes: number = MAX_CONPTY_
|
|
|
122
122
|
let activeTerminal: ProcessTerminal | null = null;
|
|
123
123
|
// Track if a terminal was ever started (for emergency restore logic)
|
|
124
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
|
+
}
|
|
125
138
|
|
|
126
139
|
const stdoutErrorHandlers = new Set<(err: Error) => void>();
|
|
127
140
|
let stdoutErrorListenerInstalled = false;
|
|
@@ -226,10 +239,15 @@ export function emergencyTerminalRestore(): void {
|
|
|
226
239
|
if (terminal) {
|
|
227
240
|
terminal.stop();
|
|
228
241
|
// stop() never touches the alternate screen — the TUI owns that
|
|
229
|
-
// state and exits it on the normal shutdown path.
|
|
230
|
-
// fullscreen overlay
|
|
231
|
-
//
|
|
232
|
-
|
|
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
|
+
}
|
|
233
251
|
terminal.showCursor();
|
|
234
252
|
} else if (terminalEverStarted) {
|
|
235
253
|
// Blind restore only if we know a terminal was started but lost track of it
|
|
@@ -244,9 +262,14 @@ export function emergencyTerminalRestore(): void {
|
|
|
244
262
|
"\x1b[<u" + // Pop kitty keyboard protocol
|
|
245
263
|
"\x1b[>4;0m" + // Disable modifyOtherKeys fallback
|
|
246
264
|
"\x1b[?1006l\x1b[?1003l\x1b[?1000l" + // Disable mouse tracking (fullscreen overlays)
|
|
247
|
-
|
|
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" : "") +
|
|
248
270
|
"\x1b[?25h", // Show cursor
|
|
249
271
|
);
|
|
272
|
+
altScreenActive = false;
|
|
250
273
|
if (process.stdin.setRawMode) {
|
|
251
274
|
process.stdin.setRawMode(false);
|
|
252
275
|
}
|
package/src/tui.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { $flag, getDebugLogPath } from "@oh-my-pi/pi-utils";
|
|
|
16
16
|
import { DEFAULT_MAX_INLINE_IMAGES, ImageBudget } from "./components/image";
|
|
17
17
|
import { planDeccaraFills } from "./deccara";
|
|
18
18
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
19
|
-
import { isConPTYHosted, type Terminal } from "./terminal";
|
|
19
|
+
import { isConPTYHosted, setAltScreenActive, type Terminal } from "./terminal";
|
|
20
20
|
import {
|
|
21
21
|
encodeKittyDeleteImage,
|
|
22
22
|
ImageProtocol,
|
|
@@ -1326,6 +1326,7 @@ export class TUI extends Container {
|
|
|
1326
1326
|
// the restored normal screen (which #previousLines still describes).
|
|
1327
1327
|
if (this.#altActive) {
|
|
1328
1328
|
this.terminal.write(`${MOUSE_TRACKING_OFF}\x1b[?1049l`);
|
|
1329
|
+
setAltScreenActive(false);
|
|
1329
1330
|
this.#altActive = false;
|
|
1330
1331
|
this.#altPreviousLines = [];
|
|
1331
1332
|
}
|
|
@@ -2049,6 +2050,7 @@ export class TUI extends Container {
|
|
|
2049
2050
|
const wantAlt = this.#wantsAltScreen();
|
|
2050
2051
|
if (wantAlt && !this.#altActive) {
|
|
2051
2052
|
this.terminal.write(`\x1b[?1049h${MOUSE_TRACKING_ON}`);
|
|
2053
|
+
setAltScreenActive(true);
|
|
2052
2054
|
this.terminal.hideCursor();
|
|
2053
2055
|
this.#forgetHardwareCursorState();
|
|
2054
2056
|
this.#recordHardwareCursorHidden();
|
|
@@ -2058,6 +2060,7 @@ export class TUI extends Container {
|
|
|
2058
2060
|
this.#altEnterHeight = height;
|
|
2059
2061
|
} else if (!wantAlt && this.#altActive) {
|
|
2060
2062
|
this.terminal.write(`${MOUSE_TRACKING_OFF}\x1b[?1049l`);
|
|
2063
|
+
setAltScreenActive(false);
|
|
2061
2064
|
this.#forgetHardwareCursorState();
|
|
2062
2065
|
this.#altActive = false;
|
|
2063
2066
|
this.#altPreviousLines = [];
|