@oh-my-pi/pi-tui 15.11.1 → 15.11.3
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 +13 -0
- package/dist/types/components/markdown.d.ts +2 -4
- package/dist/types/terminal.d.ts +2 -0
- package/dist/types/tui.d.ts +4 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +13 -6
- package/src/terminal.ts +28 -5
- package/src/tui.ts +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.11.3] - 2026-06-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the root compose letting a lower child's native-scrollback live seam overwrite a higher one: the topmost seam (and its commit-safe extension) now defines the commit boundary, so a status loader below a streaming transcript can no longer cause still-mutable transcript rows to be committed as stale history ([#2328](https://github.com/can1357/oh-my-pi/pull/2328)).
|
|
10
|
+
|
|
11
|
+
## [15.11.2] - 2026-06-11
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 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).
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
5
18
|
## [15.11.1] - 2026-06-11
|
|
6
19
|
### Added
|
|
7
20
|
|
|
@@ -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/dist/types/tui.d.ts
CHANGED
|
@@ -83,6 +83,10 @@ export interface Component {
|
|
|
83
83
|
* of history until it finalizes. Volatile live blocks (tool previews that
|
|
84
84
|
* collapse) omit it. Defaults to `liveRegionStart` when absent; a root that
|
|
85
85
|
* reports no seam at all commits everything that scrolls (shell semantics).
|
|
86
|
+
*
|
|
87
|
+
* When several root children report a seam in the same frame, the topmost
|
|
88
|
+
* one (and its commit-safe extension) defines the boundary: commits are
|
|
89
|
+
* prefix-only, so everything below the first seam is already excluded.
|
|
86
90
|
*/
|
|
87
91
|
export interface NativeScrollbackLiveRegion {
|
|
88
92
|
getNativeScrollbackLiveRegionStart(): number | undefined;
|
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.3",
|
|
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.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.11.3",
|
|
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,
|
|
@@ -185,6 +185,10 @@ export interface Component {
|
|
|
185
185
|
* of history until it finalizes. Volatile live blocks (tool previews that
|
|
186
186
|
* collapse) omit it. Defaults to `liveRegionStart` when absent; a root that
|
|
187
187
|
* reports no seam at all commits everything that scrolls (shell semantics).
|
|
188
|
+
*
|
|
189
|
+
* When several root children report a seam in the same frame, the topmost
|
|
190
|
+
* one (and its commit-safe extension) defines the boundary: commits are
|
|
191
|
+
* prefix-only, so everything below the first seam is already excluded.
|
|
188
192
|
*/
|
|
189
193
|
export interface NativeScrollbackLiveRegion {
|
|
190
194
|
getNativeScrollbackLiveRegionStart(): number | undefined;
|
|
@@ -833,7 +837,13 @@ export class TUI extends Container {
|
|
|
833
837
|
// the last render the engine actually observed.
|
|
834
838
|
reported = getRenderStablePrefixRows(child);
|
|
835
839
|
}
|
|
836
|
-
|
|
840
|
+
// Topmost seam wins. Commits are prefix-only: the first child that
|
|
841
|
+
// reports a live region (plus its own commit-safe extension) already
|
|
842
|
+
// bounds everything below it, so a lower sibling's seam (e.g. a
|
|
843
|
+
// status loader under a streaming transcript) must never overwrite
|
|
844
|
+
// it — moving the boundary down would commit the earlier child's
|
|
845
|
+
// still-mutable rows as stale history.
|
|
846
|
+
if (liveLocalStart !== undefined && this.#nativeScrollbackLiveRegionStart === undefined) {
|
|
837
847
|
this.#nativeScrollbackLiveRegionStart = offset + liveLocalStart;
|
|
838
848
|
if (commitLocalEnd !== undefined) {
|
|
839
849
|
this.#nativeScrollbackCommitSafeEnd = offset + commitLocalEnd;
|
|
@@ -1326,6 +1336,7 @@ export class TUI extends Container {
|
|
|
1326
1336
|
// the restored normal screen (which #previousLines still describes).
|
|
1327
1337
|
if (this.#altActive) {
|
|
1328
1338
|
this.terminal.write(`${MOUSE_TRACKING_OFF}\x1b[?1049l`);
|
|
1339
|
+
setAltScreenActive(false);
|
|
1329
1340
|
this.#altActive = false;
|
|
1330
1341
|
this.#altPreviousLines = [];
|
|
1331
1342
|
}
|
|
@@ -2049,6 +2060,7 @@ export class TUI extends Container {
|
|
|
2049
2060
|
const wantAlt = this.#wantsAltScreen();
|
|
2050
2061
|
if (wantAlt && !this.#altActive) {
|
|
2051
2062
|
this.terminal.write(`\x1b[?1049h${MOUSE_TRACKING_ON}`);
|
|
2063
|
+
setAltScreenActive(true);
|
|
2052
2064
|
this.terminal.hideCursor();
|
|
2053
2065
|
this.#forgetHardwareCursorState();
|
|
2054
2066
|
this.#recordHardwareCursorHidden();
|
|
@@ -2058,6 +2070,7 @@ export class TUI extends Container {
|
|
|
2058
2070
|
this.#altEnterHeight = height;
|
|
2059
2071
|
} else if (!wantAlt && this.#altActive) {
|
|
2060
2072
|
this.terminal.write(`${MOUSE_TRACKING_OFF}\x1b[?1049l`);
|
|
2073
|
+
setAltScreenActive(false);
|
|
2061
2074
|
this.#forgetHardwareCursorState();
|
|
2062
2075
|
this.#altActive = false;
|
|
2063
2076
|
this.#altPreviousLines = [];
|