Hide orphan json fences in chat bubbles (0.15.14).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 20:34:04 +03:00
co-authored by Cursor
parent 5a7a72d972
commit 61b4b51118
5 changed files with 117 additions and 19 deletions
+43 -9
View File
@@ -138,6 +138,7 @@
}
}
var FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
var OPEN_FENCE_RE = /```(?:json)?/i;
function has(obj, key) {
return obj[key] !== void 0 && obj[key] !== null;
}
@@ -194,6 +195,26 @@
}
return null;
}
function stripFenceArtifacts(text) {
let t = String(text || "").replace(/\r\n/g, "\n");
t = t.replace(/```(?:json)?\s*[\s\S]*?```/gi, "");
t = t.replace(/```(?:json)?\s*[\s\S]*$/gi, "");
t = t.replace(/(?:^|\n)\s*```(?:json)?\s*(?=\n|$)/gi, "\n");
t = t.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, "\n");
t = t.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, "\n");
return t.replace(/\n{3,}/g, "\n\n").trim();
}
function liveStreamProse(text) {
const t = String(text || "");
const open = t.search(OPEN_FENCE_RE);
if (open >= 0) {
const tail = t.slice(open);
if (!/```(?:json)?\s*[\s\S]*?```/i.test(tail)) {
return stripFenceArtifacts(t.slice(0, open));
}
}
return stripFenceArtifacts(visibleProse(t) || t);
}
function extractPatch(text) {
if (!text) {
return { prose: text || "", patch: null };
@@ -224,24 +245,24 @@
if (chosen) {
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
const len = lastTerminal ? lastTermLen : lastAnyLen;
const prose = (text.slice(0, idx) + text.slice(idx + len)).trim();
const prose = stripFenceArtifacts(text.slice(0, idx) + text.slice(idx + len));
return { prose, patch: chosen };
}
const brace = text.lastIndexOf("{");
if (brace >= 0) {
const parsed = tryParsePatchJson(text.slice(brace));
if (parsed) {
return { prose: text.slice(0, brace).trim(), patch: parsed };
return { prose: stripFenceArtifacts(text.slice(0, brace)), patch: parsed };
}
}
return { prose: text, patch: null };
return { prose: stripFenceArtifacts(text), patch: null };
}
function visibleProse(text) {
const { prose, patch } = extractPatch(text);
if (patch) {
return prose || "";
}
return String(text || "");
return stripFenceArtifacts(text);
}
function isTerminalStreamPatch(obj) {
if (!obj || typeof obj !== "object") {
@@ -279,6 +300,8 @@
SA2.extractPatch = extractPatch;
SA2.generateFlagOn = generateFlagOn2;
SA2.visibleProse = visibleProse;
SA2.stripFenceArtifacts = stripFenceArtifacts;
SA2.liveStreamProse = liveStreamProse;
}
// src/persist.js
@@ -1975,12 +1998,23 @@
t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
return t;
}
function formatAssistantProseHtml(raw) {
function stripChatFenceArtifacts(raw) {
if (window.SA && typeof SA.stripFenceArtifacts === "function") {
return SA.stripFenceArtifacts(raw);
}
let text = String(raw || "").replace(/\r\n/g, "\n");
text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, "\n");
text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, "\n");
text = text.replace(/```(?:json)?\s*[\s\S]*?```/gi, "");
text = text.replace(/\n{3,}/g, "\n\n").trim();
text = text.replace(/```(?:json)?\s*[\s\S]*$/gi, "");
return text.replace(/\n{3,}/g, "\n\n").trim();
}
function liveAssistantText(raw) {
if (window.SA && typeof SA.liveStreamProse === "function") {
return SA.liveStreamProse(raw);
}
return stripChatFenceArtifacts(raw) || String(raw || "");
}
function formatAssistantProseHtml(raw) {
let text = stripChatFenceArtifacts(raw);
if (!text) {
return "";
}
@@ -2039,7 +2073,7 @@
if (live) {
body.classList.add("sa-prose", "sa-prose-live");
body.classList.remove("sa-prose-rich");
body.textContent = raw;
body.textContent = liveAssistantText(raw);
return;
}
body.classList.add("sa-prose", "sa-prose-rich");