Ship Assistent 0.15.2: apply closed generate patches immediately, recover stalled streams, and skip repeat greetings when a chat already has replies.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 09:28:04 +03:00
co-authored by Cursor
parent 4b36850e2b
commit e53f483a00
19 changed files with 790 additions and 287 deletions
+262 -122
View File
@@ -43,6 +43,7 @@
"actions",
"generate",
"ask",
"checkpoint",
"use_init_image",
"clear_init_image",
"init_creativity",
@@ -85,6 +86,20 @@
function has(obj, key) {
return obj[key] !== void 0 && obj[key] !== null;
}
function generateFlagOn(obj) {
if (!obj || typeof obj !== "object") {
return false;
}
const g = obj.generate;
if (g === true || g === 1) {
return true;
}
if (typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim())) {
return true;
}
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
return acts.includes("generate");
}
function isPatchObject2(obj) {
if (!obj || typeof obj !== "object") {
return false;
@@ -96,7 +111,7 @@
return patch;
}
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
if (patch.generate === true || acts.includes("generate")) {
if (generateFlagOn(patch) || acts.includes("generate")) {
patch.generate = true;
}
if (typeof patch.ask === "string") {
@@ -114,31 +129,63 @@
}
return patch;
}
function tryParsePatchJson(raw) {
try {
const obj = JSON.parse(String(raw || "").trim());
if (isPatchObject2(obj)) {
return normalizePatch(obj);
}
} catch (e) {
}
return null;
}
function extractPatch(text) {
if (!text) {
return { prose: text || "", patch: null };
}
const re = new RegExp(FENCE_RE.source, "gi");
let match;
let lastPatch = null;
let prose = text;
let lastAny = null;
let lastAnyIndex = -1;
let lastAnyLen = 0;
let lastTerminal = null;
let lastTermIndex = -1;
let lastTermLen = 0;
while ((match = re.exec(text)) !== null) {
try {
const obj = JSON.parse(match[1].trim());
if (isPatchObject2(obj)) {
lastPatch = normalizePatch(obj);
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
}
} catch (e) {
const parsed = tryParsePatchJson(match[1]);
if (!parsed) {
continue;
}
lastAny = parsed;
lastAnyIndex = match.index;
lastAnyLen = match[0].length;
if (isTerminalStreamPatch(parsed)) {
lastTerminal = parsed;
lastTermIndex = match.index;
lastTermLen = match[0].length;
}
}
return { prose, patch: lastPatch };
const chosen = lastTerminal || lastAny;
if (chosen) {
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
const len = lastTerminal ? lastTermLen : lastAnyLen;
const prose = (text.slice(0, idx) + text.slice(idx + len)).trim();
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: text, patch: null };
}
function isTerminalStreamPatch(obj) {
if (!obj || typeof obj !== "object") {
return false;
}
if (obj.generate === true) {
if (generateFlagOn(obj)) {
return true;
}
if (Array.isArray(obj.ask) && obj.ask.length) {
@@ -153,14 +200,10 @@
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
return true;
}
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
if (acts.includes("generate")) {
return true;
}
if (String(obj.prompt || "").trim().length >= 48) {
return true;
}
if (obj.loras != null || obj.aspect != null || obj.steps != null || obj.width != null || obj.height != null || obj.cfg != null || obj.seed != null || obj.controls != null) {
if (obj.loras != null || obj.aspect != null || obj.steps != null || obj.width != null || obj.height != null || obj.cfg != null || obj.seed != null || obj.controls != null || obj.checkpoint != null || obj.negative != null) {
return true;
}
return false;
@@ -172,6 +215,7 @@
SA2.isTerminalStreamPatch = isTerminalStreamPatch;
SA2.normalizePatch = normalizePatch;
SA2.extractPatch = extractPatch;
SA2.generateFlagOn = generateFlagOn;
}
// src/persist.js
@@ -375,7 +419,9 @@
}
const delta = { ...raw };
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
if (delta.generate === true || acts.includes("generate")) {
const g = delta.generate;
const generateOn = g === true || g === 1 || typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim()) || acts.includes("generate");
if (generateOn) {
delta.generate = true;
}
if (typeof delta.ask === "string") {
@@ -392,7 +438,8 @@
if (!patch || typeof patch !== "object") {
return false;
}
if (patch.generate === true) {
const n = normalizeDelta(patch);
if (n?.generate === true) {
return true;
}
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
@@ -631,10 +678,12 @@
session_exact: extras.sessionExact || null
};
}
function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
const delta = normalizeDelta(patch) || {};
const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false;
const generate = !vetoed && patchWantsGenerate(delta);
const modelAsked = patchWantsGenerate(delta);
const userAsked = typeof askGenerateFn === "function" && !!askGenerateFn(userText) && !!(modelAsked || String(delta.prompt || "").trim());
const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked);
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
const look = !!(hasLook && !generate && !vetoed);
const ask = patchAskList(delta);
@@ -884,7 +933,7 @@
scrollToBottom,
hideEmpty
} = opts;
let card2 = null;
let card = null;
let listEl = null;
let titleEl = null;
let steps = [];
@@ -894,16 +943,16 @@
if (!box) {
return null;
}
if (card2 && card2.isConnected) {
return card2;
if (card && card.isConnected) {
return card;
}
if (typeof hideEmpty === "function") {
hideEmpty();
}
card2 = document.createElement("div");
card2.className = "sa-activity sa-activity-live";
card2.setAttribute("role", "status");
card2.setAttribute("aria-live", "polite");
card = document.createElement("div");
card.className = "sa-activity sa-activity-live";
card.setAttribute("role", "status");
card.setAttribute("aria-live", "polite");
const head = document.createElement("button");
head.type = "button";
head.className = "sa-activity-head";
@@ -923,18 +972,18 @@
head.appendChild(chev);
head.addEventListener("click", () => {
open = !open;
card2.classList.toggle("sa-activity-collapsed", !open);
card.classList.toggle("sa-activity-collapsed", !open);
head.setAttribute("aria-expanded", open ? "true" : "false");
});
listEl = document.createElement("div");
listEl.className = "sa-activity-steps";
card2.appendChild(head);
card2.appendChild(listEl);
box.appendChild(card2);
card.appendChild(head);
card.appendChild(listEl);
box.appendChild(card);
if (typeof scrollToBottom === "function") {
scrollToBottom();
}
return card2;
return card;
}
function renderStep(step) {
const row = document.createElement("div");
@@ -970,15 +1019,15 @@
if (titleEl) {
titleEl.textContent = running ? running.label : last?.label || "Assistent";
}
card2.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running"));
card2.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip"));
card.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running"));
card.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip"));
if (typeof scrollToBottom === "function") {
scrollToBottom();
}
}
function begin(title) {
steps = [];
card2 = null;
card = null;
listEl = null;
titleEl = null;
open = true;
@@ -1021,9 +1070,9 @@
titleEl.textContent = summary;
}
paint();
if (card2) {
card2.classList.remove("sa-activity-live");
card2.classList.add("sa-activity-done");
if (card) {
card.classList.remove("sa-activity-live");
card.classList.add("sa-activity-done");
}
}
function noteModelCommands(patch) {
@@ -1242,8 +1291,8 @@
\u041D\u0430\u043F\u0438\u0448\u0438, \u0447\u0442\u043E \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u2014 \u0438\u043B\u0438 \u043A\u0438\u043D\u044C \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441 \u0438 \u043F\u043E\u043F\u0440\u043E\u0441\u0438 \u043F\u0440\u0430\u0432\u043A\u0443.`;
let HELP_TEXT = `Slash-\u043A\u043E\u043C\u0430\u043D\u0434\u044B (\u0431\u0435\u0437 LLM):
/help \u2014 \u044D\u0442\u043E\u0442 \u0441\u043F\u0438\u0441\u043E\u043A
/new \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0441\u044F \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E)
/history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0430\u0442\u043E\u0432
/new \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0441\u044F \u0432 \u0441\u043F\u0438\u0441\u043A\u0435)
/history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0438\u043B\u0438 \u0441\u043A\u0440\u044B\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432
/compress \u2014 \u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u0432 \u0441\u0430\u043C\u043C\u0430\u0440\u0438
/debug \u2014 \u0441\u0432\u043E\u0434\u043A\u0430 UI/Exact (\u0431\u0435\u0437 LLM)
/debug ask \u2014 \u0442\u043E \u0436\u0435 + \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438
@@ -1259,11 +1308,11 @@
/inventory \u2014 rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439 + \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A LoRA
\u0427\u0438\u043F\u0441\u044B \u043D\u0430\u0434 \u043F\u043E\u043B\u0435\u043C \u0432\u0432\u043E\u0434\u0430 \u0434\u0435\u043B\u0430\u044E\u0442 \u0442\u043E \u0436\u0435 \u0434\u043B\u044F aspect / seed / vary / Turbo\xB7RAW.
\u041F\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442; \u0441\u043C\u0435\u043D\u0430 \u0447\u0430\u0442\u0430 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u0438 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.`;
\u041F\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442; \u0441\u043C\u0435\u043D\u0430 \u0447\u0430\u0442\u0430 \u0432 \u043F\u0430\u043D\u0435\u043B\u0438 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.`;
let SLASH_COMMANDS = [
{ cmd: "/help", hint: "\u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043C\u0430\u043D\u0434" },
{ cmd: "/new", hint: "\u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442" },
{ cmd: "/history", hint: "\u0438\u0441\u0442\u043E\u0440\u0438\u044F \u0447\u0430\u0442\u043E\u0432" },
{ cmd: "/history", hint: "\u043F\u0430\u043D\u0435\u043B\u044C \u0447\u0430\u0442\u043E\u0432" },
{ cmd: "/compress", hint: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B" },
{ cmd: "/debug", hint: "\u0441\u0432\u043E\u0434\u043A\u0430 \xB7 ask = \u0441 LLM" },
{ cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" },
@@ -2143,9 +2192,16 @@ ${patch.prompt}`;
function mergePromptEnRewrite(effective) {
const base = state.pendingPromptEnMerge;
state.pendingPromptEnMerge = null;
if (!base || !effective) {
if (!base) {
return effective;
}
if (!effective) {
return {
...base,
generate: true,
actions: Array.isArray(base.actions) && base.actions.length ? base.actions : ["generate"]
};
}
return {
...base,
...effective,
@@ -2184,6 +2240,21 @@ ${patch.prompt}`;
}
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
}
function userAsksGenerate(text) {
if (window.SA && typeof SA.userAsksGenerate === "function") {
return SA.userAsksGenerate(text);
}
const t = String(text || "").trim();
if (!t || userAsksNoGenerate(t)) {
return false;
}
if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) {
return true;
}
return cyrTokenRe(
"\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)"
).test(t);
}
function packWantsVision(pack) {
const p = String(pack || "");
return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit";
@@ -2191,11 +2262,16 @@ ${patch.prompt}`;
function resolveTurnIntent2(patch, userText, opts = {}) {
const S = window.SA && window.SA.session;
if (S && typeof S.resolveTurnIntent === "function") {
return S.resolveTurnIntent(patch, userText, { vetoFn: userAsksNoGenerate });
return S.resolveTurnIntent(patch, userText, {
vetoFn: userAsksNoGenerate,
askGenerateFn: userAsksGenerate,
fromAutoCritique: !!opts.fromAutoCritique
});
}
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
const modelAsked = !!(patch && (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")));
const generate = !vetoed && !opts.fromAutoCritique && modelAsked;
const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1 || typeof patch.generate === "string" && /^(true|1|yes|on)$/i.test(patch.generate) || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")));
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText) && !!(modelAsked || String(patch?.prompt || "").trim());
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
const look = !!(hasLook && !vetoed && !generate);
const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : [];
@@ -3364,6 +3440,7 @@ ${patch.prompt}`;
genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C";
genBtn.addEventListener("click", async () => {
if (isGenerateUnavailable()) {
setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F");
return;
}
startBusyUi("silent_gen");
@@ -4161,7 +4238,7 @@ ${patch.prompt}`;
const empty = document.createElement("div");
empty.className = "sa-chat-empty";
empty.id = "sa_chat_empty";
empty.innerHTML = emptyHint || '<div class="sa-chat-empty-title">\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442</div><div class="sa-chat-empty-hint">\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441.<br><strong>+</strong> \u2014 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u0447\u0430\u0442 \xB7 <strong>\u0418\u0441\u0442\u043E\u0440\u0438\u044F</strong> \u2014 \u0432\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u043A \u043F\u0440\u043E\u0448\u043B\u043E\u043C\u0443 (\u0441 \u0435\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430\u043C\u0438).</div>';
empty.innerHTML = emptyHint || '<div class="sa-chat-empty-title">\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442</div><div class="sa-chat-empty-hint">\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441.<br><strong>+</strong> \u2014 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u0447\u0430\u0442 \xB7 \u043A\u043D\u043E\u043F\u043A\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0441\u043B\u0435\u0432\u0430 \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.</div>';
box.appendChild(empty);
}
function renderHistoryIntoUi(messages) {
@@ -4222,19 +4299,34 @@ ${patch.prompt}`;
}
const chat = findChat(state.activeChatId);
el.textContent = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
el.title = (chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442") + " \u2014 \u043A\u043B\u0438\u043A: \u0418\u0441\u0442\u043E\u0440\u0438\u044F";
el.title = chat?.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
}
function chatHasTranscript(c) {
return (c?.messages || []).length > 0 || (c?.messages_count || 0) > 0;
}
function savedChatsCount() {
return (state.chats || []).filter((c) => (c.messages || []).length > 0).length;
return (state.chats || []).filter(chatHasTranscript).length;
}
function isBlankActiveChat() {
if ((state.history || []).length) {
return false;
}
const chat = findChat(state.activeChatId);
return !chat || !chatHasTranscript(chat);
}
function syncHistoryBadge() {
const btn = $2("sa_btn_chats");
if (!btn) {
return;
const countEl = $2("sa_chats_count");
const n = (state.chats || []).filter(chatHasTranscript).length;
const open = !!state.chatsPanelOpen;
if (btn) {
btn.title = open ? n ? `\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u0421\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442\u044B" : n ? `\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B (${n})` : "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0447\u0430\u0442\u044B";
btn.setAttribute("aria-label", btn.title);
}
if (countEl) {
countEl.hidden = n < 1;
countEl.textContent = n > 99 ? "99+" : String(n);
}
const n = savedChatsCount();
btn.textContent = n > 0 ? `\u0418\u0441\u0442\u043E\u0440\u0438\u044F (${n})` : "\u0418\u0441\u0442\u043E\u0440\u0438\u044F";
btn.title = n > 0 ? `\u0421\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u0447\u0430\u0442\u043E\u0432: ${n}. \u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B.` : "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0447\u0430\u0442\u043E\u0432 (\u043F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u043E)";
}
function formatChatWhen(ts) {
if (!ts) {
@@ -4304,7 +4396,7 @@ ${patch.prompt}`;
root.innerHTML = "";
syncHistoryBadge();
const q = (state.chatsQuery || "").trim().toLowerCase();
let chats = (state.chats || []).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).filter((c) => (c.messages || []).length > 0 || (c.messages_count || 0) > 0);
let chats = (state.chats || []).slice().sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).filter((c) => c.id === state.activeChatId || chatHasTranscript(c));
if (q) {
const local = chats.filter((c) => chatMatchesQuery(c, q));
const seen = new Set(local.map((c) => c.id));
@@ -4342,6 +4434,7 @@ ${patch.prompt}`;
btn?.classList.toggle("sa-sessions-toggle-active", state.chatsPanelOpen);
root?.classList.toggle("sa-drawer-open", state.chatsPanelOpen);
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? "1" : "0");
syncHistoryBadge();
if (state.chatsPanelOpen) {
saveActiveChatToStore();
const search = $2("sa_chats_search");
@@ -4353,13 +4446,17 @@ ${patch.prompt}`;
}
saveUiStateToDisk();
}
async function startNewChat({ saveCurrent = true, force = false } = {}) {
if (!force && (state.busy || state.generating)) {
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043A\u043E\u043D\u0446\u0430 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438\u043B\u0438 \u0421\u0442\u043E\u043F");
async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) {
if (!force && isBlankActiveChat()) {
setStatus("\u0423\u0436\u0435 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442");
if (openDrawer) {
setChatsPanelOpen(true);
}
$2("sa_input")?.focus();
return;
}
if (force) {
abortInFlightWork({ status: "" });
if (state.busy || state.generating) {
abortInFlightWork({ status: "", interruptSwarm: !!state.generating });
}
if (saveCurrent) {
saveActiveChatToStore({ dropEmpty: true });
@@ -4401,6 +4498,12 @@ ${patch.prompt}`;
updateCtxChip();
setStatus("\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u2014 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441");
maybeWelcome();
if (openDrawer) {
setChatsPanelOpen(true);
}
if (openDrawer || !force) {
$2("sa_input")?.focus();
}
}
async function switchToChat(id) {
if (!id || id === state.activeChatId) {
@@ -4540,7 +4643,7 @@ ${patch.prompt}`;
syncBuildGenButton();
clearPersistedHistory();
resetContextMemory({ persist: false });
resetMessagesUi('<div class="sa-chat-empty-title">\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D</div><div class="sa-chat-empty-hint">\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u044B. \u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043D\u0430 \u043C\u0435\u0441\u0442\u0435. <strong>+</strong> \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E, <strong>\u0418\u0441\u0442\u043E\u0440\u0438\u044F</strong> \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.</div>');
resetMessagesUi('<div class="sa-chat-empty-title">\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D</div><div class="sa-chat-empty-hint">\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u044B. \u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043D\u0430 \u043C\u0435\u0441\u0442\u0435. <strong>+</strong> \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442, \u043A\u043D\u043E\u043F\u043A\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0441\u043B\u0435\u0432\u0430 \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.</div>');
setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D");
updateSessionLabel();
syncHistoryBadge();
@@ -5577,7 +5680,7 @@ ${patch.prompt}`;
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
return;
}
const waitMs = state.streamFenceDone ? 4e3 : 15e3;
const waitMs = state.streamFenceDone ? 800 : state.gotDelta ? 2200 : 15e3;
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
return;
}
@@ -6159,23 +6262,30 @@ ${patch.prompt}`;
state.streamFenceDone = false;
return div;
}
function parseTerminalPatchObject(raw) {
try {
const obj = JSON.parse(String(raw || "").trim());
const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
return terminal ? obj : null;
} catch (e) {
return null;
}
}
function streamHasClosedPatchFence(text) {
const t = String(text || "");
if (!/```[\s\S]*```/.test(t)) {
return false;
}
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
let match;
while ((match = re.exec(t)) !== null) {
try {
const obj = JSON.parse(match[1].trim());
const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
if (terminal) {
if (/```[\s\S]*```/.test(t)) {
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
let match;
while ((match = re.exec(t)) !== null) {
if (parseTerminalPatchObject(match[1])) {
return true;
}
} catch (e) {
}
}
const brace = t.lastIndexOf("{");
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
return true;
}
return false;
}
function trimToClosedPatchFence(text) {
@@ -6184,16 +6294,18 @@ ${patch.prompt}`;
let match;
let lastEnd = -1;
while ((match = re.exec(t)) !== null) {
try {
const obj = JSON.parse(match[1].trim());
const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
if (terminal) {
lastEnd = match.index + match[0].length;
}
} catch (e) {
if (parseTerminalPatchObject(match[1])) {
lastEnd = match.index + match[0].length;
}
}
return lastEnd > 0 ? t.slice(0, lastEnd).trimEnd() : t;
if (lastEnd > 0) {
return t.slice(0, lastEnd).trimEnd();
}
const brace = t.lastIndexOf("{");
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
return t.trimEnd();
}
return t;
}
function appendStreamDelta(delta) {
if (state.streamFenceDone) {
@@ -6218,6 +6330,18 @@ ${patch.prompt}`;
if (streamHasClosedPatchFence(state.streamText)) {
state.streamText = trimToClosedPatchFence(state.streamText);
state.streamFenceDone = true;
setAssistantBody(state.streamEl, state.streamText, { live: true });
scrollMessagesToBottom();
const fn = state.onClosedTerminalFence;
state.onClosedTerminalFence = null;
if (typeof fn === "function") {
try {
fn(state.streamText);
} catch (e) {
console.warn("Assistent closed-fence finalize", e);
}
}
return;
}
setAssistantBody(state.streamEl, state.streamText, { live: true });
scrollMessagesToBottom();
@@ -6242,16 +6366,9 @@ ${patch.prompt}`;
const { prose, patch } = extractPatch2(fullReply);
setAssistantBody(el, prose || fullReply || "");
el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove());
if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
if (patch) {
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
mountPatchBlock(el, patch, { silent });
} else if (card) {
const wrap = document.createElement("div");
wrap.className = "sa-patch sa-card-json-preview";
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(card, null, 2);
wrap.appendChild(pre);
el.appendChild(wrap);
}
if (!(meta && meta.historical)) {
mountCurateButtons(el, meta);
@@ -7387,8 +7504,6 @@ ${data.ui.help_extra}`.trim();
const n = state.inventory.loras.length;
const ck = state.inventory.checkpoints.length;
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`);
if (state.view === "cards") {
}
if (done) {
done(state.inventory);
}
@@ -8314,7 +8429,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null };
let effective = extracted && extracted.patch ? extracted.patch : null;
if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === "function") {
if (opts.fromPromptEnRetry && typeof mergePromptEnRewrite === "function" && (effective || state.pendingPromptEnMerge)) {
effective = mergePromptEnRewrite(effective);
}
const S = window.SA && window.SA.session;
@@ -8322,11 +8437,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (effective && act && typeof act.noteModelCommands === "function") {
act.noteModelCommands(effective);
}
const promptChanged = !!(effective && String(effective.prompt || "").trim());
if (effective && S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
activityDone("delta", {
kind: "delta",
label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E",
label: promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E",
detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ")
});
try {
@@ -8342,7 +8458,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
if (typeof doInterruptNow === "function") doInterruptNow();
}
const intent = resolveTurnIntent2(effective, opts.userText || "", opts);
let intent = resolveTurnIntent2(effective, opts.userText || "", opts);
if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) {
intent = { ...intent, generate: true };
}
if (effective) {
if (intent.generate) {
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
@@ -8450,6 +8569,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
if (typeof appendSystemNote === "function") {
appendSystemNote(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u044E Generate");
}
setStatus(promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \xB7 Generate" : "Generate");
const srcOut = await runGenerateFromPatch(
{ ...effective || {}, actions: ["generate"], generate: true },
{ force: true, fromSession: true }
@@ -8459,12 +8582,20 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut);
if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut);
}
} else if (effective && $2("sa_auto_apply")?.checked) {
} else if (effective) {
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
if (promptChanged || $2("sa_auto_apply")?.checked) {
await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
if (promptChanged && typeof appendSystemNote === "function") {
appendSystemNote("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D");
}
if (promptChanged) {
setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D");
}
}
if (!state.generating && typeof stopBusyUi === "function") {
stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : "");
stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : "");
}
}
state.pendingSilentGen = false;
@@ -8628,12 +8759,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
return true;
}
if (cmd === "new" || cmd === "newchat") {
await startNewChat({ saveCurrent: true });
await startNewChat({ saveCurrent: true, openDrawer: true });
return true;
}
if (cmd === "history" || cmd === "chats" || cmd === "sessions") {
setChatsPanelOpen(true);
setStatus("/history");
setChatsPanelOpen(!state.chatsPanelOpen);
setStatus(state.chatsPanelOpen ? "/history" : "\u0427\u0430\u0442\u044B \u0441\u043A\u0440\u044B\u0442\u044B");
return true;
}
if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") {
@@ -9036,6 +9167,8 @@ ${HELP_TEXT}`);
startBusyUi(state.expectColdLoad ? "loading" : "thinking");
}
const context = collectLiveContext();
context.prior_assistant_turns = (state.history || []).filter((m) => m && m.role === "assistant" && !m.systemish).length;
context.do_not_greet = context.prior_assistant_turns > 0;
context.has_vision_image = visionReadySlots().length > 0;
context.images_in_request = !!(images && images.length);
context.attached_slot_ids = attachableSlots().map((s) => s.id);
@@ -9074,6 +9207,7 @@ ${HELP_TEXT}`);
return;
}
state.turnSettled = true;
state.onClosedTerminalFence = null;
clearStreamStall();
if (meta.system_chars != null) {
state.lastSystemChars = Number(meta.system_chars) || 0;
@@ -9128,6 +9262,7 @@ ${HELP_TEXT}`);
return;
}
state.turnSettled = true;
state.onClosedTerminalFence = null;
clearStreamStall();
state.busy = false;
setInterruptVisible(state.generating);
@@ -9144,14 +9279,19 @@ ${HELP_TEXT}`);
};
if (typeof makeWSRequest === "function") {
beginStreamMessage(msgMeta);
armStreamStall(chatEpoch, (reply) => {
const settleStreamReply = (reply) => {
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
return;
}
const raw = String(reply || state.streamText || "").trim() || (state.streamEl?.querySelector(".sa-msg-body")?.textContent || "");
if (state.streamEl) {
finalizeStreamMessage(reply, []);
finalizeStreamMessage(raw, []);
}
finishOk(reply, [], {});
finishOk(raw, [], {});
};
state.onClosedTerminalFence = (text2) => settleStreamReply(text2);
armStreamStall(chatEpoch, (reply) => {
settleStreamReply(reply);
});
makeWSRequest(
"AssistentChatWS",
@@ -9184,12 +9324,13 @@ ${HELP_TEXT}`);
if (data.delta) {
appendStreamDelta(data.delta);
}
if (data.done || data.reply != null) {
if (data.done || typeof data.reply === "string" && data.reply.length > 0 && !data.delta) {
if (state.turnSettled) {
return;
}
const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
const reply = String(state.streamText || data.reply || "").trim() || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
const civitai = data.civitai_results || [];
state.onClosedTerminalFence = null;
if (state.streamEl) {
finalizeStreamMessage(reply, civitai);
}
@@ -9454,7 +9595,14 @@ ${HELP_TEXT}`);
wireSplitter();
registerSendButton();
wireSlashInput();
$2("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true }));
$2("sa_btn_new_chat")?.addEventListener("click", (e) => {
e.stopPropagation();
startNewChat({ saveCurrent: true, openDrawer: true });
});
$2("sa_btn_new_chat_bar")?.addEventListener("click", (e) => {
e.stopPropagation();
startNewChat({ saveCurrent: true, openDrawer: true });
});
$2("sa_ctx_chip")?.addEventListener("click", (e) => {
e.stopPropagation();
toggleCtxPanel();
@@ -9489,16 +9637,6 @@ ${HELP_TEXT}`);
setChatsPanelOpen(!state.chatsPanelOpen);
});
$2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false));
$2("sa_session_label")?.addEventListener("click", (e) => {
e.stopPropagation();
setChatsPanelOpen(!state.chatsPanelOpen);
});
$2("sa_session_label")?.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setChatsPanelOpen(!state.chatsPanelOpen);
}
});
$2("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation());
$2("sa_chats_list")?.addEventListener("click", (e) => {
const row = e.target.closest(".sa-chat-row");
@@ -9513,9 +9651,7 @@ ${HELP_TEXT}`);
}
return;
}
if (e.target.closest("[data-open]")) {
switchToChat(id);
}
switchToChat(id);
});
let chatsSearchTimer = null;
$2("sa_chats_search")?.addEventListener("input", () => {
@@ -9753,7 +9889,11 @@ ${HELP_TEXT}`);
});
$2("sa_chips")?.addEventListener("click", async (e) => {
const btn = e.target.closest(".sa-chip");
if (!btn || state.busy || state.generating) {
if (!btn) {
return;
}
if (state.busy || state.generating) {
setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F");
return;
}
const aspect = btn.getAttribute("data-aspect");