Hide orphan json fences in chat bubbles (0.15.14).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -138,6 +138,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
var FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
|
var OPEN_FENCE_RE = /```(?:json)?/i;
|
||||||
function has(obj, key) {
|
function has(obj, key) {
|
||||||
return obj[key] !== void 0 && obj[key] !== null;
|
return obj[key] !== void 0 && obj[key] !== null;
|
||||||
}
|
}
|
||||||
@@ -194,6 +195,26 @@
|
|||||||
}
|
}
|
||||||
return null;
|
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) {
|
function extractPatch(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { prose: text || "", patch: null };
|
return { prose: text || "", patch: null };
|
||||||
@@ -224,24 +245,24 @@
|
|||||||
if (chosen) {
|
if (chosen) {
|
||||||
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
||||||
const len = lastTerminal ? lastTermLen : lastAnyLen;
|
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 };
|
return { prose, patch: chosen };
|
||||||
}
|
}
|
||||||
const brace = text.lastIndexOf("{");
|
const brace = text.lastIndexOf("{");
|
||||||
if (brace >= 0) {
|
if (brace >= 0) {
|
||||||
const parsed = tryParsePatchJson(text.slice(brace));
|
const parsed = tryParsePatchJson(text.slice(brace));
|
||||||
if (parsed) {
|
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) {
|
function visibleProse(text) {
|
||||||
const { prose, patch } = extractPatch(text);
|
const { prose, patch } = extractPatch(text);
|
||||||
if (patch) {
|
if (patch) {
|
||||||
return prose || "";
|
return prose || "";
|
||||||
}
|
}
|
||||||
return String(text || "");
|
return stripFenceArtifacts(text);
|
||||||
}
|
}
|
||||||
function isTerminalStreamPatch(obj) {
|
function isTerminalStreamPatch(obj) {
|
||||||
if (!obj || typeof obj !== "object") {
|
if (!obj || typeof obj !== "object") {
|
||||||
@@ -279,6 +300,8 @@
|
|||||||
SA2.extractPatch = extractPatch;
|
SA2.extractPatch = extractPatch;
|
||||||
SA2.generateFlagOn = generateFlagOn2;
|
SA2.generateFlagOn = generateFlagOn2;
|
||||||
SA2.visibleProse = visibleProse;
|
SA2.visibleProse = visibleProse;
|
||||||
|
SA2.stripFenceArtifacts = stripFenceArtifacts;
|
||||||
|
SA2.liveStreamProse = liveStreamProse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/persist.js
|
// src/persist.js
|
||||||
@@ -1975,12 +1998,23 @@
|
|||||||
t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
|
t = t.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
|
||||||
return t;
|
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");
|
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(/```(?: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) {
|
if (!text) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -2039,7 +2073,7 @@
|
|||||||
if (live) {
|
if (live) {
|
||||||
body.classList.add("sa-prose", "sa-prose-live");
|
body.classList.add("sa-prose", "sa-prose-live");
|
||||||
body.classList.remove("sa-prose-rich");
|
body.classList.remove("sa-prose-rich");
|
||||||
body.textContent = raw;
|
body.textContent = liveAssistantText(raw);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.classList.add("sa-prose", "sa-prose-rich");
|
body.classList.add("sa-prose", "sa-prose-rich");
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
ExtensionAuthor = "mrleo1nid";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.15.13";
|
Version = "0.15.14";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-5
@@ -689,12 +689,25 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Lightweight chat prose: ### Critique → «Критика», lists, bold — not a full markdown engine. */
|
/** 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');
|
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(/```(?: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) {
|
if (!text) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -755,7 +768,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
|
|||||||
if (live) {
|
if (live) {
|
||||||
body.classList.add('sa-prose', 'sa-prose-live');
|
body.classList.add('sa-prose', 'sa-prose-live');
|
||||||
body.classList.remove('sa-prose-rich');
|
body.classList.remove('sa-prose-rich');
|
||||||
body.textContent = raw;
|
body.textContent = liveAssistantText(raw);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.classList.add('sa-prose', 'sa-prose-rich');
|
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 FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
|
const OPEN_FENCE_RE = /```(?:json)?/i;
|
||||||
|
|
||||||
function has(obj, key) {
|
function has(obj, key) {
|
||||||
return obj[key] !== undefined && obj[key] !== null;
|
return obj[key] !== undefined && obj[key] !== null;
|
||||||
@@ -87,6 +88,30 @@ function tryParsePatchJson(raw) {
|
|||||||
return null;
|
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) {
|
export function extractPatch(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { prose: text || '', patch: null };
|
return { prose: text || '', patch: null };
|
||||||
@@ -117,7 +142,7 @@ export function extractPatch(text) {
|
|||||||
if (chosen) {
|
if (chosen) {
|
||||||
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
||||||
const len = lastTerminal ? lastTermLen : lastAnyLen;
|
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 };
|
return { prose, patch: chosen };
|
||||||
}
|
}
|
||||||
// Unfenced trailing object — some turns emit raw {prompt, generate:true}.
|
// Unfenced trailing object — some turns emit raw {prompt, generate:true}.
|
||||||
@@ -125,10 +150,10 @@ export function extractPatch(text) {
|
|||||||
if (brace >= 0) {
|
if (brace >= 0) {
|
||||||
const parsed = tryParsePatchJson(text.slice(brace));
|
const parsed = tryParsePatchJson(text.slice(brace));
|
||||||
if (parsed) {
|
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. */
|
/** 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) {
|
if (patch) {
|
||||||
return prose || '';
|
return prose || '';
|
||||||
}
|
}
|
||||||
return String(text || '');
|
return stripFenceArtifacts(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTerminalStreamPatch(obj) {
|
export function isTerminalStreamPatch(obj) {
|
||||||
@@ -180,4 +205,6 @@ export function attachPatch(SA) {
|
|||||||
SA.extractPatch = extractPatch;
|
SA.extractPatch = extractPatch;
|
||||||
SA.generateFlagOn = generateFlagOn;
|
SA.generateFlagOn = generateFlagOn;
|
||||||
SA.visibleProse = visibleProse;
|
SA.visibleProse = visibleProse;
|
||||||
|
SA.stripFenceArtifacts = stripFenceArtifacts;
|
||||||
|
SA.liveStreamProse = liveStreamProse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
extractPatch,
|
extractPatch,
|
||||||
isPatchObject,
|
isPatchObject,
|
||||||
|
liveStreamProse,
|
||||||
normalizePatch,
|
normalizePatch,
|
||||||
setPatchKeys,
|
setPatchKeys,
|
||||||
|
stripFenceArtifacts,
|
||||||
visibleProse,
|
visibleProse,
|
||||||
} from '../src/patch.js';
|
} from '../src/patch.js';
|
||||||
import {
|
import {
|
||||||
@@ -63,6 +65,28 @@ describe('patch.js', () => {
|
|||||||
assert.equal(visibleProse('просто чат без патча'), 'просто чат без патча');
|
assert.equal(visibleProse('просто чат без патча'), 'просто чат без патча');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('extractPatch strips orphan ```json when JSON is unfenced', () => {
|
||||||
|
const text = 'О, круто\n\n```json\n{"prompt":"girl on leather sofa, flirt","generate":true}';
|
||||||
|
const { prose, patch } = extractPatch(text);
|
||||||
|
assert.ok(patch);
|
||||||
|
assert.equal(patch.generate, true);
|
||||||
|
assert.equal(prose, 'О, круто');
|
||||||
|
assert.ok(!prose.includes('```'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liveStreamProse hides unclosed fence while streaming', () => {
|
||||||
|
const partial = 'О, круто\n\n```json\n{"prompt":"girl';
|
||||||
|
assert.equal(liveStreamProse(partial), 'О, круто');
|
||||||
|
assert.equal(
|
||||||
|
liveStreamProse('Ок\n```json\n{"prompt":"fox","generate":true}\n```'),
|
||||||
|
'Ок',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stripFenceArtifacts removes lone JSON Patch headers', () => {
|
||||||
|
assert.equal(stripFenceArtifacts('### JSON Patch\n'), '');
|
||||||
|
});
|
||||||
|
|
||||||
it('scheduler-only patch is detected with full key list', () => {
|
it('scheduler-only patch is detected with full key list', () => {
|
||||||
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
||||||
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
||||||
|
|||||||
Reference in New Issue
Block a user