@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 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.2",
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.2",
41
- "@oh-my-pi/pi-utils": "17.3.2"
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 = replaceTabs(this.#text);
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