@oh-my-pi/pi-tui 17.3.2 → 17.3.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 +6 -0
- package/package.json +3 -3
- package/src/components/markdown.ts +68 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.3] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
5
11
|
## [17.3.1] - 2026-08-13
|
|
6
12
|
|
|
7
13
|
### 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.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": "17.3.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.3.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.3.3"
|
|
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
|