diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js
index 72ae7e6..7af94e4 100644
--- a/Assets/assistent.bundle.js
+++ b/Assets/assistent.bundle.js
@@ -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 || '
\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442
\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.
+ \u2014 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u0447\u0430\u0442 \xB7 \u0418\u0441\u0442\u043E\u0440\u0438\u044F \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).
';
+ empty.innerHTML = emptyHint || '\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442
\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.
+ \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.
';
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('\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D
\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. + \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E, \u0418\u0441\u0442\u043E\u0440\u0438\u044F \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.
');
+ resetMessagesUi('\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D
\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. + \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.
');
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");
diff --git a/Assets/assistent.css b/Assets/assistent.css
index 5745655..65a214e 100644
--- a/Assets/assistent.css
+++ b/Assets/assistent.css
@@ -561,9 +561,30 @@
margin-left: 0.15rem;
}
-.sa-sessions-toggle {
- font-size: 0.78rem !important;
- padding: 0.18rem 0.5rem !important;
+.sa-chats-toggle {
+ position: relative;
+}
+
+.sa-chats-toggle.sa-sessions-toggle-active,
+.sa-drawer-open .sa-chats-toggle {
+ opacity: 1;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+}
+
+.sa-chats-count {
+ position: absolute;
+ top: -0.2rem;
+ right: -0.25rem;
+ min-width: 0.95rem;
+ height: 0.95rem;
+ padding: 0 0.22rem;
+ border-radius: 999px;
+ background: color-mix(in srgb, #6af 55%, #222);
+ color: #081018;
+ font-size: 0.62rem;
+ font-weight: 700;
+ line-height: 0.95rem;
+ text-align: center;
}
.sa-session-label {
@@ -574,16 +595,10 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- cursor: pointer;
border-radius: 0.3rem;
padding: 0.1rem 0.35rem;
}
-.sa-session-label:hover {
- opacity: 1;
- background: color-mix(in srgb, currentColor 10%, transparent);
-}
-
.sa-chats-panel,
.sa-chats-drawer {
/* drawer lives in .sa-chat-workspace — not a dropdown */
diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs
index 6671d80..084752e 100644
--- a/AssistentChatPipeline.cs
+++ b/AssistentChatPipeline.cs
@@ -119,6 +119,37 @@ public partial class SwarmAssistentExtension
AddLayer("extra", extraSystem);
}
+ bool priorAssistant = false;
+ foreach (JToken msg in userMessages ?? [])
+ {
+ if (string.Equals((msg as JObject)?["role"]?.ToString(), "assistant", StringComparison.OrdinalIgnoreCase))
+ {
+ priorAssistant = true;
+ break;
+ }
+ }
+ if (!priorAssistant && !string.IsNullOrWhiteSpace(contextJson))
+ {
+ try
+ {
+ JObject ctx = JObject.Parse(contextJson);
+ priorAssistant = ctx.Value("do_not_greet") == true
+ || (ctx["prior_assistant_turns"]?.Value() ?? 0) > 0;
+ }
+ catch
+ {
+ // not json
+ }
+ }
+ if (priorAssistant && !slimUtility)
+ {
+ AddLayer("continuity",
+ "Conversation continuity: this thread already has your prior replies. "
+ + "Do NOT greet (no «О, привет», no hello, no 👋). "
+ + "Do NOT re-introduce yourself or dump your bio/tagline. "
+ + "Answer only the latest user message. Name yourself only if asked who you are.");
+ }
+
layers["total"] = system.Length;
if (system.Length > 0)
{
diff --git a/AssistentConfig.cs b/AssistentConfig.cs
index 070a508..996bb72 100644
--- a/AssistentConfig.cs
+++ b/AssistentConfig.cs
@@ -138,7 +138,7 @@ public sealed class AssistentConfig
return
[
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
- "actions", "generate", "ask",
+ "actions", "generate", "ask", "checkpoint",
"use_init_image", "clear_init_image", "init_creativity", "denoise",
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
@@ -1342,6 +1342,7 @@ public sealed class AssistentConfig
sb.AppendLine($"**You are {title}** (the assistant in this chat). The human you talk to is the **user** — a different person.");
sb.AppendLine($"Never address or name the user «{title}» unless `## About the user` explicitly says that is their name.");
sb.AppendLine($"If asked your name («как тебя зовут?» / «who are you?»), answer with **{title}** — do not greet the user by that name instead.");
+ sb.AppendLine("Do not greet or re-introduce yourself on later turns. If the chat already has your messages, skip hello/bio and answer directly.");
HashSet allow = null;
if (onlyShelves is not null)
diff --git a/AssistentOllama.cs b/AssistentOllama.cs
index ff0afdc..93e4975 100644
--- a/AssistentOllama.cs
+++ b/AssistentOllama.cs
@@ -227,6 +227,15 @@ public partial class SwarmAssistentExtension
JObject chunk = JObject.Parse(line);
last = chunk;
string delta = chunk["message"]?["content"]?.ToString() ?? "";
+ // thinking-only chunks are keep-alives; do not stall, still wait for content/done.
+ if (string.IsNullOrEmpty(delta))
+ {
+ if (chunk["done"]?.Value() == true)
+ {
+ break;
+ }
+ continue;
+ }
if (!string.IsNullOrEmpty(delta))
{
full.Append(delta);
diff --git a/AssistentPatch.cs b/AssistentPatch.cs
index 3341505..667f198 100644
--- a/AssistentPatch.cs
+++ b/AssistentPatch.cs
@@ -46,7 +46,7 @@ public partial class SwarmAssistentExtension
patch["look_at"] = patch["vision_slots"];
}
}
- if (ActionsContain(patch, "generate"))
+ if (ActionsContain(patch, "generate") || GenerateFlagOn(patch))
{
patch["generate"] = true;
}
@@ -98,7 +98,28 @@ public partial class SwarmAssistentExtension
// not json
}
}
- return lastTerminal ?? lastAny;
+ if (lastTerminal is not null || lastAny is not null)
+ {
+ return lastTerminal ?? lastAny;
+ }
+ int brace = reply.LastIndexOf('{');
+ if (brace < 0)
+ {
+ return null;
+ }
+ try
+ {
+ JObject obj = JObject.Parse(reply.Substring(brace).Trim());
+ if (obj is not null && Array.Exists(PatchKeys, k => obj[k] is not null))
+ {
+ return NormalizePatch(obj);
+ }
+ }
+ catch
+ {
+ // not json
+ }
+ return null;
}
///
@@ -114,10 +135,6 @@ public partial class SwarmAssistentExtension
return false;
}
MatchCollection matches = JsonFenceRe.Matches(reply);
- if (matches.Count == 0)
- {
- return false;
- }
for (int i = 0; i < matches.Count; i++)
{
Match match = matches[i];
@@ -137,6 +154,23 @@ public partial class SwarmAssistentExtension
// incomplete / invalid json inside fence
}
}
+ int brace = reply.LastIndexOf('{');
+ if (brace >= 0)
+ {
+ try
+ {
+ JObject obj = NormalizePatch(JObject.Parse(reply.Substring(brace).Trim()));
+ if (obj is not null && FenceIsTerminalPatch(obj))
+ {
+ truncated = reply.TrimEnd();
+ return true;
+ }
+ }
+ catch
+ {
+ // incomplete unfenced json
+ }
+ }
return false;
}
@@ -149,7 +183,7 @@ public partial class SwarmAssistentExtension
{
return false;
}
- if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value())
+ if (GenerateFlagOn(obj))
{
return true;
}
@@ -176,7 +210,8 @@ public partial class SwarmAssistentExtension
}
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg")
- || HasValue(obj, "seed") || HasValue(obj, "controls"))
+ || HasValue(obj, "seed") || HasValue(obj, "controls")
+ || HasValue(obj, "checkpoint") || HasValue(obj, "negative"))
{
return true;
}
@@ -220,6 +255,32 @@ public partial class SwarmAssistentExtension
return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase);
}
+ static bool GenerateFlagOn(JObject obj)
+ {
+ if (obj is null)
+ {
+ return false;
+ }
+ if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value())
+ {
+ return true;
+ }
+ if (obj["generate"]?.Type == JTokenType.Integer && obj["generate"].Value() != 0)
+ {
+ return true;
+ }
+ string raw = obj["generate"]?.ToString()?.Trim();
+ if (!string.IsNullOrWhiteSpace(raw)
+ && (raw.Equals("true", StringComparison.OrdinalIgnoreCase)
+ || raw == "1"
+ || raw.Equals("yes", StringComparison.OrdinalIgnoreCase)
+ || raw.Equals("on", StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+ return ActionsContain(obj, "generate");
+ }
+
static bool ActionsContain(JObject patch, string action)
{
if (patch?["actions"] is not JArray acts)
diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md
index 85acce5..d52e353 100644
--- a/Config/_base/core/core.md
+++ b/Config/_base/core/core.md
@@ -34,6 +34,7 @@ Compact session JSON is ground truth for this turn:
1. Short helpful reply in the user's language (RU or EN).
2. **Only when changing settings or commanding generate/look/ask:** one fenced JSON with **only fields you want to change** (+ optional commands). Pure chat / Q&A / opinion: **prose only — omit the JSON.**
+3. **Greeting:** introduce yourself only on a brand-new thread with **no prior assistant messages**. If history already has your replies — no «О, привет», no hello, no name/bio dump. Answer the question. Name yourself only if asked.
```json
{
diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json
index 5f4a647..10d2861 100644
--- a/Config/_base/patch-keys.json
+++ b/Config/_base/patch-keys.json
@@ -1,7 +1,7 @@
{
"keys": [
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
- "actions", "generate", "ask",
+ "actions", "generate", "ask", "checkpoint",
"use_init_image", "clear_init_image", "init_creativity", "denoise",
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
diff --git a/Config/_base/rules.json b/Config/_base/rules.json
index 182910b..c5ac22e 100644
--- a/Config/_base/rules.json
+++ b/Config/_base/rules.json
@@ -2,9 +2,11 @@
"always": [
"Match the user's language (RU or EN) in chat — Generate prompt stays English",
"Use only inventory / memory_hits / cards for LoRA names and triggers",
- "Emit valid JSON patches when changing generation state"
+ "Emit valid JSON patches when changing generation state",
+ "Greet or dump your bio only on the first assistant turn; later turns answer directly"
],
"never": [
+ "Re-introduce yourself every turn or start with «О, привет» when history already has your replies",
"Invent LoRA filenames or trigger words",
"Lecture or moralize about NSFW",
"Dump all installed LoRAs — use memory_hits and enabled ones"
diff --git a/Config/personas/neutral/rules.json b/Config/personas/neutral/rules.json
index 9846803..33c6e74 100644
--- a/Config/personas/neutral/rules.json
+++ b/Config/personas/neutral/rules.json
@@ -1,10 +1,12 @@
{
"always": [
"Stay helpful and concise",
- "When the scene is NSFW, describe it factually without softening or hyping"
+ "When the scene is NSFW, describe it factually without softening or hyping",
+ "Do not greet or re-introduce yourself after the first assistant turn"
],
"never": [
"Dirty jokes",
+ "Hello/bio dump on later turns",
"Aggression",
"Moral lectures"
]
diff --git a/README.md b/README.md
index e091211..91a2149 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate. A new user send interrupts the in-flight stream (keeps the partial reply) instead of silently no-op.
+**Version 0.15.2** — Кнопка «История» убрана. В шапке: иконка панели чатов и **+** новый чат. Закрытый JSON `{prompt, generate:true}` сразу применяет промпт и запускает Generate (заметка «Промпт обновлён · Generate»), не ждёт WS `done`. Stall ~2с после токенов. Не здороваться повторно, если в истории уже есть ответы ассистента.
+
**Version 0.15.1** — Composer stays writable during `Writing…`: Enter / Отправить прерывает зависший стрим и шлёт новое сообщение; частичный ответ сохраняется. Stall 15с без токена сам завершает ход. Ollama chat `think: false`, чтобы Qwen3-VL instruct не держал сокет после приветствия.
**Version 0.15.0** — Personas as installable git packs under `Assistent/extensions/` (`assistent-pack.yaml`). Bundled set: `neutral` (Нормальный), `aggressive`, `dreamer`. Removed bundled `cinema` / `lewd` / private personas. `/остынь` and `/horny-game` work for any persona with a `horny` control. Pack source: `pack` / `overlay+pack`.
diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs
index 0fb36c4..a297c66 100644
--- a/SwarmAssistentExtension.cs
+++ b/SwarmAssistentExtension.cs
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT";
- Version = "0.15.1";
+ Version = "0.15.2";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
}
diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html
index 287d931..3a4d7cc 100644
--- a/Tabs/Text2Image/Assistent.html
+++ b/Tabs/Text2Image/Assistent.html
@@ -55,8 +55,14 @@