@oh-my-pi/pi-tui 17.3.2 → 17.3.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 +12 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +68 -1
- package/src/terminal.ts +15 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.4] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed a terminal Device-Attributes reply leaking into the composer as literal text (e.g. `1;22;…;52c`) when it arrived after the startup capability-probe sentinel FIFO drained, a race made observable by the added latency of an SSH/zmx PTY chain. DA1 replies (`CSI ? … c`) and split private-CSI responses are now consumed for the whole session lifetime, not only while a probe sentinel is outstanding ([#8542](https://github.com/can1357/oh-my-pi/issues/8542)).
|
|
10
|
+
|
|
11
|
+
## [17.3.3] - 2026-08-14
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed Gemini reports rendering their final headings and tables as one raw code block when the model emitted a lone closing Markdown fence without its opener.
|
|
16
|
+
|
|
5
17
|
## [17.3.1] - 2026-08-13
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.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": "17.3.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.3.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.3.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"ghostty-web": "^0.4.0"
|
|
@@ -43,6 +43,71 @@ function normalizeOsc8Terminators(text: string): string {
|
|
|
43
43
|
return text.replace(OSC8_ST_PREFIX_REGEX, "$1\x07");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const MARKDOWN_FENCE_LINE = /^ {0,3}(`{3,}|~{3,})[ \t]*(.*)$/;
|
|
47
|
+
const MARKDOWN_HEADING_LINE = /^ {0,3}#{1,6}[ \t]+\S/;
|
|
48
|
+
const FENCED_SOURCE_INTRO = /\b(?:code|example|markdown|output|snippet|source)\s*:?\s*$/i;
|
|
49
|
+
|
|
50
|
+
function isGfmTableDelimiter(line: string, headerLine: string | undefined): boolean {
|
|
51
|
+
if (!headerLine || !line.includes("|") || !headerLine.includes("|")) return false;
|
|
52
|
+
const delimiterCells = line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
|
|
53
|
+
const headerCells = headerLine.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
|
|
54
|
+
return (
|
|
55
|
+
delimiterCells.length >= 2 &&
|
|
56
|
+
headerCells.length === delimiterCells.length &&
|
|
57
|
+
delimiterCells.every(cell => /^:?-{3,}:?$/.test(cell.trim())) &&
|
|
58
|
+
headerCells.every(cell => cell.trim().length > 0)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gemini can emit a bare closing fence without its opener, then continue with
|
|
64
|
+
* headings and tables. CommonMark must interpret that lone fence as an opener,
|
|
65
|
+
* which turns the rest of an otherwise valid report into one raw code block.
|
|
66
|
+
*
|
|
67
|
+
* Repair only the unambiguous rich-document shape at final render: one
|
|
68
|
+
* unmatched bare fence after prose, followed by both an ATX heading and a GFM
|
|
69
|
+
* table delimiter. Keep ordinary incomplete code blocks, fenced Markdown
|
|
70
|
+
* examples, and every matched fence untouched.
|
|
71
|
+
*/
|
|
72
|
+
function repairOrphanClosingFence(text: string): string {
|
|
73
|
+
const lines = text.split("\n");
|
|
74
|
+
let open: { index: number; marker: string; info: string } | undefined;
|
|
75
|
+
for (let index = 0; index < lines.length; index++) {
|
|
76
|
+
const match = MARKDOWN_FENCE_LINE.exec(lines[index]!);
|
|
77
|
+
if (!match) continue;
|
|
78
|
+
const marker = match[1]!;
|
|
79
|
+
const info = match[2]!.trim();
|
|
80
|
+
if (!open) {
|
|
81
|
+
open = { index, marker, info };
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (marker[0] === open.marker[0] && marker.length >= open.marker.length && info === "") {
|
|
85
|
+
open = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (open?.info !== "") return text;
|
|
89
|
+
|
|
90
|
+
let previous = "";
|
|
91
|
+
for (let index = open.index - 1; index >= 0; index--) {
|
|
92
|
+
previous = lines[index]!.trim();
|
|
93
|
+
if (previous) break;
|
|
94
|
+
}
|
|
95
|
+
if (!previous || previous.endsWith(":") || FENCED_SOURCE_INTRO.test(previous)) return text;
|
|
96
|
+
|
|
97
|
+
let hasHeading = false;
|
|
98
|
+
let hasTableDelimiter = false;
|
|
99
|
+
for (let index = open.index + 1; index < lines.length; index++) {
|
|
100
|
+
const line = lines[index]!;
|
|
101
|
+
hasHeading ||= MARKDOWN_HEADING_LINE.test(line);
|
|
102
|
+
hasTableDelimiter ||= isGfmTableDelimiter(line, lines[index - 1]);
|
|
103
|
+
if (hasHeading && hasTableDelimiter) {
|
|
104
|
+
lines.splice(open.index, 1);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return text;
|
|
109
|
+
}
|
|
110
|
+
|
|
46
111
|
// OSC 66 (Kitty text-sizing) heading spans are emitted as a single indivisible
|
|
47
112
|
// unit by the H1 render path. Like image-protocol lines, they bypass ANSI
|
|
48
113
|
// wrapping and width padding (see `isOsc66Line` in ../utils): re-wrapping
|
|
@@ -1730,7 +1795,9 @@ export class Markdown
|
|
|
1730
1795
|
}
|
|
1731
1796
|
|
|
1732
1797
|
// Replace tabs with 3 spaces for consistent rendering
|
|
1733
|
-
const normalizedText =
|
|
1798
|
+
const normalizedText = this.transientRenderCache
|
|
1799
|
+
? replaceTabs(this.#text)
|
|
1800
|
+
: repairOrphanClosingFence(replaceTabs(this.#text));
|
|
1734
1801
|
const signature = this.#renderSignature(width, paddingX);
|
|
1735
1802
|
|
|
1736
1803
|
// L2: module-level LRU — survives component disposal/recreation across
|
package/src/terminal.ts
CHANGED
|
@@ -945,10 +945,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
945
945
|
// flush timeout elapses mid-sequence, the prefix `\x1b[?<digits>` arrives as
|
|
946
946
|
// one event and the tail `;...<terminator>` arrives as individual character
|
|
947
947
|
// events that would otherwise leak into the prompt as keystrokes. See #1238.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
)
|
|
948
|
+
// A private CSI (`\x1b[?…`) is a terminal->host report, never a keystroke, so
|
|
949
|
+
// reassembly stays armed for the whole session — not just while a probe
|
|
950
|
+
// sentinel is outstanding — otherwise a reply that lands after the sentinel
|
|
951
|
+
// FIFO drains (slow SSH/PTY links) leaks its tail into the composer (#8542).
|
|
952
|
+
if (this.#privateCsiResponseBuffer || privateCsiPartialPattern.test(sequence)) {
|
|
952
953
|
if (this.#privateCsiResponseBuffer && sequence.startsWith("\x1b")) {
|
|
953
954
|
// New escape arrived mid-reassembly — abandon partial and re-process the new sequence.
|
|
954
955
|
this.#privateCsiResponseBuffer = "";
|
|
@@ -1038,10 +1039,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
1038
1039
|
}
|
|
1039
1040
|
|
|
1040
1041
|
// DA1 response: swallow our sentinel reply regardless of whether an
|
|
1041
|
-
// earlier capability-specific response already succeeded.
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1042
|
+
// earlier capability-specific response already succeeded. `CSI ? … c` is
|
|
1043
|
+
// exclusively a terminal->host report, so it is swallowed even with no
|
|
1044
|
+
// outstanding sentinel — a reply that arrives after the FIFO drains (slow
|
|
1045
|
+
// SSH/PTY links) must never reach the composer as literal text (#8542).
|
|
1046
|
+
if (da1ResponsePattern.test(sequence)) {
|
|
1047
|
+
const owner = this.#da1SentinelOwners.shift();
|
|
1048
|
+
if (!owner) {
|
|
1049
|
+
// Late/unowned reply: nothing to resolve, just drop the bytes.
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1045
1052
|
switch (owner.kind) {
|
|
1046
1053
|
case "osc11": {
|
|
1047
1054
|
if (this.#osc11Pending) {
|