Hide orphan json fences in chat bubbles (0.15.14).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+18
-5
@@ -689,12 +689,25 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
|
||||
}
|
||||
|
||||
/** Lightweight chat prose: ### Critique → «Критика», lists, bold — not a full markdown engine. */
|
||||
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 '';
|
||||
}
|
||||
@@ -755,7 +768,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
|
||||
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');
|
||||
|
||||
+31
-4
@@ -25,6 +25,7 @@ export function getPatchKeys() {
|
||||
}
|
||||
|
||||
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
const OPEN_FENCE_RE = /```(?:json)?/i;
|
||||
|
||||
function has(obj, key) {
|
||||
return obj[key] !== undefined && obj[key] !== null;
|
||||
@@ -87,6 +88,30 @@ function tryParsePatchJson(raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Remove fenced blocks and orphan ```json openers left when JSON was parsed unfenced. */
|
||||
export 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();
|
||||
}
|
||||
|
||||
/** While streaming: hide an unclosed fence tail so ```json does not leak into the bubble. */
|
||||
export 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);
|
||||
}
|
||||
|
||||
export function extractPatch(text) {
|
||||
if (!text) {
|
||||
return { prose: text || '', patch: null };
|
||||
@@ -117,7 +142,7 @@ export function extractPatch(text) {
|
||||
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 };
|
||||
}
|
||||
// Unfenced trailing object — some turns emit raw {prompt, generate:true}.
|
||||
@@ -125,10 +150,10 @@ export function extractPatch(text) {
|
||||
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 };
|
||||
}
|
||||
|
||||
/** Chat body text: never fall back to the raw fence when a patch was extracted. */
|
||||
@@ -137,7 +162,7 @@ export function visibleProse(text) {
|
||||
if (patch) {
|
||||
return prose || '';
|
||||
}
|
||||
return String(text || '');
|
||||
return stripFenceArtifacts(text);
|
||||
}
|
||||
|
||||
export function isTerminalStreamPatch(obj) {
|
||||
@@ -180,4 +205,6 @@ export function attachPatch(SA) {
|
||||
SA.extractPatch = extractPatch;
|
||||
SA.generateFlagOn = generateFlagOn;
|
||||
SA.visibleProse = visibleProse;
|
||||
SA.stripFenceArtifacts = stripFenceArtifacts;
|
||||
SA.liveStreamProse = liveStreamProse;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user