diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js
index 1411677..2d54c5d 100644
--- a/Assets/assistent.bundle.js
+++ b/Assets/assistent.bundle.js
@@ -41,8 +41,8 @@
"sampler",
"scheduler",
"actions",
- "search_query",
- "civitai_query",
+ "generate",
+ "ask",
"use_init_image",
"clear_init_image",
"init_creativity",
@@ -70,18 +70,9 @@
"clear_prompt_images",
"slot_to_prompt_image",
"pack",
- "memories",
- "memory",
- "memory_query",
- "memory_kind",
- "tag_query",
- "user_prefs",
- "inventory_query",
- "skills",
- "persona_shelves",
- "persona_clone",
"persona",
"controls",
+ "inventory_query",
"variants"
];
var PATCH_KEYS = DEFAULT_PATCH_KEYS.slice();
@@ -94,32 +85,22 @@
function has(obj, key) {
return obj[key] !== void 0 && obj[key] !== null;
}
- function isCardObject(obj) {
- if (!obj || typeof obj !== "object") {
- return false;
- }
- const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
- const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
- if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
- return true;
- }
- return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
- }
function isPatchObject2(obj) {
if (!obj || typeof obj !== "object") {
return false;
}
- if (isCardObject(obj)) {
- return false;
- }
return PATCH_KEYS.some((k) => has(obj, k));
}
function normalizePatch(patch) {
if (!patch || typeof patch !== "object") {
return patch;
}
- if (!has(patch, "search_query") && has(patch, "civitai_query")) {
- patch.search_query = patch.civitai_query;
+ const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
+ if (patch.generate === true || acts.includes("generate")) {
+ patch.generate = true;
+ }
+ if (typeof patch.ask === "string") {
+ patch.ask = [patch.ask];
}
if (!has(patch, "init_creativity") && has(patch, "denoise")) {
patch.init_creativity = patch.denoise;
@@ -157,7 +138,13 @@
if (!obj || typeof obj !== "object") {
return false;
}
- if (isCardObject(obj)) {
+ if (obj.generate === true) {
+ return true;
+ }
+ if (Array.isArray(obj.ask) && obj.ask.length) {
+ return true;
+ }
+ if (typeof obj.ask === "string" && obj.ask) {
return true;
}
if (Array.isArray(obj.variants) && obj.variants.length) {
@@ -166,30 +153,14 @@
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
return true;
}
- if (obj.search_query != null || obj.civitai_query != null || obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
- return true;
- }
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
- const hopOrGen = [
- "skill_load",
- "persona_read",
- "memory_get",
- "memory_search",
- "lookup_tags",
- "list_inventory",
- "search_civitai",
- "interrupt",
- "generate",
- "memory_upsert",
- "user_pref_upsert"
- ];
- if (acts.some((a) => hopOrGen.includes(a))) {
+ 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 || obj.memories != null || obj.user_prefs != 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) {
return true;
}
return false;
@@ -197,7 +168,6 @@
function attachPatch(SA2) {
SA2.PATCH_KEYS = PATCH_KEYS;
SA2.setPatchKeys = setPatchKeys;
- SA2.isCardObject = isCardObject;
SA2.isPatchObject = isPatchObject2;
SA2.isTerminalStreamPatch = isTerminalStreamPatch;
SA2.normalizePatch = normalizePatch;
@@ -333,6 +303,741 @@
};
}
+ // src/session.js
+ var MAX_DATA_URL_CHARS = 35e4;
+ var GEN_KEYS = [
+ "prompt",
+ "negative",
+ "width",
+ "height",
+ "aspect",
+ "steps",
+ "cfg",
+ "sigma_shift",
+ "seed",
+ "sampler",
+ "scheduler",
+ "batch",
+ "checkpoint",
+ "loras",
+ "controls",
+ "use_init_image",
+ "clear_init_image",
+ "init_creativity",
+ "denoise",
+ "use_mask_image",
+ "clear_mask_image",
+ "mask_blur",
+ "mask_grow"
+ ];
+ function emptySession() {
+ return {
+ gen: {
+ prompt: "",
+ negative: "",
+ width: null,
+ height: null,
+ aspect: null,
+ steps: null,
+ cfg: null,
+ sigma_shift: null,
+ seed: null,
+ sampler: null,
+ scheduler: null,
+ batch: null,
+ checkpoint: null,
+ loras: [],
+ controls: {},
+ use_init_image: false,
+ clear_init_image: false,
+ init_creativity: null,
+ denoise: null,
+ use_mask_image: false,
+ clear_mask_image: false,
+ mask_blur: null,
+ mask_grow: null
+ },
+ board: {
+ slots: [],
+ selectedSlotId: "ref1",
+ genResults: [],
+ selectedGenResultId: null,
+ refSeq: 1
+ },
+ persona: "neutral",
+ pack: "ordinary",
+ context_memory: null
+ };
+ }
+ function normalizeDelta(raw) {
+ if (!raw || typeof raw !== "object") {
+ return null;
+ }
+ const delta = { ...raw };
+ const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
+ if (delta.generate === true || acts.includes("generate")) {
+ delta.generate = true;
+ }
+ if (typeof delta.ask === "string") {
+ delta.ask = [delta.ask];
+ }
+ if (!Array.isArray(delta.ask)) {
+ delete delta.ask;
+ } else {
+ delta.ask = delta.ask.map(String).filter(Boolean);
+ }
+ return delta;
+ }
+ function patchWantsGenerate(patch) {
+ if (!patch || typeof patch !== "object") {
+ return false;
+ }
+ if (patch.generate === true) {
+ return true;
+ }
+ const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
+ return acts.includes("generate");
+ }
+ function patchAskList(patch) {
+ const n = normalizeDelta(patch);
+ return Array.isArray(n?.ask) ? n.ask : [];
+ }
+ function mergeDelta(session, rawDelta) {
+ const base = session && typeof session === "object" ? structuredCloneSession(session) : emptySession();
+ const delta = normalizeDelta(rawDelta);
+ if (!delta) {
+ return base;
+ }
+ if (!base.gen) {
+ base.gen = emptySession().gen;
+ }
+ for (const key of GEN_KEYS) {
+ if (delta[key] === void 0 || delta[key] === null) {
+ continue;
+ }
+ if (key === "loras" && Array.isArray(delta.loras)) {
+ base.gen.loras = delta.loras.map((l) => ({
+ name: l?.name || l,
+ weight: l?.weight != null ? Number(l.weight) : 1,
+ triggers: Array.isArray(l?.triggers) ? l.triggers : void 0,
+ trigger_phrase: l?.trigger_phrase || void 0
+ })).filter((l) => l.name);
+ continue;
+ }
+ if (key === "controls" && typeof delta.controls === "object") {
+ base.gen.controls = { ...base.gen.controls || {}, ...delta.controls };
+ continue;
+ }
+ if (key === "checkpoint") {
+ base.gen.checkpoint = typeof delta.checkpoint === "object" ? { ...delta.checkpoint } : { name: String(delta.checkpoint) };
+ continue;
+ }
+ base.gen[key] = delta[key];
+ }
+ if (delta.images != null && delta.batch == null) {
+ base.gen.batch = delta.images;
+ }
+ if (delta.pack) {
+ base.pack = String(delta.pack);
+ }
+ if (delta.persona) {
+ base.persona = String(delta.persona);
+ }
+ return base;
+ }
+ function structuredCloneSession(session) {
+ try {
+ return JSON.parse(JSON.stringify(session));
+ } catch {
+ return emptySession();
+ }
+ }
+ function slimSrc(src) {
+ if (!src || typeof src !== "string") {
+ return null;
+ }
+ const s = src.trim();
+ if (!s || s.startsWith("#")) {
+ return null;
+ }
+ if (s.startsWith("data:") && s.length > MAX_DATA_URL_CHARS) {
+ return null;
+ }
+ return s;
+ }
+ function snapshotFromLive({
+ genFields,
+ board,
+ persona,
+ pack,
+ context_memory
+ }) {
+ const session = emptySession();
+ if (genFields && typeof genFields === "object") {
+ for (const key of GEN_KEYS) {
+ if (genFields[key] !== void 0) {
+ session.gen[key] = genFields[key];
+ }
+ }
+ }
+ session.persona = persona || "neutral";
+ session.pack = pack || "ordinary";
+ if (context_memory && typeof context_memory === "object") {
+ session.context_memory = context_memory;
+ }
+ if (board && typeof board === "object") {
+ session.board = {
+ slots: (board.slots || []).map((s) => ({
+ id: s.id,
+ type: s.type,
+ label: s.label,
+ src: slimSrc(s.src),
+ attach: !!s.attach,
+ note: s.note || null
+ })),
+ selectedSlotId: board.selectedSlotId || "ref1",
+ genResults: (board.genResults || []).map((r) => ({
+ id: r.id,
+ label: r.label,
+ src: slimSrc(r.src),
+ patch: r.patch || null
+ })),
+ selectedGenResultId: board.selectedGenResultId || null,
+ refSeq: board.refSeq || 1
+ };
+ }
+ return session;
+ }
+ function sessionFromLegacyParams(params) {
+ if (!params || typeof params !== "object") {
+ return emptySession();
+ }
+ if (params.gen && typeof params.gen === "object") {
+ const s2 = emptySession();
+ s2.gen = { ...s2.gen, ...params.gen };
+ if (params.board && typeof params.board === "object") {
+ s2.board = { ...s2.board, ...params.board };
+ }
+ s2.persona = params.persona || s2.persona;
+ s2.pack = params.pack || s2.pack;
+ if (params.context_memory && typeof params.context_memory === "object") {
+ s2.context_memory = params.context_memory;
+ }
+ return s2;
+ }
+ const s = emptySession();
+ for (const key of GEN_KEYS) {
+ if (params[key] !== void 0 && params[key] !== null) {
+ s.gen[key] = params[key];
+ }
+ }
+ if (Array.isArray(params.loras)) {
+ s.gen.loras = params.loras;
+ }
+ if (params.checkpoint) {
+ s.gen.checkpoint = typeof params.checkpoint === "object" ? params.checkpoint : { name: String(params.checkpoint) };
+ }
+ s.persona = params.persona || "neutral";
+ s.pack = params.pack || "ordinary";
+ if (params.context_memory && typeof params.context_memory === "object") {
+ s.context_memory = params.context_memory;
+ }
+ s.board.genResults = Array.isArray(params.genResults) ? params.genResults : [];
+ s.board.selectedGenResultId = params.selectedGenResultId || null;
+ if (Array.isArray(params.slots)) {
+ s.board.slots = params.slots;
+ }
+ if (params.selectedSlotId) {
+ s.board.selectedSlotId = params.selectedSlotId;
+ }
+ if (params.refSeq) {
+ s.board.refSeq = params.refSeq;
+ }
+ return s;
+ }
+ function toPersistParams(session) {
+ const s = session && typeof session === "object" ? session : emptySession();
+ const out = {
+ gen: s.gen || emptySession().gen,
+ board: s.board || emptySession().board,
+ persona: s.persona || "neutral",
+ pack: s.pack || "ordinary"
+ };
+ if (s.context_memory && typeof s.context_memory === "object") {
+ out.context_memory = s.context_memory;
+ }
+ return out;
+ }
+ function slimText(t, max) {
+ const s = String(t || "");
+ if (s.length <= max) {
+ return s;
+ }
+ return `${s.slice(0, max)}\u2026`;
+ }
+ function compactContext(session, extras = {}) {
+ const s = session && typeof session === "object" ? session : emptySession();
+ const g = s.gen || {};
+ const board = s.board || {};
+ const slots = board.slots || [];
+ const genSlot = slots.find((x) => x.type === "generate" || x.id === "generate");
+ const refs = slots.filter((x) => x.type === "ref" || String(x.id || "").startsWith("ref"));
+ return {
+ session: true,
+ prompt: slimText(g.prompt, extras.promptMax || 2e3),
+ negative: slimText(g.negative, 500),
+ aspect: g.aspect || null,
+ width: g.width ?? null,
+ height: g.height ?? null,
+ steps: g.steps ?? null,
+ cfg: g.cfg ?? null,
+ seed: g.seed ?? null,
+ sigma_shift: g.sigma_shift ?? null,
+ sampler: g.sampler || null,
+ scheduler: g.scheduler || null,
+ batch: g.batch ?? null,
+ checkpoint: g.checkpoint?.name || g.checkpoint || null,
+ selected_loras: (g.loras || []).map((l) => ({
+ name: l.name || l,
+ weight: l.weight != null ? l.weight : 1
+ })),
+ persona: s.persona || "neutral",
+ pack: s.pack || "ordinary",
+ board: {
+ has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)),
+ refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })),
+ gen_results: (board.genResults || []).map((r) => ({
+ id: r.id,
+ label: r.label,
+ has_image: !!r.src,
+ selected: r.id === board.selectedGenResultId
+ })),
+ selected_slot: board.selectedSlotId || null
+ },
+ architecture_ok: extras.architecture_ok !== false,
+ ...extras.extra
+ };
+ }
+ function fullSettingsDump(session, extras = {}) {
+ const compact = compactContext(session, extras);
+ const s = session && typeof session === "object" ? session : emptySession();
+ return {
+ ...compact,
+ detail: "settings",
+ gen: { ...s.gen || {} },
+ controls: s.gen?.controls || {},
+ exact: extras.exact || null,
+ krea_profiles: extras.kreaProfiles || null,
+ session_exact: extras.sessionExact || null
+ };
+ }
+ function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
+ const delta = normalizeDelta(patch) || {};
+ const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false;
+ const generate = !vetoed && patchWantsGenerate(delta);
+ const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
+ const look = !!(hasLook && !generate && !vetoed);
+ const ask = patchAskList(delta);
+ return { generate, look, vetoed, ask };
+ }
+ function attachSession(SA2) {
+ SA2.session = {
+ emptySession,
+ normalizeDelta,
+ mergeDelta,
+ patchWantsGenerate,
+ patchAskList,
+ snapshotFromLive,
+ sessionFromLegacyParams,
+ toPersistParams,
+ compactContext,
+ fullSettingsDump,
+ resolveTurnIntent,
+ GEN_KEYS
+ };
+ }
+
+ // src/context.js
+ function emptyContextMemory() {
+ return {
+ summary: "",
+ untilCount: 0,
+ foldedTurns: 0,
+ at: 0,
+ uiCollapsed: false,
+ promptEvalCount: null
+ };
+ }
+ function normalizeContextMemory(raw) {
+ if (!raw || typeof raw !== "object") {
+ return emptyContextMemory();
+ }
+ const summary = String(raw.summary || "").trim();
+ return {
+ summary,
+ untilCount: Math.max(0, Number(raw.untilCount) || 0),
+ foldedTurns: Math.max(0, Number(raw.foldedTurns) || 0),
+ at: Number(raw.at) || 0,
+ uiCollapsed: !!raw.uiCollapsed && !!summary,
+ promptEvalCount: raw.promptEvalCount != null ? Number(raw.promptEvalCount) || null : null
+ };
+ }
+ function charsToTokens(chars, charsPerToken = 3.2) {
+ const cpt = Math.max(1.5, Number(charsPerToken) || 3.2);
+ const n = Math.max(0, Number(chars) || 0);
+ return Math.ceil(n / cpt);
+ }
+ function estimateBudget(opts = {}) {
+ const numCtx = Math.max(1024, Number(opts.numCtx) || 16384);
+ const numPredict = Math.max(256, Number(opts.numPredict) || 3072);
+ const charsPerToken = Math.max(1.5, Number(opts.charsPerToken) || 3.2);
+ const compressAt = Math.min(0.95, Math.max(0.4, Number(opts.compressAt) || 0.7));
+ const systemChars = Math.max(0, Number(opts.systemChars) || 0);
+ const historyChars = Math.max(0, Number(opts.historyChars) || 0);
+ const memoryChars = Math.max(0, Number(opts.memoryChars) || 0);
+ const inputChars = systemChars + historyChars + memoryChars;
+ const estimated = charsToTokens(inputChars, charsPerToken);
+ const used = opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0 ? Number(opts.promptEvalCount) : estimated;
+ const headroom = Math.max(1024, numCtx - numPredict);
+ const threshold = Math.floor(headroom * compressAt);
+ const ratio = numCtx > 0 ? used / numCtx : 0;
+ let level = "ok";
+ if (ratio >= 0.85 || used >= threshold) {
+ level = "hot";
+ } else if (ratio >= 0.65 || used >= threshold * 0.85) {
+ level = "warn";
+ }
+ return {
+ numCtx,
+ numPredict,
+ headroom,
+ threshold,
+ systemChars,
+ historyChars,
+ memoryChars,
+ inputChars,
+ estimated,
+ used,
+ fromEval: opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0,
+ ratio,
+ level,
+ charsPerToken,
+ compressAt
+ };
+ }
+ function shouldCompress(budget, memory, historyLen, opts = {}) {
+ const keep = Math.max(2, Number(opts.keepMessages) || 8);
+ const len = Math.max(0, Number(historyLen) || 0);
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const uncovered = Math.max(0, len - until);
+ if (uncovered <= keep) {
+ return false;
+ }
+ const used = budget?.used ?? 0;
+ const threshold = budget?.threshold ?? Infinity;
+ return used >= threshold;
+ }
+ function assembleModelMessages(history, memory, keepTurns) {
+ const keep = Math.max(1, Number(keepTurns) || 4) * 2;
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish);
+ const afterSummary = until > 0 ? list.slice(until) : list;
+ const window2 = afterSummary.length > keep ? afterSummary.slice(-keep) : afterSummary;
+ return window2.map((m) => ({
+ role: m.role,
+ content: String(m.content || "").slice(0, 4e3)
+ }));
+ }
+ function messagesToFold(history, memory, keepTurns) {
+ const keep = Math.max(1, Number(keepTurns) || 4) * 2;
+ const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish);
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const foldEnd = Math.max(until, list.length - keep);
+ if (foldEnd <= until) {
+ return [];
+ }
+ return list.slice(until, foldEnd);
+ }
+ function mergeSummary(oldSummary, incoming) {
+ const next = String(incoming || "").trim();
+ if (!next) {
+ return String(oldSummary || "").trim();
+ }
+ const prev = String(oldSummary || "").trim();
+ if (!prev) {
+ return next;
+ }
+ return next;
+ }
+ function formatTokenShort(n) {
+ const v = Math.max(0, Number(n) || 0);
+ if (v >= 1e4) {
+ return `${(v / 1e3).toFixed(1)}k`;
+ }
+ if (v >= 1e3) {
+ return `${(v / 1e3).toFixed(1)}k`;
+ }
+ return String(Math.round(v));
+ }
+ function conversationMemoryBlock(memory, maxChars = 2400) {
+ const m = normalizeContextMemory(memory);
+ if (!m.summary) {
+ return null;
+ }
+ let text = m.summary;
+ if (text.length > maxChars) {
+ text = `${text.slice(0, maxChars)}\u2026`;
+ }
+ return {
+ summary: text,
+ until_count: m.untilCount,
+ folded_turns: m.foldedTurns
+ };
+ }
+ function attachContext(SA2) {
+ SA2.context = {
+ emptyContextMemory,
+ normalizeContextMemory,
+ charsToTokens,
+ estimateBudget,
+ shouldCompress,
+ assembleModelMessages,
+ messagesToFold,
+ mergeSummary,
+ formatTokenShort,
+ conversationMemoryBlock
+ };
+ }
+
+ // src/activity.js
+ var STEP_ICONS = {
+ think: "\u25C7",
+ stream: "\u270E",
+ delta: "\u21E2",
+ ask: "?",
+ look: "\u25CE",
+ prep: "\u21BB",
+ generate: "\u25B7",
+ merge: "\u2295",
+ warm: "\u25B2",
+ park: "\u25BC",
+ inventory: "\u25A4",
+ compress: "\u25A4",
+ done: "\u2713",
+ skip: "\u2013",
+ error: "!"
+ };
+ function createActivityController(opts = {}) {
+ const {
+ getMessagesEl,
+ scrollToBottom,
+ hideEmpty
+ } = opts;
+ let card2 = null;
+ let listEl = null;
+ let titleEl = null;
+ let steps = [];
+ let open = true;
+ function ensureCard() {
+ const box = typeof getMessagesEl === "function" ? getMessagesEl() : null;
+ if (!box) {
+ return null;
+ }
+ if (card2 && card2.isConnected) {
+ return card2;
+ }
+ 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");
+ const head = document.createElement("button");
+ head.type = "button";
+ head.className = "sa-activity-head";
+ head.setAttribute("aria-expanded", "true");
+ const spin = document.createElement("span");
+ spin.className = "sa-activity-spin";
+ spin.setAttribute("aria-hidden", "true");
+ titleEl = document.createElement("span");
+ titleEl.className = "sa-activity-title";
+ titleEl.textContent = "Assistent";
+ const chev = document.createElement("span");
+ chev.className = "sa-activity-chev";
+ chev.setAttribute("aria-hidden", "true");
+ chev.textContent = "\u25BE";
+ head.appendChild(spin);
+ head.appendChild(titleEl);
+ head.appendChild(chev);
+ head.addEventListener("click", () => {
+ open = !open;
+ card2.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);
+ if (typeof scrollToBottom === "function") {
+ scrollToBottom();
+ }
+ return card2;
+ }
+ function renderStep(step) {
+ const row = document.createElement("div");
+ row.className = `sa-activity-step sa-activity-${step.status || "running"}`;
+ row.dataset.id = step.id;
+ const icon = document.createElement("span");
+ icon.className = "sa-activity-icon";
+ icon.setAttribute("aria-hidden", "true");
+ icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
+ const body = document.createElement("div");
+ body.className = "sa-activity-body";
+ const label = document.createElement("div");
+ label.className = "sa-activity-label";
+ label.textContent = step.label || step.id;
+ body.appendChild(label);
+ if (step.detail) {
+ const detail = document.createElement("div");
+ detail.className = "sa-activity-detail";
+ detail.textContent = step.detail;
+ body.appendChild(detail);
+ }
+ row.appendChild(icon);
+ row.appendChild(body);
+ return row;
+ }
+ function paint() {
+ if (!ensureCard() || !listEl) {
+ return;
+ }
+ listEl.replaceChildren(...steps.map(renderStep));
+ const running = steps.find((s) => s.status === "running");
+ const last = steps[steps.length - 1];
+ 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"));
+ if (typeof scrollToBottom === "function") {
+ scrollToBottom();
+ }
+ }
+ function begin(title) {
+ steps = [];
+ card2 = null;
+ listEl = null;
+ titleEl = null;
+ open = true;
+ ensureCard();
+ if (titleEl && title) {
+ titleEl.textContent = title;
+ }
+ paint();
+ }
+ function upsert(id, patch) {
+ ensureCard();
+ let step = steps.find((s) => s.id === id);
+ if (!step) {
+ step = { id, kind: "think", label: id, status: "running", detail: "" };
+ steps.push(step);
+ }
+ Object.assign(step, patch);
+ if (!step.status) {
+ step.status = "running";
+ }
+ paint();
+ return step;
+ }
+ function done(id, patch = {}) {
+ return upsert(id, { ...patch, status: "done" });
+ }
+ function skip(id, patch = {}) {
+ return upsert(id, { ...patch, status: "skip" });
+ }
+ function fail(id, patch = {}) {
+ return upsert(id, { ...patch, status: "error" });
+ }
+ function finish(summary) {
+ steps.forEach((s) => {
+ if (s.status === "running") {
+ s.status = "done";
+ }
+ });
+ if (summary && titleEl) {
+ titleEl.textContent = summary;
+ }
+ paint();
+ if (card2) {
+ card2.classList.remove("sa-activity-live");
+ card2.classList.add("sa-activity-done");
+ }
+ }
+ function noteModelCommands(patch) {
+ if (!patch || typeof patch !== "object") {
+ return;
+ }
+ const keys = Object.keys(patch).filter((k) => patch[k] != null && !["actions", "generate", "ask", "look_at", "vision_from", "vision_slots", "notes", "variants"].includes(k));
+ if (keys.length) {
+ done("delta", {
+ kind: "delta",
+ label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E",
+ detail: keys.slice(0, 10).join(", ")
+ });
+ }
+ const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : patch.ask ? [String(patch.ask)] : [];
+ if (ask.length) {
+ upsert("ask", {
+ kind: "ask",
+ label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${ask.join(", ")}`,
+ detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026",
+ status: "running"
+ });
+ }
+ if (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null) {
+ const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []);
+ upsert("look", {
+ kind: "look",
+ label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440",
+ detail: slots.map(String).slice(0, 4).join(", "),
+ status: "running"
+ });
+ }
+ if (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) {
+ upsert("generate", {
+ kind: "generate",
+ label: "Generate",
+ detail: "\u0436\u0434\u0451\u0442 \u043F\u0430\u0439\u043F\u043B\u0430\u0439\u043D\u2026",
+ status: "running"
+ });
+ }
+ if (Array.isArray(patch.variants) && patch.variants.length) {
+ upsert("variants", {
+ kind: "generate",
+ label: `\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \xD7${patch.variants.length}`,
+ status: "running"
+ });
+ }
+ }
+ return {
+ begin,
+ upsert,
+ done,
+ skip,
+ fail,
+ finish,
+ noteModelCommands,
+ get steps() {
+ return steps.slice();
+ }
+ };
+ }
+ function attachActivity(SA2) {
+ SA2.createActivityController = createActivityController;
+ }
+
// src/app.js
(function() {
const LS_BASE = "swarm_assistent_base_url";
@@ -362,6 +1067,9 @@
let HISTORY_KEEP_TURNS = 4;
let INVENTORY_PROMPT_RICH = 12;
let INVENTORY_PROMPT_NAMES = 24;
+ let COMPRESS_AT = 0.7;
+ let CHARS_PER_TOKEN = 3.2;
+ let COMPRESS_AUTO = true;
let ASPECT_TABLE = {
"1:1": [1024, 1024],
"4:3": [1184, 896],
@@ -389,10 +1097,7 @@
inpaint: "inpaint_edit",
inpaint_edit: "inpaint_edit",
describe: "describe_ref",
- describe_ref: "describe_ref",
- card: "catalog_card",
- catalog: "catalog_card",
- catalog_card: "catalog_card"
+ describe_ref: "describe_ref"
};
let WELCOME_HTML = `
Assistent \xB7 Krea 2
@@ -408,6 +1113,7 @@
/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
+/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
/why \u2014 \u0441\u0440\u0430\u0437\u0443 /debug ask
@@ -418,8 +1124,7 @@
/aspect 16:9 \u2014 \u0440\u0430\u0437\u043C\u0435\u0440 \u0438\u0437 \u0442\u0430\u0431\u043B\u0438\u0446\u044B 1K
/seed lock|random \u2014 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u043D\u0434\u043E\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C seed
/vary \u2014 \u043D\u043E\u0432\u044B\u0439 seed, \u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442
-/pack write|critique|compose|params|inpaint|describe|card
-/civitai \u2014 \u043F\u043E\u0438\u0441\u043A LoRA (Confirm \u0432 \u0447\u0430\u0442\u0435)
+/pack write|critique|compose|params|inpaint|describe
/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.
@@ -428,6 +1133,7 @@
{ 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: "/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" },
{ cmd: "/gen", hint: "Generate \u0441\u0435\u0439\u0447\u0430\u0441" },
@@ -440,7 +1146,6 @@
{ cmd: "/seed ", hint: "lock|random" },
{ cmd: "/vary", hint: "\u043D\u043E\u0432\u044B\u0439 seed" },
{ cmd: "/pack ", hint: "write|critique|\u2026" },
- { cmd: "/civitai ", hint: "\u0437\u0430\u043F\u0440\u043E\u0441 LoRA" },
{ cmd: "/inventory", hint: "rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439" }
];
const state = {
@@ -472,6 +1177,10 @@
lastSystemChars: 0,
lastSystemLayers: null,
lastContextChars: 0,
+ lastPromptEvalCount: null,
+ contextMemory: null,
+ ctxPanelOpen: false,
+ compressing: false,
busyPhase: "idle",
busyStarted: 0,
gotDelta: false,
@@ -486,8 +1195,7 @@
view: "chat",
boardTab: "generate",
personas: [],
- modelCards: {},
- cardsSelection: null,
+ chatSession: null,
pendingPersonaNote: null,
chats: [],
activeChatId: null,
@@ -503,17 +1211,53 @@
userPrefs: [],
settingsTab: "behavior",
settingsPersonaId: null,
- wanted: { count: 0, items: [] },
- wantedKeys: /* @__PURE__ */ new Set(),
ollamaHealth: "unknown",
- trainingLock: false
+ trainingLock: false,
+ activity: null
};
+ function getActivity() {
+ if (state.activity) {
+ return state.activity;
+ }
+ if (window.SA && typeof SA.createActivityController === "function") {
+ state.activity = SA.createActivityController({
+ getMessagesEl: () => $2("sa_messages"),
+ scrollToBottom: () => scrollMessagesToBottom(),
+ hideEmpty: () => hideChatEmpty()
+ });
+ }
+ return state.activity;
+ }
+ function activityBegin(title) {
+ const a = getActivity();
+ if (a) {
+ a.begin(title || "Assistent");
+ }
+ }
+ function activityStep(id, patch) {
+ const a = getActivity();
+ if (a) {
+ a.upsert(id, patch);
+ }
+ }
+ function activityDone(id, patch) {
+ const a = getActivity();
+ if (a) {
+ a.done(id, patch);
+ }
+ }
+ function activityFinish(summary) {
+ const a = getActivity();
+ if (a) {
+ a.finish(summary);
+ }
+ }
const HOP_BUDGET = 4;
function isContinuationTurn(opts) {
- return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromEmptyPatchRetry));
+ return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromAskHop));
}
function isMachineTurn(opts) {
- return isContinuationTurn(opts) || !!(opts && (opts.fromCards || opts.fromDownload || opts.fromDebug));
+ return isContinuationTurn(opts) || !!(opts && opts.fromDebug);
}
function resetTurnHops() {
state.turnHops = [];
@@ -618,9 +1362,37 @@
parking: "\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM (park LLM)\u2026",
applying: "Applying patch\u2026",
silent_gen: "\u041F\u0440\u0438\u043C\u0435\u043D\u044F\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026",
- refining: "Civitai search done \u2014 refining\u2026"
+ refining: "\u0423\u0442\u043E\u0447\u043D\u044F\u044E \u043E\u0442\u0432\u0435\u0442\u2026",
+ compressing: "\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026"
};
const text = labels[state.busyPhase] || "Working\u2026";
+ const phaseKind = {
+ thinking: "think",
+ streaming: "stream",
+ waiting: "think",
+ loading: "warm",
+ warming: "warm",
+ parking: "park",
+ encoding: "look",
+ generating: "generate",
+ applying: "merge",
+ silent_gen: "generate",
+ refining: "prep",
+ compressing: "compress"
+ };
+ activityStep(`phase:${state.busyPhase}`, {
+ kind: phaseKind[state.busyPhase] || "think",
+ label: text,
+ status: "running"
+ });
+ const a = getActivity();
+ if (a && Array.isArray(a.steps)) {
+ for (const s of a.steps) {
+ if (s.id.startsWith("phase:") && s.id !== `phase:${state.busyPhase}` && s.status === "running") {
+ a.done(s.id);
+ }
+ }
+ }
const barText = $2("sa_livebar_text");
if (barText) {
barText.textContent = text;
@@ -672,6 +1444,7 @@
}
const elapsed = Date.now() - (state.busyStarted || Date.now());
state.busyPhase = "idle";
+ activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E");
$2("swarm_assistent_root")?.classList.remove("sa-is-busy");
$2("sa_composer")?.classList.remove("sa-composer-busy");
const send = $2("sa_btn_send");
@@ -1102,23 +1875,6 @@
}
return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t);
}
- function userAsksGenerate(text) {
- const t = String(text || "").trim();
- if (!t) {
- return false;
- }
- if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
- return true;
- }
- if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
- return true;
- }
- const letter = "[0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451]";
- const stem = `${letter}*`;
- return cyrTokenRe(
- `\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C|\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|generate|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|run\\s+generat|/gen`
- ).test(t);
- }
function userAsksContinue(text) {
const t = String(text || "").trim();
if (!t) {
@@ -1162,24 +1918,6 @@
}
return null;
}
- function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) {
- const wantsFrame = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(userText);
- if (!wantsFrame && !replyMissingJsonPatch(reply)) {
- return null;
- }
- const prompt = extractPromptFromProse(reply) || state.lastPatch?.prompt || null;
- if (!prompt) {
- return null;
- }
- const patch = wantsFrame ? { prompt, actions: ["generate"] } : { prompt };
- if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) {
- patch.loras = state.lastPatch.loras;
- }
- if (state.lastPatch?.aspect) {
- patch.aspect = state.lastPatch.aspect;
- }
- return patch;
- }
function promptLooksKreaReady(prompt) {
const t = String(prompt || "").trim();
if (t.length < 80) {
@@ -1258,23 +1996,16 @@ ${patch.prompt}`;
}
function userAsksNoGenerate(text) {
const t = String(text || "").trim();
- if (!t || userAsksGenerate(t)) {
+ if (!t) {
return false;
}
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
return true;
}
return cyrTokenRe(
- "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440"
+ "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440[\u0430-\u044F\u0451]*|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*"
).test(t);
}
- function userCommandsGenerate(text) {
- const t = String(text || "").trim();
- if (!t || userAsksNoGenerate(t)) {
- return false;
- }
- return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t);
- }
function userAsksLook(text) {
const t = String(text || "").trim();
if (!t) {
@@ -1291,79 +2022,22 @@ ${patch.prompt}`;
}
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
}
- function userIsChatNotFrame(text) {
- const t = String(text || "").trim();
- if (!t) {
- return false;
- }
- if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) {
- return true;
- }
- if (cyrTokenRe(
- "\u0447\u0442\u043E\\s+\u0442\u0430\u043A\u043E\u0435|\u043A\u0430\u043A\\s+\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442|\u0437\u0430\u0447\u0435\u043C\\s+|\u043A\u0430\u043A\u0438\u0435\\s+(\u043B\u043E\u0440|\u043C\u043E\u0434\u0435\u043B|\u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442)|\u0441\u043F\u0438\u0441\u043E\u043A\\s+\u043B\u043E\u0440|\u0433\u0434\u0435\\s+\u043D\u0430\u0441\u0442\u0440\u043E\u0439|\u0447\u0442\u043E\\s+\u0437\u043D\u0430\u0447\u0438\u0442|\u043D\u0440\u0430\u0432\u0438\u0442|\u0441\u043F\u0430\u0441\u0438\u0431\u043E|\u0431\u043B\u0430\u0433\u043E\u0434\u0430\u0440|\u043F\u043E\u0447\u0435\u043C\u0443\\s+\u0442\u0430\u043A|\u0447\u0442\u043E\\s+\u0442\u044B\\s+(\u0441\u0434\u0435\u043B\u0430\u043B|\u0438\u0437\u043C\u0435\u043D\u0438\u043B)|\u0442\u043E\u043B\u044C\u043A\u043E\\s+(\u043E\u0442\u0432\u0435\u0442\u044C|\u0441\u043A\u0430\u0436\u0438|\u043E\u0431\u044A\u044F\u0441\u043D\u0438)|\u0431\u0435\u0437\\s+(\u043A\u0430\u0434\u0440|\u0433\u0435\u043D\u0435\u0440\u0430\u0446)|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u043A\u0430\u0434\u0440"
- ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) {
- return true;
- }
- return false;
- }
- function userImpliesGenerate(text) {
- const t = String(text || "").trim();
- if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) {
- return false;
- }
- if (userCommandsGenerate(t)) {
- return true;
- }
- if (t.length < 8) {
- return false;
- }
- const wantsLook = userAsksLook(t);
- const wantsRedraw = cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0443\u043B\u0443\u0447\u0448\u0438|\u043F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0439").test(t) || /\b(fix|redo|redraw|improve)\b/i.test(t);
- if (wantsLook && !wantsRedraw) {
- return false;
- }
- if (cyrTokenRe(
- "\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u043A\u0430\u0434\u0440)|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u0443\u0432\u0438\u0434\u0435\u0442\u044C|\u0432\u0438\u0434\u0435\u0442\u044C)|\u043F\u043E\u043A\u0430\u0436\u0438\\s+\u043A\u0430\u043A\\s+(\u043E\u043D\u0430|\u043E\u043D|\u044D\u0442\u043E)|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u0435\u0451|\u0435\u0435|\u0435\u0433\u043E|\u043C\u043D\u0435)\\s|\u043F\u0443\u0441\u0442\u044C\\s+\u0431\u0443\u0434\u0435\u0442|\u0434\u0440\u0443\u0433\u043E\u0439\\s+(\u0440\u0430\u043A\u0443\u0440\u0441|\u0441\u0432\u0435\u0442|\u043D\u0430\u0440\u044F\u0434|\u043F\u043E\u0437\u0430)|\u043F\u043E\u043C\u0435\u043D\u044F\u0439\\s+(\u043F\u043E\u0437\u0443|\u0441\u0432\u0435\u0442|\u043E\u0434\u0435\u0436\u0434|\u0444\u043E\u043D)|\u0434\u043E\u0431\u0430\u0432\u044C\\s+(\u0441\u0432\u0435\u0442|\u0434\u0435\u0442\u0430\u043B)|\u0435\u0449\u0451\\s+\u043E\u0434\u043D|\u0435\u0449\u0435\\s+\u043E\u0434\u043D"
- ).test(t)) {
- return true;
- }
- if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) {
- return true;
- }
- const isQuestion = /[??]\s*$/.test(t);
- if (isQuestion) {
- return cyrTokenRe("\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043C\u043E\u0436\u0435\u0448\u044C\\s+(\u0441\u0434\u0435\u043B\u0430\u0442\u044C|\u043D\u0430\u0440\u0438\u0441\u043E\u0432\u0430\u0442\u044C)|\u043C\u043E\u0436\u043D\u043E\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0441\u0433\u0435\u043D\u0435\u0440)").test(t);
- }
- return false;
- }
- function packBlocksAutoGenerate(pack) {
- const p = String(pack || "");
- return p === "describe_ref" || p === "catalog_card" || p === "author_persona" || p === "debug_explain";
- }
function packWantsVision(pack) {
const p = String(pack || "");
return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit";
}
- function resolveTurnIntent(patch, userText, opts = {}) {
- const machine = isMachineTurn(opts);
- const vetoed = !machine && userAsksNoGenerate(userText);
- const commanded = !!opts.userWantsGenerate || !machine && userCommandsGenerate(userText);
- const implied = !machine && userImpliesGenerate(userText);
- const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes("generate");
- let generate;
- if (vetoed || opts.fromAutoCritique) {
- generate = false;
- } else if (commanded) {
- generate = true;
- } else if (packBlocksAutoGenerate($2("sa_pack")?.value || "")) {
- generate = false;
- } else {
- generate = modelAsked || implied;
+ 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 });
}
+ 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 hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
- const honorLook = opts.fromAutoCritique || opts.fromVisionHop || !machine && userAsksLook(userText) || packWantsVision($2("sa_pack")?.value);
- const look = !!(hasLook && !vetoed && !generate && honorLook);
- return { generate, look, vetoed };
+ const look = !!(hasLook && !vetoed && !generate);
+ const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : [];
+ return { generate, look, vetoed, ask };
}
function stripGenerateAction(patch) {
if (!patch || typeof patch !== "object") {
@@ -1398,7 +2072,7 @@ ${patch.prompt}`;
return out;
}
function rememberLastPatch(patch) {
- if (patch && typeof patch === "object" && !isCardObject2(patch)) {
+ if (patch && typeof patch === "object") {
state.lastPatch = patch;
syncBuildGenButton();
}
@@ -1434,7 +2108,6 @@ ${patch.prompt}`;
fix_params: "params",
inpaint_edit: "inpaint",
describe_ref: "describe",
- catalog_card: "card",
author_persona: "persona"
};
const short = shortMap[pack] || pack.replace(/_/g, " ").slice(0, 12);
@@ -1624,6 +2297,391 @@ ${patch.prompt}`;
const turns = Math.max(1, Number(HISTORY_KEEP_TURNS) || 4);
return turns * 2;
}
+ function ctxApi() {
+ return window.SA?.context || null;
+ }
+ function getContextMemory() {
+ const C = ctxApi();
+ if (C?.normalizeContextMemory) {
+ return C.normalizeContextMemory(state.contextMemory);
+ }
+ return state.contextMemory && typeof state.contextMemory === "object" ? state.contextMemory : { summary: "", untilCount: 0, foldedTurns: 0, at: 0, uiCollapsed: false, promptEvalCount: null };
+ }
+ function setContextMemory(raw, { persist = true } = {}) {
+ const C = ctxApi();
+ state.contextMemory = C?.normalizeContextMemory ? C.normalizeContextMemory(raw) : raw && typeof raw === "object" ? raw : null;
+ if (state.chatSession && typeof state.chatSession === "object") {
+ state.chatSession.context_memory = state.contextMemory?.summary ? state.contextMemory : null;
+ }
+ if (persist && !state.restoringChat) {
+ persistHistory();
+ }
+ updateCtxChip();
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
+ function resetContextMemory({ persist = true } = {}) {
+ setContextMemory(ctxApi()?.emptyContextMemory?.() || {
+ summary: "",
+ untilCount: 0,
+ foldedTurns: 0,
+ at: 0,
+ uiCollapsed: false,
+ promptEvalCount: null
+ }, { persist });
+ }
+ function historyCharsForBudget(messages) {
+ return (messages || []).reduce((n, m) => n + String(m?.content || "").length, 0);
+ }
+ function currentBudgetEstimate(modelMessages) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const memChars = mem.summary ? mem.summary.length : 0;
+ const hist = modelMessages || assembleOutgoingMessages();
+ const numCtx = Number($2("sa_num_ctx")?.value) || state.config?.assistant?.num_ctx || 16384;
+ const numPredict = Number(state.config?.assistant?.num_predict) || 3072;
+ if (!C?.estimateBudget) {
+ return {
+ used: 0,
+ numCtx,
+ level: "ok",
+ estimated: 0,
+ fromEval: false,
+ systemChars: state.lastSystemChars || 0,
+ historyChars: historyCharsForBudget(hist),
+ memoryChars: memChars,
+ threshold: Math.floor((numCtx - numPredict) * COMPRESS_AT)
+ };
+ }
+ return C.estimateBudget({
+ systemChars: state.lastSystemChars || 0,
+ historyChars: historyCharsForBudget(hist),
+ memoryChars: memChars,
+ numCtx,
+ numPredict,
+ charsPerToken: CHARS_PER_TOKEN,
+ compressAt: COMPRESS_AT,
+ promptEvalCount: state.lastPromptEvalCount ?? mem.promptEvalCount
+ });
+ }
+ function assembleOutgoingMessages({ includePendingUser } = {}) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ let msgs;
+ if (C?.assembleModelMessages) {
+ msgs = C.assembleModelMessages(state.history, mem, HISTORY_KEEP_TURNS).map((m) => {
+ let content = String(m.content || "");
+ if (m.role === "assistant") {
+ content = stripJsonFencesForHistory(content);
+ }
+ return { role: m.role, content: content.slice(0, 4e3) };
+ });
+ } else {
+ msgs = state.history.slice(-historyMessageLimit()).map((m) => {
+ let content = String(m.content || "");
+ if (m.role === "assistant") {
+ content = stripJsonFencesForHistory(content);
+ }
+ return { role: m.role, content: content.slice(0, 4e3) };
+ });
+ }
+ if (includePendingUser) {
+ msgs.push({ role: "user", content: String(includePendingUser) });
+ }
+ return msgs;
+ }
+ function buildCompressUserPrompt() {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : [];
+ const lines = [];
+ if (mem.summary) {
+ lines.push("## Previous conversation memory");
+ lines.push(mem.summary);
+ lines.push("");
+ }
+ lines.push("## Dialogue chunk to fold");
+ for (const m of fold) {
+ const role = m.role === "assistant" ? "Assistant" : "User";
+ let content = String(m.content || "");
+ if (m.role === "assistant") {
+ content = stripJsonFencesForHistory(content);
+ }
+ content = content.slice(0, 1500);
+ lines.push(`### ${role}`);
+ lines.push(content || "(empty)");
+ lines.push("");
+ }
+ lines.push("Compress the chunk into the required heading format. Merge with previous memory when present.");
+ return { prompt: lines.join("\n"), foldCount: fold.length, fold };
+ }
+ function applyCompressResult(summaryText, foldCount, { uiCollapsed = false } = {}) {
+ const C = ctxApi();
+ const prev = getContextMemory();
+ const merged = C?.mergeSummary ? C.mergeSummary(prev.summary, summaryText) : String(summaryText || "").trim() || prev.summary;
+ const nextUntil = prev.untilCount + Math.max(0, foldCount);
+ setContextMemory({
+ summary: merged,
+ untilCount: nextUntil,
+ foldedTurns: Math.floor(nextUntil / 2),
+ at: Date.now(),
+ uiCollapsed: uiCollapsed || prev.uiCollapsed,
+ promptEvalCount: state.lastPromptEvalCount
+ });
+ }
+ function callOllamaOnce(payload) {
+ return new Promise((resolve, reject) => {
+ const fail = (err) => reject(new Error(String(err || "Chat failed")));
+ const ok = (data) => {
+ if (data?.error) {
+ fail(data.error);
+ return;
+ }
+ resolve(data || {});
+ };
+ if (typeof makeWSRequest === "function") {
+ let settled = false;
+ makeWSRequest(
+ "AssistentChatWS",
+ payload,
+ (data) => {
+ if (settled) {
+ return;
+ }
+ if (data?.error) {
+ settled = true;
+ fail(data.error);
+ return;
+ }
+ if (data?.done || data?.reply != null) {
+ settled = true;
+ ok(data);
+ }
+ },
+ 0,
+ (err) => {
+ if (settled) {
+ return;
+ }
+ genericRequest("AssistentChat", payload, (data) => {
+ settled = true;
+ ok(data);
+ }, 0, (err2) => {
+ settled = true;
+ fail(err2 || err);
+ });
+ }
+ );
+ return;
+ }
+ genericRequest("AssistentChat", payload, ok, 0, fail);
+ });
+ }
+ async function runCompressTurn({ uiCollapsed = false, chatEpoch = state.chatEpoch } = {}) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : [];
+ if (!fold.length) {
+ return false;
+ }
+ const model = $2("sa_model")?.value;
+ if (!model) {
+ setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
+ return false;
+ }
+ const { prompt, foldCount } = buildCompressUserPrompt();
+ if (!foldCount) {
+ return false;
+ }
+ state.compressing = true;
+ updateCtxChip();
+ setBusyPhase("compressing");
+ setStatus("\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026");
+ const persona = $2("sa_persona")?.value || "neutral";
+ const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
+ const payload = {
+ baseUrl,
+ model,
+ pack: "compress_history",
+ persona,
+ includeBase: false,
+ messages: [{ role: "user", content: prompt }],
+ context_json: JSON.stringify({
+ compress: true,
+ previous_memory: mem.summary || null,
+ fold_count: foldCount
+ }),
+ skills: [],
+ embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || ""
+ };
+ try {
+ const data = await callOllamaOnce(payload);
+ if (chatEpoch !== state.chatEpoch) {
+ return false;
+ }
+ if (data.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(data.prompt_eval_count) || null;
+ } else if (data.raw?.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(data.raw.prompt_eval_count) || null;
+ }
+ const reply = String(data.reply || "").trim();
+ if (!reply) {
+ setStatus("\u0421\u0436\u0430\u0442\u0438\u0435: \u043F\u0443\u0441\u0442\u043E\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438");
+ return false;
+ }
+ applyCompressResult(reply, foldCount, { uiCollapsed });
+ if (uiCollapsed) {
+ renderHistoryIntoUi(state.history);
+ }
+ setStatus("\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442");
+ return true;
+ } catch (e) {
+ console.warn("Assistent compress failed", e);
+ setStatus(`\u0421\u0436\u0430\u0442\u0438\u0435 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${e.message || e}`);
+ return false;
+ } finally {
+ state.compressing = false;
+ updateCtxChip();
+ }
+ }
+ function maybeAutoCompressBeforeSend(chatEpoch) {
+ if (!COMPRESS_AUTO) {
+ return Promise.resolve(false);
+ }
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const msgs = assembleOutgoingMessages();
+ const budget = currentBudgetEstimate(msgs);
+ const should = C?.shouldCompress ? C.shouldCompress(budget, mem, (state.history || []).filter((m) => m && !m.systemish).length, {
+ keepMessages: historyMessageLimit()
+ }) : false;
+ if (!should) {
+ return Promise.resolve(false);
+ }
+ return runCompressTurn({ uiCollapsed: false, chatEpoch });
+ }
+ function formatCtxChipLabel(budget) {
+ const C = ctxApi();
+ const used = C?.formatTokenShort ? C.formatTokenShort(budget.used) : String(budget.used || 0);
+ const cap = C?.formatTokenShort ? C.formatTokenShort(budget.numCtx) : String(budget.numCtx || 0);
+ return `${used} / ${cap}`;
+ }
+ function updateCtxChip() {
+ const chip = $2("sa_ctx_chip");
+ if (!chip) {
+ return;
+ }
+ const budget = currentBudgetEstimate();
+ const mem = getContextMemory();
+ const label = formatCtxChipLabel(budget);
+ const textEl = chip.querySelector(".sa-ctx-chip-text");
+ if (textEl) {
+ textEl.textContent = label;
+ } else {
+ chip.textContent = label;
+ }
+ chip.classList.remove("sa-ctx-ok", "sa-ctx-warn", "sa-ctx-hot", "sa-ctx-compressing", "sa-ctx-has-mem");
+ if (state.compressing) {
+ chip.classList.add("sa-ctx-compressing");
+ } else {
+ chip.classList.add(`sa-ctx-${budget.level || "ok"}`);
+ }
+ if (mem.summary) {
+ chip.classList.add("sa-ctx-has-mem");
+ }
+ const src = budget.fromEval ? "\u0444\u0430\u043A\u0442 Ollama" : "\u043E\u0446\u0435\u043D\u043A\u0430";
+ chip.title = `\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 \xB7 ${src}${mem.summary ? " \xB7 \u0435\u0441\u0442\u044C \u0441\u0430\u043C\u043C\u0430\u0440\u0438" : ""}`;
+ const dot = chip.querySelector(".sa-ctx-dot");
+ if (dot) {
+ dot.hidden = !mem.summary;
+ }
+ }
+ function toggleCtxPanel(force) {
+ const panel = $2("sa_ctx_panel");
+ const chip = $2("sa_ctx_chip");
+ if (!panel || !chip) {
+ return;
+ }
+ const open = force != null ? !!force : !state.ctxPanelOpen;
+ state.ctxPanelOpen = open;
+ panel.hidden = !open;
+ chip.setAttribute("aria-expanded", open ? "true" : "false");
+ if (open) {
+ renderCtxPanel();
+ }
+ }
+ function renderCtxPanel() {
+ const body = $2("sa_ctx_panel_body");
+ const bar = $2("sa_ctx_bar_fill");
+ const auto = $2("sa_ctx_auto");
+ if (!body) {
+ return;
+ }
+ const budget = currentBudgetEstimate();
+ const mem = getContextMemory();
+ const layers = state.lastSystemLayers || {};
+ const layerRows = Object.entries(layers).filter(([k]) => k !== "total").map(([k, v]) => `${escapeHtml2(k)} ${Number(v) || 0}
`).join("");
+ const keep = HISTORY_KEEP_TURNS;
+ const uncovered = Math.max(0, (state.history || []).filter((m) => m && !m.systemish).length - (mem.untilCount || 0));
+ body.innerHTML = `
+ ${budget.fromEval ? "\u0422\u043E\u043A\u0435\u043D\u044B (prompt_eval)" : "\u041E\u0446\u0435\u043D\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u043E\u0432"} \xB7 \u043F\u043E\u0440\u043E\u0433 ${budget.threshold || "\u2014"}
+ system ${budget.systemChars || 0}
+ history ${budget.historyChars || 0}
+ memory ${budget.memoryChars || 0}
+ ${layerRows ? `system_layers
${layerRows}` : ""}
+ \u041C\u043E\u0434\u0435\u043B\u044C \u0432\u0438\u0434\u0438\u0442: ${mem.summary ? `\u0441\u0430\u043C\u043C\u0430\u0440\u0438 (${mem.foldedTurns || 0} \u0445\u043E\u0434\u043E\u0432) +` : ""} \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${keep} \u0445\u043E\u0434\u043E\u0432 \xB7 \u0441\u044B\u0440\u044B\u0445 \u0432 \u043E\u043A\u043D\u0435 \u2248 ${Math.min(uncovered, historyMessageLimit())}
+ ${mem.summary ? `${escapeHtml2(mem.summary.slice(0, 800))}${mem.summary.length > 800 ? "\u2026" : ""} ` : '\u0421\u0430\u043C\u043C\u0430\u0440\u0438 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u0431\u0440\u0430\u0441\u044B\u0432\u0430\u044E\u0442\u0441\u044F.
'}
+ `;
+ if (bar) {
+ const pct = Math.max(0, Math.min(100, budget.used / (budget.numCtx || 1) * 100));
+ bar.style.width = `${pct}%`;
+ bar.dataset.level = budget.level || "ok";
+ }
+ if (auto) {
+ auto.checked = !!COMPRESS_AUTO;
+ }
+ updateCtxChip();
+ }
+ async function compressNowFromUi() {
+ if (state.busy || state.generating || state.compressing) {
+ 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");
+ return;
+ }
+ const model = $2("sa_model")?.value;
+ if (!model) {
+ setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
+ return;
+ }
+ state.busy = true;
+ setInterruptVisible(true);
+ startBusyUi("compressing");
+ const epoch = state.chatEpoch;
+ try {
+ const ok = await runCompressTurn({ uiCollapsed: true, chatEpoch: epoch });
+ if (!ok) {
+ setStatus("\u041D\u0435\u0447\u0435\u0433\u043E \u0441\u0436\u0438\u043C\u0430\u0442\u044C (\u0445\u0432\u043E\u0441\u0442 \u2264 keep)");
+ }
+ } finally {
+ if (epoch === state.chatEpoch) {
+ state.busy = false;
+ setInterruptVisible(state.generating);
+ stopBusyUi(getContextMemory().summary ? "\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442" : "\u0413\u043E\u0442\u043E\u0432\u043E");
+ }
+ updateCtxChip();
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
+ }
+ function resetCompressionFromUi() {
+ resetContextMemory();
+ renderHistoryIntoUi(state.history);
+ setStatus("\u0421\u0436\u0430\u0442\u0438\u0435 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u043E \u2014 \u043C\u043E\u0434\u0435\u043B\u044C \u0441\u043D\u043E\u0432\u0430 \u0432\u0438\u0434\u0438\u0442 \u0442\u043E\u043B\u044C\u043A\u043E last-K");
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
function flashImagePane(slotId) {
const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`);
if (!el) {
@@ -1978,8 +3036,8 @@ ${patch.prompt}`;
if (silent) {
const note = document.createElement("div");
note.className = "sa-patch-actions sa-patch-silent sa-patch-current";
- const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!state.pendingSilentGen;
- note.textContent = willGen ? "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \xB7 Generate\u2026" : "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438";
+ const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!patch.generate || !!state.pendingSilentGen;
+ note.textContent = willGen ? "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E \xB7 Generate\u2026" : "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E";
host.appendChild(note);
return;
}
@@ -1998,17 +3056,43 @@ ${patch.prompt}`;
btn.addEventListener("click", () => applyPatch(patch, which));
actions.appendChild(btn);
}
+ const toSession = document.createElement("button");
+ toSession.type = "button";
+ toSession.className = "basic-button";
+ toSession.textContent = "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E";
+ toSession.addEventListener("click", async () => {
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
+ }
+ await pushSessionToSwarm(state.chatSession);
+ rememberLastPatch(patch);
+ try {
+ const chat = findChat(state.activeChatId);
+ if (chat) {
+ chat.params = snapshotChatParams();
+ persistChatsStore();
+ }
+ } catch (e) {
+ }
+ setStatus("\u041F\u0430\u0442\u0447 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438");
+ });
+ actions.appendChild(toSession);
const genBtn = document.createElement("button");
genBtn.type = "button";
genBtn.className = "basic-button sa-btn-gen";
- genBtn.textContent = "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C + Generate";
+ genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C";
genBtn.addEventListener("click", async () => {
if (isGenerateUnavailable()) {
return;
}
startBusyUi("silent_gen");
- await applyPatch(patch, "all");
- await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true });
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
+ }
+ await pushSessionToSwarm(state.chatSession);
+ await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true, fromSession: true });
});
actions.appendChild(genBtn);
host.appendChild(actions);
@@ -2019,22 +3103,19 @@ ${patch.prompt}`;
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;
}
- if (isGenerateUnavailable()) {
+ if (typeof isGenerateUnavailable === "function" && isGenerateUnavailable()) {
setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C SwarmUI");
return;
}
- const patch = state.lastPatch;
- if (patch) {
- startBusyUi("silent_gen");
- setStatus("\u0421\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026");
- await applyPatch(patch, "all");
- syncLiveParamsBar();
- await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true });
- return;
+ pullLiveIntoSession();
+ const S = window.SA && window.SA.session;
+ if (state.lastPatch && S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch);
}
- startBusyUi("generating");
- setStatus("Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C\u2026");
- await runGenerateFromPatch({ actions: ["generate"] }, { force: true });
+ if (typeof startBusyUi === "function") startBusyUi(state.lastPatch ? "silent_gen" : "generating");
+ setStatus(state.lastPatch ? "\u0421\u0435\u0441\u0441\u0438\u044F \u2192 Generate\u2026" : "Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439\u2026");
+ await pushSessionToSwarm(state.chatSession);
+ await runGenerateFromPatch({ actions: ["generate"] }, { force: true, fromSession: true });
}
function renderBoard() {
const board = $2("sa_board");
@@ -2495,7 +3576,7 @@ ${patch.prompt}`;
const t = String(u?.content || "").replace(/\s+/g, " ").trim();
return t ? t.slice(0, 52) : "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442";
}
- function snapshotChatParams() {
+ function readLiveGenFields() {
let loras = [];
try {
if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) {
@@ -2506,17 +3587,19 @@ ${patch.prompt}`;
}
} catch (e) {
}
- let lastPatch = null;
+ let checkpoint = null;
try {
- lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null;
+ if (typeof resolveCurrentCheckpoint === "function") {
+ const m = resolveCurrentCheckpoint();
+ if (m && (m.name || m.title)) {
+ checkpoint = {
+ name: m.name || m.title || null,
+ architecture: m.architecture || m.compat_class || m.class || null,
+ title: m.title || null
+ };
+ }
+ }
} catch (e) {
- lastPatch = null;
- }
- let sessionExact = {};
- try {
- sessionExact = state.sessionExact && typeof state.sessionExact === "object" ? JSON.parse(JSON.stringify(state.sessionExact)) : {};
- } catch (e) {
- sessionExact = {};
}
return {
prompt: val("alt_prompt_textbox") || val("input_prompt") || "",
@@ -2531,110 +3614,108 @@ ${patch.prompt}`;
scheduler: val("input_scheduler") || null,
batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null,
loras,
- persona: $2("sa_persona")?.value || "neutral",
- pack: $2("sa_pack")?.value || defaultPackId(),
- sessionExact,
- lastPatch,
+ checkpoint
+ };
+ }
+ function boardSnapshotForSession() {
+ if (typeof ensureBoard === "function") ensureBoard();
+ return {
+ slots: (state.slots || []).map((s) => ({
+ id: s.id,
+ type: s.type,
+ label: s.label,
+ src: s.src || null,
+ attach: !!s.attach,
+ note: s.note || null
+ })),
+ selectedSlotId: state.selectedSlotId || "ref1",
genResults: Array.isArray(state.genResults) ? state.genResults.map((r) => ({
id: r.id,
label: r.label,
src: r.src || null,
patch: r.patch || null
})) : [],
- selectedGenResultId: state.selectedGenResultId || null
+ selectedGenResultId: state.selectedGenResultId || null,
+ refSeq: state.refSeq || 1
};
}
+ function pullLiveIntoSession() {
+ const S = window.SA && window.SA.session;
+ if (!S || typeof S.snapshotFromLive !== "function") return state.chatSession;
+ state.chatSession = S.snapshotFromLive({
+ genFields: readLiveGenFields(),
+ board: boardSnapshotForSession(),
+ persona: $2("sa_persona")?.value || "neutral",
+ pack: $2("sa_pack")?.value || "ordinary",
+ context_memory: getContextMemory()
+ });
+ return state.chatSession;
+ }
+ async function pushSessionToSwarm(session) {
+ const S = window.SA && window.SA.session;
+ const sess = session || state.chatSession || S && S.emptySession && S.emptySession();
+ if (!sess || !sess.gen) return;
+ if (typeof applyPatch === "function") await applyPatch({ ...sess.gen }, "all");
+ try {
+ const name = sess.gen.checkpoint?.name || (typeof sess.gen.checkpoint === "string" ? sess.gen.checkpoint : null);
+ if (name && typeof currentModelHelper !== "undefined" && currentModelHelper?.setModel) {
+ currentModelHelper.setModel(name);
+ }
+ } catch (e) {
+ }
+ if (sess.pack && typeof setPackValue === "function") setPackValue(sess.pack, { flash: false });
+ if (sess.persona && typeof applyPersonaForChat === "function") {
+ await applyPersonaForChat(sess.persona, { quiet: true });
+ }
+ state.chatSession = sess;
+ }
+ function snapshotChatParams() {
+ const S = window.SA && window.SA.session;
+ const session = pullLiveIntoSession();
+ if (S && typeof S.toPersistParams === "function") return S.toPersistParams(session);
+ return session || {};
+ }
async function restoreChatParams(params) {
state.restoringChat = true;
try {
state.sessionExact = {};
state.lastPatch = null;
state.lastUserParamIntent = false;
+ const S = window.SA && window.SA.session;
+ state.chatSession = S && typeof S.sessionFromLegacyParams === "function" ? S.sessionFromLegacyParams(params) : params && params.gen ? params : S && S.emptySession ? S.emptySession() : { gen: {}, board: {} };
if (!params || typeof params !== "object") {
- clearGenResults();
- syncBuildGenButton();
- syncLiveParamsBar();
- syncModeBadge();
- renderBoard();
+ if (typeof clearGenResults === "function") clearGenResults();
+ if (typeof renderBoard === "function") renderBoard();
+ setContextMemory(null, { persist: false });
return { restored: false };
}
- const promptBox = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt");
- if (promptBox) {
- promptBox.value = params.prompt != null ? String(params.prompt) : "";
- promptBox.dispatchEvent(new Event("input", { bubbles: true }));
- promptBox.dispatchEvent(new Event("change", { bubbles: true }));
+ await pushSessionToSwarm(state.chatSession);
+ const board = state.chatSession.board || {};
+ if (Array.isArray(board.slots) && board.slots.length) {
+ state.slots = board.slots.map((s) => ({ ...s }));
+ if (board.selectedSlotId) state.selectedSlotId = board.selectedSlotId;
+ if (board.refSeq) state.refSeq = board.refSeq;
}
- setVal("input_negativeprompt", params.negative != null ? String(params.negative) : "");
- if (document.getElementById("alt_negativeprompt_textbox")) {
- setVal("alt_negativeprompt_textbox", params.negative != null ? String(params.negative) : "");
- }
- if (params.width != null) {
- setVal("input_width", String(params.width));
- }
- if (params.height != null) {
- setVal("input_height", String(params.height));
- }
- if (params.steps != null) {
- setVal("input_steps", String(params.steps));
- }
- if (params.cfg != null) {
- if (document.getElementById("input_cfgscale")) {
- setVal("input_cfgscale", String(params.cfg));
- } else {
- setVal("input_cfg", String(params.cfg));
- }
- }
- if (params.sigma_shift != null) {
- setVal("input_sigmashift", String(params.sigma_shift));
- }
- if (params.seed != null && params.seed !== "") {
- setVal("input_seed", String(params.seed));
- }
- if (params.sampler) {
- setVal("input_sampler", String(params.sampler));
- }
- if (params.scheduler) {
- setVal("input_scheduler", String(params.scheduler));
- }
- if (params.batch != null) {
- if (document.getElementById("input_images")) {
- setVal("input_images", String(params.batch));
- } else if (document.getElementById("input_batchsize")) {
- setVal("input_batchsize", String(params.batch));
- }
- }
- const loras = Array.isArray(params.loras) ? params.loras : [];
- await applyPatch({ loras }, "loras");
- if (params.pack) {
- setPackValue(params.pack, { flash: false });
- }
- if (params.persona) {
- await applyPersonaForChat(params.persona, { quiet: true });
- }
- state.sessionExact = params.sessionExact && typeof params.sessionExact === "object" ? { ...params.sessionExact } : {};
- state.lastPatch = params.lastPatch || null;
- if (Array.isArray(params.genResults) && params.genResults.length) {
- state.genResults = params.genResults.map((r, i) => ({
- id: r.id || `var${i + 1}`,
- label: r.label || `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}`,
+ if (Array.isArray(board.genResults) && board.genResults.length) {
+ state.genResults = board.genResults.map((r, i) => ({
+ id: r.id || "var" + (i + 1),
+ label: r.label || "\u0412\u0430\u0440\u0438\u0430\u043D\u0442 " + (i + 1),
src: r.src || null,
patch: r.patch || null
}));
- state.selectedGenResultId = params.selectedGenResultId || state.genResults.find((r) => r.src)?.id || state.genResults[0]?.id || null;
- const selected = state.genResults.find((r) => r.id === state.selectedGenResultId);
- const gen = generateSlot();
- if (gen && selected?.src) {
- gen.src = selected.src;
- }
- } else {
+ state.selectedGenResultId = board.selectedGenResultId || state.genResults.find((x) => x.src)?.id || state.genResults[0]?.id || null;
+ const selected = state.genResults.find((x) => x.id === state.selectedGenResultId);
+ const gen = typeof generateSlot === "function" ? generateSlot() : null;
+ if (gen && selected?.src) gen.src = selected.src;
+ } else if (typeof clearGenResults === "function") {
clearGenResults();
}
- syncBuildGenButton();
- syncLiveParamsBar();
- syncModeBadge();
- renderLoraChips();
- syncChipHighlight();
- renderBoard();
+ if (typeof syncBuildGenButton === "function") syncBuildGenButton();
+ if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
+ if (typeof syncModeBadge === "function") syncModeBadge();
+ if (typeof renderLoraChips === "function") renderLoraChips();
+ if (typeof renderBoard === "function") renderBoard();
+ setContextMemory(state.chatSession?.context_memory || null);
return { restored: true };
} finally {
state.restoringChat = false;
@@ -2812,9 +3893,36 @@ ${patch.prompt}`;
const list = slimHistoryMessages(messages);
if (!list.length) {
resetMessagesUi();
+ updateCtxChip();
return;
}
- for (const m of list) {
+ const mem = getContextMemory();
+ let start = 0;
+ if (mem.uiCollapsed && mem.summary && mem.untilCount > 0) {
+ const folded = list.slice(0, Math.min(mem.untilCount, list.length));
+ start = folded.length;
+ const details = document.createElement("details");
+ details.className = "sa-msg sa-msg-compress";
+ const summary = document.createElement("summary");
+ summary.textContent = `\u0421\u0436\u0430\u0442\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \xB7 ${mem.foldedTurns || Math.floor(folded.length / 2)} \u0445\u043E\u0434\u043E\u0432`;
+ details.appendChild(summary);
+ const body = document.createElement("div");
+ body.className = "sa-msg-compress-body";
+ const pre = document.createElement("pre");
+ pre.className = "sa-ctx-summary";
+ pre.textContent = mem.summary;
+ body.appendChild(pre);
+ for (const m of folded) {
+ const row = document.createElement("div");
+ row.className = `sa-msg-compress-row sa-msg-compress-${m.role}`;
+ row.textContent = `${m.role === "assistant" ? "\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442" : "\u0412\u044B"}: ${String(m.content || "").slice(0, 500)}`;
+ body.appendChild(row);
+ }
+ details.appendChild(body);
+ box.appendChild(details);
+ }
+ for (let i = start; i < list.length; i++) {
+ const m = list[i];
if (m.role === "user") {
appendMessage("user", m.content, null, null, { historical: true });
} else {
@@ -2825,6 +3933,7 @@ ${patch.prompt}`;
});
}
}
+ updateCtxChip();
}
function updateSessionLabel() {
const el = $2("sa_session_label");
@@ -2976,6 +4085,7 @@ ${patch.prompt}`;
}
state.streamEl = null;
}
+ resetContextMemory({ persist: false });
syncBuildGenButton();
resetMessagesUi();
persistChatsStore();
@@ -2983,6 +4093,7 @@ ${patch.prompt}`;
renderBoard();
syncHistoryBadge();
renderChatsList();
+ 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();
}
@@ -3030,6 +4141,7 @@ ${patch.prompt}`;
}
state.streamEl = null;
}
+ setContextMemory(chat.params?.context_memory || null, { persist: false });
renderHistoryIntoUi(state.history);
const result = await restoreChatParams(chat.params);
updateSessionLabel();
@@ -3122,11 +4234,13 @@ ${patch.prompt}`;
clearGenResults();
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.
');
setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D");
updateSessionLabel();
syncHistoryBadge();
renderBoard();
+ updateCtxChip();
}
function hideSlashMenu() {
const menu = $2("sa_slash_menu");
@@ -3228,6 +4342,7 @@ ${patch.prompt}`;
}
if (state.settingsTab === "more") {
fillKnobsFromConfig(data);
+ updateCtxChip();
}
}
});
@@ -3407,213 +4522,36 @@ ${patch.prompt}`;
return s;
}
function collectLiveContext() {
- const inv = state.inventory || {};
- const initCtx = readInitContext();
- const ctx = {
- architecture_ok: isKreaSelected(),
- checkpoint: null,
- prompt: slimPromptForContext(val("alt_prompt_textbox") || val("input_prompt") || ""),
- negative: slimPromptForContext(val("input_negativeprompt") || val("alt_negativeprompt_textbox") || ""),
- width: parseInt(val("input_width") || "0", 10) || null,
- height: parseInt(val("input_height") || "0", 10) || null,
- steps: parseInt(val("input_steps") || "0", 10) || null,
- cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null,
- sigma_shift: parseFloat(val("input_sigmashift") || "") || null,
- seed: val("input_seed") || null,
- sampler: val("input_sampler") || val("input_samplerate") || null,
- scheduler: val("input_scheduler") || null,
- batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null,
- prompt_image_count: countPromptImages(),
- selected_loras: [],
- available_loras: [],
- available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 8),
- wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 20),
- inventory_at: inv.inventory_at || null,
- has_vision_image: visionReadySlots().length > 0,
- image_slots: slotCatalog(),
- attached_slot_ids: attachableSlots().map((s) => s.id),
- gen_results: (state.genResults || []).map((r) => ({
- id: r.id,
- label: r.label,
- has_image: !!r.src,
- selected: r.id === state.selectedGenResultId
- })),
- selected_gen_result: state.selectedGenResultId || null,
- has_civitai_key: !!inv.has_civitai_key,
+ pullLiveIntoSession();
+ const S = window.SA && window.SA.session;
+ let initCtx = {};
+ try {
+ if (typeof readInitContext === "function") initCtx = readInitContext();
+ } catch (e) {
+ }
+ const extra = {
+ prompt_image_count: typeof countPromptImages === "function" ? countPromptImages() : 0,
+ has_vision_image: typeof visionReadySlots === "function" ? visionReadySlots().length > 0 : false,
+ image_slots: typeof slotCatalog === "function" ? slotCatalog() : [],
+ attached_slot_ids: typeof attachableSlots === "function" ? attachableSlots().map((s) => s.id) : [],
auto_apply: !!$2("sa_auto_apply")?.checked,
- auto_generate: !!$2("sa_auto_generate")?.checked,
- persona: $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral",
- model_cards: [],
- user_prefs_count: 0,
+ auto_generate: true,
...initCtx
};
- {
- const slim = slimInventoryLoras(inv.loras || [], INVENTORY_PROMPT_NAMES);
- ctx.available_loras = slim;
- if ((inv.loras || []).length > slim.length) {
- ctx.available_loras_truncated = true;
- ctx.available_loras_total = (inv.loras || []).length;
+ if (S && typeof S.compactContext === "function") {
+ const ctx = S.compactContext(state.chatSession, {
+ architecture_ok: typeof isKreaSelected === "function" ? isKreaSelected() : true,
+ promptMax: typeof CONTEXT_PROMPT_MAX !== "undefined" ? CONTEXT_PROMPT_MAX : 2e3,
+ extra
+ });
+ const block = ctxApi()?.conversationMemoryBlock?.(getContextMemory());
+ if (block) {
+ ctx.conversation_memory = block;
}
+ return ctx;
}
- try {
- const model = resolveCurrentCheckpoint();
- if (model.name || model.architecture) {
- ctx.checkpoint = {
- name: model.name || model.title || null,
- architecture: model.architecture || model.compat_class || model.class || null,
- title: model.title || null
- };
- }
- } catch (e) {
- }
- try {
- if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) {
- const byName = /* @__PURE__ */ new Map();
- for (const l of inv.loras || []) {
- if (l?.name) {
- byName.set(String(l.name).toLowerCase(), l);
- }
- }
- ctx.selected_loras = loraHelper.selected.map((l) => {
- const name = l.name || l;
- const invRow = byName.get(String(name).toLowerCase()) || {};
- const out = {
- name,
- weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[name] || invRow.default_weight || 1
- };
- if (invRow.trigger_phrase) {
- out.trigger_phrase = invRow.trigger_phrase;
- }
- if (Array.isArray(invRow.triggers) && invRow.triggers.length) {
- out.triggers = invRow.triggers.slice(0, 8);
- }
- if (invRow.blurb) {
- out.blurb = invRow.blurb;
- }
- return out;
- });
- }
- } catch (e) {
- }
- const cardKeys = [];
- const seenCard = /* @__PURE__ */ new Set();
- const addKey = (kind, name) => {
- if (!kind || !name) {
- return;
- }
- const key = `${kind}:${name}`;
- if (seenCard.has(key)) {
- return;
- }
- seenCard.add(key);
- cardKeys.push({ kind, name });
- };
- if (ctx.checkpoint?.name) {
- addKey("checkpoint", ctx.checkpoint.name);
- }
- for (const l of ctx.selected_loras || []) {
- if (l?.name) {
- addKey("lora", l.name);
- }
- }
- for (const k of cardKeys) {
- const cached = state.modelCards[`${k.kind}:${k.name}`];
- if (!cached) {
- continue;
- }
- const slim = slimCardForContext(cached);
- if (!slim) {
- continue;
- }
- if (k.kind === "lora") {
- const sel = (ctx.selected_loras || []).find(
- (l) => String(l.name || "").toLowerCase() === String(k.name).toLowerCase()
- );
- const inventoryRich = !!(sel && (sel.triggers?.length || sel.trigger_phrase || sel.blurb));
- const cardExtra = !!(slim.when || slim.avoid || slim.prompt_hint || slim.notes);
- if (inventoryRich && !cardExtra) {
- continue;
- }
- }
- ctx.model_cards.push(slim);
- }
- if (!ctx.available_loras.length) {
- try {
- const models = typeof allModels !== "undefined" && allModels || typeof model_list !== "undefined" && model_list || [];
- const list = Array.isArray(models) ? models : Object.values(models || {});
- for (const m of list) {
- if (!m) {
- continue;
- }
- const folder = `${m.folder || m.path || ""}`;
- const isLora = /lora/i.test(m.category || m.type || "") || m.name && String(m.name).toLowerCase().includes("lora");
- const inLoraFolder = /lora/i.test(folder);
- if (!(isLora || inLoraFolder)) {
- continue;
- }
- ctx.available_loras.push({
- name: m.name || m.title,
- title: m.title || m.name,
- trigger_phrase: m.trigger_phrase || m.trigger || m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger) || null,
- architecture: m.architecture || null
- });
- }
- if (ctx.available_loras.length > INVENTORY_PROMPT_NAMES) {
- ctx.available_loras = ctx.available_loras.slice(0, INVENTORY_PROMPT_NAMES);
- }
- } catch (e) {
- }
- }
- try {
- const model = ctx.checkpoint || {};
- const blob = `${model.name || ""} ${model.title || ""}`.toLowerCase();
- const hasRaw = /\braw\b/.test(blob);
- const hasTurbo = /\bturbo\b/.test(blob);
- const profile = hasRaw && !hasTurbo ? "raw" : "turbo";
- ctx.krea_profile = profile;
- const defaults = mergedGenerationDefaults(profile);
- const rec = {
- steps: defaults.steps ?? 8,
- cfg: defaults.cfg ?? 1,
- sigma_shift: defaults.sigma_shift ?? 1.15
- };
- if (defaults.aspect) {
- rec.aspect = defaults.aspect;
- }
- const liveDiffers = ctx.steps != null && ctx.steps !== rec.steps || ctx.cfg != null && ctx.cfg !== rec.cfg || ctx.sigma_shift != null && ctx.sigma_shift !== rec.sigma_shift;
- if (liveDiffers) {
- ctx.recommended_params = rec;
- }
- } catch (e) {
- ctx.krea_profile = "turbo";
- }
- ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length ? { ...state.sessionExact } : void 0;
- if (!ctx.session_exact) {
- delete ctx.session_exact;
- }
- return ctx;
- }
- function slimCardForContext(card) {
- if (!card || typeof card !== "object") {
- return null;
- }
- const out = {
- kind: card.kind || null,
- name: card.name || null,
- triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : void 0,
- weight: card.weight != null ? card.weight : void 0,
- when: card.when ? String(card.when).slice(0, 160) : void 0,
- avoid: card.avoid ? String(card.avoid).slice(0, 120) : void 0,
- prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : void 0,
- notes: card.notes ? String(card.notes).slice(0, 200) : void 0
- };
- const clean = {};
- for (const [k, v] of Object.entries(out)) {
- if (v != null && v !== "") {
- clean[k] = v;
- }
- }
- return clean;
+ const g = state.chatSession && state.chatSession.gen || {};
+ return { session: true, prompt: g.prompt || "", negative: g.negative || "", ...extra };
}
function slimInventoryLoras(list, limit) {
const selected = /* @__PURE__ */ new Set();
@@ -3726,46 +4664,6 @@ ${patch.prompt}`;
next.unshift(v.slice(0, 80));
return next.slice(0, max);
}
- function isCardObject2(obj) {
- if (window.SA && typeof SA.isCardObject === "function") {
- return SA.isCardObject(obj);
- }
- if (!obj || typeof obj !== "object") {
- return false;
- }
- const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
- const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
- if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
- return true;
- }
- return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
- }
- function extractCardJson(text) {
- if (!text) {
- return null;
- }
- const re = /```(?:json)?\s*([\s\S]*?)```/gi;
- let match;
- let last = null;
- while ((match = re.exec(text)) !== null) {
- try {
- const obj = JSON.parse(match[1].trim());
- if (isCardObject2(obj)) {
- last = obj;
- }
- } catch (e) {
- }
- }
- if (last) {
- return last;
- }
- try {
- const obj = JSON.parse(text.trim());
- return isCardObject2(obj) ? obj : null;
- } catch (e) {
- return null;
- }
- }
function extractPatch2(text) {
if (window.SA && typeof SA.extractPatch === "function") {
return SA.extractPatch(text);
@@ -4519,8 +5417,19 @@ ${patch.prompt}`;
return true;
}
async function runGenerateFromPatch(patch, opts = {}) {
+ if (typeof pullLiveIntoSession === "function") pullLiveIntoSession();
+ if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) {
+ const fromSess = { ...state.chatSession.gen, generate: true, actions: ["generate"] };
+ if (patch && typeof patch === "object") {
+ for (const k of Object.keys(patch)) {
+ if (patch[k] != null) fromSess[k] = patch[k];
+ }
+ }
+ patch = fromSess;
+ if (typeof pushSessionToSwarm === "function") await pushSessionToSwarm(state.chatSession);
+ }
const force = !!opts.force;
- if (!force && !$2("sa_auto_generate")?.checked || !patchHasGenTrigger(patch)) {
+ if (!force && false || !patchHasGenTrigger(patch)) {
return null;
}
const variantItems = normalizeVariantList(patch);
@@ -4806,9 +5715,6 @@ ${patch.prompt}`;
const silent = !!(meta && meta.silentPatch);
mountPatchBlock(div, finalPatch, { silent });
}
- if (civitaiResults && civitaiResults.length) {
- div.appendChild(buildCivitaiCards(civitaiResults));
- }
if (role === "assistant" && !(meta && meta.historical)) {
mountCurateButtons(div, meta);
}
@@ -4846,7 +5752,7 @@ ${patch.prompt}`;
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) : isPatchObject(obj) || isCardObject2(obj);
+ const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
if (terminal) {
return true;
}
@@ -4863,7 +5769,7 @@ ${patch.prompt}`;
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) : isPatchObject(obj) || isCardObject2(obj);
+ const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj);
if (terminal) {
lastEnd = match.index + match[0].length;
}
@@ -4912,11 +5818,10 @@ ${patch.prompt}`;
}
el.classList.remove("sa-streaming", "sa-typing");
mountAssistantMeta(el, meta || void 0);
- const card = extractCardJson(fullReply);
const { prose, patch } = extractPatch2(fullReply);
setAssistantBody(el, prose || fullReply || "");
el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove());
- if (patch && !isCardObject2(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
+ if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
mountPatchBlock(el, patch, { silent });
} else if (card) {
@@ -4927,9 +5832,6 @@ ${patch.prompt}`;
wrap.appendChild(pre);
el.appendChild(wrap);
}
- if (civitaiResults && civitaiResults.length) {
- el.appendChild(buildCivitaiCards(civitaiResults));
- }
if (!(meta && meta.historical)) {
mountCurateButtons(el, meta);
}
@@ -4998,134 +5900,6 @@ ${patch.prompt}`;
function isTrainingLocked() {
return !!state.trainingLock || document.getElementById("swarm_assistent_root")?.classList.contains("sa-root-training-lock");
}
- function buildCivitaiCards(results) {
- const list = document.createElement("div");
- list.className = "sa-civitai-list";
- for (const r of results) {
- const card = document.createElement("div");
- card.className = "sa-civitai-card" + (r.already_installed ? " sa-installed" : "");
- const title = document.createElement("div");
- title.className = "sa-civitai-title";
- title.textContent = r.name || r.file_name || "LoRA";
- card.appendChild(title);
- const meta = document.createElement("div");
- meta.className = "sa-civitai-meta";
- const bits = [
- r.base_model || "?",
- r.krea_likely ? "Krea?" : null,
- r.already_installed ? "already installed" : null,
- (r.triggers || []).slice(0, 3).join(", ") || null
- ].filter(Boolean);
- meta.textContent = bits.join(" \xB7 ");
- card.appendChild(meta);
- const actions = document.createElement("div");
- actions.className = "sa-civitai-actions";
- if (r.already_installed) {
- const note = document.createElement("span");
- note.textContent = "\u0423\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430";
- actions.appendChild(note);
- } else if (r.download_url) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "basic-button sa-primary";
- btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435";
- btn.addEventListener("click", () => downloadCivitaiLoRA(r, btn));
- actions.appendChild(btn);
- } else {
- const note = document.createElement("span");
- note.textContent = "\u041D\u0435\u0442 URL \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F";
- actions.appendChild(note);
- }
- card.appendChild(actions);
- list.appendChild(card);
- }
- return list;
- }
- function downloadCivitaiLoRA(card, btn) {
- if (!card.download_url) {
- return;
- }
- if (btn) {
- btn.disabled = true;
- btn.textContent = "\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E\u2026";
- }
- setStatus(`\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E ${card.file_name || card.name}\u2026`);
- setInterruptVisible(true);
- const payload = {
- url: card.download_url,
- type: "LoRA",
- name: card.file_name || card.name || "lora"
- };
- const onDone = (ok, msg) => {
- setInterruptVisible(state.busy || state.generating);
- if (ok) {
- setStatus(`\u0421\u043A\u0430\u0447\u0430\u043D\u043E ${payload.name}`);
- if (btn) {
- btn.textContent = "\u0421\u043A\u0430\u0447\u0430\u043D\u043E";
- }
- refreshInventory(async () => {
- await maybeWriteCardAfterDownload({
- kind: "lora",
- name: payload.name,
- civitai: card
- });
- }, { rescan: true });
- } else {
- setStatus(msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F");
- if (btn) {
- btn.disabled = false;
- btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435";
- }
- appendMessage("error", msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F");
- }
- };
- if (typeof makeWSRequest === "function") {
- makeWSRequest(
- "DoModelDownloadWS",
- payload,
- (data) => {
- if (data.error) {
- onDone(false, String(data.error));
- return;
- }
- if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
- if (data.success || data.overall_percent >= 0.99) {
- triggerSwarmModelRefresh(() => onDone(true));
- } else if (data.current_percent != null) {
- setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
- }
- }
- },
- 0,
- (err) => onDone(false, String(err || "Download failed"))
- );
- } else {
- onDone(false, "makeWSRequest unavailable");
- }
- }
- async function maybeWriteCardAfterDownload({ kind, name, civitai }) {
- const display = name || civitai?.file_name || civitai?.name || "model";
- appendSystemNote(`Downloaded ${display}. Writing a recommendation card\u2026`);
- setPackValue("catalog_card", { flash: true });
- const meta = {
- triggers: civitai?.triggers || [],
- base_model: civitai?.base_model,
- civitai_url: civitai?.url || civitai?.civitai_url,
- version_id: civitai?.version_id || civitai?.modelVersionId,
- name: display
- };
- if ($2("sa_input")) {
- $2("sa_input").value = "";
- }
- await sendChat({
- forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`,
- skipSlash: true,
- skipAutoPack: true,
- fromDownload: true,
- fromCards: true,
- cardTarget: { kind: kind || "lora", name: display, meta }
- });
- }
function wantsAutoVision() {
return !!$2("sa_auto_vision")?.checked;
}
@@ -5306,7 +6080,7 @@ ${patch.prompt}`;
persona = "aggressive";
localStorage.setItem(LS_PERSONA, persona);
}
- const view = localStorage.getItem(LS_VIEW);
+ let view = localStorage.getItem(LS_VIEW);
const auto = localStorage.getItem(LS_AUTO_VISION);
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
@@ -5351,7 +6125,10 @@ ${patch.prompt}`;
if (paneW) {
document.documentElement.style.setProperty("--sa-image-width", paneW);
}
- if (view === "cards" || view === "chat" || view === "settings" || view === "train") {
+ if (view === "cards") {
+ view = "chat";
+ }
+ if (view === "chat" || view === "settings" || view === "train") {
state.view = view;
}
const drawer = localStorage.getItem(LS_CHATS_DRAWER);
@@ -5370,7 +6147,7 @@ ${patch.prompt}`;
persona: $2("sa_persona")?.value || "neutral",
auto_vision: !!$2("sa_auto_vision")?.checked,
auto_apply: !!$2("sa_auto_apply")?.checked,
- auto_generate: !!$2("sa_auto_generate")?.checked,
+ auto_generate: true,
auto_critique: !!$2("sa_auto_critique")?.checked,
auto_download: !!$2("sa_auto_download")?.checked,
park_llm: !!$2("sa_park_llm")?.checked,
@@ -5426,7 +6203,10 @@ ${patch.prompt}`;
}
});
fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty("--sa-image-width", v));
- if (ui.view === "cards" || ui.view === "chat" || ui.view === "settings" || ui.view === "train") {
+ if (ui.view === "cards") {
+ ui.view = "chat";
+ }
+ if (ui.view === "chat" || ui.view === "settings" || ui.view === "train") {
fill(LS_VIEW, ui.view, (v) => {
state.view = v;
});
@@ -5574,6 +6354,15 @@ ${data.ui.help_extra}`.trim();
if (asst.history_keep_turns != null) {
HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4);
}
+ if (asst.compress_at != null) {
+ COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(asst.compress_at) || 0.7));
+ }
+ if (asst.chars_per_token != null) {
+ CHARS_PER_TOKEN = Math.max(1.5, Number(asst.chars_per_token) || 3.2);
+ }
+ if (asst.compress_auto != null) {
+ COMPRESS_AUTO = !!asst.compress_auto;
+ }
if (asst.max_ref_slots != null) {
MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4);
}
@@ -5590,6 +6379,7 @@ ${data.ui.help_extra}`.trim();
INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24);
}
fillKnobsFromConfig(data);
+ updateCtxChip();
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
@@ -6173,9 +6963,7 @@ ${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)" : ""}`);
- prefetchActiveModelCards();
if (state.view === "cards") {
- renderCardsList();
}
if (done) {
done(state.inventory);
@@ -6366,7 +7154,6 @@ ${data.ui.help_extra}`.trim();
});
if (state.settingsTab === "craft") {
refreshMemoryList();
- refreshWantedQueue();
}
if (state.settingsTab === "user") {
refreshUserPrefs();
@@ -6396,6 +7183,12 @@ ${data.ui.help_extra}`.trim();
};
setNum("sa_num_ctx", asst.num_ctx);
setNum("sa_history_keep", asst.history_keep_turns);
+ setNum("sa_compress_at", asst.compress_at != null ? asst.compress_at : COMPRESS_AT);
+ setNum("sa_chars_per_token", asst.chars_per_token != null ? asst.chars_per_token : CHARS_PER_TOKEN);
+ const autoEl = $2("sa_compress_auto");
+ if (autoEl) {
+ autoEl.checked = asst.compress_auto != null ? !!asst.compress_auto : COMPRESS_AUTO;
+ }
setNum("sa_memory_top_k", asst.memory_top_k);
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
const weightEl = $2("sa_user_prefs_weight");
@@ -6426,6 +7219,9 @@ ${data.ui.help_extra}`.trim();
const assistant = {
num_ctx: num("sa_num_ctx"),
history_keep_turns: num("sa_history_keep"),
+ compress_at: num("sa_compress_at"),
+ chars_per_token: num("sa_chars_per_token"),
+ compress_auto: $2("sa_compress_auto") ? !!$2("sa_compress_auto").checked : null,
memory_top_k: num("sa_memory_top_k"),
user_prefs_weight: num("sa_user_prefs_weight")
};
@@ -6459,6 +7255,16 @@ ${data.ui.help_extra}`.trim();
exact: data.exact || state.config?.exact
});
}
+ if (assistant.compress_at != null) {
+ COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(assistant.compress_at) || 0.7));
+ }
+ if (assistant.chars_per_token != null) {
+ CHARS_PER_TOKEN = Math.max(1.5, Number(assistant.chars_per_token) || 3.2);
+ }
+ if (assistant.compress_auto != null) {
+ COMPRESS_AUTO = !!assistant.compress_auto;
+ }
+ updateCtxChip();
setStatus("Knobs \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B \u0432 overlay");
},
0,
@@ -6822,58 +7628,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function modelKeyLeaf(name) {
return String(name || "").replace(/\\/g, "/").split("/").pop().replace(/\.(safetensors|ckpt|pt|pth|gguf|bin)$/i, "").trim().toLowerCase();
}
- function refreshWantedQueue() {
- if (typeof genericRequest !== "function") {
- return;
- }
- genericRequest(
- "AssistentListWanted",
- {},
- (data) => {
- const items = Array.isArray(data?.items) ? data.items : [];
- state.wanted = { count: data?.count ?? items.length, items };
- const keys = /* @__PURE__ */ new Set();
- for (const item of items) {
- const leaf = modelKeyLeaf(item?.title);
- if (leaf) {
- keys.add(leaf);
- }
- if (item?.version_id) {
- keys.add(`v${item.version_id}`);
- }
- }
- state.wantedKeys = keys;
- const el = $2("sa_mem_wanted");
- if (el) {
- el.textContent = state.wanted.count ? `\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: ${state.wanted.count} (\u0441\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C up)` : "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u043F\u0443\u0441\u0442\u043E";
- el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join("\n");
- }
- if (state.view === "cards") {
- renderCardsList();
- }
- },
- 0,
- () => {
- const el = $2("sa_mem_wanted");
- if (el) {
- el.textContent = "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u2014";
- }
- }
- );
- }
- function isWantedModel(row) {
- const keys = state.wantedKeys;
- if (!keys || !keys.size) {
- return false;
- }
- for (const candidate of [row?.name, row?.title]) {
- const leaf = modelKeyLeaf(candidate);
- if (leaf && keys.has(leaf)) {
- return true;
- }
- }
- return false;
- }
function setOllamaHealth(level, text, title) {
state.ollamaHealth = level;
const el = $2("sa_ollama_health");
@@ -6912,16 +7666,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
(err) => setOllamaHealth("down", "Ollama \u2715", `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")} \xB7 ${baseUrl}`)
);
}
- function setCardStatus(msg) {
- const el = $2("sa_card_status");
- if (el) {
- el.textContent = msg || "";
- }
- }
function setView(view) {
if (view === "cards") {
- state.view = "cards";
- } else if (view === "settings") {
+ view = "chat";
+ }
+ if (view === "settings") {
state.view = "settings";
} else if (view === "train") {
state.view = "train";
@@ -6929,15 +7678,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
state.view = "chat";
}
const chat = $2("sa_view_chat");
- const cards = $2("sa_view_cards");
const settings = $2("sa_view_settings");
const train = $2("sa_view_train");
if (chat) {
chat.hidden = state.view !== "chat";
}
- if (cards) {
- cards.hidden = state.view !== "cards";
- }
if (settings) {
settings.hidden = state.view !== "settings";
}
@@ -6950,13 +7695,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
$2(id)?.setAttribute("aria-selected", on ? "true" : "false");
};
tabActive("sa_tab_chat", state.view === "chat");
- tabActive("sa_tab_cards", state.view === "cards");
tabActive("sa_tab_settings", state.view === "settings");
tabActive("sa_tab_train", state.view === "train");
saveSettings();
- if (state.view === "cards") {
- renderCardsList();
- } else if (state.view === "settings") {
+ if (state.view === "settings") {
setSettingsTab(state.settingsTab || "behavior");
} else if (state.view === "train") {
window.SA?.training?.render?.();
@@ -6973,479 +7715,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function closeSettings() {
setView("chat");
}
- function prefetchCard(kind, name) {
- return new Promise((resolve) => {
- if (!kind || !name || typeof genericRequest !== "function") {
- resolve(null);
- return;
- }
- const key = `${kind}:${name}`;
- genericRequest(
- "AssistentGetCard",
- { kind, name },
- (data) => {
- if (data?.card) {
- state.modelCards[key] = data.card;
- }
- resolve(data?.card || null);
- },
- 0,
- () => resolve(null)
- );
- });
- }
- async function prefetchActiveModelCards() {
- const keys = [];
- const seen = /* @__PURE__ */ new Set();
- const add = (kind, name) => {
- if (!kind || !name) {
- return;
- }
- const key = `${kind}:${name}`;
- if (seen.has(key)) {
- return;
- }
- seen.add(key);
- keys.push({ kind, name });
- };
- try {
- const ck = resolveCurrentCheckpoint();
- if (ck?.name) {
- add("checkpoint", ck.name);
- }
- } catch (e) {
- }
- try {
- if (typeof loraHelper !== "undefined" && Array.isArray(loraHelper?.selected)) {
- for (const l of loraHelper.selected) {
- add("lora", l?.name || l);
- }
- }
- } catch (e) {
- }
- for (const l of state.inventory?.loras || []) {
- if (l?.has_card) {
- add("lora", l.name);
- }
- if (keys.length >= 14) {
- break;
- }
- }
- for (const c of state.inventory?.checkpoints || []) {
- if (c?.has_card) {
- add("checkpoint", c.name);
- }
- if (keys.length >= 16) {
- break;
- }
- }
- await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
- }
- function cardsCatalog() {
- const kind = $2("sa_cards_kind")?.value || "all";
- const inv = state.inventory || {};
- const rows = [];
- if (kind === "all" || kind === "checkpoint") {
- for (const c of inv.checkpoints || []) {
- rows.push({
- kind: "checkpoint",
- name: c.name,
- title: c.title || c.name,
- has_card: !!c.has_card,
- hash: c.hash || "",
- preview_url: c.preview_url || null,
- has_sidecar: !!c.has_sidecar
- });
- }
- }
- if (kind === "all" || kind === "lora") {
- for (const l of inv.loras || []) {
- rows.push({
- kind: "lora",
- name: l.name,
- title: l.title || l.name,
- has_card: !!l.has_card,
- trigger: l.trigger_phrase,
- hash: l.hash || "",
- preview_url: l.preview_url || null,
- has_sidecar: !!l.has_sidecar
- });
- }
- }
- return rows;
- }
- function renderCardsList() {
- const root = $2("sa_cards_list");
- if (!root) {
- return;
- }
- root.innerHTML = "";
- const rows = cardsCatalog();
- if (!rows.length) {
- root.innerHTML = 'Inventory \u043F\u0443\u0441\u0442 \u2014 \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C.
';
- return;
- }
- for (const row of rows) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "sa-card-row";
- if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
- btn.classList.add("sa-selected");
- }
- const thumb = row.preview_url ? ` ` : '
';
- const metaBits = [];
- metaBits.push(row.has_card ? "card \u2713" : "\u043D\u0435\u0442 card");
- if (row.has_sidecar) {
- metaBits.push("sidecar");
- }
- if (isWantedModel(row)) {
- metaBits.push("\u23F3 wanted");
- btn.classList.add("sa-card-row-wanted");
- }
- if (row.trigger) {
- metaBits.push(String(row.trigger).slice(0, 40));
- }
- btn.innerHTML = `${thumb}${escapeHtml2(row.kind)}
${escapeHtml2(row.title || row.name)}
${escapeHtml2(metaBits.join(" \xB7 "))}
\u2197 `;
- btn.addEventListener("click", (e) => {
- if (e.target?.closest?.("[data-chat]")) {
- e.preventDefault();
- e.stopPropagation();
- sendCardToChat(row);
- return;
- }
- selectCardModel(row);
- });
- root.appendChild(btn);
- }
- }
- function sendCardToChat(row) {
- if (!row?.name) {
- return;
- }
- selectCardModel(row);
- setView("chat");
- const kind = row.kind === "checkpoint" ? "checkpoint" : "LoRA";
- const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : "";
- if ($2("sa_input")) {
- $2("sa_input").value = `\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 ${kind} \xAB${row.name}\xBB.${triggers} \u0423\u0447\u0442\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443/triggers \u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u043F\u0430\u0442\u0447.`;
- $2("sa_input").focus();
- }
- setStatus(`\u0412 \u0447\u0430\u0442 \u2192 ${row.name}`);
- }
- function wireCardForm() {
- const sync = () => {
- if ($2("sa_card_show_json")?.checked) {
- syncCardJsonFromForm();
- }
- };
- ["sa_card_triggers", "sa_card_weight", "sa_card_when", "sa_card_avoid", "sa_card_hint", "sa_card_notes", "sa_card_url"].forEach((id) => $2(id)?.addEventListener("change", sync));
- $2("sa_card_show_json")?.addEventListener("change", () => {
- const on = !!$2("sa_card_show_json")?.checked;
- const ta = $2("sa_card_json");
- if (ta) {
- ta.hidden = !on;
- if (on) {
- syncCardJsonFromForm();
- }
- }
- });
- $2("sa_card_json")?.addEventListener("change", () => {
- if ($2("sa_card_show_json")?.checked) {
- applyCardToForm(readCardDraft() || {});
- }
- });
- }
- function applyCardToForm(card) {
- card = card || {};
- const triggers = Array.isArray(card.triggers) ? card.triggers.join(", ") : card.triggers || "";
- if ($2("sa_card_triggers")) {
- $2("sa_card_triggers").value = triggers;
- }
- if ($2("sa_card_weight")) {
- $2("sa_card_weight").value = card.weight != null ? card.weight : state.cardsSelection?.kind === "lora" ? 0.8 : 1;
- }
- if ($2("sa_card_when")) {
- $2("sa_card_when").value = card.when || "";
- }
- if ($2("sa_card_avoid")) {
- $2("sa_card_avoid").value = card.avoid || "";
- }
- if ($2("sa_card_hint")) {
- $2("sa_card_hint").value = card.prompt_hint || "";
- }
- if ($2("sa_card_notes")) {
- $2("sa_card_notes").value = card.notes || "";
- }
- if ($2("sa_card_url")) {
- $2("sa_card_url").value = card.civitai_url || "";
- }
- if ($2("sa_card_json")) {
- $2("sa_card_json").value = JSON.stringify(card, null, 2);
- }
- }
- function syncCardJsonFromForm() {
- const sel = state.cardsSelection || {};
- let base = {};
- try {
- base = JSON.parse($2("sa_card_json")?.value || "{}");
- } catch (e) {
- base = {};
- }
- const triggers = String($2("sa_card_triggers")?.value || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean);
- const card = {
- ...base,
- kind: sel.kind || base.kind || "lora",
- name: sel.name || base.name || "",
- triggers,
- weight: parseFloat($2("sa_card_weight")?.value || "0.8") || 0.8,
- when: $2("sa_card_when")?.value || "",
- avoid: $2("sa_card_avoid")?.value || "",
- prompt_hint: $2("sa_card_hint")?.value || "",
- notes: $2("sa_card_notes")?.value || "",
- civitai_url: $2("sa_card_url")?.value || "",
- version_id: base.version_id != null ? base.version_id : null
- };
- if ($2("sa_card_json")) {
- $2("sa_card_json").value = JSON.stringify(card, null, 2);
- }
- return card;
- }
- function renderCardPreviews(urls) {
- const root = $2("sa_card_previews");
- if (!root) {
- return;
- }
- const list = (urls || []).filter(Boolean).slice(0, 6);
- root.innerHTML = "";
- if (!list.length) {
- root.hidden = true;
- return;
- }
- root.hidden = false;
- for (const url of list) {
- const img = document.createElement("img");
- img.className = "sa-card-thumb";
- img.src = url;
- img.alt = "preview";
- img.title = "\u041A\u043B\u0438\u043A \u2014 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0443 Refs";
- img.addEventListener("click", () => {
- setBoardTab("refs");
- addRefFromUrl(url);
- setStatus("\u041F\u0440\u0435\u0432\u044C\u044E \u2192 Refs");
- });
- root.appendChild(img);
- }
- }
- function mergeCivitaiIntoCard(card, data) {
- const out = { ...card || {} };
- const civ = data?.civitai;
- if (!out.triggers?.length && data?.trigger_phrase) {
- out.triggers = [data.trigger_phrase];
- }
- if (civ) {
- const trained = civ.trainedWords || civ.trained_words;
- if ((!out.triggers || !out.triggers.length) && Array.isArray(trained) && trained.length) {
- out.triggers = trained.slice(0, 12);
- }
- if (!out.civitai_url) {
- const mid = civ.modelId || civ.model?.id || civ.model?.modelId;
- const vid = civ.id || data.version_id;
- if (mid && vid) {
- out.civitai_url = `https://civitai.red/models/${mid}?modelVersionId=${vid}`;
- } else if (vid) {
- out.civitai_url = `https://civitai.red/models/0?modelVersionId=${vid}`;
- }
- }
- if (out.version_id == null && (civ.id || data.version_id)) {
- out.version_id = civ.id || data.version_id;
- }
- if (!out.notes && civ.description) {
- out.notes = String(civ.description).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 400);
- }
- }
- if (out.version_id == null && data?.version_id) {
- out.version_id = data.version_id;
- }
- return out;
- }
- function formatMetaStatus(data) {
- if (!data) {
- return "\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430";
- }
- if (data.error) {
- return String(data.error);
- }
- const parts = [];
- if (data.has_sidecar) {
- parts.push(`\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044C \u2713 \xB7 version ${data.version_id || "?"}`);
- } else if (data.fetched) {
- parts.push(`Civitai \u2713 \xB7 version ${data.version_id || "?"}`);
- } else {
- parts.push("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442");
- }
- const n = (data.example_urls || data.preview_urls || []).length;
- if (n) {
- parts.push(`${n} \u043A\u0430\u0434\u0440${n === 1 ? "" : "\u0430"}`);
- }
- if (data.has_card) {
- parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u2713");
- } else {
- parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 Assistent \u043D\u0435\u0442");
- }
- if (data.fetch_error) {
- parts.push(String(data.fetch_error));
- }
- return parts.join(" \xB7 ");
- }
- function applyCardMetaResponse(row, data, { preserveUser } = {}) {
- let card = data.card || {
- kind: row.kind,
- name: row.name,
- triggers: data.trigger_phrase ? [data.trigger_phrase] : [],
- weight: row.kind === "lora" ? 0.8 : 1,
- when: "",
- avoid: "",
- prompt_hint: "",
- notes: "",
- civitai_url: "",
- version_id: data.version_id || null
- };
- if (preserveUser) {
- const current = syncCardJsonFromForm();
- card = {
- ...mergeCivitaiIntoCard(card, data),
- when: current.when || card.when || "",
- avoid: current.avoid || card.avoid || "",
- prompt_hint: current.prompt_hint || card.prompt_hint || "",
- notes: current.notes || card.notes || ""
- };
- } else {
- card = mergeCivitaiIntoCard(card, data);
- }
- applyCardToForm(card);
- state.modelCards[`${row.kind}:${row.name}`] = card;
- const urls = [
- ...data.preview_urls || [],
- ...data.example_urls || []
- ];
- renderCardPreviews(urls);
- const badge = $2("sa_card_badge");
- if (badge) {
- badge.hidden = false;
- badge.textContent = data.has_card ? "card \u2713" : data.has_sidecar || data.fetched ? "meta \u2713" : "\u043D\u0435\u0442 \u043C\u0435\u0442\u044B";
- }
- setCardStatus(formatMetaStatus(data));
- }
- function selectCardModel(row) {
- state.cardsSelection = row;
- renderCardsList();
- if ($2("sa_card_title")) {
- $2("sa_card_title").textContent = row.title || row.name;
- }
- const badge = $2("sa_card_badge");
- if (badge) {
- badge.hidden = false;
- badge.textContent = "\u2026";
- }
- setCardStatus("\u0427\u0438\u0442\u0430\u044E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0443\u044E \u043C\u0435\u0442\u0443\u2026");
- genericRequest(
- "AssistentGetCardMeta",
- { kind: row.kind, name: row.name, fetch: false },
- (data) => applyCardMetaResponse(row, data || {}),
- 0,
- (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438"))
- );
- }
- function fetchCardMetaLive() {
- const row = state.cardsSelection;
- if (!row) {
- setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C");
- return;
- }
- setCardStatus("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442 \xB7 \u0438\u0449\u0443 \u043F\u043E SHA\u2026");
- genericRequest(
- "AssistentGetCardMeta",
- { kind: row.kind, name: row.name, fetch: true },
- (data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }),
- 0,
- (err) => setCardStatus(String(err || "Civitai: \u043E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u043F\u0440\u043E\u0441\u0430"))
- );
- }
async function addRefFromUrl(url) {
if (!url) {
return;
}
addRefSlot({ src: url, select: false });
}
- function readCardDraft() {
- const fromForm = syncCardJsonFromForm();
- if ($2("sa_card_show_json")?.checked) {
- const raw = $2("sa_card_json")?.value || "";
- try {
- return JSON.parse(raw);
- } catch (e) {
- setCardStatus("\u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON");
- return null;
- }
- }
- return fromForm;
- }
- function saveCurrentCard({ enqueue } = {}) {
- const sel = state.cardsSelection;
- if (!sel) {
- setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C");
- return;
- }
- const card = readCardDraft();
- if (!card) {
- return;
- }
- card.kind = card.kind || sel.kind;
- card.name = card.name || sel.name;
- setCardStatus("\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u044E\u2026");
- genericRequest(
- "AssistentSaveCard",
- { kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue },
- (data) => {
- if (data.error) {
- setCardStatus(data.error);
- return;
- }
- state.modelCards[`${sel.kind}:${sel.name}`] = card;
- setCardStatus(data.installed ? `\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \xB7 ${data.path}` : `\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A + wanted \xB7 ${data.path}`);
- refreshInventory(() => renderCardsList());
- if (enqueue || !data.installed) {
- refreshWantedQueue();
- }
- },
- 0,
- (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F"))
- );
- }
- function enqueueWantedOnly() {
- const sel = state.cardsSelection;
- const card = readCardDraft() || {};
- if (!sel && !card.civitai_url) {
- setCardStatus("\u041D\u0443\u0436\u043D\u0430 \u043C\u043E\u0434\u0435\u043B\u044C \u0438\u043B\u0438 civitai_url");
- return;
- }
- genericRequest(
- "AssistentEnqueueWanted",
- {
- kind: card.kind || sel?.kind || "lora",
- url: card.civitai_url || "",
- version_id: card.version_id || 0,
- title: card.name || sel?.name || "",
- card
- },
- (data) => {
- setCardStatus(data.already ? "\u0423\u0436\u0435 \u0432 wanted" : `Wanted \u2192 ${data.path}`);
- refreshWantedQueue();
- },
- 0,
- (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 enqueue"))
- );
- }
function shortLoraName(name) {
const s = String(name || "");
const base = s.split(/[/\\]/).pop() || s;
@@ -7567,40 +7842,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
setTimeout(() => document.addEventListener("mousedown", onDoc), 0);
filter.focus();
}
- async function generateCardWithAssistent() {
- const sel = state.cardsSelection;
- if (!sel) {
- setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C");
- return;
- }
- if (state.busy) {
- setCardStatus("\u0427\u0430\u0442 \u0437\u0430\u043D\u044F\u0442");
- return;
- }
- setPackValue("catalog_card", { flash: true });
- setView("chat");
- const meta = await new Promise((resolve) => {
- genericRequest(
- "AssistentGetCardMeta",
- { kind: sel.kind, name: sel.name },
- (data) => resolve(data),
- 0,
- () => resolve(null)
- );
- });
- const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`;
- await sendChat({
- forcedUserText: forced,
- skipSlash: true,
- skipAutoPack: true,
- fromCards: true,
- cardTarget: {
- kind: sel.kind,
- name: sel.name,
- meta
- }
- });
- }
function inventoryIsStale(maxAgeMs = 2e4) {
if (!state.inventoryFetchedAt) {
return true;
@@ -7635,151 +7876,159 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
);
}
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
- const { fromAutoCritique, fromVisionHop, fromCards, fromDebug } = opts;
- if (fromCards) {
- const card = extractCardJson(reply);
- if (card) {
- if ($2("sa_card_json")) {
- $2("sa_card_json").value = JSON.stringify(card, null, 2);
- }
- if (opts.fromDownload || opts.cardTarget) {
- const kind = card.kind || opts.cardTarget?.kind || "lora";
- const name = card.name || opts.cardTarget?.name;
- if (name && typeof genericRequest === "function") {
- genericRequest(
- "AssistentSaveCard",
- { kind, name, card, enqueue_wanted: false },
- (data) => {
- if (data?.path) {
- state.modelCards[`${kind}:${name}`] = card;
- setCardStatus(data.installed ? `Card saved \u2192 ${data.path}` : `Card draft \u2192 ${data.path}`);
- setStatus(`Card saved for ${name}`);
- }
- },
- 0,
- () => setCardStatus("Card draft ready \u2014 Save manually")
- );
- }
- } else {
- setView("cards");
- setCardStatus("Draft from Assistent \u2014 review & Save");
- }
- }
- return;
- }
- const { patch } = extractPatch2(reply);
+ const { fromAutoCritique, fromVisionHop, fromDebug } = opts;
+ void civitaiResults;
if (fromDebug) {
state.pendingSilentGen = false;
return;
}
- const commanded = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(opts.userText || "");
- let effective = patch;
- if (!effective && !fromVisionHop && !fromAutoCritique) {
- const aspect = parseAspectFromUserText(opts.userText || "");
- if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ""))) {
- effective = { aspect, actions: ["generate"] };
- if (state.lastPatch?.prompt) {
- effective.prompt = state.lastPatch.prompt;
- }
- appendSystemNote(`\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043B aspect ${aspect} \u0441\u0430\u043C.`);
- }
- }
- if (!effective && !fromVisionHop && !fromAutoCritique) {
- const synthesized = synthesizePatchAfterEmptyFence(reply, opts.userText || "", opts);
- if (synthesized) {
- effective = synthesized;
- appendSystemNote(synthesized.actions ? "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B Generate." : "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430.");
- }
- }
- if (!effective && !fromVisionHop && !fromAutoCritique && commanded && claimTurnHop("empty_patch")) {
- appendSystemNote("\u041D\u0443\u0436\u0435\u043D \u043A\u0430\u0434\u0440 \u2014 \u043F\u0440\u043E\u0448\u0443 JSON \u0441 prompt + generate.");
- await sendChat({
- skipSlash: true,
- skipAutoPack: true,
- fromEmptyPatchRetry: true,
- userWantsGenerate: true,
- forcedUserText: '\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0443\u0436\u0435 \u043F\u0440\u043E\u0441\u0438\u0442 \u043A\u0430\u0434\u0440 (\u044D\u0442\u043E \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u0438\u0437 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F, \u0434\u0430\u0436\u0435 \u0431\u0435\u0437 \u0441\u043B\u043E\u0432\u0430 \xAB\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB). \u041E\u0442\u0432\u0435\u0442\u044C \u0422\u041E\u041B\u042C\u041A\u041E \u043E\u0434\u043D\u0438\u043C fenced JSON: {"prompt":"","negative":"","actions":["generate"]}. prompt \u2014 \u0430\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439. \u0411\u0435\u0437 \u043F\u0440\u043E\u0437\u044B, \u0431\u0435\u0437 \xAB\u0441\u043A\u0430\u0436\u0438 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB.'
- });
- return;
- }
- if (opts.fromPromptEnRetry && effective) {
+ const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null };
+ let effective = extracted && extracted.patch ? extracted.patch : null;
+ if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === "function") {
effective = mergePromptEnRewrite(effective);
}
- if (effective) {
- rememberLastPatch(effective);
+ const S = window.SA && window.SA.session;
+ const act = getActivity();
+ if (effective && act && typeof act.noteModelCommands === "function") {
+ act.noteModelCommands(effective);
+ }
+ 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",
+ detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ")
+ });
+ try {
+ const chat = typeof findChat === "function" ? findChat(state.activeChatId) : null;
+ if (chat && typeof snapshotChatParams === "function") {
+ chat.params = snapshotChatParams();
+ if (typeof persistChatsStore === "function") persistChatsStore();
+ }
+ } catch (e) {
+ }
+ if (typeof rememberLastPatch === "function") rememberLastPatch(effective);
}
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) {
- doInterruptNow();
+ if (typeof doInterruptNow === "function") doInterruptNow();
}
- if (civitaiResults && civitaiResults.length && $2("sa_auto_download")?.checked) {
- const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely) || civitaiResults.find((r) => !r.already_installed && r.download_url);
- if (pick) {
- downloadCivitaiLoRA(pick, null);
- }
- }
- const intent = resolveTurnIntent(effective, opts.userText || "", opts);
+ const intent = resolveTurnIntent2(effective, opts.userText || "", opts);
if (effective) {
if (intent.generate) {
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
- if (!acts.includes("generate")) {
- effective = { ...effective, actions: acts.concat("generate") };
- }
- } else {
+ effective = { ...effective, actions: acts.includes("generate") ? acts : acts.concat("generate"), generate: true };
+ } else if (typeof stripGenerateAction === "function") {
effective = stripGenerateAction(effective);
+ if (effective && effective.generate) {
+ effective = { ...effective };
+ delete effective.generate;
+ }
}
- if (!intent.look) {
- effective = stripLookAt(effective);
- }
- rememberLastPatch(effective);
+ if (!intent.look && typeof stripLookAt === "function") effective = stripLookAt(effective);
+ if (typeof rememberLastPatch === "function") rememberLastPatch(effective);
}
if (intent.vetoed) {
state.pendingSilentGen = false;
+ const a = getActivity();
+ if (a) {
+ a.skip("generate", {
+ kind: "generate",
+ label: "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D",
+ detail: "\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043D\u0435 \u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"
+ });
+ }
}
- if (effective && intent.look && !fromVisionHop && !fromAutoCritique) {
- const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
- if (hopped) {
+ const askList = Array.isArray(intent.ask) ? intent.ask : [];
+ if (askList.length && typeof claimTurnHop === "function" && claimTurnHop("ask")) {
+ activityStep("ask", {
+ kind: "ask",
+ label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${askList.join(", ")}`,
+ detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026",
+ status: "running"
+ });
+ pullLiveIntoSession();
+ const bits = [];
+ if (askList.some((a) => /settings/i.test(String(a)))) {
+ const dump = S && typeof S.fullSettingsDump === "function" ? S.fullSettingsDump(state.chatSession, {
+ exact: state.exact,
+ kreaProfiles: state.kreaProfiles,
+ sessionExact: state.sessionExact
+ }) : collectLiveContext();
+ bits.push("SETTINGS_JSON:\n" + JSON.stringify(dump));
+ }
+ if (askList.some((a) => /inventory/i.test(String(a)))) {
+ const inv = state.inventory || {};
+ bits.push("INVENTORY_JSON:\n" + JSON.stringify({
+ loras: (inv.loras || []).slice(0, 40).map((l) => ({ name: l.name || l, trigger_phrase: l.trigger_phrase || null })),
+ checkpoints: (inv.checkpoints || []).slice(0, 16).map((c) => c.name || c),
+ wildcards: (inv.wildcards || []).slice(0, 20).map((w) => w.name || w)
+ }));
+ }
+ if (bits.length) {
+ activityDone("ask", { detail: "\u0434\u0435\u0442\u0430\u043B\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B \u043C\u043E\u0434\u0435\u043B\u0438" });
+ await sendChat({ skipSlash: true, skipAutoPack: true, fromAskHop: true, forcedUserText: bits.join("\n\n") });
return;
}
}
- if (intent.generate && effective?.prompt && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && claimTurnHop("krea_prep")) {
+ if (effective && intent.look && !fromVisionHop && !fromAutoCritique) {
+ activityStep("look", {
+ kind: "look",
+ label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440",
+ status: "running"
+ });
+ if (typeof maybeVisionHop === "function") {
+ const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
+ if (hopped) {
+ activityDone("look", { detail: "vision hop" });
+ return;
+ }
+ activityDone("look", { detail: "\u043A\u0430\u0434\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" });
+ }
+ }
+ if (intent.generate && effective?.prompt && typeof promptNeedsKreaPrep === "function" && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && typeof claimTurnHop === "function" && claimTurnHop("krea_prep")) {
state.pendingPromptEnMerge = { ...effective };
- appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea 2 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u044C\u044E (EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430)\u2026");
- setBusyPhase("refining");
+ activityStep("prep", {
+ kind: "prep",
+ label: "\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea",
+ detail: "EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
+ status: "running"
+ });
+ if (typeof appendSystemNote === "function") appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea\u2026");
await sendChat({
skipSlash: true,
skipAutoPack: true,
fromPromptEnRetry: true,
userWantsGenerate: true,
- forcedUserText: buildKreaPromptPrepRequest(effective)
+ forcedUserText: typeof buildKreaPromptPrepRequest === "function" ? buildKreaPromptPrepRequest(effective) : String(effective.prompt || "")
});
return;
}
- const doApply = !!(effective && (intent.generate || $2("sa_auto_apply")?.checked));
- if (doApply) {
- if (intent.generate) {
- startBusyUi("silent_gen");
- } else {
- setBusyPhase("applying");
+ if (intent.generate) {
+ activityStep("generate", {
+ kind: "generate",
+ label: intent.vetoed ? "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D (\u0432\u0435\u0442\u043E)" : "Generate",
+ status: intent.vetoed ? "skip" : "running"
+ });
+ if (typeof startBusyUi === "function") startBusyUi("silent_gen");
+ if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
+ await pushSessionToSwarm(state.chatSession);
+ if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
+ const srcOut = await runGenerateFromPatch(
+ { ...effective || {}, actions: ["generate"], generate: true },
+ { force: true, fromSession: true }
+ );
+ activityDone("generate", { detail: srcOut ? "\u043A\u0430\u0434\u0440 \u0433\u043E\u0442\u043E\u0432" : "\u0431\u0435\u0437 \u043A\u0430\u0434\u0440\u0430" });
+ if (srcOut) {
+ if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut);
+ if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut);
}
- await applyPatch(effective, "all");
- syncLiveParamsBar();
- if (intent.generate) {
- if (effective?.prompt && promptNeedsKreaPrep(effective.prompt)) {
- setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u0432\u0441\u0451 \u0435\u0449\u0451 \u043D\u0435 EN/Krea-ready \u2014 Generate \u0441 \u0442\u0435\u043C \u0447\u0442\u043E \u0435\u0441\u0442\u044C");
- }
- const src = await runGenerateFromPatch(
- { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ["generate"] },
- { force: true }
- );
- if (src) {
- await maybeAutoCritique(src);
- await maybeAutoVisionLook(src);
- }
- } else if (!state.generating) {
+ } else if (effective && $2("sa_auto_apply")?.checked) {
+ if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
+ await pushSessionToSwarm(state.chatSession);
+ if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
+ if (!state.generating && typeof stopBusyUi === "function") {
stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : "");
}
- } else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) {
- setStatus("\u041E\u0442\u0432\u0435\u0442 \u0431\u0435\u0437 JSON-\u043F\u0430\u0442\u0447\u0430 \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E");
}
state.pendingSilentGen = false;
}
@@ -7790,11 +8039,15 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
const prevIntent = state.lastUserParamIntent;
state.lastUserParamIntent = true;
- await applyPatch(withActions, "all");
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
+ }
+ await pushSessionToSwarm(state.chatSession);
state.lastUserParamIntent = prevIntent;
setStatus(note || "Applied");
- if ($2("sa_auto_generate")?.checked) {
- await runGenerateFromPatch(withActions);
+ if (patchHasGenTrigger(withActions)) {
+ await runGenerateFromPatch(withActions, { force: true, fromSession: true });
}
syncChipHighlight();
}
@@ -7882,7 +8135,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
`persona=${persona} \xB7 pack=${pack}`,
`chat=${chatModel} \xB7 embed=${embed}`,
`skills=${(state.enabledSkills || []).join(",") || "\u2014"}`,
- `auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${!!$2("sa_auto_generate")?.checked} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`,
+ `auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${true} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`,
"",
"Live SwarmUI:",
` ckpt=${ctx.checkpoint?.name || "\u2014"} \xB7 krea_profile=${ctx.krea_profile || profile}`,
@@ -7895,6 +8148,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
` has_vision_image=${!!ctx.has_vision_image} \xB7 images_in_request=${!!ctx.images_in_request} \xB7 vision_ready=${visionReadySlots().length}`,
` context_json_chars\u2248${JSON.stringify(ctx).length} \xB7 last_system_chars=${state.lastSystemChars || "\u2014"} \xB7 last_context_chars=${state.lastContextChars || "\u2014"}`,
state.lastSystemLayers ? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : " system_layers: \u2014 (\u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u044C)",
+ (() => {
+ const b = currentBudgetEstimate();
+ const mem = getContextMemory();
+ return ` budget\u2248${b.used}/${b.numCtx} (${b.fromEval ? "eval" : "est"}) \xB7 level=${b.level} \xB7 memory_until=${mem.untilCount || 0} \xB7 auto_compress=${COMPRESS_AUTO}`;
+ })(),
"",
"Exact defaults (merged):",
` generation=${JSON.stringify(exactGen)}`,
@@ -7938,6 +8196,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
setStatus("/history");
return true;
}
+ if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") {
+ await compressNowFromUi();
+ return true;
+ }
if (cmd === "debug" || cmd === "dbg" || cmd === "why") {
const dump = buildDebugSummary();
appendSystemNote(dump);
@@ -8077,7 +8339,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
}
if (cmd === "pack") {
if (!setPackValue(arg, { flash: true, user: true })) {
- setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|card|persona");
+ setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|persona");
} else {
setStatus(`Pack \u2192 ${$2("sa_pack")?.value}`);
}
@@ -8091,21 +8353,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
await startHornyGame();
return true;
}
- if (cmd === "civitai") {
- if (!arg) {
- setStatus("/civitai ");
- return true;
- }
- if ($2("sa_input")) {
- $2("sa_input").value = `Find a Krea 2 LoRA for: ${arg}`;
- }
- setPackValue(defaultPackId(), { flash: true });
- await sendChat({
- skipAutoPack: true,
- forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`
- });
- return true;
- }
if (cmd === "persona") {
const sub = (parts[1] || "new").toLowerCase();
const rest = parts.slice(2).join(" ").trim();
@@ -8186,7 +8433,7 @@ ${HELP_TEXT}`);
if (!isMachineTurn(opts)) {
state.lastUserParamIntent = userTextMentionsParams(text);
state.lastUserControlIntent = userTextMentionsControls(text);
- state.pendingSilentGen = userImpliesGenerate(text);
+ state.pendingSilentGen = false;
}
if (!isMachineTurn(opts) && !opts.skipSlash) {
if (rawInput.startsWith("/")) {
@@ -8231,8 +8478,8 @@ ${HELP_TEXT}`);
setPackValue(guessed, { flash: true });
}
}
- if (!opts.fromDebug && (opts.fromCards || state.view === "cards")) {
- setPackValue("catalog_card", { flash: false });
+ if (!opts.fromDebug && false) {
+ setPackValue("ordinary", { flash: false });
}
const pack = opts.fromDebug ? "debug_explain" : $2("sa_pack")?.value || defaultPackId();
const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral";
@@ -8246,6 +8493,16 @@ ${HELP_TEXT}`);
state.busy = true;
state.llmParked = false;
setInterruptVisible(true);
+ if (!isContinuationTurn(opts) && !opts.fromAskHop) {
+ activityBegin(opts.fromDebug ? "Debug" : "\u0425\u043E\u0434 Assistent");
+ activityStep("think", { kind: "think", label: "\u0414\u0443\u043C\u0430\u044E\u2026", status: "running" });
+ } else if (opts.fromAskHop) {
+ activityStep("think", { kind: "think", label: "\u041E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0441 \u0434\u0435\u0442\u0430\u043B\u044F\u043C\u0438\u2026", status: "running" });
+ } else if (opts.fromVisionHop) {
+ activityStep("look", { kind: "look", label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u2026", status: "running" });
+ } else if (opts.fromPromptEnRetry) {
+ activityStep("prep", { kind: "prep", label: "\u0414\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u044E EN-\u043F\u0440\u043E\u043C\u043F\u0442\u2026", status: "running" });
+ }
if (state.expectColdLoad && !isContinuationTurn(opts)) {
startBusyUi("warming");
setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026");
@@ -8263,7 +8520,6 @@ ${HELP_TEXT}`);
setStatus("\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E inventory\u2026");
try {
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
- await prefetchActiveModelCards();
} catch (e) {
console.warn("Assistent inventory refresh", e);
}
@@ -8323,34 +8579,31 @@ ${HELP_TEXT}`);
persistHistory();
}
}
+ if (!opts.fromDebug && !opts.fromCompress && !opts.fromAskHop && !opts.fromVisionHop && !opts.fromAutoCritique && !isContinuationTurn(opts)) {
+ try {
+ await maybeAutoCompressBeforeSend(chatEpoch);
+ } catch (e) {
+ console.warn("Assistent auto-compress", e);
+ }
+ if (chatEpoch !== state.chatEpoch) {
+ return;
+ }
+ startBusyUi(state.expectColdLoad ? "loading" : "thinking");
+ }
const context = collectLiveContext();
context.has_vision_image = visionReadySlots().length > 0;
context.images_in_request = !!(images && images.length);
context.attached_slot_ids = attachableSlots().map((s) => s.id);
context.vision_slot_ids = visionSlots.map((s) => s.id);
context.persona = persona;
- if (opts.cardTarget) {
- context.card_target = opts.cardTarget;
- }
- if (opts.fromCards || pack === "catalog_card") {
- context.auto_apply = false;
- context.auto_generate = false;
- }
- await prefetchActiveModelCards();
if (chatEpoch !== state.chatEpoch) {
return;
}
- const refreshed = collectLiveContext();
- context.model_cards = refreshed.model_cards;
- const messages = state.history.slice(-historyMessageLimit()).map((m) => {
- let content = String(m.content || "");
- if (m.role === "assistant") {
- content = stripJsonFencesForHistory(content);
- }
- return { role: m.role, content: content.slice(0, 4e3) };
- });
- if (opts.skipAppendUser) {
- messages.push({ role: "user", content: text });
+ let messages = assembleOutgoingMessages(
+ opts.skipAppendUser ? { includePendingUser: text } : void 0
+ );
+ if (!messages.length && text) {
+ messages = [{ role: "user", content: text }];
}
if (images && messages.length) {
messages[messages.length - 1].images = images;
@@ -8378,11 +8631,19 @@ ${HELP_TEXT}`);
if (meta.system_layers && typeof meta.system_layers === "object") {
state.lastSystemLayers = meta.system_layers;
}
+ if (meta.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(meta.prompt_eval_count) || null;
+ const mem = getContextMemory();
+ if (mem.summary) {
+ setContextMemory({ ...mem, promptEvalCount: state.lastPromptEvalCount }, { persist: true });
+ }
+ }
try {
state.lastContextChars = context && JSON.stringify(context).length || 0;
} catch (e) {
state.lastContextChars = 0;
}
+ updateCtxChip();
const prose = extractPatch2(reply).prose || reply;
state.history.push({ role: "assistant", content: prose, persona, pack });
persistHistory();
@@ -8406,6 +8667,7 @@ ${HELP_TEXT}`);
state.busy = false;
setInterruptVisible(true);
}
+ updateCtxChip();
}
};
const finishErr = (msg) => {
@@ -8463,7 +8725,11 @@ ${HELP_TEXT}`);
const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || "";
const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai);
- finishOk(reply, civitai, { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, civitai, {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
+ });
}
},
0,
@@ -8490,7 +8756,11 @@ ${HELP_TEXT}`);
}
const reply = data.reply || "";
appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta);
- finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, data.civitai_results || [], {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
+ });
},
0,
(err2) => finishErr(String(err2 || err || "Chat failed"))
@@ -8512,7 +8782,11 @@ ${HELP_TEXT}`);
}
const reply = data.reply || "";
appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta);
- finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, data.civitai_results || [], {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count
+ });
},
0,
(err) => finishErr(String(err || "Chat failed"))
@@ -8680,12 +8954,10 @@ ${HELP_TEXT}`);
loadConfig(localStorage.getItem(LS_PERSONA) || "neutral", () => {
refreshModels();
refreshInventory(() => {
- renderCardsList();
renderLoraChips();
});
});
probeOllamaHealth();
- refreshWantedQueue();
}
function wire() {
if (!$2("swarm_assistent_root")) {
@@ -8714,8 +8986,36 @@ ${HELP_TEXT}`);
wireSplitter();
registerSendButton();
wireSlashInput();
- wireCardForm();
$2("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true }));
+ $2("sa_ctx_chip")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ toggleCtxPanel();
+ });
+ $2("sa_ctx_close")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ toggleCtxPanel(false);
+ });
+ $2("sa_ctx_panel")?.addEventListener("click", (e) => e.stopPropagation());
+ $2("sa_ctx_compress")?.addEventListener("click", () => compressNowFromUi());
+ $2("sa_ctx_reset")?.addEventListener("click", () => resetCompressionFromUi());
+ $2("sa_ctx_auto")?.addEventListener("change", () => {
+ COMPRESS_AUTO = !!$2("sa_ctx_auto")?.checked;
+ const settingsAuto = $2("sa_compress_auto");
+ if (settingsAuto) {
+ settingsAuto.checked = COMPRESS_AUTO;
+ }
+ if (state.config?.assistant) {
+ state.config.assistant.compress_auto = COMPRESS_AUTO;
+ }
+ setStatus(COMPRESS_AUTO ? "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E" : "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E");
+ });
+ $2("sa_compress_auto")?.addEventListener("change", () => {
+ COMPRESS_AUTO = !!$2("sa_compress_auto")?.checked;
+ const panelAuto = $2("sa_ctx_auto");
+ if (panelAuto) {
+ panelAuto.checked = COMPRESS_AUTO;
+ }
+ });
$2("sa_btn_chats")?.addEventListener("click", (e) => {
e.stopPropagation();
setChatsPanelOpen(!state.chatsPanelOpen);
@@ -8773,25 +9073,17 @@ ${HELP_TEXT}`);
}, 220);
});
$2("sa_tab_chat")?.addEventListener("click", () => setView("chat"));
- $2("sa_tab_cards")?.addEventListener("click", () => setView("cards"));
$2("sa_tab_train")?.addEventListener("click", () => setView("train"));
$2("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior"));
$2("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate"));
$2("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs"));
$2("sa_persona")?.addEventListener("change", onPersonaChanged);
$2("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona());
- $2("sa_cards_kind")?.addEventListener("change", renderCardsList);
- $2("sa_btn_cards_refresh")?.addEventListener("click", () => refreshInventory(() => renderCardsList(), { rescan: true }));
- $2("sa_btn_card_meta")?.addEventListener("click", () => fetchCardMetaLive());
- $2("sa_btn_card_generate")?.addEventListener("click", () => generateCardWithAssistent());
- $2("sa_btn_card_save")?.addEventListener("click", () => saveCurrentCard());
- $2("sa_btn_card_wanted")?.addEventListener("click", () => enqueueWantedOnly());
document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => {
btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab")));
});
$2("sa_btn_mem_refresh")?.addEventListener("click", () => {
refreshMemoryList();
- refreshWantedQueue();
});
$2("sa_mem_kind")?.addEventListener("change", renderMemoryList);
$2("sa_mem_scope")?.addEventListener("change", renderMemoryList);
@@ -8898,7 +9190,6 @@ ${HELP_TEXT}`);
probeOllamaHealth();
});
$2("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => {
- renderCardsList();
renderLoraChips();
}, { rescan: true }));
$2("sa_btn_add_ref")?.addEventListener("click", () => {
@@ -8965,17 +9256,13 @@ ${HELP_TEXT}`);
closeAllMoreMenus();
clearPatchBlocksOnly();
});
- $2("sa_btn_card_to_chat")?.addEventListener("click", () => {
- if (state.cardsSelection) {
- sendCardToChat(state.cardsSelection);
- } else {
- setCardStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0432 \u0441\u043F\u0438\u0441\u043A\u0435");
- }
- });
document.addEventListener("click", () => {
if (state.chatsPanelOpen) {
setChatsPanelOpen(false);
}
+ if (state.ctxPanelOpen) {
+ toggleCtxPanel(false);
+ }
closeAllMoreMenus();
});
$2("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation());
@@ -9052,7 +9339,6 @@ ${HELP_TEXT}`);
}, 45e3);
setInterval(() => {
if (!state.busy && !state.generating) {
- refreshWantedQueue();
}
}, 12e4);
window.addEventListener("beforeunload", () => {
@@ -9781,6 +10067,9 @@ ${HELP_TEXT}`);
attachApi(window.SA);
attachPatch(window.SA);
attachPersist(window.SA);
+ attachSession(window.SA);
+ attachContext(window.SA);
+ attachActivity(window.SA);
window.SA.applyConfigPatchKeys = function(config) {
const keys = config?.patch_keys;
if (Array.isArray(keys) && keys.length) {
diff --git a/Assets/assistent.css b/Assets/assistent.css
index 858c162..8c6f635 100644
--- a/Assets/assistent.css
+++ b/Assets/assistent.css
@@ -118,15 +118,15 @@
}
.sa-chats-drawer {
- flex: 0 0 var(--sa-chats-drawer-width, 16rem);
- width: var(--sa-chats-drawer-width, 16rem);
+ flex: 0 0 var(--sa-chats-drawer-width, 17.5rem);
+ width: var(--sa-chats-drawer-width, 17.5rem);
display: flex;
flex-direction: column;
- gap: 0.35rem;
+ gap: 0.45rem;
min-height: 0;
- border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent);
- background: color-mix(in srgb, currentColor 4%, transparent);
- padding: 0.45rem 0.5rem;
+ border-left: 1px solid color-mix(in srgb, currentColor 12%, transparent);
+ background: color-mix(in srgb, #000 22%, transparent);
+ padding: 0.55rem 0.4rem 0.55rem 0.45rem;
overflow: hidden;
transition: width 0.2s ease, flex-basis 0.2s ease, opacity 0.2s ease;
}
@@ -140,12 +140,54 @@
align-items: center;
justify-content: space-between;
gap: 0.35rem;
- font-size: 0.82rem;
+ padding: 0.15rem 0.35rem 0.1rem;
+}
+
+.sa-chats-drawer-label {
+ font-size: 0.72rem;
+ font-weight: 650;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ opacity: 0.55;
}
.sa-chats-drawer-actions {
display: inline-flex;
- gap: 0.2rem;
+ gap: 0.1rem;
+}
+
+.sa-chats-icon-btn {
+ appearance: none;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ width: 1.65rem;
+ height: 1.65rem;
+ border-radius: 0.4rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0.55;
+}
+
+.sa-chats-icon-btn:hover {
+ opacity: 1;
+ background: color-mix(in srgb, currentColor 10%, transparent);
+}
+
+.sa-chats-search-wrap {
+ position: relative;
+ margin: 0 0.2rem;
+}
+
+.sa-chats-search-ico {
+ position: absolute;
+ left: 0.55rem;
+ top: 50%;
+ transform: translateY(-50%);
+ opacity: 0.4;
+ pointer-events: none;
}
.sa-layout {
@@ -555,8 +597,8 @@
box-shadow: none;
border-radius: 0;
border: none;
- border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent);
- background: color-mix(in srgb, currentColor 4%, transparent);
+ border-left: 1px solid color-mix(in srgb, currentColor 12%, transparent);
+ background: color-mix(in srgb, #000 22%, transparent);
}
.sa-chats-panel-head {
@@ -570,51 +612,67 @@
appearance: none;
width: 100%;
box-sizing: border-box;
- border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
- background: color-mix(in srgb, #000 28%, transparent);
+ border: 1px solid transparent;
+ background: color-mix(in srgb, currentColor 7%, transparent);
color: inherit;
- border-radius: 0.35rem;
- padding: 0.35rem 0.5rem;
+ border-radius: 0.5rem;
+ padding: 0.42rem 0.55rem 0.42rem 1.7rem;
font: inherit;
- font-size: 0.82rem;
+ font-size: 0.8rem;
}
.sa-chats-search:focus {
- outline: 1px solid color-mix(in srgb, #6cf 55%, currentColor);
+ outline: none;
+ border-color: color-mix(in srgb, currentColor 22%, transparent);
+ background: color-mix(in srgb, currentColor 10%, transparent);
}
.sa-chats-panel-hint {
- font-size: 0.72rem;
- opacity: 0.65;
- line-height: 1.35;
+ display: none;
}
.sa-chats-list {
overflow: auto;
display: flex;
flex-direction: column;
- gap: 0.25rem;
+ gap: 0.1rem;
min-height: 0;
+ padding: 0.15rem 0.15rem 0.4rem;
+ flex: 1;
}
.sa-chats-empty {
- font-size: 0.82rem;
- opacity: 0.65;
- padding: 0.5rem 0.25rem;
+ font-size: 0.8rem;
+ opacity: 0.5;
+ padding: 0.85rem 0.55rem;
+ line-height: 1.4;
}
.sa-chat-row {
display: flex;
- align-items: stretch;
- gap: 0.2rem;
- border-radius: 0.4rem;
- border: 1px solid transparent;
- background: color-mix(in srgb, currentColor 5%, transparent);
+ align-items: center;
+ gap: 0;
+ border-radius: 0.5rem;
+ border: 0;
+ background: transparent;
+ position: relative;
}
.sa-chat-row-active {
- border-color: color-mix(in srgb, #6cf 45%, currentColor);
- background: color-mix(in srgb, #6cf 12%, transparent);
+ background: color-mix(in srgb, currentColor 11%, transparent);
+}
+
+.sa-chat-row-active::before {
+ content: '';
+ position: absolute;
+ left: 0.35rem;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 0.35rem;
+ height: 0.35rem;
+ border-radius: 50%;
+ background: #f59e0b;
+ box-shadow: 0 0 0 2px color-mix(in srgb, #f59e0b 25%, transparent);
}
.sa-chat-row-main {
@@ -625,27 +683,60 @@
text-align: left;
flex: 1;
min-width: 0;
- padding: 0.4rem 0.5rem;
+ padding: 0.48rem 0.45rem 0.48rem 0.55rem;
cursor: pointer;
- display: flex;
- flex-direction: column;
- gap: 0.12rem;
+ display: grid;
+ grid-template-columns: 1.1rem 1fr auto;
+ align-items: center;
+ gap: 0.45rem;
+ border-radius: 0.5rem;
+}
+
+.sa-chat-row-active .sa-chat-row-main {
+ padding-left: 1.05rem;
+}
+
+.sa-chat-row-main:hover {
+ background: color-mix(in srgb, currentColor 7%, transparent);
+}
+
+.sa-chat-row-active .sa-chat-row-main:hover {
+ background: transparent;
+}
+
+.sa-chat-row-ico {
+ width: 1.05rem;
+ height: 1.05rem;
+ opacity: 0.45;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.sa-chat-row-active .sa-chat-row-ico {
+ opacity: 0.75;
}
.sa-chat-row-title {
- font-size: 0.85rem;
- font-weight: 600;
+ font-size: 0.84rem;
+ font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ min-width: 0;
}
-.sa-chat-row-meta {
+.sa-chat-row-active .sa-chat-row-title {
+ font-weight: 600;
+}
+
+.sa-chat-row-when {
font-size: 0.72rem;
- opacity: 0.7;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ opacity: 0.42;
+ font-variant-numeric: tabular-nums;
+ flex-shrink: 0;
+ padding-right: 0.15rem;
}
.sa-chat-row-del {
@@ -653,16 +744,28 @@
border: 0;
background: transparent;
color: inherit;
- opacity: 0.55;
+ opacity: 0;
cursor: pointer;
- padding: 0 0.55rem;
- font-size: 1.1rem;
+ width: 1.55rem;
+ height: 1.55rem;
+ border-radius: 0.35rem;
+ font-size: 0.95rem;
line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-right: 0.15rem;
+}
+
+.sa-chat-row:hover .sa-chat-row-del {
+ opacity: 0.45;
}
.sa-chat-row-del:hover {
- opacity: 1;
+ opacity: 1 !important;
color: #f66;
+ background: color-mix(in srgb, #f66 12%, transparent);
}
.sa-live-dot {
@@ -1213,6 +1316,180 @@
border-color: color-mix(in srgb, #f2777a 45%, transparent);
}
+.sa-ctx-wrap {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.sa-ctx-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.72rem;
+ font-variant-numeric: tabular-nums;
+ padding: 0.15rem 0.5rem;
+ border-radius: 0.75rem;
+ border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
+ background: color-mix(in srgb, currentColor 6%, transparent);
+ cursor: pointer;
+ white-space: nowrap;
+ color: inherit;
+ line-height: 1.2;
+}
+
+.sa-ctx-chip:hover {
+ border-color: color-mix(in srgb, currentColor 45%, transparent);
+}
+
+.sa-ctx-dot {
+ width: 0.4rem;
+ height: 0.4rem;
+ border-radius: 50%;
+ background: color-mix(in srgb, #6ea8fe 80%, currentColor);
+ flex-shrink: 0;
+}
+
+.sa-ctx-ok {
+ border-color: color-mix(in srgb, #6ee7a8 35%, transparent);
+}
+
+.sa-ctx-warn {
+ color: color-mix(in srgb, #e3b341 85%, currentColor);
+ border-color: color-mix(in srgb, #e3b341 45%, transparent);
+}
+
+.sa-ctx-hot {
+ color: color-mix(in srgb, #f2777a 85%, currentColor);
+ border-color: color-mix(in srgb, #f2777a 50%, transparent);
+}
+
+.sa-ctx-compressing {
+ color: color-mix(in srgb, #6ea8fe 85%, currentColor);
+ border-color: color-mix(in srgb, #6ea8fe 50%, transparent);
+}
+
+.sa-ctx-panel {
+ position: absolute;
+ top: calc(100% + 0.35rem);
+ right: 0;
+ z-index: 40;
+ width: min(22rem, 78vw);
+ padding: 0.65rem 0.75rem 0.75rem;
+ border-radius: 0.55rem;
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
+ background: color-mix(in srgb, #1a1d24 92%, transparent);
+ box-shadow: 0 0.6rem 1.4rem color-mix(in srgb, #000 45%, transparent);
+ display: flex;
+ flex-direction: column;
+ gap: 0.45rem;
+}
+
+.sa-ctx-panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.4rem;
+ font-size: 0.85rem;
+}
+
+.sa-ctx-bar {
+ height: 0.35rem;
+ border-radius: 0.25rem;
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ overflow: hidden;
+}
+
+.sa-ctx-bar-fill {
+ height: 100%;
+ width: 0%;
+ background: color-mix(in srgb, #6ee7a8 70%, currentColor);
+ transition: width 0.2s ease;
+}
+
+.sa-ctx-bar-fill[data-level="warn"] {
+ background: color-mix(in srgb, #e3b341 75%, currentColor);
+}
+
+.sa-ctx-bar-fill[data-level="hot"] {
+ background: color-mix(in srgb, #f2777a 75%, currentColor);
+}
+
+.sa-ctx-panel-body {
+ font-size: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ max-height: 14rem;
+ overflow: auto;
+}
+
+.sa-ctx-layer {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.5rem;
+ opacity: 0.85;
+}
+
+.sa-ctx-layers-label,
+.sa-ctx-meta,
+.sa-ctx-see {
+ opacity: 0.65;
+ font-size: 0.7rem;
+ margin-top: 0.15rem;
+}
+
+.sa-ctx-summary {
+ margin: 0.2rem 0 0;
+ padding: 0.4rem 0.45rem;
+ font-size: 0.7rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 7rem;
+ overflow: auto;
+ border-radius: 0.35rem;
+ background: color-mix(in srgb, currentColor 8%, transparent);
+ border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
+}
+
+.sa-ctx-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+}
+
+.sa-ctx-auto {
+ font-size: 0.75rem;
+}
+
+.sa-msg-compress {
+ margin: 0.35rem 0.55rem;
+ padding: 0.35rem 0.55rem;
+ border-radius: 0.45rem;
+ border: 1px dashed color-mix(in srgb, #6ea8fe 40%, transparent);
+ background: color-mix(in srgb, #6ea8fe 8%, transparent);
+ font-size: 0.8rem;
+}
+
+.sa-msg-compress > summary {
+ cursor: pointer;
+ font-weight: 600;
+}
+
+.sa-msg-compress-body {
+ margin-top: 0.4rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+}
+
+.sa-msg-compress-row {
+ opacity: 0.8;
+ font-size: 0.72rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
.sa-card-row-wanted {
border-color: color-mix(in srgb, #e3b341 40%, transparent);
}
@@ -2481,3 +2758,150 @@
pointer-events: none;
opacity: 0.45;
}
+
+/* Turn activity timeline (model commands + pipeline) */
+.sa-activity {
+ align-self: stretch;
+ margin: 0.35rem 0 0.55rem;
+ border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
+ border-radius: 0.65rem;
+ background:
+ linear-gradient(180deg,
+ color-mix(in srgb, currentColor 7%, transparent),
+ color-mix(in srgb, currentColor 3%, transparent));
+ overflow: hidden;
+ box-shadow: 0 1px 0 color-mix(in srgb, #fff 4%, transparent) inset;
+}
+
+.sa-activity-live {
+ border-color: color-mix(in srgb, #6af 35%, transparent);
+}
+
+.sa-activity-done {
+ opacity: 0.92;
+}
+
+.sa-activity-head {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ width: 100%;
+ padding: 0.55rem 0.75rem;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.sa-activity-head:hover {
+ background: color-mix(in srgb, currentColor 5%, transparent);
+}
+
+.sa-activity-spin {
+ width: 0.7rem;
+ height: 0.7rem;
+ border-radius: 50%;
+ border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
+ border-top-color: color-mix(in srgb, #8cf 90%, currentColor);
+ flex-shrink: 0;
+}
+
+.sa-activity-live .sa-activity-spin {
+ animation: sa-spin 0.7s linear infinite;
+}
+
+.sa-activity-done .sa-activity-spin {
+ border-color: color-mix(in srgb, #6c6 55%, transparent);
+ border-top-color: #6c6;
+ animation: none;
+ background: color-mix(in srgb, #6c6 35%, transparent);
+}
+
+.sa-activity-title {
+ flex: 1;
+ min-width: 0;
+ font-size: 0.9rem;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sa-activity-chev {
+ opacity: 0.45;
+ font-size: 0.75rem;
+ transition: transform 0.15s ease;
+}
+
+.sa-activity-collapsed .sa-activity-chev {
+ transform: rotate(-90deg);
+}
+
+.sa-activity-collapsed .sa-activity-steps {
+ display: none;
+}
+
+.sa-activity-steps {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ padding: 0 0.55rem 0.6rem 0.75rem;
+}
+
+.sa-activity-step {
+ display: grid;
+ grid-template-columns: 1.1rem 1fr;
+ gap: 0.45rem;
+ align-items: start;
+ padding: 0.28rem 0.35rem;
+ border-radius: 0.4rem;
+}
+
+.sa-activity-step.sa-activity-running {
+ background: color-mix(in srgb, #6af 10%, transparent);
+}
+
+.sa-activity-step.sa-activity-done {
+ opacity: 0.85;
+}
+
+.sa-activity-step.sa-activity-skip {
+ opacity: 0.45;
+}
+
+.sa-activity-step.sa-activity-error {
+ background: color-mix(in srgb, #c44 12%, transparent);
+}
+
+.sa-activity-icon {
+ font-size: 0.78rem;
+ line-height: 1.35;
+ opacity: 0.7;
+ text-align: center;
+}
+
+.sa-activity-running .sa-activity-icon {
+ opacity: 1;
+ color: color-mix(in srgb, #8cf 80%, currentColor);
+}
+
+.sa-activity-label {
+ font-size: 0.86rem;
+ line-height: 1.35;
+}
+
+.sa-activity-detail {
+ margin-top: 0.1rem;
+ font-size: 0.78rem;
+ line-height: 1.35;
+ opacity: 0.62;
+ word-break: break-word;
+}
+
+@keyframes sa-spin {
+ to { transform: rotate(360deg); }
+}
+
diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs
index b84bc41..a6f1418 100644
--- a/AssistentChatPipeline.cs
+++ b/AssistentChatPipeline.cs
@@ -1,1262 +1,1120 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-using SwarmUI.Accounts;
-using SwarmUI.Core;
-using SwarmUI.Text2Image;
-using SwarmUI.Utils;
-
-namespace Mrleo1nid.SwarmAssistent;
-
-/// Prompt assembly, memory retrieval/writeback and the Civitai search hop loop.
-public partial class SwarmAssistentExtension
-{
- const int MaxCivitaiHopsFallback = 2;
- const int MaxToolHopsFallback = 4;
-
- static bool IsSlimDebugPack(string packName) =>
- string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase);
-
- (List messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable skillIds = null)
- {
- List ollamaMessages = [];
- StringBuilder system = new();
- JObject layers = new();
- string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
-
- void AddLayer(string name, string block)
- {
- if (string.IsNullOrWhiteSpace(block))
- {
- return;
- }
- if (system.Length > 0)
- {
- system.AppendLine();
- }
- int before = system.Length;
- system.AppendLine(block.TrimEnd());
- layers[name] = system.Length - before;
- }
-
- if (includeBase)
- {
- AddLayer("core", Config.LoadCorePrompt(pid));
- }
-
- bool slimDebug = IsSlimDebugPack(packName);
-
- if (Memory is not null && !slimDebug)
- {
- try
- {
- JObject asst = Config.LoadAssistant(pid);
- double weight = asst["user_prefs_weight"]?.Value() ?? 1.0;
- int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16;
- AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs));
- }
- catch (Exception ex)
- {
- Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}");
- }
- }
-
- JObject exact = Config.LoadExactForPrompt(pid);
- if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson))
- {
- exact.Remove("aspect_table");
- }
- if (exact is not null && exact.Count > 0)
- {
- AddLayer("exact",
- "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n"
- + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```");
- }
-
- if (!slimDebug)
- {
- StringBuilder skillsBlock = new();
- foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null))
- {
- string skillText = Config.LoadSkillPrompt(pid, skillId);
- if (!string.IsNullOrWhiteSpace(skillText))
- {
- if (skillsBlock.Length > 0)
- {
- skillsBlock.AppendLine();
- }
- skillsBlock.AppendLine($"## Skill: {skillId}");
- skillsBlock.AppendLine(skillText.TrimEnd());
- }
- }
- AddLayer("skills", skillsBlock.ToString());
- AddLayer("identity", Config.RenderIdentityBlock(pid));
- }
-
- if (!string.IsNullOrWhiteSpace(packName))
- {
- string situational = Config.LoadPackPrompt(pid, packName);
- if (!string.IsNullOrWhiteSpace(situational))
- {
- AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}");
- }
- }
- if (!string.IsNullOrWhiteSpace(contextJson))
- {
- AddLayer("live",
- "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n"
- + contextJson + "\n```");
- }
- if (!string.IsNullOrWhiteSpace(extraSystem))
- {
- AddLayer("extra", extraSystem);
- }
-
- layers["total"] = system.Length;
- if (system.Length > 0)
- {
- ollamaMessages.Add(new JObject
- {
- ["role"] = "system",
- ["content"] = system.ToString(),
- });
- }
- foreach (JToken msg in userMessages ?? [])
- {
- if (msg is not JObject mo)
- {
- continue;
- }
- JObject copy = new()
- {
- ["role"] = mo["role"]?.ToString() ?? "user",
- ["content"] = mo["content"]?.ToString() ?? "",
- };
- if (mo["images"] is JArray images && images.Count > 0)
- {
- copy["images"] = images;
- }
- ollamaMessages.Add(copy);
- }
- return (ollamaMessages, layers);
- }
-
- async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops(
- Session session,
- string root,
- string modelName,
- string packName,
- bool includeBase,
- string contextJson,
- JArray userMessages,
- Func onDelta = null,
- Func onHopStart = null,
- string personaId = null,
- JArray skillIds = null,
- string embedModel = null)
- {
- string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
- List skills = Config.ResolveEnabledSkills(pid, skillIds);
- string embed = string.IsNullOrWhiteSpace(embedModel)
- ? (Config.LoadSettings()["embed_model"]?.ToString()
- ?? Config.LoadAssistant(pid)["embed_model"]?.ToString()
- ?? "nomic-embed-text")
- : embedModel;
-
- try
- {
- await Memory.EnsureSeedAsync(root, Config, embed);
- }
- catch (Exception ex)
- {
- Logs.Debug($"Assistent memory seed: {ex.Message}");
- }
-
- bool slimDebug = IsSlimDebugPack(packName);
- JArray hits = [];
- if (!slimDebug)
- {
- string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName);
- try
- {
- AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid);
- hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt);
- hits = FilterHeardHitsIfDisabled(hits);
- }
- catch (Exception ex)
- {
- Logs.Debug($"Assistent memory retrieve: {ex.Message}");
- }
- }
-
- string enrichedContext = InjectMemoryHits(contextJson, hits, pid);
- if (!slimDebug)
- {
- enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName);
- }
- (List messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
- int systemChars = systemLayers["total"]?.Value()
- ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length
- ?? 0;
- JArray civitaiResults = [];
- string reply = "";
- JObject lastRaw = null;
- int maxHops = slimDebug
- ? 1
- : Math.Max(CfgInt("max_civitai_hops", MaxCivitaiHopsFallback), CfgInt("max_tool_hops", MaxToolHopsFallback));
- HashSet hopDone = new(StringComparer.OrdinalIgnoreCase);
- var chain = Config.PersonaExtendsChain(pid);
- for (int hop = 0; hop < maxHops; hop++)
- {
- if (onHopStart is not null)
- {
- await onHopStart(hop);
- }
- (reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
- if (slimDebug)
- {
- break;
- }
- JObject patch = TryParsePatch(reply);
- await ApplyMemoryActions(root, patch, embed, pid);
- ApplyUserPrefActions(patch, pid);
- ApplyPersonaActions(patch, ref pid);
- if (hop + 1 >= maxHops)
- {
- break;
- }
- string follow = null;
- JArray civitaiHop = null;
- HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase);
- while (true)
- {
- string tool = NextToolHop(patch, hopSkip);
- if (string.IsNullOrWhiteSpace(tool))
- {
- follow = null;
- break;
- }
- (follow, civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone);
- if (follow is not null)
- {
- break;
- }
- hopSkip.Add(tool);
- }
- if (follow is null)
- {
- break;
- }
- if (civitaiHop is { Count: > 0 })
- {
- civitaiResults = civitaiHop;
- }
- // Re-feed only the parsed patch JSON (not full prose) to save hop tokens.
- string assistantContent = patch is not null
- ? patch.ToString(Newtonsoft.Json.Formatting.None)
- : reply;
- messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent });
- messages.Add(new JObject { ["role"] = "user", ["content"] = follow });
- }
- return (reply, lastRaw, civitaiResults, systemChars, systemLayers);
- }
-
- static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null)
- {
- StringBuilder sb = new();
- if (!string.IsNullOrWhiteSpace(packName))
- {
- sb.Append(packName).Append(' ');
- }
- if (!string.IsNullOrWhiteSpace(contextJson))
- {
- try
- {
- JObject ctx = JObject.Parse(contextJson);
- string ckpt = ctx["checkpoint"]?.ToString() ?? ctx["current_model"]?.ToString();
- if (!string.IsNullOrWhiteSpace(ckpt))
- {
- sb.Append(ckpt).Append(' ');
- }
- if (ctx["selected_loras"] is JArray selLoras)
- {
- foreach (JToken t in selLoras.Take(12))
- {
- string n = t?["name"]?.ToString() ?? t?.ToString();
- if (!string.IsNullOrWhiteSpace(n))
- {
- sb.Append(n).Append(' ');
- }
- }
- }
- else if (ctx["enabled_loras"] is JArray en)
- {
- foreach (JToken t in en.Take(12))
- {
- string n = t?["name"]?.ToString() ?? t?.ToString();
- if (!string.IsNullOrWhiteSpace(n))
- {
- sb.Append(n).Append(' ');
- }
- }
- }
- if (ctx["krea_profile"] != null)
- {
- sb.Append("krea ").Append(ctx["krea_profile"]).Append(' ');
- }
- string aspect = ctx["aspect"]?.ToString();
- if (!string.IsNullOrWhiteSpace(aspect))
- {
- sb.Append(aspect).Append(' ');
- }
- string prompt = ctx["prompt"]?.ToString();
- if (!string.IsNullOrWhiteSpace(prompt))
- {
- sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' ');
- }
- }
- catch
- {
- // ignore
- }
- }
- foreach (JToken msg in (userMessages ?? []).Reverse().Take(3))
- {
- if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase))
- {
- string c = mo["content"]?.ToString() ?? "";
- sb.Append(c.Length > 500 ? c[..500] : c).Append(' ');
- }
- }
- string q = CollapseWs(sb.ToString());
- return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q;
- }
-
- AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid)
- {
- JObject a = Config.LoadAssistant(pid) ?? new JObject();
- JObject agent = Config.LoadTrainingAgent();
- AssistentMemory.RetrieveOptions opt = new()
- {
- TopK = a["memory_top_k"]?.Value() ?? 8,
- MinScore = a["memory_min_score"]?.Value() ?? 0.32f,
- ApplyQuotas = true,
- };
- Dictionary quotas = AssistentMemory.CopyDefaultQuotas();
- if (a["memory_quotas"] is JObject qOverrides)
- {
- foreach (JProperty p in qOverrides.Properties())
- {
- quotas[p.Name] = p.Value?.Value() ?? 2;
- }
- }
- if (agent["enabled"]?.Value() != false)
- {
- quotas["heard"] = agent["heard_quota"]?.Value() ?? 3;
- }
- else
- {
- quotas.Remove("heard");
- }
- opt.Quotas = quotas;
- return opt;
- }
-
- JArray FilterHeardHitsIfDisabled(JArray hits)
- {
- if (Config.LoadTrainingAgent()["enabled"]?.Value() != false)
- {
- return hits;
- }
- JArray filtered = [];
- foreach (JToken t in hits ?? [])
- {
- if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
- filtered.Add(t);
- }
- return filtered;
- }
-
- async Task<(string follow, JArray civitai)> RunToolHop(
- Session session,
- string root,
- string embed,
- string pid,
- IEnumerable chain,
- JObject patch,
- string tool,
- HashSet hopDone)
- {
- if (tool == "memory_get")
- {
- JArray got = [];
- foreach (JToken t in patch["memories"] as JArray ?? [])
- {
- if (t is not JObject mo)
- {
- continue;
- }
- string kind = mo["kind"]?.ToString() ?? "note";
- string key = mo["key"]?.ToString() ?? "";
- if (string.IsNullOrWhiteSpace(key))
- {
- continue;
- }
- string sig = $"get:{kind}:{key}";
- if (!hopDone.Add(sig))
- {
- continue;
- }
- JObject row = Memory.Get(kind, key, chain);
- got.Add(row ?? new JObject { ["kind"] = kind, ["key"] = key, ["missing"] = true });
- }
- if (got.Count == 0)
- {
- return (null, null);
- }
- return (
- "memory_get results (JSON). Use these facts; omit memory_get unless you need a different key.\n```json\n"
- + got.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- null);
- }
- if (tool == "memory_search")
- {
- string q = ExtractMemoryQuery(patch);
- if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("search:" + q))
- {
- return (null, null);
- }
- string kind = patch["memory_kind"]?.ToString();
- int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value() ?? 8;
- JArray rows = await Memory.SearchAsync(root, q, kind, topK, embed, chain);
- return (
- "memory_search results (JSON, hybrid FTS+vector). Omit memory_search unless you need a different query.\n```json\n"
- + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- null);
- }
- if (tool == "heard_search")
- {
- if (Config.LoadTrainingAgent()["enabled"]?.Value() == false)
- {
- return ("heard_search disabled in training-agent settings.", null);
- }
- string q = patch["memory_query"]?.ToString()?.Trim()
- ?? patch["search_query"]?.ToString()?.Trim()
- ?? ExtractMemoryQuery(patch);
- if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("heard:" + q))
- {
- return (null, null);
- }
- int topK = Config.LoadTrainingAgent()["heard_quota"]?.Value() ?? 3;
- JArray rows = await Memory.SearchAsync(root, q, AssistentMemory.HeardKind, topK, embed, chain);
- JArray examples = [];
- foreach (JToken t in rows)
- {
- if (t is JObject ho)
- {
- JObject ex = Memory.BuildHeardExampleFromHit(ho, chain);
- if (ex is not null)
- {
- examples.Add(ex);
- }
- }
- }
- return (
- "heard_search — curated dialogue examples the assistant learned (style/reference, not hard rules). Use tone and structure; omit heard_search unless you need more examples.\n```json\n"
- + examples.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- null);
- }
- if (tool == "lookup_tags")
- {
- string q = ExtractTagQuery(patch);
- if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("tags:" + q))
- {
- return (null, null);
- }
- int lim = Config.LoadAssistant(pid)["tag_lookup_limit"]?.Value() ?? 20;
- JArray tags = Memory.LookupTags(q, lim);
- return (
- "lookup_tags results from Danbooru csv (canonical name, aliases, post_count). Krea prompts stay natural prose — use this to check spelling/aliases, do not dump tag soup.\n```json\n"
- + tags.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- null);
- }
- if (tool == "list_inventory")
- {
- string q = patch["inventory_query"]?.ToString()?.Trim() ?? "";
- string sig = "inv:" + q.ToLowerInvariant();
- if (!hopDone.Add(sig))
- {
- return (null, null);
- }
- int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value() ?? 20;
- JArray rows = SearchInventoryForHop(q, lim);
- return (
- "list_inventory results (rich LoRA/checkpoint rows). Use exact names + listed triggers; omit list_inventory unless you need a different query.\n```json\n"
- + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- null);
- }
- if (tool == "skill_load")
- {
- List ids = [];
- if (patch["skills"] is JArray skArr)
- {
- foreach (JToken t in skArr)
- {
- string sid = AssistentConfig.SafeId(t?.ToString());
- if (!string.IsNullOrWhiteSpace(sid))
- {
- ids.Add(sid);
- }
- }
- }
- if (ids.Count == 0)
- {
- ids.Add("memory");
- }
- StringBuilder sb = new();
- foreach (string sid in ids)
- {
- string sig = "skill:" + sid;
- if (!hopDone.Add(sig))
- {
- continue;
- }
- string text = Config.LoadSkillPrompt(pid, sid);
- if (string.IsNullOrWhiteSpace(text))
- {
- continue;
- }
- sb.AppendLine($"## Skill: {sid}");
- sb.AppendLine(text);
- sb.AppendLine();
- }
- if (sb.Length == 0)
- {
- return (null, null);
- }
- return (
- "skill_load results. Follow these skill rules on the next reply; omit skill_load unless you need another skill.\n\n"
- + sb.ToString().TrimEnd(),
- null);
- }
- if (tool == "persona_read")
- {
- List shelves = [];
- if (patch["persona_shelves"] is JArray shArr)
- {
- foreach (JToken t in shArr)
- {
- string name = t?.ToString()?.Trim();
- if (!string.IsNullOrWhiteSpace(name))
- {
- shelves.Add(name);
- }
- }
- }
- else if (patch["persona_shelves"] is JObject shObj)
- {
- // Model sometimes echoes shelf objects; treat keys as names.
- foreach (JProperty p in shObj.Properties())
- {
- shelves.Add(p.Name);
- }
- }
- string sig = "persona_read:" + string.Join(",", shelves);
- if (!hopDone.Add(sig))
- {
- return (null, null);
- }
- string body = Config.RenderPersonaReadBlock(pid, shelves.Count > 0 ? shelves : null);
- if (string.IsNullOrWhiteSpace(body))
- {
- return ("persona_read: no additional lore shelves for this persona.", null);
- }
- return (
- "persona_read results (lore shelves). Use for roleplay/appearance/outfit detail; omit persona_read unless you need different shelves.\n\n"
- + body,
- null);
- }
- if (tool == "civitai")
- {
- string query = ExtractSearchQuery(patch);
- if (string.IsNullOrWhiteSpace(query))
- {
- if (!hopDone.Add("civitai:missing_query"))
- {
- return (null, null);
- }
- return (
- "search_civitai skipped: provide a short search_query (LoRA keywords only). "
- + "Never search with the whole user message. Then retry with actions:[\"search_civitai\"] + search_query, "
- + "or continue using available_loras only.",
- null);
- }
- if (!hopDone.Add("civitai:" + query))
- {
- return (null, null);
- }
- JObject search = await AssistentSearchCivitai(session, query, 8);
- if (search["error"] is not null)
- {
- return ($"Civitai search failed: {search["error"]}. Continue without download — use only available_loras from context.", null);
- }
- JArray civitaiResults = search["results"] as JArray ?? [];
- return (
- "Civitai search results (JSON). Prefer `krea_likely: true`. Do NOT download yourself — the UI shows Confirm cards. " +
- "Pick useful LoRAs from results or available_loras, emit a normal patch (prompt/loras). " +
- "Omit search_civitai from actions unless you need a different query.\n```json\n" +
- civitaiResults.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
- civitaiResults);
- }
- return (null, null);
- }
-
- /// Substring filter over current LoRA/checkpoint inventory for list_inventory hop.
- JArray SearchInventoryForHop(string query, int limit)
- {
- int lim = Math.Max(1, Math.Min(limit, 40));
- string q = (query ?? "").Trim().ToLowerInvariant();
- JArray outRows = [];
- void AddFromHandler(string setName, string kind)
- {
- if (!Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
- {
- return;
- }
- IEnumerable models = handler.Models.Values
- .OrderByDescending(LooksLikeKreaArch)
- .ThenBy(m => m.Name);
- foreach (T2IModel model in models)
- {
- if (outRows.Count >= lim)
- {
- return;
- }
- string name = model.Name ?? "";
- if (!string.IsNullOrEmpty(q))
- {
- string blob = $"{name} {model.Metadata?.UsageHint} {model.Metadata?.Description}".ToLowerInvariant();
- if (!blob.Contains(q, StringComparison.Ordinal))
- {
- continue;
- }
- }
- outRows.Add(BuildInventoryModelEntry(model, kind));
- }
- }
- AddFromHandler("LoRA", "lora");
- if (outRows.Count < lim)
- {
- AddFromHandler("Stable-Diffusion", "checkpoint");
- }
- return outRows;
- }
-
- string InjectMemoryHits(string contextJson, JArray hits, string personaId = null)
- {
- JObject ctx;
- try
- {
- ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson);
- }
- catch
- {
- ctx = new JObject { ["_raw_context"] = contextJson };
- }
-
- int hitChars = 240;
- try
- {
- string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
- hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240;
- }
- catch
- {
- hitChars = 240;
- }
- hitChars = Math.Max(80, Math.Min(hitChars, 800));
-
- string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
- IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? [];
-
- JArray clippedHits = [];
- JArray heardExamples = [];
- foreach (JToken t in hits ?? [])
- {
- if (t is not JObject ho)
- {
- continue;
- }
- if (IsExactPointerHit(ho))
- {
- continue;
- }
- if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
- {
- JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain);
- if (ex is not null)
- {
- heardExamples.Add(ex);
- }
- continue;
- }
- JObject copy = (JObject)ho.DeepClone();
- string text = copy["text"]?.ToString() ?? "";
- if (text.Length > hitChars)
- {
- copy["text"] = text[..hitChars] + "…";
- copy["truncated"] = true;
- }
- clippedHits.Add(copy);
- }
- ctx["memory_hits"] = clippedHits;
- if (heardExamples.Count > 0)
- {
- ctx["heard_examples"] = heardExamples;
- }
- else
- {
- ctx.Remove("heard_examples");
- }
- ctx.Remove("taste_profile");
- ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed
- try
- {
- JObject asst = Config?.LoadAssistant(pid) ?? new JObject();
- double weight = asst["user_prefs_weight"]?.Value() ?? 1.0;
- int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16;
- ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0;
- }
- catch
- {
- ctx["user_prefs_count"] = 0;
- }
- // Never re-inject full Exact into live context (already in system prompt).
- ctx.Remove("exact");
- if (ctx["session_exact"] is JObject se && !se.Properties().Any())
- {
- ctx.Remove("session_exact");
- }
-
- // Always normalize inventory: keep enabled + rich top-N; name-only for the rest.
- SlimAvailableLorasInContext(ctx, hits);
- DropNullOrEmpty(ctx);
- return ctx.ToString(Newtonsoft.Json.Formatting.None);
- }
-
- static bool LiveContextHasSize(string contextJson)
- {
- if (string.IsNullOrWhiteSpace(contextJson))
- {
- return false;
- }
- try
- {
- JObject ctx = JObject.Parse(contextJson);
- int? w = ctx["width"]?.Value();
- int? h = ctx["height"]?.Value();
- return w is > 0 && h is > 0;
- }
- catch
- {
- return false;
- }
- }
-
- /// Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…").
- static bool IsExactPointerHit(JObject ho)
- {
- if (ho is null)
- {
- return false;
- }
- string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant();
- if (key.StartsWith("krea2_", StringComparison.Ordinal))
- {
- return true;
- }
- string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant();
- if (string.IsNullOrWhiteSpace(text))
- {
- return false;
- }
- return text.Contains("live in exact memory", StringComparison.Ordinal)
- || text.Contains("see exact memory", StringComparison.Ordinal)
- || text.Contains("prefer exact kv", StringComparison.Ordinal)
- || text.Contains("exact.facts.", StringComparison.Ordinal)
- || text.Contains("exact memory profiles.", StringComparison.Ordinal);
- }
-
- void SlimAvailableLorasInContext(JObject ctx, JArray hits)
- {
- if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0)
- {
- return;
- }
- int richCap = 12;
- int namesCap = 24;
- try
- {
- string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
- JObject asst = Config?.LoadAssistant(pid) ?? new JObject();
- richCap = asst["inventory_prompt_rich"]?.Value() ?? 12;
- namesCap = asst["inventory_prompt_names"]?.Value() ?? 24;
- }
- catch { /* defaults */ }
- richCap = Math.Max(4, Math.Min(richCap, 40));
- namesCap = Math.Max(richCap, Math.Min(namesCap, 80));
-
- HashSet keepRich = new(StringComparer.OrdinalIgnoreCase);
- if (ctx["enabled_loras"] is JArray en)
- {
- foreach (JToken t in en)
- {
- string n = t?["name"]?.ToString() ?? t?.ToString();
- if (!string.IsNullOrWhiteSpace(n))
- {
- keepRich.Add(n);
- }
- }
- }
- if (ctx["selected_loras"] is JArray sel)
- {
- foreach (JToken t in sel)
- {
- string n = t?["name"]?.ToString() ?? t?.ToString();
- if (!string.IsNullOrWhiteSpace(n))
- {
- keepRich.Add(n);
- }
- }
- }
- foreach (JToken hit in hits ?? [])
- {
- if (string.Equals(hit?["kind"]?.ToString(), "lora", StringComparison.OrdinalIgnoreCase)
- || string.Equals(hit?["kind"]?.ToString(), "card", StringComparison.OrdinalIgnoreCase))
- {
- string k = hit?["key"]?.ToString();
- if (!string.IsNullOrWhiteSpace(k))
- {
- keepRich.Add(k);
- }
- }
- }
-
- List ordered = allLoras
- .OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0)
- .ThenByDescending(t => t?["krea_likely"]?.Value() == true ? 50 : 0)
- .ThenBy(t => t?["name"]?.ToString() ?? "", StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- JArray slim = [];
- int richCount = 0;
- foreach (JToken t in ordered)
- {
- if (slim.Count >= namesCap)
- {
- break;
- }
- string n = t?["name"]?.ToString();
- if (string.IsNullOrWhiteSpace(n))
- {
- continue;
- }
- bool wantRich = keepRich.Contains(n) || (t?["krea_likely"]?.Value() == true && richCount < richCap);
- if (wantRich)
- {
- JObject rich = EnrichLoraRowForPrompt(t as JObject ?? new JObject { ["name"] = n });
- slim.Add(rich);
- if (!keepRich.Contains(n))
- {
- richCount++;
- }
- }
- else
- {
- JObject nameOnly = new() { ["name"] = n };
- if (t?["krea_likely"]?.Value() == true)
- {
- nameOnly["krea_likely"] = true;
- }
- slim.Add(nameOnly);
- }
- }
- ctx["available_loras"] = slim;
- if (allLoras.Count > slim.Count)
- {
- ctx["available_loras_truncated"] = true;
- // Prefer client total if already set (full disk inventory count).
- if (ctx["available_loras_total"] is null)
- {
- ctx["available_loras_total"] = allLoras.Count;
- }
- }
- }
-
- /// Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only.
- JObject EnrichLoraRowForPrompt(JObject row)
- {
- if (row is null)
- {
- return new JObject();
- }
- JObject outRow = (JObject)row.DeepClone();
- string name = outRow["name"]?.ToString();
- bool needsTriggers = string.IsNullOrWhiteSpace(outRow["trigger_phrase"]?.ToString())
- && (outRow["triggers"] is not JArray tr || tr.Count == 0);
- bool needsBlurb = string.IsNullOrWhiteSpace(outRow["blurb"]?.ToString());
- if (!needsTriggers && !needsBlurb)
- {
- return outRow;
- }
- JObject fromDisk = FindInventoryLoraByName(name);
- if (fromDisk is null)
- {
- return outRow;
- }
- if (needsTriggers)
- {
- if (!string.IsNullOrWhiteSpace(fromDisk["trigger_phrase"]?.ToString()))
- {
- outRow["trigger_phrase"] = fromDisk["trigger_phrase"];
- }
- if (fromDisk["triggers"] is JArray ft && ft.Count > 0)
- {
- outRow["triggers"] = ft.DeepClone();
- }
- }
- if (needsBlurb && !string.IsNullOrWhiteSpace(fromDisk["blurb"]?.ToString()))
- {
- outRow["blurb"] = fromDisk["blurb"];
- }
- if (outRow["default_weight"] is null && fromDisk["default_weight"] is not null)
- {
- outRow["default_weight"] = fromDisk["default_weight"];
- }
- if (fromDisk["krea_likely"]?.Value() == true)
- {
- outRow["krea_likely"] = true;
- }
- if (fromDisk["has_card"]?.Value() == true)
- {
- outRow["has_card"] = true;
- }
- return outRow;
- }
-
- JObject FindInventoryLoraByName(string name)
- {
- if (string.IsNullOrWhiteSpace(name) || !Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
- {
- return null;
- }
- T2IModel model = handler.Models.Values.FirstOrDefault(m =>
- string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)
- || string.Equals(Path.GetFileNameWithoutExtension(m.Name), Path.GetFileNameWithoutExtension(name), StringComparison.OrdinalIgnoreCase)
- || (m.Name?.EndsWith("/" + name, StringComparison.OrdinalIgnoreCase) ?? false));
- return model is null ? null : BuildInventoryModelEntry(model, "lora");
- }
-
- static void DropNullOrEmpty(JObject ctx)
- {
- List remove = [];
- foreach (JProperty p in ctx.Properties())
- {
- if (p.Value is null || p.Value.Type == JTokenType.Null)
- {
- remove.Add(p.Name);
- }
- else if (p.Value is JObject jo && !jo.Properties().Any())
- {
- remove.Add(p.Name);
- }
- else if (p.Value is JArray ja && ja.Count == 0 && p.Name is not "memory_hits")
- {
- remove.Add(p.Name);
- }
- }
- foreach (string k in remove)
- {
- ctx.Remove(k);
- }
- }
-
- string EnrichPersonaContext(string contextJson, string personaId, string packName)
- {
- JObject ctx;
- try
- {
- ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson);
- }
- catch
- {
- ctx = new JObject { ["_raw_context"] = contextJson };
- }
- string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
- ctx["persona_source"] = Config.PersonaSource(pid);
- JObject schema = Config.LoadControlsSchema(pid);
- JObject values = Config.LoadControlValues(pid);
- bool authorPack = string.Equals(packName, "author_persona", StringComparison.OrdinalIgnoreCase)
- || string.Equals(packName, "persona", StringComparison.OrdinalIgnoreCase);
- // Values only outside author pack (schema is fat). Author pack gets full schema.
- if (values.Properties().Any())
- {
- if (authorPack && schema.Properties().Any())
- {
- ctx["persona_controls"] = new JObject
- {
- ["schema"] = schema,
- ["values"] = values,
- };
- }
- else
- {
- ctx["persona_controls"] = new JObject { ["values"] = values };
- }
- }
- JArray catalog = [];
- if (authorPack)
- {
- foreach (var p in Config.ListPersonaCatalog())
- {
- catalog.Add(new JObject
- {
- ["id"] = p.id,
- ["title"] = p.title,
- });
- }
- ctx["personas"] = catalog;
- JObject shelves = Config.LoadIdentityParts(pid);
- shelves.Remove("extra");
- ctx["persona_shelves"] = shelves;
- if (schema.Properties().Any())
- {
- ctx["persona_controls_schema"] = schema;
- }
- }
- DropNullOrEmpty(ctx);
- return ctx.ToString(Newtonsoft.Json.Formatting.None);
- }
-
- static string MemoryWritePersona(JObject mo, string currentPersonaId)
- {
- string scope = (mo?["scope"]?.ToString() ?? "").Trim().ToLowerInvariant();
- if (scope is "shared" or "common" or "global")
- {
- return AssistentMemory.SharedPersona;
- }
- // Personal only — never let the model write into another personality's store.
- return AssistentConfig.SafeId(currentPersonaId) ?? AssistentMemory.SharedPersona;
- }
-
- /// Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch.
- void ApplyPersonaActions(JObject patch, ref string personaId)
- {
- if (patch is null || Config is null)
- {
- return;
- }
- // Never honor delete from the model.
- bool wantClone = false, wantWrite = false;
- if (patch["actions"] is JArray acts)
- {
- foreach (JToken a in acts)
- {
- string s = a?.ToString() ?? "";
- if (string.Equals(s, "persona_clone", StringComparison.OrdinalIgnoreCase))
- {
- wantClone = true;
- }
- if (string.Equals(s, "persona_write", StringComparison.OrdinalIgnoreCase))
- {
- wantWrite = true;
- }
- }
- }
- if (patch["persona_clone"] is JObject)
- {
- wantClone = true;
- }
- // persona_shelves as object of content = write; as array of names = persona_read hop (ignore here).
- if (patch["persona_shelves"] is JObject && !ActionsContain(patch, "persona_read"))
- {
- wantWrite = true;
- }
- try
- {
- if (wantClone && patch["persona_clone"] is JObject clone)
- {
- string from = AssistentConfig.SafeId(clone["from"]?.ToString()) ?? personaId;
- string to = AssistentConfig.SafeId(clone["to"]?.ToString());
- string title = clone["title"]?.ToString();
- bool overwrite = clone["overwrite"]?.Value() == true;
- if (to is not null)
- {
- Config.ClonePersonaToOverlay(from, to, title, overwrite);
- personaId = to;
- patch["_persona_cloned"] = to;
- }
- }
- if (wantWrite && patch["persona_shelves"] is JObject shelves)
- {
- string target = AssistentConfig.SafeId(patch["persona"]?.ToString())
- ?? AssistentConfig.SafeId(patch["persona_clone"]?["to"]?.ToString())
- ?? personaId;
- if (target is not null)
- {
- Config.SavePersonaShelves(target, shelves);
- patch["_persona_written"] = target;
- }
- }
- // Control values from model patch (Exact). Generate patches never touch sliders.
- if (patch["controls"] is JObject ctrlVals)
- {
- if (AssistentConfig.PatchLooksLikeGeneration(patch))
- {
- patch.Remove("controls");
- }
- else
- {
- string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId;
- JObject schema = Config.LoadControlsSchema(ctrlPid);
- JObject current = Config.LoadControlValues(ctrlPid);
- JObject filtered = AssistentConfig.FilterEchoedControlDefaults(
- schema, current, ctrlVals, patchLooksLikeGen: false);
- if (filtered.Count > 0)
- {
- Config.SaveControlValues(ctrlPid, filtered);
- patch["controls"] = filtered;
- patch["_controls_saved"] = true;
- }
- else
- {
- patch.Remove("controls");
- }
- }
- }
- }
- catch (Exception ex)
- {
- Logs.Warning($"Assistent persona actions: {ex.Message}");
- patch["_persona_error"] = ex.Message;
- }
- }
-
- async Task ApplyMemoryActions(string root, JObject patch, string embedModel, string personaId)
- {
- if (patch is null || Memory is null)
- {
- return;
- }
- bool upsert = false, forget = false;
- if (patch["actions"] is JArray acts)
- {
- foreach (JToken a in acts)
- {
- string s = a?.ToString() ?? "";
- if (string.Equals(s, "memory_upsert", StringComparison.OrdinalIgnoreCase))
- {
- upsert = true;
- }
- if (string.Equals(s, "memory_forget", StringComparison.OrdinalIgnoreCase))
- {
- forget = true;
- }
- }
- }
- JArray memories = patch["memories"] as JArray;
- if (memories is null || memories.Count == 0)
- {
- return;
- }
- foreach (JToken t in memories)
- {
- if (t is not JObject mo)
- {
- continue;
- }
- string kind = mo["kind"]?.ToString() ?? "note";
- string key = mo["key"]?.ToString() ?? "";
- string text = mo["text"]?.ToString() ?? "";
- string target = MemoryWritePersona(mo, personaId);
- try
- {
- if (forget && string.IsNullOrWhiteSpace(text))
- {
- Memory.Forget(kind, key, persona: target);
- }
- else if (upsert || !string.IsNullOrWhiteSpace(text))
- {
- await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel, target);
- }
- }
- catch (Exception ex)
- {
- Logs.Debug($"ApplyMemoryActions: {ex.Message}");
- }
- }
- }
-
- void ApplyUserPrefActions(JObject patch, string personaId)
- {
- if (patch is null || Memory is null)
- {
- return;
- }
- bool upsert = false, forget = false;
- if (patch["actions"] is JArray acts)
- {
- foreach (JToken a in acts)
- {
- string s = a?.ToString() ?? "";
- if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase))
- {
- upsert = true;
- }
- if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase))
- {
- forget = true;
- }
- }
- }
- JArray prefs = patch["user_prefs"] as JArray;
- if (prefs is null || prefs.Count == 0)
- {
- return;
- }
- string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
- foreach (JToken t in prefs)
- {
- if (t is not JObject mo)
- {
- continue;
- }
- string key = mo["key"]?.ToString() ?? "";
- string text = mo["text"]?.ToString() ?? "";
- string scope = mo["scope"]?.ToString() ?? "global";
- bool pinned = mo["pinned"]?.Value() == true;
- try
- {
- if (forget && string.IsNullOrWhiteSpace(text))
- {
- Memory.ForgetUserPref(key, scope, pid);
- }
- else if (upsert || !string.IsNullOrWhiteSpace(text))
- {
- Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned);
- }
- }
- catch (Exception ex)
- {
- Logs.Debug($"ApplyUserPrefActions: {ex.Message}");
- }
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using SwarmUI.Accounts;
+using SwarmUI.Core;
+using SwarmUI.Text2Image;
+using SwarmUI.Utils;
+
+namespace Mrleo1nid.SwarmAssistent;
+
+/// Prompt assembly, memory retrieval, and ask-only server hop loop (settings / inventory).
+public partial class SwarmAssistentExtension
+{
+ const int MaxToolHopsFallback = 2;
+
+ static bool IsSlimUtilityPack(string packName) =>
+ string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(packName, "compress_history", StringComparison.OrdinalIgnoreCase);
+
+ [Obsolete("Use IsSlimUtilityPack")]
+ static bool IsSlimDebugPack(string packName) => IsSlimUtilityPack(packName);
+
+ (List messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable skillIds = null)
+ {
+ List ollamaMessages = [];
+ StringBuilder system = new();
+ JObject layers = new();
+ string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
+
+ void AddLayer(string name, string block)
+ {
+ if (string.IsNullOrWhiteSpace(block))
+ {
+ return;
+ }
+ if (system.Length > 0)
+ {
+ system.AppendLine();
+ }
+ int before = system.Length;
+ system.AppendLine(block.TrimEnd());
+ layers[name] = system.Length - before;
+ }
+
+ if (includeBase)
+ {
+ AddLayer("core", Config.LoadCorePrompt(pid));
+ }
+
+ bool slimUtility = IsSlimUtilityPack(packName);
+
+ if (Memory is not null && !slimUtility)
+ {
+ try
+ {
+ JObject asst = Config.LoadAssistant(pid);
+ double weight = asst["user_prefs_weight"]?.Value() ?? 1.0;
+ int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16;
+ AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs));
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}");
+ }
+ }
+
+ JObject exact = Config.LoadExactForPrompt(pid);
+ if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson))
+ {
+ exact.Remove("aspect_table");
+ }
+ if (exact is not null && exact.Count > 0)
+ {
+ AddLayer("exact",
+ "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n"
+ + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```");
+ }
+
+ if (!slimUtility)
+ {
+ StringBuilder skillsBlock = new();
+ foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null))
+ {
+ string skillText = Config.LoadSkillPrompt(pid, skillId);
+ if (!string.IsNullOrWhiteSpace(skillText))
+ {
+ if (skillsBlock.Length > 0)
+ {
+ skillsBlock.AppendLine();
+ }
+ skillsBlock.AppendLine($"## Skill: {skillId}");
+ skillsBlock.AppendLine(skillText.TrimEnd());
+ }
+ }
+ AddLayer("skills", skillsBlock.ToString());
+ AddLayer("identity", Config.RenderIdentityBlock(pid));
+ }
+
+ if (!string.IsNullOrWhiteSpace(packName))
+ {
+ string situational = Config.LoadPackPrompt(pid, packName);
+ if (!string.IsNullOrWhiteSpace(situational))
+ {
+ AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}");
+ }
+ }
+ if (!string.IsNullOrWhiteSpace(contextJson))
+ {
+ AddLayer("live",
+ "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n"
+ + contextJson + "\n```");
+ }
+ if (!string.IsNullOrWhiteSpace(extraSystem))
+ {
+ AddLayer("extra", extraSystem);
+ }
+
+ layers["total"] = system.Length;
+ if (system.Length > 0)
+ {
+ ollamaMessages.Add(new JObject
+ {
+ ["role"] = "system",
+ ["content"] = system.ToString(),
+ });
+ }
+ foreach (JToken msg in userMessages ?? [])
+ {
+ if (msg is not JObject mo)
+ {
+ continue;
+ }
+ JObject copy = new()
+ {
+ ["role"] = mo["role"]?.ToString() ?? "user",
+ ["content"] = mo["content"]?.ToString() ?? "",
+ };
+ if (mo["images"] is JArray images && images.Count > 0)
+ {
+ copy["images"] = images;
+ }
+ ollamaMessages.Add(copy);
+ }
+ return (ollamaMessages, layers);
+ }
+
+ async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops(
+ Session session,
+ string root,
+ string modelName,
+ string packName,
+ bool includeBase,
+ string contextJson,
+ JArray userMessages,
+ Func onDelta = null,
+ Func onHopStart = null,
+ string personaId = null,
+ JArray skillIds = null,
+ string embedModel = null)
+ {
+ string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
+ List skills = Config.ResolveEnabledSkills(pid, skillIds);
+ string embed = string.IsNullOrWhiteSpace(embedModel)
+ ? (Config.LoadSettings()["embed_model"]?.ToString()
+ ?? Config.LoadAssistant(pid)["embed_model"]?.ToString()
+ ?? "nomic-embed-text")
+ : embedModel;
+
+ try
+ {
+ await Memory.EnsureSeedAsync(root, Config, embed);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"Assistent memory seed: {ex.Message}");
+ }
+
+ bool slimUtility = IsSlimUtilityPack(packName);
+ JArray hits = [];
+ if (!slimUtility)
+ {
+ string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName);
+ try
+ {
+ AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid);
+ hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt);
+ hits = FilterHeardHitsIfDisabled(hits);
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"Assistent memory retrieve: {ex.Message}");
+ }
+ }
+
+ string enrichedContext = InjectMemoryHits(contextJson, hits, pid);
+ if (!slimUtility)
+ {
+ enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName);
+ }
+ (List messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
+ int systemChars = systemLayers["total"]?.Value()
+ ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length
+ ?? 0;
+ string reply = "";
+ JObject lastRaw = null;
+ int maxHops = slimUtility ? 1 : CfgInt("max_tool_hops", MaxToolHopsFallback);
+ HashSet hopDone = new(StringComparer.OrdinalIgnoreCase);
+ for (int hop = 0; hop < maxHops; hop++)
+ {
+ if (onHopStart is not null)
+ {
+ await onHopStart(hop);
+ }
+ (reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
+ if (slimUtility)
+ {
+ break;
+ }
+ JObject patch = TryParsePatch(reply);
+ // 0.14: no ApplyMemoryActions / ApplyUserPrefActions / ApplyPersonaActions / Civitai from server hop loop.
+ if (hop + 1 >= maxHops)
+ {
+ break;
+ }
+ string follow = null;
+ HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase);
+ while (true)
+ {
+ string tool = NextToolHop(patch, hopSkip);
+ if (string.IsNullOrWhiteSpace(tool))
+ {
+ follow = null;
+ break;
+ }
+ follow = await RunToolHop(session, pid, patch, tool, hopDone);
+ if (follow is not null)
+ {
+ break;
+ }
+ hopSkip.Add(tool);
+ }
+ if (follow is null)
+ {
+ break;
+ }
+ // Re-feed only the parsed patch JSON (not full prose) to save hop tokens.
+ string assistantContent = patch is not null
+ ? patch.ToString(Newtonsoft.Json.Formatting.None)
+ : reply;
+ messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent });
+ messages.Add(new JObject { ["role"] = "user", ["content"] = follow });
+ }
+ return (reply, lastRaw, [], systemChars, systemLayers);
+ }
+
+ static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null)
+ {
+ StringBuilder sb = new();
+ if (!string.IsNullOrWhiteSpace(packName))
+ {
+ sb.Append(packName).Append(' ');
+ }
+ if (!string.IsNullOrWhiteSpace(contextJson))
+ {
+ try
+ {
+ JObject ctx = JObject.Parse(contextJson);
+ string ckpt = ctx["checkpoint"]?.ToString() ?? ctx["current_model"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(ckpt))
+ {
+ sb.Append(ckpt).Append(' ');
+ }
+ if (ctx["selected_loras"] is JArray selLoras)
+ {
+ foreach (JToken t in selLoras.Take(12))
+ {
+ string n = t?["name"]?.ToString() ?? t?.ToString();
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ sb.Append(n).Append(' ');
+ }
+ }
+ }
+ else if (ctx["enabled_loras"] is JArray en)
+ {
+ foreach (JToken t in en.Take(12))
+ {
+ string n = t?["name"]?.ToString() ?? t?.ToString();
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ sb.Append(n).Append(' ');
+ }
+ }
+ }
+ if (ctx["krea_profile"] != null)
+ {
+ sb.Append("krea ").Append(ctx["krea_profile"]).Append(' ');
+ }
+ string aspect = ctx["aspect"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(aspect))
+ {
+ sb.Append(aspect).Append(' ');
+ }
+ string prompt = ctx["prompt"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(prompt))
+ {
+ sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' ');
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+ foreach (JToken msg in (userMessages ?? []).Reverse().Take(3))
+ {
+ if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase))
+ {
+ string c = mo["content"]?.ToString() ?? "";
+ sb.Append(c.Length > 500 ? c[..500] : c).Append(' ');
+ }
+ }
+ string q = CollapseWs(sb.ToString());
+ return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q;
+ }
+
+ AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid)
+ {
+ JObject a = Config.LoadAssistant(pid) ?? new JObject();
+ JObject agent = Config.LoadTrainingAgent();
+ AssistentMemory.RetrieveOptions opt = new()
+ {
+ TopK = a["memory_top_k"]?.Value() ?? 8,
+ MinScore = a["memory_min_score"]?.Value() ?? 0.32f,
+ ApplyQuotas = true,
+ };
+ Dictionary quotas = AssistentMemory.CopyDefaultQuotas();
+ if (a["memory_quotas"] is JObject qOverrides)
+ {
+ foreach (JProperty p in qOverrides.Properties())
+ {
+ quotas[p.Name] = p.Value?.Value() ?? 2;
+ }
+ }
+ if (agent["enabled"]?.Value() != false)
+ {
+ quotas["heard"] = agent["heard_quota"]?.Value() ?? 3;
+ }
+ else
+ {
+ quotas.Remove("heard");
+ }
+ opt.Quotas = quotas;
+ return opt;
+ }
+
+ JArray FilterHeardHitsIfDisabled(JArray hits)
+ {
+ if (Config.LoadTrainingAgent()["enabled"]?.Value() != false)
+ {
+ return hits;
+ }
+ JArray filtered = [];
+ foreach (JToken t in hits ?? [])
+ {
+ if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ filtered.Add(t);
+ }
+ return filtered;
+ }
+
+ /// Ask-only server hops: Exact/assistant settings dump or truncated inventory.
+ async Task RunToolHop(
+ Session session,
+ string pid,
+ JObject patch,
+ string tool,
+ HashSet hopDone)
+ {
+ if (tool == "ask_settings")
+ {
+ if (!hopDone.Add("ask_settings"))
+ {
+ return null;
+ }
+ JObject dump = BuildAskSettingsDump(pid);
+ return
+ "ask:settings dump (Exact + assistant knobs from server). "
+ + "Live session fields arrive via client compact context. "
+ + "Reply with a sparse delta JSON only if needed; omit ask:settings unless you need a refresh.\n```json\n"
+ + dump.ToString(Newtonsoft.Json.Formatting.None) + "\n```";
+ }
+ if (tool == "ask_inventory")
+ {
+ string q = patch?["inventory_query"]?.ToString()?.Trim() ?? "";
+ string sig = "ask_inventory:" + q.ToLowerInvariant();
+ if (!hopDone.Add(sig))
+ {
+ return null;
+ }
+ int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value() ?? 20;
+ JArray rows;
+ if (string.IsNullOrWhiteSpace(q))
+ {
+ JObject inv = await AssistentListInventory(session, rescan: false);
+ rows = TruncateInventoryForAsk(inv, lim);
+ }
+ else
+ {
+ rows = SearchInventoryForHop(q, lim);
+ }
+ return
+ "ask:inventory truncated LoRA/checkpoint list. Use exact names + listed triggers; "
+ + "omit ask:inventory unless you need a different query.\n```json\n"
+ + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```";
+ }
+ return null;
+ }
+
+ static JArray TruncateInventoryForAsk(JObject inv, int limit)
+ {
+ int lim = Math.Max(1, Math.Min(limit, 40));
+ JArray outRows = [];
+ foreach (JToken t in inv?["loras"] as JArray ?? [])
+ {
+ if (outRows.Count >= lim)
+ {
+ break;
+ }
+ outRows.Add(t);
+ }
+ foreach (JToken t in inv?["checkpoints"] as JArray ?? [])
+ {
+ if (outRows.Count >= lim)
+ {
+ break;
+ }
+ outRows.Add(t);
+ }
+ return outRows;
+ }
+
+ JObject BuildAskSettingsDump(string pid)
+ {
+ JObject exact = Config.LoadExact(pid) ?? new JObject();
+ JObject asst = Config.LoadAssistant(pid) ?? new JObject();
+ // Knobs the model may need — not the full assistant.json blob (quotas/seed noise).
+ JObject knobs = new()
+ {
+ ["num_ctx"] = asst["num_ctx"],
+ ["num_predict"] = asst["num_predict"],
+ ["max_tool_hops"] = asst["max_tool_hops"],
+ ["inventory_hop_limit"] = asst["inventory_hop_limit"],
+ ["max_loras_inventory"] = asst["max_loras_inventory"],
+ ["max_checkpoints_inventory"] = asst["max_checkpoints_inventory"],
+ ["max_gen_variants"] = asst["max_gen_variants"],
+ ["max_ref_slots"] = asst["max_ref_slots"],
+ ["default_pack"] = asst["default_pack"],
+ ["default_persona"] = asst["default_persona"],
+ ["gate"] = asst["gate"],
+ ["context_prompt_max"] = asst["context_prompt_max"],
+ ["history_keep_turns"] = asst["history_keep_turns"],
+ ["compress_at"] = asst["compress_at"],
+ ["chars_per_token"] = asst["chars_per_token"],
+ ["compress_auto"] = asst["compress_auto"],
+ };
+ return new JObject
+ {
+ ["detail"] = "settings",
+ ["exact"] = exact,
+ ["assistant"] = knobs,
+ ["persona"] = pid,
+ };
+ }
+
+ /// Substring filter over current LoRA/checkpoint inventory for ask:inventory hop.
+ JArray SearchInventoryForHop(string query, int limit)
+ {
+ int lim = Math.Max(1, Math.Min(limit, 40));
+ string q = (query ?? "").Trim().ToLowerInvariant();
+ JArray outRows = [];
+ void AddFromHandler(string setName, string kind)
+ {
+ if (!Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
+ {
+ return;
+ }
+ IEnumerable models = handler.Models.Values
+ .OrderByDescending(LooksLikeKreaArch)
+ .ThenBy(m => m.Name);
+ foreach (T2IModel model in models)
+ {
+ if (outRows.Count >= lim)
+ {
+ return;
+ }
+ string name = model.Name ?? "";
+ if (!string.IsNullOrEmpty(q))
+ {
+ string blob = $"{name} {model.Metadata?.UsageHint} {model.Metadata?.Description}".ToLowerInvariant();
+ if (!blob.Contains(q, StringComparison.Ordinal))
+ {
+ continue;
+ }
+ }
+ outRows.Add(BuildInventoryModelEntry(model, kind));
+ }
+ }
+ AddFromHandler("LoRA", "lora");
+ if (outRows.Count < lim)
+ {
+ AddFromHandler("Stable-Diffusion", "checkpoint");
+ }
+ return outRows;
+ }
+
+ string InjectMemoryHits(string contextJson, JArray hits, string personaId = null)
+ {
+ JObject ctx;
+ try
+ {
+ ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson);
+ }
+ catch
+ {
+ ctx = new JObject { ["_raw_context"] = contextJson };
+ }
+
+ int hitChars = 240;
+ try
+ {
+ string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
+ hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240;
+ }
+ catch
+ {
+ hitChars = 240;
+ }
+ hitChars = Math.Max(80, Math.Min(hitChars, 800));
+
+ string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
+ IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? [];
+
+ JArray clippedHits = [];
+ JArray heardExamples = [];
+ foreach (JToken t in hits ?? [])
+ {
+ if (t is not JObject ho)
+ {
+ continue;
+ }
+ if (IsExactPointerHit(ho))
+ {
+ continue;
+ }
+ if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase))
+ {
+ JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain);
+ if (ex is not null)
+ {
+ heardExamples.Add(ex);
+ }
+ continue;
+ }
+ JObject copy = (JObject)ho.DeepClone();
+ string text = copy["text"]?.ToString() ?? "";
+ if (text.Length > hitChars)
+ {
+ copy["text"] = text[..hitChars] + "…";
+ copy["truncated"] = true;
+ }
+ clippedHits.Add(copy);
+ }
+ ctx["memory_hits"] = clippedHits;
+ if (heardExamples.Count > 0)
+ {
+ ctx["heard_examples"] = heardExamples;
+ }
+ else
+ {
+ ctx.Remove("heard_examples");
+ }
+ ctx.Remove("taste_profile");
+ ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed
+ try
+ {
+ JObject asst = Config?.LoadAssistant(pid) ?? new JObject();
+ double weight = asst["user_prefs_weight"]?.Value() ?? 1.0;
+ int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16;
+ ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0;
+ }
+ catch
+ {
+ ctx["user_prefs_count"] = 0;
+ }
+ // Never re-inject full Exact into live context (already in system prompt).
+ ctx.Remove("exact");
+ if (ctx["session_exact"] is JObject se && !se.Properties().Any())
+ {
+ ctx.Remove("session_exact");
+ }
+
+ // Always normalize inventory: keep enabled + rich top-N; name-only for the rest.
+ SlimAvailableLorasInContext(ctx, hits);
+ DropNullOrEmpty(ctx);
+ return ctx.ToString(Newtonsoft.Json.Formatting.None);
+ }
+
+ static bool LiveContextHasSize(string contextJson)
+ {
+ if (string.IsNullOrWhiteSpace(contextJson))
+ {
+ return false;
+ }
+ try
+ {
+ JObject ctx = JObject.Parse(contextJson);
+ int? w = ctx["width"]?.Value();
+ int? h = ctx["height"]?.Value();
+ return w is > 0 && h is > 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ /// Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…").
+ static bool IsExactPointerHit(JObject ho)
+ {
+ if (ho is null)
+ {
+ return false;
+ }
+ string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant();
+ if (key.StartsWith("krea2_", StringComparison.Ordinal))
+ {
+ return true;
+ }
+ string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant();
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return false;
+ }
+ return text.Contains("live in exact memory", StringComparison.Ordinal)
+ || text.Contains("see exact memory", StringComparison.Ordinal)
+ || text.Contains("prefer exact kv", StringComparison.Ordinal)
+ || text.Contains("exact.facts.", StringComparison.Ordinal)
+ || text.Contains("exact memory profiles.", StringComparison.Ordinal);
+ }
+
+ void SlimAvailableLorasInContext(JObject ctx, JArray hits)
+ {
+ if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0)
+ {
+ return;
+ }
+ int richCap = 12;
+ int namesCap = 24;
+ try
+ {
+ string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
+ JObject asst = Config?.LoadAssistant(pid) ?? new JObject();
+ richCap = asst["inventory_prompt_rich"]?.Value() ?? 12;
+ namesCap = asst["inventory_prompt_names"]?.Value() ?? 24;
+ }
+ catch { /* defaults */ }
+ richCap = Math.Max(4, Math.Min(richCap, 40));
+ namesCap = Math.Max(richCap, Math.Min(namesCap, 80));
+
+ HashSet keepRich = new(StringComparer.OrdinalIgnoreCase);
+ if (ctx["enabled_loras"] is JArray en)
+ {
+ foreach (JToken t in en)
+ {
+ string n = t?["name"]?.ToString() ?? t?.ToString();
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ keepRich.Add(n);
+ }
+ }
+ }
+ if (ctx["selected_loras"] is JArray sel)
+ {
+ foreach (JToken t in sel)
+ {
+ string n = t?["name"]?.ToString() ?? t?.ToString();
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ keepRich.Add(n);
+ }
+ }
+ }
+ foreach (JToken hit in hits ?? [])
+ {
+ if (string.Equals(hit?["kind"]?.ToString(), "lora", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(hit?["kind"]?.ToString(), "card", StringComparison.OrdinalIgnoreCase))
+ {
+ string k = hit?["key"]?.ToString();
+ if (!string.IsNullOrWhiteSpace(k))
+ {
+ keepRich.Add(k);
+ }
+ }
+ }
+
+ List ordered = allLoras
+ .OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0)
+ .ThenByDescending(t => t?["krea_likely"]?.Value() == true ? 50 : 0)
+ .ThenBy(t => t?["name"]?.ToString() ?? "", StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ JArray slim = [];
+ int richCount = 0;
+ foreach (JToken t in ordered)
+ {
+ if (slim.Count >= namesCap)
+ {
+ break;
+ }
+ string n = t?["name"]?.ToString();
+ if (string.IsNullOrWhiteSpace(n))
+ {
+ continue;
+ }
+ bool wantRich = keepRich.Contains(n) || (t?["krea_likely"]?.Value() == true && richCount < richCap);
+ if (wantRich)
+ {
+ JObject rich = EnrichLoraRowForPrompt(t as JObject ?? new JObject { ["name"] = n });
+ slim.Add(rich);
+ if (!keepRich.Contains(n))
+ {
+ richCount++;
+ }
+ }
+ else
+ {
+ JObject nameOnly = new() { ["name"] = n };
+ if (t?["krea_likely"]?.Value() == true)
+ {
+ nameOnly["krea_likely"] = true;
+ }
+ slim.Add(nameOnly);
+ }
+ }
+ ctx["available_loras"] = slim;
+ if (allLoras.Count > slim.Count)
+ {
+ ctx["available_loras_truncated"] = true;
+ // Prefer client total if already set (full disk inventory count).
+ if (ctx["available_loras_total"] is null)
+ {
+ ctx["available_loras_total"] = allLoras.Count;
+ }
+ }
+ }
+
+ /// Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only.
+ JObject EnrichLoraRowForPrompt(JObject row)
+ {
+ if (row is null)
+ {
+ return new JObject();
+ }
+ JObject outRow = (JObject)row.DeepClone();
+ string name = outRow["name"]?.ToString();
+ bool needsTriggers = string.IsNullOrWhiteSpace(outRow["trigger_phrase"]?.ToString())
+ && (outRow["triggers"] is not JArray tr || tr.Count == 0);
+ bool needsBlurb = string.IsNullOrWhiteSpace(outRow["blurb"]?.ToString());
+ if (!needsTriggers && !needsBlurb)
+ {
+ return outRow;
+ }
+ JObject fromDisk = FindInventoryLoraByName(name);
+ if (fromDisk is null)
+ {
+ return outRow;
+ }
+ if (needsTriggers)
+ {
+ if (!string.IsNullOrWhiteSpace(fromDisk["trigger_phrase"]?.ToString()))
+ {
+ outRow["trigger_phrase"] = fromDisk["trigger_phrase"];
+ }
+ if (fromDisk["triggers"] is JArray ft && ft.Count > 0)
+ {
+ outRow["triggers"] = ft.DeepClone();
+ }
+ }
+ if (needsBlurb && !string.IsNullOrWhiteSpace(fromDisk["blurb"]?.ToString()))
+ {
+ outRow["blurb"] = fromDisk["blurb"];
+ }
+ if (outRow["default_weight"] is null && fromDisk["default_weight"] is not null)
+ {
+ outRow["default_weight"] = fromDisk["default_weight"];
+ }
+ if (fromDisk["krea_likely"]?.Value() == true)
+ {
+ outRow["krea_likely"] = true;
+ }
+ return outRow;
+ }
+
+ JObject FindInventoryLoraByName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name) || !Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
+ {
+ return null;
+ }
+ T2IModel model = handler.Models.Values.FirstOrDefault(m =>
+ string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(Path.GetFileNameWithoutExtension(m.Name), Path.GetFileNameWithoutExtension(name), StringComparison.OrdinalIgnoreCase)
+ || (m.Name?.EndsWith("/" + name, StringComparison.OrdinalIgnoreCase) ?? false));
+ return model is null ? null : BuildInventoryModelEntry(model, "lora");
+ }
+
+ static void DropNullOrEmpty(JObject ctx)
+ {
+ List remove = [];
+ foreach (JProperty p in ctx.Properties())
+ {
+ if (p.Value is null || p.Value.Type == JTokenType.Null)
+ {
+ remove.Add(p.Name);
+ }
+ else if (p.Value is JObject jo && !jo.Properties().Any())
+ {
+ remove.Add(p.Name);
+ }
+ else if (p.Value is JArray ja && ja.Count == 0 && p.Name is not "memory_hits")
+ {
+ remove.Add(p.Name);
+ }
+ }
+ foreach (string k in remove)
+ {
+ ctx.Remove(k);
+ }
+ }
+
+ string EnrichPersonaContext(string contextJson, string personaId, string packName)
+ {
+ JObject ctx;
+ try
+ {
+ ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson);
+ }
+ catch
+ {
+ ctx = new JObject { ["_raw_context"] = contextJson };
+ }
+ string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
+ ctx["persona_source"] = Config.PersonaSource(pid);
+ JObject schema = Config.LoadControlsSchema(pid);
+ JObject values = Config.LoadControlValues(pid);
+ bool authorPack = string.Equals(packName, "author_persona", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(packName, "persona", StringComparison.OrdinalIgnoreCase);
+ // Values only outside author pack (schema is fat). Author pack gets full schema.
+ if (values.Properties().Any())
+ {
+ if (authorPack && schema.Properties().Any())
+ {
+ ctx["persona_controls"] = new JObject
+ {
+ ["schema"] = schema,
+ ["values"] = values,
+ };
+ }
+ else
+ {
+ ctx["persona_controls"] = new JObject { ["values"] = values };
+ }
+ }
+ JArray catalog = [];
+ if (authorPack)
+ {
+ foreach (var p in Config.ListPersonaCatalog())
+ {
+ catalog.Add(new JObject
+ {
+ ["id"] = p.id,
+ ["title"] = p.title,
+ });
+ }
+ ctx["personas"] = catalog;
+ JObject shelves = Config.LoadIdentityParts(pid);
+ shelves.Remove("extra");
+ ctx["persona_shelves"] = shelves;
+ if (schema.Properties().Any())
+ {
+ ctx["persona_controls_schema"] = schema;
+ }
+ }
+ DropNullOrEmpty(ctx);
+ return ctx.ToString(Newtonsoft.Json.Formatting.None);
+ }
+
+ static string MemoryWritePersona(JObject mo, string currentPersonaId)
+ {
+ string scope = (mo?["scope"]?.ToString() ?? "").Trim().ToLowerInvariant();
+ if (scope is "shared" or "common" or "global")
+ {
+ return AssistentMemory.SharedPersona;
+ }
+ // Personal only — never let the model write into another personality's store.
+ return AssistentConfig.SafeId(currentPersonaId) ?? AssistentMemory.SharedPersona;
+ }
+
+ /// Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch.
+ void ApplyPersonaActions(JObject patch, ref string personaId)
+ {
+ if (patch is null || Config is null)
+ {
+ return;
+ }
+ // Never honor delete from the model.
+ bool wantClone = false, wantWrite = false;
+ if (patch["actions"] is JArray acts)
+ {
+ foreach (JToken a in acts)
+ {
+ string s = a?.ToString() ?? "";
+ if (string.Equals(s, "persona_clone", StringComparison.OrdinalIgnoreCase))
+ {
+ wantClone = true;
+ }
+ if (string.Equals(s, "persona_write", StringComparison.OrdinalIgnoreCase))
+ {
+ wantWrite = true;
+ }
+ }
+ }
+ if (patch["persona_clone"] is JObject)
+ {
+ wantClone = true;
+ }
+ // persona_shelves as object of content = write; as array of names = persona_read hop (ignore here).
+ if (patch["persona_shelves"] is JObject && !ActionsContain(patch, "persona_read"))
+ {
+ wantWrite = true;
+ }
+ try
+ {
+ if (wantClone && patch["persona_clone"] is JObject clone)
+ {
+ string from = AssistentConfig.SafeId(clone["from"]?.ToString()) ?? personaId;
+ string to = AssistentConfig.SafeId(clone["to"]?.ToString());
+ string title = clone["title"]?.ToString();
+ bool overwrite = clone["overwrite"]?.Value() == true;
+ if (to is not null)
+ {
+ Config.ClonePersonaToOverlay(from, to, title, overwrite);
+ personaId = to;
+ patch["_persona_cloned"] = to;
+ }
+ }
+ if (wantWrite && patch["persona_shelves"] is JObject shelves)
+ {
+ string target = AssistentConfig.SafeId(patch["persona"]?.ToString())
+ ?? AssistentConfig.SafeId(patch["persona_clone"]?["to"]?.ToString())
+ ?? personaId;
+ if (target is not null)
+ {
+ Config.SavePersonaShelves(target, shelves);
+ patch["_persona_written"] = target;
+ }
+ }
+ // Control values from model patch (Exact). Generate patches never touch sliders.
+ if (patch["controls"] is JObject ctrlVals)
+ {
+ if (AssistentConfig.PatchLooksLikeGeneration(patch))
+ {
+ patch.Remove("controls");
+ }
+ else
+ {
+ string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId;
+ JObject schema = Config.LoadControlsSchema(ctrlPid);
+ JObject current = Config.LoadControlValues(ctrlPid);
+ JObject filtered = AssistentConfig.FilterEchoedControlDefaults(
+ schema, current, ctrlVals, patchLooksLikeGen: false);
+ if (filtered.Count > 0)
+ {
+ Config.SaveControlValues(ctrlPid, filtered);
+ patch["controls"] = filtered;
+ patch["_controls_saved"] = true;
+ }
+ else
+ {
+ patch.Remove("controls");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logs.Warning($"Assistent persona actions: {ex.Message}");
+ patch["_persona_error"] = ex.Message;
+ }
+ }
+
+ async Task ApplyMemoryActions(string root, JObject patch, string embedModel, string personaId)
+ {
+ if (patch is null || Memory is null)
+ {
+ return;
+ }
+ bool upsert = false, forget = false;
+ if (patch["actions"] is JArray acts)
+ {
+ foreach (JToken a in acts)
+ {
+ string s = a?.ToString() ?? "";
+ if (string.Equals(s, "memory_upsert", StringComparison.OrdinalIgnoreCase))
+ {
+ upsert = true;
+ }
+ if (string.Equals(s, "memory_forget", StringComparison.OrdinalIgnoreCase))
+ {
+ forget = true;
+ }
+ }
+ }
+ JArray memories = patch["memories"] as JArray;
+ if (memories is null || memories.Count == 0)
+ {
+ return;
+ }
+ foreach (JToken t in memories)
+ {
+ if (t is not JObject mo)
+ {
+ continue;
+ }
+ string kind = mo["kind"]?.ToString() ?? "note";
+ string key = mo["key"]?.ToString() ?? "";
+ string text = mo["text"]?.ToString() ?? "";
+ string target = MemoryWritePersona(mo, personaId);
+ try
+ {
+ if (forget && string.IsNullOrWhiteSpace(text))
+ {
+ Memory.Forget(kind, key, persona: target);
+ }
+ else if (upsert || !string.IsNullOrWhiteSpace(text))
+ {
+ await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel, target);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"ApplyMemoryActions: {ex.Message}");
+ }
+ }
+ }
+
+ void ApplyUserPrefActions(JObject patch, string personaId)
+ {
+ if (patch is null || Memory is null)
+ {
+ return;
+ }
+ bool upsert = false, forget = false;
+ if (patch["actions"] is JArray acts)
+ {
+ foreach (JToken a in acts)
+ {
+ string s = a?.ToString() ?? "";
+ if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase))
+ {
+ upsert = true;
+ }
+ if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase))
+ {
+ forget = true;
+ }
+ }
+ }
+ JArray prefs = patch["user_prefs"] as JArray;
+ if (prefs is null || prefs.Count == 0)
+ {
+ return;
+ }
+ string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
+ foreach (JToken t in prefs)
+ {
+ if (t is not JObject mo)
+ {
+ continue;
+ }
+ string key = mo["key"]?.ToString() ?? "";
+ string text = mo["text"]?.ToString() ?? "";
+ string scope = mo["scope"]?.ToString() ?? "global";
+ bool pinned = mo["pinned"]?.Value() == true;
+ try
+ {
+ if (forget && string.IsNullOrWhiteSpace(text))
+ {
+ Memory.ForgetUserPref(key, scope, pid);
+ }
+ else if (upsert || !string.IsNullOrWhiteSpace(text))
+ {
+ Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"ApplyUserPrefActions: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/AssistentConfig.cs b/AssistentConfig.cs
index cce693e..9994e40 100644
--- a/AssistentConfig.cs
+++ b/AssistentConfig.cs
@@ -138,16 +138,14 @@ public sealed class AssistentConfig
return
[
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
- "actions", "search_query", "civitai_query",
+ "actions", "generate", "ask",
"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",
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
"creativity", "intensity", "complexity", "movement",
- "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
- "memory_query", "memory_kind", "tag_query", "user_prefs",
- "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls",
- "variants",
+ "clear_prompt_images", "slot_to_prompt_image", "pack", "persona", "controls",
+ "inventory_query", "variants",
];
}
diff --git a/AssistentInventory.cs b/AssistentInventory.cs
index d334c5e..eb6fc16 100644
--- a/AssistentInventory.cs
+++ b/AssistentInventory.cs
@@ -2,22 +2,16 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using FreneticUtilities.FreneticExtensions;
using Newtonsoft.Json.Linq;
using SwarmUI.Accounts;
using SwarmUI.Core;
using SwarmUI.Text2Image;
using SwarmUI.Utils;
-using SwarmUI.WebAPI;
namespace Mrleo1nid.SwarmAssistent;
-/// Server-side model inventory, assistant cards and Civitai lookups.
+/// Server-side model inventory (LoRA / checkpoint / wildcard lists).
public partial class SwarmAssistentExtension
{
const int MaxLorasInInventoryFallback = 150;
@@ -25,444 +19,6 @@ public partial class SwarmAssistentExtension
const int MaxCheckpointsInInventoryFallback = 60;
const int InventoryBlurbMaxFallback = 140;
- static string ModelWeightPath(string setName, string modelName)
- {
- if (string.IsNullOrWhiteSpace(modelName) || !Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
- {
- return null;
- }
- if (!handler.Models.TryGetValue(modelName, out T2IModel model) && !handler.Models.TryGetValue(modelName.Replace('\\', '/'), out model))
- {
- // Try suffix match
- model = handler.Models.Values.FirstOrDefault(m =>
- string.Equals(m.Name, modelName, StringComparison.OrdinalIgnoreCase)
- || m.Name.EndsWith("/" + modelName, StringComparison.OrdinalIgnoreCase)
- || Path.GetFileNameWithoutExtension(m.Name) == Path.GetFileNameWithoutExtension(modelName));
- }
- if (model is null)
- {
- return null;
- }
- try
- {
- // SwarmUI T2IModel exposes RawFilePath in recent builds.
- return model.RawFilePath;
- }
- catch
- {
- return null;
- }
- }
-
- static string CardPathForWeight(string weightPath)
- {
- if (string.IsNullOrWhiteSpace(weightPath))
- {
- return null;
- }
- string dir = Path.GetDirectoryName(weightPath);
- string stem = Path.GetFileNameWithoutExtension(weightPath);
- if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(stem))
- {
- return null;
- }
- return Path.Combine(dir, $"{stem}.assistent.json");
- }
-
- static string SetNameForKind(string kind)
- {
- return (kind ?? "").Trim().ToLowerInvariant() switch
- {
- "lora" => "LoRA",
- "checkpoint" or "ckpt" or "stable-diffusion" => "Stable-Diffusion",
- _ => null,
- };
- }
-
- JObject ReadCardObject(string kind, string name)
- {
- string set = SetNameForKind(kind);
- string weight = ModelWeightPath(set, name);
- string card = CardPathForWeight(weight);
- if (card is null || !File.Exists(card))
- {
- return null;
- }
- try
- {
- return JObject.Parse(File.ReadAllText(card, Encoding.UTF8));
- }
- catch
- {
- return null;
- }
- }
-
- public async Task AssistentGetCard(Session session, string kind, string name)
- {
- await Task.CompletedTask;
- if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
- {
- return new JObject { ["error"] = "kind and name required" };
- }
- JObject card = ReadCardObject(kind, name);
- string set = SetNameForKind(kind);
- string weight = ModelWeightPath(set, name);
- return new JObject
- {
- ["success"] = true,
- ["kind"] = kind,
- ["name"] = name,
- ["has_card"] = card is not null,
- ["weight_path"] = weight,
- ["card"] = card,
- };
- }
-
- public async Task AssistentSaveCard(Session session, string kind, string name, JObject card, bool enqueue_wanted = false)
- {
- await Task.CompletedTask;
- if (card is null)
- {
- return new JObject { ["error"] = "card required" };
- }
- kind = (kind ?? card["kind"]?.ToString() ?? "").Trim();
- name = (name ?? card["name"]?.ToString() ?? "").Trim();
- if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
- {
- return new JObject { ["error"] = "kind and name required" };
- }
- card["kind"] = kind;
- card["name"] = name;
-
- string set = SetNameForKind(kind);
- string weight = ModelWeightPath(set, name);
- if (!string.IsNullOrWhiteSpace(weight) && File.Exists(weight))
- {
- string path = CardPathForWeight(weight);
- File.WriteAllText(path, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
- _ = IngestCardToMemory(card, name);
- return new JObject { ["success"] = true, ["path"] = path, ["installed"] = true };
- }
-
- // Not installed — draft into wanted-cards + optionally enqueue download for next up.
- Directory.CreateDirectory(WantedCardsDir());
- string rawVid = card["version_id"]?.ToString() ?? "draft";
- string vid = Regex.IsMatch(rawVid, @"^\d+$") ? rawVid : "draft";
- string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
- File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
- if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
- {
- await AssistentEnqueueWanted(session, kind, card["civitai_url"]?.ToString(), card["version_id"]?.Value() ?? 0, card["title"]?.ToString() ?? name, card);
- }
- _ = IngestCardToMemory(card, name);
- return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true };
- }
-
- async Task IngestCardToMemory(JObject card, string name)
- {
- if (Memory is null || card is null)
- {
- return;
- }
- try
- {
- string kind = (card["kind"]?.ToString() ?? "lora").Trim().ToLowerInvariant();
- string key = (card["name"]?.ToString() ?? name ?? "").Trim();
- List bits = [];
- foreach (string field in new[] { "when", "avoid", "prompt_hint", "notes" })
- {
- string v = card[field]?.ToString();
- if (!string.IsNullOrWhiteSpace(v))
- {
- bits.Add($"{field}: {v.Trim()}");
- }
- }
- if (card["triggers"] is JArray tr)
- {
- string joined = string.Join(", ", tr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
- if (!string.IsNullOrWhiteSpace(joined))
- {
- bits.Add("triggers: " + joined);
- }
- }
- if (bits.Count == 0 || string.IsNullOrWhiteSpace(key))
- {
- return;
- }
- string text = $"{kind} {key}. " + string.Join(" ", bits);
- string baseUrl = NormalizeBaseUrl(Config.LoadSettings()["base_url"]?.ToString());
- string embedModel = Config.LoadSettings()["embed_model"]?.ToString()
- ?? Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString();
- await Memory.UpsertTextAsync(baseUrl, "card", key, text, "user", card, embedModel, AssistentMemory.SharedPersona);
- }
- catch (Exception ex)
- {
- Logs.Debug($"IngestCardToMemory: {ex.Message}");
- }
- }
-
- public async Task AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0, bool fetch = false)
- {
- string set = SetNameForKind(kind);
- string weight = ModelWeightPath(set, name);
- JObject civitai = null;
- JArray exampleUrls = [];
- JArray previewUrls = [];
- bool hasSidecar = false;
- string fetchError = null;
- bool fetched = false;
-
- if (!string.IsNullOrWhiteSpace(weight))
- {
- string stem = Path.GetFileNameWithoutExtension(weight);
- string dir = Path.GetDirectoryName(weight);
- string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
- if (File.Exists(side))
- {
- hasSidecar = true;
- try
- {
- civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
- }
- catch
- {
- // ignore
- }
- }
- foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
- {
- string prev = Path.Combine(dir ?? "", stem + suffix);
- if (File.Exists(prev))
- {
- // Swarm View path — relative URL works in the same origin browser session.
- previewUrls.Add($"View/Models/{(kind == "lora" ? "Lora" : "Stable-Diffusion")}/{Path.GetFileName(prev)}");
- break;
- }
- }
- }
-
- if (civitai is not null)
- {
- if (version_id <= 0)
- {
- version_id = civitai["id"]?.Value() ?? 0;
- }
- CollectExampleUrls(civitai, exampleUrls);
- }
-
- string hash = null;
- string trigger = null;
- try
- {
- if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
- && (h.Models.TryGetValue(name, out T2IModel m)
- || h.Models.TryGetValue(name.Replace('\\', '/'), out m)))
- {
- trigger = m.Metadata?.TriggerPhrase;
- hash = m.Metadata?.Hash;
- }
- }
- catch
- {
- // ignore
- }
-
- if (fetch && civitai is null)
- {
- string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
- if (string.IsNullOrWhiteSpace(apiKey))
- {
- fetchError = "Civitai: нет ключа в User Settings";
- }
- else
- {
- try
- {
- JObject remote = null;
- if (version_id > 0)
- {
- remote = await FetchCivitaiModelVersion(apiKey, version_id);
- }
- if (remote is null && !string.IsNullOrWhiteSpace(hash))
- {
- string sha = hash.Trim().ToLowerInvariant();
- if (sha.StartsWith("sha256:"))
- {
- sha = sha["sha256:".Length..];
- }
- if (sha.Length == 64)
- {
- remote = await FetchCivitaiByHash(apiKey, sha);
- }
- else
- {
- fetchError ??= "Civitai: хеш модели не SHA256";
- }
- }
- if (remote is not null)
- {
- civitai = remote;
- fetched = true;
- version_id = remote["id"]?.Value() ?? version_id;
- CollectExampleUrls(remote, exampleUrls);
- if (!string.IsNullOrWhiteSpace(weight))
- {
- try
- {
- string stem = Path.GetFileNameWithoutExtension(weight);
- string dir = Path.GetDirectoryName(weight);
- string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
- File.WriteAllText(side, remote.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
- hasSidecar = true;
- }
- catch (Exception ex)
- {
- Logs.Debug($"AssistentGetCardMeta write sidecar: {ex.Message}");
- }
- }
- }
- else if (fetchError is null)
- {
- fetchError = string.IsNullOrWhiteSpace(hash)
- ? "Civitai: нет hash и version_id"
- : "Хеш не найден на Civitai";
- }
- }
- catch (Exception ex)
- {
- fetchError = $"Civitai: {ex.Message}";
- }
- }
- }
-
- JObject card = ReadCardObject(kind, name);
- return new JObject
- {
- ["success"] = true,
- ["kind"] = kind,
- ["name"] = name,
- ["version_id"] = version_id,
- ["trigger_phrase"] = trigger,
- ["has_card"] = card is not null,
- ["has_sidecar"] = hasSidecar,
- ["fetched"] = fetched,
- ["fetch_error"] = fetchError,
- ["card"] = card,
- ["civitai"] = civitai,
- ["example_urls"] = exampleUrls,
- ["preview_urls"] = previewUrls,
- ["weight_path"] = weight,
- ["hash"] = hash,
- };
- }
-
- static void CollectExampleUrls(JObject civitai, JArray exampleUrls)
- {
- if (civitai?["images"] is not JArray imgs)
- {
- return;
- }
- foreach (JToken img in imgs.Take(6))
- {
- string u = img?["url"]?.ToString();
- if (!string.IsNullOrWhiteSpace(u))
- {
- exampleUrls.Add(u);
- }
- }
- }
-
- async Task FetchCivitaiByHash(string apiKey, string sha)
- {
- string[] hosts = ["civitai.red", "civitai.com"];
- Exception last = null;
- foreach (string host in hosts)
- {
- try
- {
- string url = $"https://{host}/api/v1/model-versions/by-hash/{sha}";
- using HttpRequestMessage req = new(HttpMethod.Get, url);
- if (!string.IsNullOrWhiteSpace(apiKey))
- {
- req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
- }
- using HttpResponseMessage resp = await HttpClient.SendAsync(req);
- string body = await resp.Content.ReadAsStringAsync();
- if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- continue;
- }
- if (!resp.IsSuccessStatusCode)
- {
- last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
- if ((int)resp.StatusCode is 401 or 403)
- {
- throw last;
- }
- continue;
- }
- return JObject.Parse(body);
- }
- catch (Exception ex) when (ex is not HttpRequestException && ex.Message.Contains("401"))
- {
- throw;
- }
- catch (Exception ex)
- {
- last = ex;
- }
- }
- if (last is not null)
- {
- throw last;
- }
- return null;
- }
-
- async Task FetchCivitaiModelVersion(string apiKey, int versionId)
- {
- string[] hosts = ["civitai.red", "civitai.com"];
- Exception last = null;
- foreach (string host in hosts)
- {
- try
- {
- string url = $"https://{host}/api/v1/model-versions/{versionId}";
- using HttpRequestMessage req = new(HttpMethod.Get, url);
- if (!string.IsNullOrWhiteSpace(apiKey))
- {
- req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
- }
- using HttpResponseMessage resp = await HttpClient.SendAsync(req);
- string body = await resp.Content.ReadAsStringAsync();
- if (!resp.IsSuccessStatusCode)
- {
- last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
- if ((int)resp.StatusCode is 401 or 403)
- {
- throw last;
- }
- continue;
- }
- return JObject.Parse(body);
- }
- catch (Exception ex)
- {
- last = ex;
- if (ex.Message.Contains("401") || ex.Message.Contains("403"))
- {
- throw;
- }
- }
- }
- if (last is not null)
- {
- throw last;
- }
- return null;
- }
-
/// Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).
public async Task AssistentListInventory(Session session, bool rescan = false)
@@ -553,8 +109,6 @@ public partial class SwarmAssistentExtension
{
string weight = null;
try { weight = model.RawFilePath; } catch { /* ignore */ }
- string cardPath = CardPathForWeight(weight);
- bool hasCard = !string.IsNullOrWhiteSpace(cardPath) && File.Exists(cardPath);
string usage = model.Metadata?.UsageHint;
string desc = model.Metadata?.Description;
@@ -571,29 +125,10 @@ public partial class SwarmAssistentExtension
}
string blurb = null;
- if (hasCard)
+ string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
+ if (!string.IsNullOrWhiteSpace(raw))
{
- try
- {
- JObject card = JObject.Parse(File.ReadAllText(cardPath, Encoding.UTF8));
- string fromCard = (card["notes"] ?? card["when"] ?? card["prompt_hint"])?.ToString();
- if (!string.IsNullOrWhiteSpace(fromCard))
- {
- blurb = Clip(fromCard.Trim(), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
- }
- }
- catch
- {
- // ignore bad card json
- }
- }
- if (string.IsNullOrWhiteSpace(blurb))
- {
- string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
- if (!string.IsNullOrWhiteSpace(raw))
- {
- blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
- }
+ blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
}
JArray tags = null;
@@ -618,7 +153,6 @@ public partial class SwarmAssistentExtension
["architecture"] = model.ModelClass?.ID,
["compat_class"] = model.ModelClass?.CompatClass?.ID,
["hash"] = model.Metadata?.Hash ?? "",
- ["has_card"] = hasCard,
["krea_likely"] = LooksLikeKreaArch(model),
};
if (!string.IsNullOrWhiteSpace(weight))
@@ -665,165 +199,4 @@ public partial class SwarmAssistentExtension
}
return entry;
}
-
- /// Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key.
- public async Task AssistentSearchCivitai(Session session, string query, int limit = 8)
- {
- string q = (query ?? "").Trim();
- if (string.IsNullOrWhiteSpace(q))
- {
- return new JObject { ["error"] = "query is required" };
- }
- limit = Math.Clamp(limit, 1, 20);
- string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
- HashSet installedNames = CollectInstalledLoraNames();
- HashSet installedHashes = CollectInstalledLoraHashes();
-
- string[] hosts = ["civitai.red", "civitai.com"];
- Exception lastEx = null;
- foreach (string host in hosts)
- {
- try
- {
- string url = $"https://{host}/api/v1/models?limit={limit}&types=LORA&query={Uri.EscapeDataString(q)}";
- using HttpRequestMessage req = new(HttpMethod.Get, url);
- if (!string.IsNullOrWhiteSpace(apiKey))
- {
- req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
- }
- using HttpResponseMessage resp = await HttpClient.SendAsync(req);
- string body = await resp.Content.ReadAsStringAsync();
- if (!resp.IsSuccessStatusCode)
- {
- lastEx = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 200)}");
- continue;
- }
- JObject parsed = JObject.Parse(body);
- JArray items = parsed["items"] as JArray ?? [];
- JArray results = [];
- foreach (JToken item in items)
- {
- if (item is not JObject mo)
- {
- continue;
- }
- JObject card = BuildCivitaiCard(mo, installedNames, installedHashes);
- if (card is not null)
- {
- results.Add(card);
- }
- }
- // Prefer Krea-compatible first
- JArray sorted = new(results.OrderByDescending(t => LooksLikeKrea(t["base_model"]?.ToString())).ThenBy(t => t["name"]?.ToString()));
- return new JObject
- {
- ["success"] = true,
- ["query"] = q,
- ["host"] = host,
- ["results"] = sorted,
- ["has_civitai_key"] = !string.IsNullOrWhiteSpace(apiKey),
- };
- }
- catch (Exception ex)
- {
- lastEx = ex;
- }
- }
- return new JObject { ["error"] = $"Civitai search failed: {lastEx?.Message ?? "unknown"}" };
- }
-
- static bool LooksLikeKrea(string text) => !string.IsNullOrEmpty(text) && Regex.IsMatch(text, @"krea", RegexOptions.IgnoreCase);
-
- static HashSet CollectInstalledLoraNames()
- {
- HashSet names = new(StringComparer.OrdinalIgnoreCase);
- if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
- {
- return names;
- }
- foreach (T2IModel m in handler.Models.Values)
- {
- names.Add(m.Name);
- string leaf = m.Name.Replace('\\', '/').AfterLast('/');
- if (!string.IsNullOrEmpty(leaf))
- {
- names.Add(leaf);
- names.Add(Path.GetFileNameWithoutExtension(leaf));
- }
- }
- return names;
- }
-
- static HashSet CollectInstalledLoraHashes()
- {
- HashSet hashes = new(StringComparer.OrdinalIgnoreCase);
- if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler))
- {
- return hashes;
- }
- foreach (T2IModel m in handler.Models.Values)
- {
- string h = m.Metadata?.Hash;
- if (!string.IsNullOrWhiteSpace(h))
- {
- hashes.Add(h.Trim().ToLowerInvariant());
- }
- }
- return hashes;
- }
-
- static JObject BuildCivitaiCard(JObject model, HashSet installedNames, HashSet installedHashes)
- {
- string name = model["name"]?.ToString() ?? "";
- JArray versions = model["modelVersions"] as JArray;
- JObject ver = versions?.FirstOrDefault() as JObject;
- if (ver is null)
- {
- return null;
- }
- string baseModel = ver["baseModel"]?.ToString() ?? "";
- JArray trained = ver["trainedWords"] as JArray ?? [];
- List triggers = trained.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)).Take(8).ToList();
- JObject file = null;
- foreach (JToken f in ver["files"] as JArray ?? [])
- {
- if (f is JObject fo && (fo["primary"]?.Value() == true || (fo["name"]?.ToString() ?? "").EndsWith(".safetensors", StringComparison.OrdinalIgnoreCase)))
- {
- file = fo;
- break;
- }
- }
- file ??= (ver["files"] as JArray)?.FirstOrDefault() as JObject;
- string downloadUrl = file?["downloadUrl"]?.ToString() ?? ver["downloadUrl"]?.ToString() ?? "";
- string fileName = file?["name"]?.ToString() ?? "";
- string sha = file?["hashes"]?["SHA256"]?.ToString() ?? file?["hashes"]?["AutoV2"]?.ToString() ?? "";
- string saveName = string.IsNullOrWhiteSpace(fileName)
- ? Regex.Replace(name, @"[^\w\-.]+", "_").Trim('_')
- : Path.GetFileNameWithoutExtension(fileName);
-
- bool already = false;
- if (!string.IsNullOrWhiteSpace(sha) && installedHashes.Contains(sha.Trim().ToLowerInvariant()))
- {
- already = true;
- }
- else if (installedNames.Contains(saveName) || installedNames.Contains(name) || installedNames.Contains(fileName))
- {
- already = true;
- }
-
- return new JObject
- {
- ["id"] = model["id"],
- ["version_id"] = ver["id"],
- ["name"] = name,
- ["base_model"] = baseModel,
- ["krea_likely"] = LooksLikeKrea(baseModel),
- ["triggers"] = new JArray(triggers),
- ["download_url"] = downloadUrl,
- ["file_name"] = saveName,
- ["sha256"] = sha,
- ["already_installed"] = already,
- ["n_sfw"] = model["nsfw"]?.Value() ?? false,
- };
- }
}
diff --git a/AssistentMemoryApi.cs b/AssistentMemoryApi.cs
index 7ad1f69..3e8761f 100644
--- a/AssistentMemoryApi.cs
+++ b/AssistentMemoryApi.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using SwarmUI.Accounts;
@@ -10,7 +8,7 @@ using SwarmUI.Utils;
namespace Mrleo1nid.SwarmAssistent;
-/// Read/write routes for the vector memory list in ⚙ and the gpu-rent wanted queue badge.
+/// Read/write routes for the vector memory list in ⚙.
public partial class SwarmAssistentExtension
{
/// Embed model the UI should use: settings overlay wins, then persona assistant.json.
@@ -274,44 +272,4 @@ public partial class SwarmAssistentExtension
return new JObject { ["error"] = $"memory clear: {ex.Message}" };
}
}
-
- /// The gpu-rent wanted queue (models pending the next up ) — count + entries.
- public async Task AssistentListWanted(Session session)
- {
- await Task.CompletedTask;
- string path = WantedModelsPath();
- JArray items = [];
- if (!File.Exists(path))
- {
- return new JObject { ["success"] = true, ["count"] = 0, ["items"] = items, ["path"] = path };
- }
- try
- {
- Dictionary> sections = LoadWantedYaml(File.ReadAllText(path, Encoding.UTF8));
- foreach ((string kind, List list) in sections.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
- {
- foreach (WantedEntry entry in list)
- {
- items.Add(new JObject
- {
- ["kind"] = kind,
- ["url"] = entry.Url,
- ["title"] = entry.Title,
- ["version_id"] = entry.VersionId,
- });
- }
- }
- return new JObject
- {
- ["success"] = true,
- ["count"] = items.Count,
- ["items"] = items,
- ["path"] = path,
- };
- }
- catch (Exception ex)
- {
- return new JObject { ["error"] = $"wanted queue: {ex.Message}" };
- }
- }
}
diff --git a/AssistentOllama.cs b/AssistentOllama.cs
index 0d5600c..35f4f4c 100644
--- a/AssistentOllama.cs
+++ b/AssistentOllama.cs
@@ -297,7 +297,22 @@ public partial class SwarmAssistentExtension
return null;
}
- /// Proxy to Ollama /api/chat (non-stream), with optional Civitai search hop.
+ /// Extract Ollama prompt token count when present.
+ static int? ReadPromptEvalCount(JObject raw)
+ {
+ if (raw is null)
+ {
+ return null;
+ }
+ int? n = raw["prompt_eval_count"]?.Value();
+ if (n is null || n <= 0)
+ {
+ n = raw["promptEvalCount"]?.Value();
+ }
+ return n is > 0 ? n : null;
+ }
+
+ /// Proxy to Ollama /api/chat (non-stream), with optional ask hops.
public async Task AssistentChat(Session session, string baseUrl, string model, string pack, bool includeBase, JObject raw)
{
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills);
@@ -314,7 +329,7 @@ public partial class SwarmAssistentExtension
{
(string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops(
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel);
- return new JObject
+ JObject result = new()
{
["success"] = true,
["reply"] = reply,
@@ -326,6 +341,12 @@ public partial class SwarmAssistentExtension
["system_chars"] = systemChars,
["system_layers"] = systemLayers,
};
+ int? promptEval = ReadPromptEvalCount(parsed);
+ if (promptEval is not null)
+ {
+ result["prompt_eval_count"] = promptEval.Value;
+ }
+ return result;
}
catch (Exception ex)
{
@@ -333,7 +354,7 @@ public partial class SwarmAssistentExtension
}
}
- /// WebSocket streaming chat (Ollama stream:true) + Civitai hops.
+ /// WebSocket streaming chat (Ollama stream:true) + ask hops.
public async Task AssistentChatWS(Session session, WebSocket ws, string baseUrl, string model, string pack, bool includeBase, JObject raw)
{
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills);
@@ -372,13 +393,13 @@ public partial class SwarmAssistentExtension
{
["clear_stream"] = true,
["hop"] = hop + 1,
- ["notice"] = "Civitai search done — refining…",
+ ["notice"] = "Ask hop — refining…",
}, API.WebsocketTimeout);
}
}
(string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops(
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel);
- await ws.SendJson(new JObject
+ JObject done = new()
{
["success"] = true,
["done"] = true,
@@ -390,7 +411,13 @@ public partial class SwarmAssistentExtension
["civitai_results"] = civitai,
["system_chars"] = systemChars,
["system_layers"] = systemLayers,
- }, API.WebsocketTimeout);
+ };
+ int? promptEval = ReadPromptEvalCount(parsed);
+ if (promptEval is not null)
+ {
+ done["prompt_eval_count"] = promptEval.Value;
+ }
+ await ws.SendJson(done, API.WebsocketTimeout);
}
catch (Exception ex)
{
diff --git a/AssistentPatch.cs b/AssistentPatch.cs
index f975ae9..2beb7ae 100644
--- a/AssistentPatch.cs
+++ b/AssistentPatch.cs
@@ -46,28 +46,25 @@ public partial class SwarmAssistentExtension
patch["look_at"] = patch["vision_slots"];
}
}
+ if (ActionsContain(patch, "generate"))
+ {
+ patch["generate"] = true;
+ }
+ if (patch["ask"] is JValue askVal && askVal.Type == JTokenType.String)
+ {
+ string one = askVal.ToString()?.Trim();
+ if (!string.IsNullOrWhiteSpace(one))
+ {
+ patch["ask"] = new JArray(one);
+ }
+ else
+ {
+ patch.Remove("ask");
+ }
+ }
return patch;
}
- static bool LooksLikeCardObject(JObject obj)
- {
- if (obj is null)
- {
- return false;
- }
- bool cardish = HasValue(obj, "kind") || HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint");
- bool genish = HasValue(obj, "prompt") || HasValue(obj, "negative") || HasValue(obj, "loras") || HasValue(obj, "actions")
- || HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "steps") || HasValue(obj, "cfg")
- || HasValue(obj, "aspect") || HasValue(obj, "seed") || HasValue(obj, "search_query")
- || HasValue(obj, "civitai_query") || HasValue(obj, "look_at") || HasValue(obj, "controls");
- if (cardish && !genish && (HasValue(obj, "name") || HasValue(obj, "triggers") || HasValue(obj, "when")))
- {
- return true;
- }
- return HasValue(obj, "kind") && HasValue(obj, "name")
- && (HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint") || HasValue(obj, "notes"));
- }
-
JObject TryParsePatch(string reply)
{
if (string.IsNullOrWhiteSpace(reply))
@@ -82,7 +79,7 @@ public partial class SwarmAssistentExtension
try
{
JObject obj = JObject.Parse(raw);
- if (obj is null || LooksLikeCardObject(obj))
+ if (obj is null)
{
continue;
}
@@ -90,7 +87,7 @@ public partial class SwarmAssistentExtension
{
JObject normalized = NormalizePatch(obj);
lastAny = normalized;
- if (FenceIsTerminalPatch(obj))
+ if (FenceIsTerminalPatch(normalized))
{
lastTerminal = normalized;
}
@@ -105,10 +102,9 @@ public partial class SwarmAssistentExtension
}
///
- /// If the reply already contains a closed fenced patch/card that is "done enough" to act on,
+ /// If the reply already contains a closed fenced patch that is "done enough" to act on,
/// cut everything after it. Do NOT stop on weak fences (pack/creativity/notes-only) — models
- /// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt
- /// and blocks skill_load / generate.
+ /// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt.
///
static bool TryTruncateAtCompleteFence(string reply, out string truncated)
{
@@ -128,7 +124,7 @@ public partial class SwarmAssistentExtension
string raw = match.Groups[1].Value.Trim();
try
{
- JObject obj = JObject.Parse(raw);
+ JObject obj = NormalizePatch(JObject.Parse(raw));
if (obj is null || !FenceIsTerminalPatch(obj))
{
continue;
@@ -145,7 +141,7 @@ public partial class SwarmAssistentExtension
}
///
- /// True when a closed fence is worth aborting the Ollama stream (real deliverable or tool hop).
+ /// True when a closed fence is worth aborting the Ollama stream (real deliverable or ask hop).
///
static bool FenceIsTerminalPatch(JObject obj)
{
@@ -153,7 +149,11 @@ public partial class SwarmAssistentExtension
{
return false;
}
- if (LooksLikeCardObject(obj))
+ if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value())
+ {
+ return true;
+ }
+ if (HasAsk(obj))
{
return true;
}
@@ -165,50 +165,18 @@ public partial class SwarmAssistentExtension
{
return true;
}
- if (HasValue(obj, "search_query") || HasValue(obj, "civitai_query"))
+ if (ActionsContain(obj, "generate"))
{
return true;
}
- if (HasValue(obj, "memory_query") || HasValue(obj, "tag_query") || HasValue(obj, "inventory_query"))
- {
- return true;
- }
- if (obj["actions"] is JArray acts)
- {
- foreach (JToken a in acts)
- {
- string s = a?.ToString() ?? "";
- if (string.IsNullOrWhiteSpace(s))
- {
- continue;
- }
- if (s.Equals("skill_load", StringComparison.OrdinalIgnoreCase)
- || s.Equals("persona_read", StringComparison.OrdinalIgnoreCase)
- || s.Equals("memory_get", StringComparison.OrdinalIgnoreCase)
- || s.Equals("memory_search", StringComparison.OrdinalIgnoreCase)
- || s.Equals("heard_search", StringComparison.OrdinalIgnoreCase)
- || s.Equals("lookup_tags", StringComparison.OrdinalIgnoreCase)
- || s.Equals("list_inventory", StringComparison.OrdinalIgnoreCase)
- || s.Equals("search_civitai", StringComparison.OrdinalIgnoreCase)
- || s.Equals("interrupt", StringComparison.OrdinalIgnoreCase)
- || s.Equals("generate", StringComparison.OrdinalIgnoreCase)
- || s.Equals("memory_upsert", StringComparison.OrdinalIgnoreCase)
- || s.Equals("user_pref_upsert", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
string prompt = obj["prompt"]?.ToString()?.Trim() ?? "";
if (prompt.Length >= 48)
{
return true;
}
- // Real param change without prose notes
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, "memories") || HasValue(obj, "user_prefs"))
+ || HasValue(obj, "seed") || HasValue(obj, "controls"))
{
return true;
}
@@ -216,14 +184,40 @@ public partial class SwarmAssistentExtension
return false;
}
- static string ExtractSearchQuery(JObject patch)
+ static bool HasAsk(JObject patch)
{
- if (patch is null)
+ if (patch?["ask"] is JArray asks)
{
- return null;
+ foreach (JToken t in asks)
+ {
+ if (!string.IsNullOrWhiteSpace(t?.ToString()))
+ {
+ return true;
+ }
+ }
+ return false;
}
- string q = (patch["search_query"] ?? patch["civitai_query"])?.ToString()?.Trim();
- return string.IsNullOrWhiteSpace(q) ? null : q;
+ return !string.IsNullOrWhiteSpace(patch?["ask"]?.ToString());
+ }
+
+ static bool AskContains(JObject patch, string name)
+ {
+ if (patch is null || string.IsNullOrWhiteSpace(name))
+ {
+ return false;
+ }
+ if (patch["ask"] is JArray asks)
+ {
+ foreach (JToken t in asks)
+ {
+ if (string.Equals(t?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase);
}
static bool ActionsContain(JObject patch, string action)
@@ -242,26 +236,7 @@ public partial class SwarmAssistentExtension
return false;
}
- static string ExtractMemoryQuery(JObject patch)
- {
- string q = patch?["memory_query"]?.ToString()?.Trim();
- if (!string.IsNullOrWhiteSpace(q))
- {
- return q;
- }
- return ActionsContain(patch, "memory_search") ? ExtractSearchQuery(patch) : null;
- }
-
- static string ExtractTagQuery(JObject patch)
- {
- string q = patch?["tag_query"]?.ToString()?.Trim();
- if (!string.IsNullOrWhiteSpace(q))
- {
- return q;
- }
- return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null;
- }
-
+ /// Server tool hops are ask-only: settings dump or truncated inventory.
static string NextToolHop(JObject patch, HashSet skip = null)
{
if (patch is null)
@@ -269,42 +244,13 @@ public partial class SwarmAssistentExtension
return null;
}
bool Skip(string tool) => skip is not null && skip.Contains(tool);
- if (ActionsContain(patch, "memory_get") && !Skip("memory_get"))
+ if (AskContains(patch, "settings") && !Skip("ask_settings"))
{
- return "memory_get";
+ return "ask_settings";
}
- if ((ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString()))
- && !Skip("memory_search"))
+ if (AskContains(patch, "inventory") && !Skip("ask_inventory"))
{
- return "memory_search";
- }
- if ((ActionsContain(patch, "heard_search") || string.Equals(patch["heard_query"]?.ToString(), "1", StringComparison.Ordinal))
- && !Skip("heard_search"))
- {
- return "heard_search";
- }
- if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString()))
- && !Skip("lookup_tags"))
- {
- return "lookup_tags";
- }
- if ((ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString()))
- && !Skip("list_inventory"))
- {
- return "list_inventory";
- }
- if (ActionsContain(patch, "skill_load") && !Skip("skill_load"))
- {
- return "skill_load";
- }
- if (ActionsContain(patch, "persona_read") && !Skip("persona_read"))
- {
- return "persona_read";
- }
- if ((ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
- && !Skip("civitai"))
- {
- return "civitai";
+ return "ask_inventory";
}
return null;
}
diff --git a/AssistentPersonaApi.cs b/AssistentPersonaApi.cs
index d199ce2..70d4c97 100644
--- a/AssistentPersonaApi.cs
+++ b/AssistentPersonaApi.cs
@@ -190,7 +190,10 @@ public partial class SwarmAssistentExtension
if (assistant is not null && assistant.Count > 0)
{
JObject sparse = new();
- foreach (string key in new[] { "num_ctx", "history_keep_turns", "memory_top_k", "user_prefs_weight", "user_prefs_max" })
+ foreach (string key in new[] {
+ "num_ctx", "history_keep_turns", "memory_top_k", "user_prefs_weight", "user_prefs_max",
+ "compress_at", "chars_per_token", "compress_auto", "num_predict",
+ })
{
if (assistant[key] is not null)
{
diff --git a/AssistentWanted.cs b/AssistentWanted.cs
deleted file mode 100644
index d7940a0..0000000
--- a/AssistentWanted.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-using SwarmUI.Accounts;
-
-namespace Mrleo1nid.SwarmAssistent;
-
-/// Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture).
-public partial class SwarmAssistentExtension
-{
- static readonly object WantedFileLock = new();
-
- string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml");
-
- string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards");
-
- public async Task AssistentEnqueueWanted(Session session, string kind, string url, int version_id = 0, string title = null, JObject card = null)
- {
- await Task.CompletedTask;
- kind = (kind ?? "lora").Trim().ToLowerInvariant();
- if (kind is not ("lora" or "checkpoint" or "vae" or "embedding" or "controlnet" or "upscaler" or "clip"))
- {
- kind = "lora";
- }
- url = (url ?? "").Trim();
- if (string.IsNullOrWhiteSpace(url) && version_id > 0)
- {
- url = $"https://civitai.red/models/0?modelVersionId={version_id}";
- }
- if (string.IsNullOrWhiteSpace(url))
- {
- return new JObject { ["error"] = "url or version_id required" };
- }
- if (version_id <= 0)
- {
- Match m = Regex.Match(url, @"modelVersionId=(\d+)", RegexOptions.IgnoreCase);
- if (m.Success)
- {
- version_id = int.Parse(m.Groups[1].Value);
- }
- }
-
- string path = WantedModelsPath();
- Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot());
- lock (WantedFileLock)
- {
- Dictionary> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "");
-
- if (version_id > 0)
- {
- foreach (List list in sections.Values)
- {
- if (list.Any(e => e.VersionId == version_id))
- {
- return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id };
- }
- }
- }
- else
- {
- foreach (List list in sections.Values)
- {
- if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase)))
- {
- return new JObject { ["success"] = true, ["already"] = true, ["path"] = path };
- }
- }
- }
-
- if (!sections.TryGetValue(kind, out List bucket))
- {
- bucket = [];
- sections[kind] = bucket;
- }
- bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id });
- File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8);
- }
-
- if (card is not null && version_id > 0)
- {
- Directory.CreateDirectory(WantedCardsDir());
- string draft = Path.Combine(WantedCardsDir(), $"{version_id}.assistent.json");
- File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
- }
- return new JObject { ["success"] = true, ["path"] = path, ["version_id"] = version_id };
- }
-
- sealed class WantedEntry
- {
- public string Url;
- public string Title;
- public int VersionId;
- }
-
- static Dictionary> LoadWantedYaml(string raw)
- {
- Dictionary> sections = new(StringComparer.OrdinalIgnoreCase);
- string currentKind = null;
- WantedEntry cur = null;
- void Flush()
- {
- if (cur is null || string.IsNullOrWhiteSpace(cur.Url) || string.IsNullOrWhiteSpace(currentKind))
- {
- cur = null;
- return;
- }
- if (!sections.TryGetValue(currentKind, out List list))
- {
- list = [];
- sections[currentKind] = list;
- }
- list.Add(cur);
- cur = null;
- }
- foreach (string line in (raw ?? "").Split('\n'))
- {
- string t = line.TrimEnd();
- if (string.IsNullOrWhiteSpace(t) || t.TrimStart().StartsWith('#'))
- {
- continue;
- }
- Match kindLine = Regex.Match(t, @"^([A-Za-z0-9_-]+):\s*$");
- if (kindLine.Success && !t.TrimStart().StartsWith('-'))
- {
- Flush();
- currentKind = kindLine.Groups[1].Value.Trim().ToLowerInvariant();
- continue;
- }
- Match urlLine = Regex.Match(t, @"^\s*-\s*url:\s*[""']?(.+?)[""']?\s*$");
- if (urlLine.Success)
- {
- Flush();
- cur = new WantedEntry { Url = urlLine.Groups[1].Value.Trim() };
- continue;
- }
- if (cur is null)
- {
- continue;
- }
- Match titleLine = Regex.Match(t, @"^\s*title:\s*[""']?(.+?)[""']?\s*$");
- if (titleLine.Success)
- {
- cur.Title = titleLine.Groups[1].Value.Trim();
- continue;
- }
- Match vidLine = Regex.Match(t, @"^\s*version_id:\s*(\d+)\s*$");
- if (vidLine.Success && int.TryParse(vidLine.Groups[1].Value, out int vid))
- {
- cur.VersionId = vid;
- }
- }
- Flush();
- return sections;
- }
-
- static string WriteWantedYaml(Dictionary> sections)
- {
- StringBuilder sb = new();
- sb.AppendLine("# Assistent wanted queue — merged into local models.yaml on gpu-rent up/capture");
- string[] order = ["checkpoint", "lora", "vae", "embedding", "controlnet", "upscaler", "clip"];
- HashSet seen = new(StringComparer.OrdinalIgnoreCase);
- foreach (string kind in order.Concat(sections.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)))
- {
- if (!seen.Add(kind) || !sections.TryGetValue(kind, out List list) || list.Count == 0)
- {
- continue;
- }
- sb.AppendLine($"{kind}:");
- foreach (WantedEntry e in list)
- {
- sb.AppendLine($" - url: \"{e.Url.Replace("\"", "%22")}\"");
- if (!string.IsNullOrWhiteSpace(e.Title))
- {
- sb.AppendLine($" title: \"{e.Title.Replace("\"", "'")}\"");
- }
- if (e.VersionId > 0)
- {
- sb.AppendLine($" version_id: {e.VersionId}");
- }
- }
- }
- return sb.ToString();
- }
-}
diff --git a/Config/_base/assistant.json b/Config/_base/assistant.json
index 21f9aa9..99938ee 100644
--- a/Config/_base/assistant.json
+++ b/Config/_base/assistant.json
@@ -1,7 +1,6 @@
{
"num_ctx": 16384,
"num_predict": 3072,
- "max_civitai_hops": 2,
"max_loras_inventory": 150,
"max_checkpoints_inventory": 60,
"max_wildcards_inventory": 80,
@@ -19,7 +18,7 @@
"memory_min_score": 0.32,
"user_prefs_weight": 1.0,
"user_prefs_max": 16,
- "max_tool_hops": 4,
+ "max_tool_hops": 2,
"tag_lookup_limit": 20,
"identity_always_shelves": ["persona", "voice", "rules", "likes", "dislikes"],
"memory_quotas": {
@@ -36,5 +35,8 @@
"keywords": ["krea"]
},
"context_prompt_max": 2000,
- "history_keep_turns": 4
+ "history_keep_turns": 4,
+ "compress_at": 0.70,
+ "chars_per_token": 3.2,
+ "compress_auto": true
}
diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md
index 93d3ce3..5a3aeb1 100644
--- a/Config/_base/core/core.md
+++ b/Config/_base/core/core.md
@@ -10,84 +10,63 @@ When instructions conflict, apply this order (highest wins):
1. **This core contract** — output format, never invent LoRA/checkpoint names or triggers, never use CFG 0, never depict or request anyone 17 or under (adults only).
2. **Current user message** — explicit “use steps 20 / aspect 16:9 now” wins for that turn.
3. **About the user** (`## About the user`) — durable preferences (global + this persona). Respect unless this turn overrides.
-4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat).
+4. **Chat session** (`## Live SwarmUI context` / session JSON) — current generation settings for **this chat** (prompt, params, LoRAs, board). Source of truth for Generate.
5. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged.
-6. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
-7. **`memory_hits` (hybrid FTS + vector RAG)** — craft notes / LoRA blurbs (often truncated). Prefer over guesses; never override Exact, About the user, or the user’s param request. Full row → `memory_get`; more search → `memory_search`; Danbooru spelling → `lookup_tags` (no tag soup in Krea prompts).
-8. Guesses — last resort only.
+6. Guesses — last resort only.
-Exact = defaults encyclopedia. About the user = human taste. RAG = soft craft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact (or `session_exact`) and the user did not ask to change them.
+Exact = defaults encyclopedia. About the user = human taste. Session = what Generate will run. Soft craft notes may appear in context from the server — never override Exact, About the user, or the user’s param request.
-Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. If you described the next frame / prompt in prose, the fence **must** include that `prompt` and usually `actions: ["generate"]` in the **same** turn — never stop after the header. **`prompt` must be English** (Krea 2 / Qwen3-VL) — translate + structure per skill `prompting`; chat prose may stay RU. Keep prose short (a few lines). **After the closing ``` of the JSON fence, STOP** — no «Готово!», no second fenced patch, no “сейчас сгенерирую оба” in prose. **One turn = one patch.** When the user asks for several options (разный свет / оба / варианты), put 2–4 items in **`variants`** (partial patches with optional `label`); the UI runs them sequentially and shows a grid. Do not emit two fences. Ordinary single-image requests stay one patch without `variants`. Prompt prose structure lives in skill `prompting` — do not invent a second recipe here.
+**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / full `prompt` when they already match the session and the user did not ask to change them.
-## Live context
+Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Chat prose may stay RU; **`prompt` must be English** (Krea 2). Keep prose short. **After the closing ``` of the JSON fence, STOP.** **One turn = one patch.** Several options → `variants` (2–4). Prompt structure lives in skill `prompting`.
-"Live SwarmUI context" JSON is ground truth for this turn:
+## Live context (chat session)
-- Use only LoRA/checkpoint **names** from `selected_loras` / `available_loras` (or Civitai hop results). `selected_loras` = currently enabled. Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**.
-- Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
-- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
-- `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. **Do not** `look_at` just because a frame exists. Emit `look_at` only when you cannot continue without pixels (user asked to look/critique/describe/compare, or a defect you cannot infer from the prompt). Never invent what the image looks like. A new Generate / «ещё» / prompt edit does **not** need vision.
-- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `fix_params`.
-- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack.
+Compact session JSON is ground truth for this turn:
-## Memory (short)
-
-- Craft RAG write: `memory_upsert` / `memory_forget` + `memories: [{kind,key,text,scope}]` (default personal).
-- About the user: `user_pref_upsert` / `user_pref_forget` + `user_prefs: [{key,text,scope}]`. Do **not** put human taste into craft `memories`.
-- Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook.
+- `selected_loras` / checkpoint — use only names present there (or after `ask:["inventory"]`).
+- Board: `board.has_generate`, ref ids — for `look_at` only when you need pixels.
+- For full numeric/Exact dump: `"ask": ["settings"]`. For LoRA/ckpt list: `"ask": ["inventory"]`.
## Output contract (mandatory)
1. Short helpful reply in the user's language (RU or EN).
-2. **Frame turns only:** one fenced JSON patch with **only fields you want to change**. Chat / Q&A / opinion / remember: **prose only — omit the JSON patch.**
+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.**
```json
{
"prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…",
- "negative": "bad quality, worst quality",
- "loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}],
"aspect": "16:9",
- "actions": ["generate"],
- "notes": "one-line why"
+ "generate": true
}
```
-Several options in one ask (still one fence):
+Several options (still one fence):
```json
{
"prompt": "same subject base…",
- "aspect": "16:9",
- "actions": ["generate"],
+ "generate": true,
"variants": [
{ "label": "warm light", "prompt": "… warm window light …" },
- { "label": "cool light", "prompt": "… cool moonlight …" },
- { "label": "portrait 9:16", "aspect": "9:16" }
+ { "label": "cool light", "prompt": "… cool moonlight …" }
]
}
```
### Patch rules
-- Omit unchanged **params** (`steps`/`cfg`/`sigma_shift`/`aspect`). For Generate, still include **`negative`**: create if live is empty, lightly supplement if the scene needs a specific omit, or echo the live/Exact box unchanged — never drop it.
-- Prefer omitting Exact-matching **`controls`** (e.g. do not re-emit `"horny": 35` / `"preference_bias": 0.35` when unchanged) — echoing defaults in a Generate patch resets the UI sliders.
-- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height.
-- Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries, **`variants`**) — use when needed; packs list the ones for that mode.
-- **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids.
+- Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit.
+- `loras` replaces the full intended set for this chat when you change LoRAs.
+- Prefer `aspect` over raw width/height.
+- Optional: seed, vary, init/mask, controls, pack, `variants`.
- Do not invent model or LoRA filenames.
-### Actions / hops
+### Commands
-- `"generate"` — Apply + start generation when this turn is a **new/updated frame** (they described a shot, asked to draw/edit/«ещё», or clearly want to see a result). They do **not** have to type «генерируй». **Do not** emit `generate` for chat, opinions («нравится»), trivia, look/critique without a redraw, remember/save, describe_ref, Cards/authoring. Chat-only turns: prose, no JSON patch (or prompt-only without `actions`).
-- If the user only asks to **remember / save** a prompt as base/template («запомни», «как базовый промпт», «шаблон») and did **not** ask for a new image: **omit** `actions: ["generate"]`, do **not** `look_at`. Prefer `memory_upsert` (kind `note`, key like `base_prompt`) or a short ack; you may echo `prompt` in the patch only to sync the SwarmUI box — the UI will not Generate on remember turns.
-- `"search_civitai"` + **required** short `search_query` — Civitai hop (user Confirms downloads). Without `search_query` the hop is skipped (never search the whole user message).
-- `"interrupt"` — stop generation.
-- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops.
-- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
-- `"skill_load"` + `skills: ["memory"]` — load fat skill text.
-- `"persona_read"` — load lore shelves not in always-on identity (e.g. `roleplay` / `craft` / `humor` when NSFW tone or craft detail needs them).
-- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes.
-- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`.
-- `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up). Opt-in: user asked, or you truly need pixels. Do not pair `look_at` with `actions:["generate"]` on a normal write turn (that stares at the *old* frame and delays the new one).
+- `"generate": true` — merge this delta into the chat session and run Generate (new/updated frame). **Omit** for chat, opinions, remember/save, look-only. Legacy `actions:["generate"]` is accepted as the same.
+- If the user says not to generate / only remember / only answer — **omit** `generate` and do not look.
+- `"look_at": ["generate"|"ref1"|…]` — vision hop (JPEG on follow-up). Use when you need pixels; do not pair with `generate` on the same normal write turn.
+- `"ask": ["settings"]` — request full settings dump (Exact + all fields).
+- `"ask": ["inventory"]` — request LoRA/checkpoint list.
- Pure Q&A: omit the JSON patch.
diff --git a/Config/_base/packs/catalog_card.json b/Config/_base/packs/catalog_card.json
deleted file mode 100644
index f5b1097..0000000
--- a/Config/_base/packs/catalog_card.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "id": "catalog_card",
- "title": "Карточка модели",
- "order": 70,
- "aliases": ["card", "catalog"],
- "prompt_file": "catalog_card.md"
-}
diff --git a/Config/_base/packs/catalog_card.md b/Config/_base/packs/catalog_card.md
deleted file mode 100644
index 973c0d4..0000000
--- a/Config/_base/packs/catalog_card.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Mode: catalog_card
-
-Goal: write a **recommendation card** for one checkpoint or LoRA so future Assistent turns know how to use it.
-
-## Inputs
-
-Live context includes `card_target` (name, kind, Civitai metadata, triggers) and may attach example images as vision.
-
-## Output
-
-Reply briefly in the user's language, then **one** fenced JSON object (not a generation patch):
-
-```json
-{
- "kind": "lora",
- "name": "exact_filename_or_swarm_name",
- "civitai_url": "https://civitai.red/models/…?modelVersionId=…",
- "version_id": 123,
- "triggers": ["exact", "from", "metadata"],
- "weight": 0.8,
- "when": "when to enable this model",
- "avoid": "when not to use it",
- "prompt_hint": "how to weave triggers into a Krea 2 prompt",
- "notes": "1–3 sentences for the agent"
-}
-```
-
-## Rules
-
-- Prefer triggers from metadata / trainedWords — **never invent**.
-- `weight` typical 0.6–1.0 for LoRA; omit or 1.0 for checkpoints.
-- Do **not** emit `actions: ["generate"]`. This mode does not start Generate.
-- Do not invent other LoRAs. Stay on the single `card_target`.
-- Persona tone still applies (lewd/neutral/aggressive) to `when` / `prompt_hint` wording.
diff --git a/Config/_base/packs/compose_scene.md b/Config/_base/packs/compose_scene.md
index 490f9ea..10c2bcf 100644
--- a/Config/_base/packs/compose_scene.md
+++ b/Config/_base/packs/compose_scene.md
@@ -9,7 +9,7 @@ Goal: co-create a scene / moodboard direction for **Krea 2** (local Swarm).
- **Moodboard via board:** if refs exist, `look_at` several refs, extract palette/texture/mood into **text**, then write the prompt. Prefer text distillation over dumping refs as Prompt Images.
- If using Prompt Images / `slot_to_prompt_image`, warn they often **overpower** the text prompt.
- Suggest available LoRAs only from the live list, with triggers.
-- Missing style LoRA → `search_civitai` + `search_query` (Krea-compatible).
+- Missing style LoRA → ask the user or use `"ask": ["inventory"]` for names already on disk (Krea-compatible).
- Optional intensity/complexity/movement → bake into prose (stylized, dense, kinetic…).
## Deliverable
diff --git a/Config/_base/packs/compress_history.json b/Config/_base/packs/compress_history.json
new file mode 100644
index 0000000..b3f4e06
--- /dev/null
+++ b/Config/_base/packs/compress_history.json
@@ -0,0 +1,9 @@
+{
+ "id": "compress_history",
+ "title": "Compress history",
+ "order": 998,
+ "hidden": true,
+ "enabled": true,
+ "aliases": ["compress_history", "compress"],
+ "prompt_file": "compress_history.md"
+}
diff --git a/Config/_base/packs/compress_history.md b/Config/_base/packs/compress_history.md
new file mode 100644
index 0000000..27b2381
--- /dev/null
+++ b/Config/_base/packs/compress_history.md
@@ -0,0 +1,32 @@
+# Mode: compress_history (hidden)
+
+You compress older chat turns into a rolling memory for the next Assistent turns. This is not a generation turn and not a Q&A with the user.
+
+## Hard rules
+
+- Write in the **user's language** (match the dialogue).
+- **No** fenced JSON. **No** `### JSON Patch`. **No** `actions`. **No** `generate`. **No** `look_at`. **No** tool hops.
+- Do **not** invent parameters, LoRAs, or facts that are not in the prior memory or the dialogue chunk.
+- Prefer concrete decisions (aspect, steps, LoRA names, prompt direction) over chit-chat.
+- Keep the whole reply under ~600 words.
+
+## Input
+
+You receive:
+1. Optional **previous conversation memory** (already compressed).
+2. A chunk of **older user/assistant turns** that must be folded into memory.
+3. Recent turns may be omitted — they stay as raw history.
+
+## Output format (exact headings)
+
+## Факты
+- bullet facts the next turn must remember
+
+## Решения (параметры, LoRA, aspect)
+- agreed Generate/session decisions
+
+## Открытые просьбы
+- still-open user requests
+
+## Кратко
+2–6 short sentences merging prior memory + this chunk.
diff --git a/Config/_base/packs/ordinary.md b/Config/_base/packs/ordinary.md
index f325556..6e64c28 100644
--- a/Config/_base/packs/ordinary.md
+++ b/Config/_base/packs/ordinary.md
@@ -1,31 +1,30 @@
# Mode: ordinary (комбайн)
-Default all-rounder. Handle this turn from the user message + live context — do **not** wait for a specialized pack.
+Default all-rounder. Handle this turn from the user message + **chat session** context — do **not** wait for a specialized pack.
## What you cover here
-- **Write / improve prompt** → patch with `prompt` + `negative` (+ `loras`) and `actions: ["generate"]` only when they want a **new frame** (scene to draw, edit, «ещё») — context is enough, magic word is not required. Chat / «нравится» / Q&A → prose only, **no** generate. Do **not** `look_at` the last frame first. `negative` on Generate: create / supplement / echo live.
-- **Light critique / improve last frame** → only when the user asks to look / critique / describe the picture. Then `look_at: ["generate"]` if `images_in_request` is false. Otherwise edit the prompt from text; `has_vision_image` alone is not a reason to look.
-- **Scene / mood** → compose direction into the prompt (same patch rules).
-- **Params** → only when they ask (steps/CFG/aspect/seed); omit Exact-matching numbers otherwise.
-- **Inpaint / img2img** → set init/mask fields when they ask and flags allow; else say what is missing.
-- **Describe a ref** → only with a real attached / look_at frame.
+- **Write / improve prompt** → sparse JSON with only changed fields (`prompt`, optional `negative`/`loras`) and `"generate": true` when they want a **new frame**. Chat / «нравится» / Q&A → prose only, **no** JSON. Do **not** `look_at` the last frame first.
+- **Light critique** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request.
+- **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise.
+- **Inpaint / img2img** → set init/mask fields when they ask.
+- Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops).
Prompt prose recipe = skill `prompting`. Creativity sliders = skill `creativity_sliders`.
## When to leave this mode
-Emit `"pack": ""` in the JSON patch only if the user clearly needs a dedicated workflow:
+Emit `"pack": ""` only if they clearly need a dedicated workflow:
- `critique_image` — deep frame critique loop
- `inpaint_edit` — regional edit / mask workflow
-- `catalog_card` / `author_persona` — Cards or persona authoring
+- `author_persona` — persona authoring
- `describe_ref` — reverse-prompt a reference at length
-Otherwise **stay in ordinary** and just do the work.
+Otherwise **stay in ordinary**.
## Deliverable
-Short reply + fenced JSON **only on frame turns** (`actions: ["generate"]` when they want a new/updated image). Chat/Q&A/opinion/remember: prose only — no fence.
-«давай дальше» / next frame = new English `prompt` + `negative` (echo live if unchanged) + `actions:["generate"]` in the **same** turn — never leave an empty `### JSON Patch` header. Chat may be RU; **Generate `prompt` is always EN** (skill `prompting`).
-Several options in one ask → `variants` (2–4 partial patches with `label`); still one fence, still STOP after it.
+Short reply + fenced JSON **only when changing session fields or commanding generate/look/ask**. Chat/Q&A: prose only.
+«давай дальше» / next frame = English `prompt` (+ `negative` if needed) + `"generate": true` in the **same** turn.
+Several options → `variants` (2–4); still one fence, still STOP after it.
diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json
index b1ed1b2..5f4a647 100644
--- a/Config/_base/patch-keys.json
+++ b/Config/_base/patch-keys.json
@@ -1,15 +1,13 @@
{
"keys": [
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
- "actions", "search_query", "civitai_query",
+ "actions", "generate", "ask",
"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",
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
"creativity", "intensity", "complexity", "movement",
- "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
- "memory_query", "memory_kind", "tag_query", "user_prefs",
- "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls",
- "variants"
+ "clear_prompt_images", "slot_to_prompt_image", "pack", "persona", "controls",
+ "inventory_query", "variants"
]
}
diff --git a/Config/_base/skills/prompting.md b/Config/_base/skills/prompting.md
index 5973247..ec7e06e 100644
--- a/Config/_base/skills/prompting.md
+++ b/Config/_base/skills/prompting.md
@@ -12,15 +12,16 @@ The **chat model** prepares the Generate-box text for **Krea 2** (Qwen3-VL). Do
5. **`negative` on every Generate** — create if live is empty (Exact `generation.negative`), supplement if the scene needs a specific omit, or echo live unchanged. Put “no blur / no people” ideas as positives in `prompt` instead of stuffing the negative box. Never clear `negative`.
6. Short ideas: expand into a full Krea paragraph. Thin or RU drafts: rewrite before Generate — do not hand Krea a half-ready line.
-## Prep checklist (before `actions:["generate"]`)
+## Prep checklist (before `"generate": true`)
- English only in `prompt`
-- `negative` present (new / supplemented / echoed — not omitted)
+- `negative` present when needed (new / supplemented / echoed — not omitted if live empty)
- Subject and action clear in the first sentence
- Wardrobe / body / setting concrete
- Camera + lighting present
- One coherent scene; NSFW stated in plain English if needed
- Triggers placed next to what they modify
+- Only changed fields in the JSON — session already holds the rest
## Deliverable
diff --git a/Config/_base/ui.json b/Config/_base/ui.json
index b01b531..ad854fd 100644
--- a/Config/_base/ui.json
+++ b/Config/_base/ui.json
@@ -1,6 +1,6 @@
{
- "welcome_html": "Assistent · Krea 2
Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.Галочка vision на окне — отправить кадр модели. Чипсы aspect / seed / Vary / Turbo·RAW. В чате: /help. Кнопки патча только у последнего предложения. Напиши, что сгенерировать — или кинь референс и попроси правку.",
- "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate сейчас\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|card|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone — клон с указанной\n/persona save — записать согласованный черновик\n/civitai — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate, клик / Открыть / Enter — просмотр.\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).",
+ "welcome_html": "Assistent · Krea 2
Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.У каждого чата свои параметры, LoRA, последний кадр и refs. Чипсы aspect / seed / Vary / Turbo·RAW. В чате: /help. Модель шлёт только дельту настроек + generate. Напиши, что сгенерировать — или кинь референс и попроси правку.",
+ "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate из сессии чата\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.",
"chips": [
{ "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" },
{ "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" },
@@ -19,9 +19,10 @@
{ "cmd": "/help", "hint": "список команд", "action": "help" },
{ "cmd": "/new", "hint": "новый чат", "action": "new" },
{ "cmd": "/history", "hint": "история чатов", "action": "history" },
+ { "cmd": "/compress", "hint": "сжать старые ходы", "action": "compress" },
{ "cmd": "/debug", "hint": "сводка · ask = с LLM", "action": "debug" },
{ "cmd": "/why", "hint": "debug + пояснение LLM", "action": "why" },
- { "cmd": "/gen", "hint": "Generate сейчас", "action": "gen" },
+ { "cmd": "/gen", "hint": "Generate из сессии", "action": "gen" },
{ "cmd": "/look ", "hint": "generate|refN", "action": "look" },
{ "cmd": "/init", "hint": "как Init", "action": "init" },
{ "cmd": "/mask", "hint": "как Mask", "action": "mask" },
@@ -31,7 +32,6 @@
{ "cmd": "/seed ", "hint": "lock|random", "action": "seed" },
{ "cmd": "/vary", "hint": "новый seed", "action": "vary" },
{ "cmd": "/pack ", "hint": "write|critique|…", "action": "pack" },
- { "cmd": "/civitai ", "hint": "запрос LoRA", "action": "civitai" },
{ "cmd": "/inventory", "hint": "rescan моделей", "action": "inventory" },
{ "cmd": "/persona new", "hint": "клон / новая личность", "action": "persona_new" },
{ "cmd": "/persona clone ", "hint": "клон с id", "action": "persona_clone" },
@@ -55,9 +55,6 @@
"inpaint_edit": "inpaint_edit",
"describe": "describe_ref",
"describe_ref": "describe_ref",
- "card": "catalog_card",
- "catalog": "catalog_card",
- "catalog_card": "catalog_card",
"persona": "author_persona",
"author": "author_persona",
"author_persona": "author_persona",
diff --git a/README.md b/README.md
index 7421ade..ab35700 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 that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
+**Version 0.14.0** — **Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`.
+
**Version 0.13.1** — Сборка 0.13.0: `using` для `WebSocket`/`HttpClient`, instance-методы с `Config`/`FilePath`, Sqlite dll рядом с extension (иначе вкладка не грузится / API пустые).
**Version 0.13.0** — **Реальный QLoRA-пайплайн**: `train_qlora.py` (TRL SFTTrainer + PEFT), HF-датасеты с маппингом (preset fiction title/tags→text), `max_samples`, полный post-train: safetensors → GGUF (`convert_lora_to_gguf.py`) → `ollama create` с `FROM ollama_base` + `ADAPTER`. Раннер: `builtin` + `custom`. Зависимости: `scripts/requirements-train.txt`.
@@ -150,10 +152,9 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
| `/aspect 16:9` | Set size from the official 1K table |
| `/seed lock\|random` | Lock or randomize seed |
| `/vary` | New seed, same prompt (+ generate if auto) |
-| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|card\|persona` | Switch pack |
+| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|persona` | Switch pack |
| `/persona new\|clone\|save` | Overlay persona authoring |
-| `/civitai ` | Ask LLM to search Civitai |
-| `/inventory` | Rescan models + refresh LoRA list |
+| `/inventory` | Rescan моделей + обновить список LoRA |
## Requirements
@@ -176,7 +177,7 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
## Packs & skills
-**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
+**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `author_persona`.
Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client via `AssistentGetConfig.patch_keys`.
@@ -200,12 +201,8 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v
| `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles |
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
| `AssistentListPersonas` | Persona catalog |
-| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
-| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
-| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
| `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user |
-| `AssistentSearchCivitai` | Civitai LoRA search |
-| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
+| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + ask hops) |
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store |
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key |
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) |
diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs
index 254c51e..2a1ab4d 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.13.1";
+ Version = "0.14.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
}
@@ -47,11 +47,6 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentGetConfig, false, PermUse);
API.RegisterAPICall(AssistentSaveSettings, true, PermUse);
API.RegisterAPICall(AssistentListInventory, false, PermUse);
- API.RegisterAPICall(AssistentGetCard, false, PermUse);
- API.RegisterAPICall(AssistentSaveCard, true, PermUse);
- API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
- API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
- API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
API.RegisterAPICall(AssistentChat, true, PermUse);
API.RegisterAPICall(AssistentChatWS, true, PermUse);
API.RegisterAPICall(AssistentListChats, false, PermUse);
@@ -68,7 +63,6 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentSearchMemory, false, PermUse);
API.RegisterAPICall(AssistentGetMemory, false, PermUse);
API.RegisterAPICall(AssistentLookupTags, false, PermUse);
- API.RegisterAPICall(AssistentListWanted, false, PermUse);
API.RegisterAPICall(AssistentSaveControls, true, PermUse);
API.RegisterAPICall(AssistentGetPersonaShelves, false, PermUse);
API.RegisterAPICall(AssistentClonePersona, true, PermUse);
@@ -104,7 +98,7 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse);
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse);
- Logs.Init("Swarm Assistent extension loaded (0.13.1 QLoRA pipeline)");
+ Logs.Init("Swarm Assistent extension loaded (0.14.0 chat session)");
}
int CfgInt(string key, int fallback)
diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html
index 72d48da..852b1a4 100644
--- a/Tabs/Text2Image/Assistent.html
+++ b/Tabs/Text2Image/Assistent.html
@@ -10,7 +10,6 @@
Чат
- Карточки
Обучение
Настройки
@@ -61,6 +60,25 @@
-
-
-
-
-
- Все
- Checkpoints
- LoRAs
-
- Обновить
-
-
-
-
-
-
-
memory_top_k
@@ -433,6 +414,9 @@
Контекст Ollama и Exact Turbo/RAW (пишется в overlay).
num_ctx
history_keep_turns
+
compress_at
+
chars_per_token
+
Автосжатие перед отправкой
Exact · Turbo
steps
diff --git a/package.json b/package.json
index 845f464..9efeec8 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"scripts": {
"build": "node scripts/build.mjs",
"watch": "node scripts/build.mjs --watch",
- "test": "node --test test/intent.test.js test/patch.test.js"
+ "test": "node --test test/intent.test.js test/patch.test.js test/context.test.js"
},
"devDependencies": {
"esbuild": "^0.25.0"
diff --git a/src/activity.js b/src/activity.js
new file mode 100644
index 0000000..71bd150
--- /dev/null
+++ b/src/activity.js
@@ -0,0 +1,263 @@
+/**
+ * Turn activity timeline — shows model commands and pipeline steps in chat.
+ * Cursor-like, but compact and chat-native.
+ */
+
+const STEP_ICONS = {
+ think: '◇',
+ stream: '✎',
+ delta: '⇢',
+ ask: '?',
+ look: '◎',
+ prep: '↻',
+ generate: '▷',
+ merge: '⊕',
+ warm: '▲',
+ park: '▼',
+ inventory: '▤',
+ compress: '▤',
+ done: '✓',
+ skip: '–',
+ error: '!',
+};
+
+export function createActivityController(opts = {}) {
+ const {
+ getMessagesEl,
+ scrollToBottom,
+ hideEmpty,
+ } = opts;
+
+ let card = null;
+ let listEl = null;
+ let titleEl = null;
+ let steps = [];
+ let open = true;
+
+ function ensureCard() {
+ const box = typeof getMessagesEl === 'function' ? getMessagesEl() : null;
+ if (!box) {
+ return null;
+ }
+ if (card && card.isConnected) {
+ return card;
+ }
+ if (typeof hideEmpty === 'function') {
+ hideEmpty();
+ }
+ 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';
+ head.setAttribute('aria-expanded', 'true');
+
+ const spin = document.createElement('span');
+ spin.className = 'sa-activity-spin';
+ spin.setAttribute('aria-hidden', 'true');
+
+ titleEl = document.createElement('span');
+ titleEl.className = 'sa-activity-title';
+ titleEl.textContent = 'Assistent';
+
+ const chev = document.createElement('span');
+ chev.className = 'sa-activity-chev';
+ chev.setAttribute('aria-hidden', 'true');
+ chev.textContent = '▾';
+
+ head.appendChild(spin);
+ head.appendChild(titleEl);
+ head.appendChild(chev);
+ head.addEventListener('click', () => {
+ open = !open;
+ card.classList.toggle('sa-activity-collapsed', !open);
+ head.setAttribute('aria-expanded', open ? 'true' : 'false');
+ });
+
+ listEl = document.createElement('div');
+ listEl.className = 'sa-activity-steps';
+
+ card.appendChild(head);
+ card.appendChild(listEl);
+ box.appendChild(card);
+ if (typeof scrollToBottom === 'function') {
+ scrollToBottom();
+ }
+ return card;
+ }
+
+ function renderStep(step) {
+ const row = document.createElement('div');
+ row.className = `sa-activity-step sa-activity-${step.status || 'running'}`;
+ row.dataset.id = step.id;
+
+ const icon = document.createElement('span');
+ icon.className = 'sa-activity-icon';
+ icon.setAttribute('aria-hidden', 'true');
+ icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think;
+
+ const body = document.createElement('div');
+ body.className = 'sa-activity-body';
+
+ const label = document.createElement('div');
+ label.className = 'sa-activity-label';
+ label.textContent = step.label || step.id;
+
+ body.appendChild(label);
+ if (step.detail) {
+ const detail = document.createElement('div');
+ detail.className = 'sa-activity-detail';
+ detail.textContent = step.detail;
+ body.appendChild(detail);
+ }
+
+ row.appendChild(icon);
+ row.appendChild(body);
+ return row;
+ }
+
+ function paint() {
+ if (!ensureCard() || !listEl) {
+ return;
+ }
+ listEl.replaceChildren(...steps.map(renderStep));
+ const running = steps.find((s) => s.status === 'running');
+ const last = steps[steps.length - 1];
+ if (titleEl) {
+ titleEl.textContent = running
+ ? running.label
+ : (last?.label || 'Assistent');
+ }
+ 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 = [];
+ card = null;
+ listEl = null;
+ titleEl = null;
+ open = true;
+ ensureCard();
+ if (titleEl && title) {
+ titleEl.textContent = title;
+ }
+ paint();
+ }
+
+ function upsert(id, patch) {
+ ensureCard();
+ let step = steps.find((s) => s.id === id);
+ if (!step) {
+ step = { id, kind: 'think', label: id, status: 'running', detail: '' };
+ steps.push(step);
+ }
+ Object.assign(step, patch);
+ if (!step.status) {
+ step.status = 'running';
+ }
+ paint();
+ return step;
+ }
+
+ function done(id, patch = {}) {
+ return upsert(id, { ...patch, status: 'done' });
+ }
+
+ function skip(id, patch = {}) {
+ return upsert(id, { ...patch, status: 'skip' });
+ }
+
+ function fail(id, patch = {}) {
+ return upsert(id, { ...patch, status: 'error' });
+ }
+
+ function finish(summary) {
+ steps.forEach((s) => {
+ if (s.status === 'running') {
+ s.status = 'done';
+ }
+ });
+ if (summary && titleEl) {
+ titleEl.textContent = summary;
+ }
+ paint();
+ if (card) {
+ card.classList.remove('sa-activity-live');
+ card.classList.add('sa-activity-done');
+ }
+ }
+
+ /** Describe a sparse model patch as human-readable activity steps. */
+ function noteModelCommands(patch) {
+ if (!patch || typeof patch !== 'object') {
+ return;
+ }
+ const keys = Object.keys(patch).filter((k) => patch[k] != null
+ && !['actions', 'generate', 'ask', 'look_at', 'vision_from', 'vision_slots', 'notes', 'variants'].includes(k));
+ if (keys.length) {
+ done('delta', {
+ kind: 'delta',
+ label: 'Обновил сессию',
+ detail: keys.slice(0, 10).join(', '),
+ });
+ }
+ const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : (patch.ask ? [String(patch.ask)] : []);
+ if (ask.length) {
+ upsert('ask', {
+ kind: 'ask',
+ label: `Запросил ${ask.join(', ')}`,
+ detail: 'подгружаю детали…',
+ status: 'running',
+ });
+ }
+ if (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null) {
+ const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []);
+ upsert('look', {
+ kind: 'look',
+ label: 'Смотрит на кадр',
+ detail: slots.map(String).slice(0, 4).join(', '),
+ status: 'running',
+ });
+ }
+ if (patch.generate === true
+ || (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))) {
+ upsert('generate', {
+ kind: 'generate',
+ label: 'Generate',
+ detail: 'ждёт пайплайн…',
+ status: 'running',
+ });
+ }
+ if (Array.isArray(patch.variants) && patch.variants.length) {
+ upsert('variants', {
+ kind: 'generate',
+ label: `Варианты ×${patch.variants.length}`,
+ status: 'running',
+ });
+ }
+ }
+
+ return {
+ begin,
+ upsert,
+ done,
+ skip,
+ fail,
+ finish,
+ noteModelCommands,
+ get steps() {
+ return steps.slice();
+ },
+ };
+}
+
+export function attachActivity(SA) {
+ SA.createActivityController = createActivityController;
+}
diff --git a/src/app.js b/src/app.js
index 0f29172..eed82bb 100644
--- a/src/app.js
+++ b/src/app.js
@@ -31,6 +31,9 @@
let HISTORY_KEEP_TURNS = 4;
let INVENTORY_PROMPT_RICH = 12;
let INVENTORY_PROMPT_NAMES = 24;
+ let COMPRESS_AT = 0.7;
+ let CHARS_PER_TOKEN = 3.2;
+ let COMPRESS_AUTO = true;
let ASPECT_TABLE = {
'1:1': [1024, 1024],
@@ -61,9 +64,6 @@
inpaint_edit: 'inpaint_edit',
describe: 'describe_ref',
describe_ref: 'describe_ref',
- card: 'catalog_card',
- catalog: 'catalog_card',
- catalog_card: 'catalog_card',
};
let WELCOME_HTML = `
@@ -81,6 +81,7 @@
/help — этот список
/new — новый чат (текущий сохранится в Историю)
/history — открыть список чатов
+/compress — сжать старые ходы в саммари
/debug — сводка UI/Exact (без LLM)
/debug ask — то же + короткий ответ модели
/why — сразу /debug ask
@@ -91,8 +92,7 @@
/aspect 16:9 — размер из таблицы 1K
/seed lock|random — зафиксировать или рандомизировать seed
/vary — новый seed, тот же промпт
-/pack write|critique|compose|params|inpaint|describe|card
-/civitai
— поиск LoRA (Confirm в чате)
+/pack write|critique|compose|params|inpaint|describe
/inventory — rescan моделей + обновить список LoRA
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.
@@ -102,6 +102,7 @@
{ cmd: '/help', hint: 'список команд' },
{ cmd: '/new', hint: 'новый чат' },
{ cmd: '/history', hint: 'история чатов' },
+ { cmd: '/compress', hint: 'сжать старые ходы' },
{ cmd: '/debug', hint: 'сводка · ask = с LLM' },
{ cmd: '/why', hint: 'debug + пояснение LLM' },
{ cmd: '/gen', hint: 'Generate сейчас' },
@@ -114,7 +115,6 @@
{ cmd: '/seed ', hint: 'lock|random' },
{ cmd: '/vary', hint: 'новый seed' },
{ cmd: '/pack ', hint: 'write|critique|…' },
- { cmd: '/civitai ', hint: 'запрос LoRA' },
{ cmd: '/inventory', hint: 'rescan моделей' },
];
@@ -147,6 +147,10 @@
lastSystemChars: 0,
lastSystemLayers: null,
lastContextChars: 0,
+ lastPromptEvalCount: null,
+ contextMemory: null,
+ ctxPanelOpen: false,
+ compressing: false,
busyPhase: 'idle',
busyStarted: 0,
gotDelta: false,
@@ -161,8 +165,7 @@
view: 'chat',
boardTab: 'generate',
personas: [],
- modelCards: {},
- cardsSelection: null,
+ chatSession: null,
pendingPersonaNote: null,
chats: [],
activeChatId: null,
@@ -178,12 +181,53 @@
userPrefs: [],
settingsTab: 'behavior',
settingsPersonaId: null,
- wanted: { count: 0, items: [] },
- wantedKeys: new Set(),
ollamaHealth: 'unknown',
trainingLock: false,
+ activity: null,
};
+ function getActivity() {
+ if (state.activity) {
+ return state.activity;
+ }
+ if (window.SA && typeof SA.createActivityController === 'function') {
+ state.activity = SA.createActivityController({
+ getMessagesEl: () => $('sa_messages'),
+ scrollToBottom: () => scrollMessagesToBottom(),
+ hideEmpty: () => hideChatEmpty(),
+ });
+ }
+ return state.activity;
+ }
+
+ function activityBegin(title) {
+ const a = getActivity();
+ if (a) {
+ a.begin(title || 'Assistent');
+ }
+ }
+
+ function activityStep(id, patch) {
+ const a = getActivity();
+ if (a) {
+ a.upsert(id, patch);
+ }
+ }
+
+ function activityDone(id, patch) {
+ const a = getActivity();
+ if (a) {
+ a.done(id, patch);
+ }
+ }
+
+ function activityFinish(summary) {
+ const a = getActivity();
+ if (a) {
+ a.finish(summary);
+ }
+ }
+
// ---- Turn lifecycle --------------------------------------------------
// A turn is one user utterance. It can fan out into nested LLM hops: Krea
// prompt prep, empty-patch retry, vision, auto-critique. They share one
@@ -195,13 +239,13 @@
/** Nested hop continuing the current turn — allowed through the busy gate. */
function isContinuationTurn(opts) {
return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique
- || opts.fromPromptEnRetry || opts.fromEmptyPatchRetry));
+ || opts.fromPromptEnRetry || opts.fromAskHop));
}
/** Turn whose prompt the client authored: no intent parsing, no auto-pack, no slash. */
function isMachineTurn(opts) {
return isContinuationTurn(opts)
- || !!(opts && (opts.fromCards || opts.fromDownload || opts.fromDebug));
+ || !!(opts && (opts.fromDebug));
}
function resetTurnHops() {
@@ -326,9 +370,29 @@
parking: 'Освобождаю VRAM (park LLM)…',
applying: 'Applying patch…',
silent_gen: 'Применяю патч → Generate…',
- refining: 'Civitai search done — refining…',
+ refining: 'Уточняю ответ…',
+ compressing: 'Сжимаю контекст…',
};
const text = labels[state.busyPhase] || 'Working…';
+ const phaseKind = {
+ thinking: 'think', streaming: 'stream', waiting: 'think', loading: 'warm',
+ warming: 'warm', parking: 'park', encoding: 'look', generating: 'generate',
+ applying: 'merge', silent_gen: 'generate', refining: 'prep', compressing: 'compress',
+ };
+ activityStep(`phase:${state.busyPhase}`, {
+ kind: phaseKind[state.busyPhase] || 'think',
+ label: text,
+ status: 'running',
+ });
+ // Mark prior phase:* steps done when switching phase
+ const a = getActivity();
+ if (a && Array.isArray(a.steps)) {
+ for (const s of a.steps) {
+ if (s.id.startsWith('phase:') && s.id !== `phase:${state.busyPhase}` && s.status === 'running') {
+ a.done(s.id);
+ }
+ }
+ }
const barText = $('sa_livebar_text');
if (barText) {
barText.textContent = text;
@@ -382,6 +446,7 @@
}
const elapsed = Date.now() - (state.busyStarted || Date.now());
state.busyPhase = 'idle';
+ activityFinish(finalStatus || 'Готово');
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
$('sa_composer')?.classList.remove('sa-composer-busy');
const send = $('sa_btn_send');
@@ -872,29 +937,6 @@
return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t);
}
- function userAsksGenerate(text) {
- const t = String(text || '').trim();
- if (!t) {
- return false;
- }
- // Short imperatives only — do NOT treat bare «давай» as Generate (false positive on chat).
- if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
- return true;
- }
- if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
- return true;
- }
- // Do NOT use \b or \w — ASCII-only in JS; breaks «сделай картинку».
- const letter = '[0-9A-Za-z_А-Яа-яЁё]';
- const stem = `${letter}*`;
- return cyrTokenRe(
- 'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|'
- + `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
- + `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
- + 'run\\s+generat|/gen',
- ).test(t);
- }
-
/** «давай дальше / продолжай / следующий кадр» — continue the series with Generate. */
function userAsksContinue(text) {
const t = String(text || '').trim();
@@ -942,32 +984,6 @@
}
return null;
}
-
- /**
- * The model wrote «### JSON Patch» and left the fence empty. Recover the prompt
- * from prose so the turn is not a dead end — but only claim a frame when the
- * user ordered one; otherwise this stays a plain prompt update.
- */
- function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) {
- const wantsFrame = !!opts.userWantsGenerate
- || (!isMachineTurn(opts) && userImpliesGenerate(userText));
- if (!wantsFrame && !replyMissingJsonPatch(reply)) {
- return null;
- }
- const prompt = extractPromptFromProse(reply) || state.lastPatch?.prompt || null;
- if (!prompt) {
- return null;
- }
- const patch = wantsFrame ? { prompt, actions: ['generate'] } : { prompt };
- if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) {
- patch.loras = state.lastPatch.loras;
- }
- if (state.lastPatch?.aspect) {
- patch.aspect = state.lastPatch.aspect;
- }
- return patch;
- }
-
/**
* Chat model prepares Generate prompts for Krea. Skip the prep hop only when the
* prompt already looks like solid English Krea prose for Qwen3-VL.
@@ -1061,7 +1077,7 @@
/** «запомни как базовый промпт» — apply/save only, never Generate / auto look_at. */
function userAsksNoGenerate(text) {
const t = String(text || '').trim();
- if (!t || userAsksGenerate(t)) {
+ if (!t) {
return false;
}
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
@@ -1070,24 +1086,10 @@
return cyrTokenRe(
'запомн|запомни|запомним|сохрани|сохраним|шаблон|'
+ 'базов(ый|ого|ому|ым|ая|ую|ое)?\\s+промпт|'
- + 'не\\s+генерир|без\\s+генерац|не\\s+надо\\s+генер|только\\s+запомн|пока\\s+запомн|'
- + 'не\\s+рисуй|не\\s+запускай\\s+генер',
+ + 'не\\s+генерир[а-яё]*|без\\s+генерац[а-яё]*|не\\s+надо\\s+генер[а-яё]*|только\\s+запомн[а-яё]*|пока\\s+запомн[а-яё]*|'
+ + 'не\\s+рисуй|не\\s+запускай\\s+генер[а-яё]*',
).test(t);
}
-
- /**
- * The only reason the client adds a frame the model did not ask for: the
- * user gave a direct order. Everything softer («другой ракурс», «покажи как
- * она…») is the model's call via actions:["generate"] — see core.md.
- */
- function userCommandsGenerate(text) {
- const t = String(text || '').trim();
- if (!t || userAsksNoGenerate(t)) {
- return false;
- }
- return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t);
- }
-
function userAsksLook(text) {
const t = String(text || '').trim();
if (!t) {
@@ -1105,115 +1107,27 @@
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
}
- /** Opinion / thanks / trivia — not a new frame. */
- function userIsChatNotFrame(text) {
- const t = String(text || '').trim();
- if (!t) {
- return false;
- }
- if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) {
- return true;
- }
- if (cyrTokenRe(
- 'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|'
- + 'список\\s+лор|где\\s+настрой|что\\s+значит|'
- + 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|'
- + 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр',
- ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) {
- return true;
- }
- return false;
- }
-
- /**
- * Scene / edit / «ещё» — a frame without the magic word «генерируй».
- * Chat, opinions, trivia, look-only stay false. No noun-only fallback.
- */
- function userImpliesGenerate(text) {
- const t = String(text || '').trim();
- if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) {
- return false;
- }
- if (userCommandsGenerate(t)) {
- return true;
- }
- if (t.length < 8) {
- return false;
- }
- const wantsLook = userAsksLook(t);
- const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t)
- || /\b(fix|redo|redraw|improve)\b/i.test(t);
- if (wantsLook && !wantsRedraw) {
- return false;
- }
- if (cyrTokenRe(
- 'нарису|сгенер|перерису|'
- + 'сделай\\s+(картинк|изображен|фото|кадр)|'
- + 'хочу\\s+(картинк|изображен|фото|увидеть|видеть)|'
- + 'покажи\\s+как\\s+(она|он|это)|'
- + 'сделай\\s+(её|ее|его|мне)\\s|'
- + 'пусть\\s+будет|'
- + 'другой\\s+(ракурс|свет|наряд|поза)|'
- + 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|'
- + 'ещё\\s+одн|еще\\s+одн',
- ).test(t)) {
- return true;
- }
- if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) {
- return true;
- }
- const isQuestion = /[??]\s*$/.test(t);
- if (isQuestion) {
- return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t);
- }
- return false;
- }
-
- function packBlocksAutoGenerate(pack) {
- const p = String(pack || '');
- return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain';
- }
-
function packWantsVision(pack) {
const p = String(pack || '');
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
}
-
- /**
- * Client owns Generate: a frame only when the user commanded or implied one.
- * Model `actions:["generate"]` on chat/Q&A is stripped. look_at is opt-in
- * (user /look, vision pack, or auto-critique hop) — not every model look_at.
- */
function resolveTurnIntent(patch, userText, opts = {}) {
- const machine = isMachineTurn(opts);
- const vetoed = !machine && userAsksNoGenerate(userText);
- const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText));
- const implied = !machine && userImpliesGenerate(userText);
- const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate');
-
- let generate;
- if (vetoed || opts.fromAutoCritique) {
- generate = false;
- } else if (commanded) {
- generate = true;
- } else if (packBlocksAutoGenerate($('sa_pack')?.value || '')) {
- generate = false;
- } else {
- // The model's own call, with the RU heuristics as a backstop when it forgets.
- generate = modelAsked || implied;
+ const S = window.SA && window.SA.session;
+ if (S && typeof S.resolveTurnIntent === 'function') {
+ return S.resolveTurnIntent(patch, userText, { vetoFn: userAsksNoGenerate });
}
-
+ 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 hasLook = !!patch
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
- const honorLook = opts.fromAutoCritique || opts.fromVisionHop
- || (!machine && userAsksLook(userText))
- || packWantsVision($('sa_pack')?.value);
- // Unsolicited look_at on ordinary/write is ignored (0.11.4 opt-in).
- const look = !!(hasLook && !vetoed && !generate && honorLook);
-
- return { generate, look, vetoed };
+ const look = !!(hasLook && !vetoed && !generate);
+ const ask = Array.isArray(patch?.ask)
+ ? patch.ask.map(String)
+ : (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
+ return { generate, look, vetoed, ask };
}
-
function stripGenerateAction(patch) {
if (!patch || typeof patch !== 'object') {
return patch;
@@ -1249,7 +1163,7 @@
}
function rememberLastPatch(patch) {
- if (patch && typeof patch === 'object' && !isCardObject(patch)) {
+ if (patch && typeof patch === 'object') {
state.lastPatch = patch;
syncBuildGenButton();
}
@@ -1290,7 +1204,6 @@
fix_params: 'params',
inpaint_edit: 'inpaint',
describe_ref: 'describe',
- catalog_card: 'card',
author_persona: 'persona',
};
const short = shortMap[pack] || pack.replace(/_/g, ' ').slice(0, 12);
@@ -1495,6 +1408,421 @@
return turns * 2;
}
+ function ctxApi() {
+ return window.SA?.context || null;
+ }
+
+ function getContextMemory() {
+ const C = ctxApi();
+ if (C?.normalizeContextMemory) {
+ return C.normalizeContextMemory(state.contextMemory);
+ }
+ return state.contextMemory && typeof state.contextMemory === 'object'
+ ? state.contextMemory
+ : { summary: '', untilCount: 0, foldedTurns: 0, at: 0, uiCollapsed: false, promptEvalCount: null };
+ }
+
+ function setContextMemory(raw, { persist = true } = {}) {
+ const C = ctxApi();
+ state.contextMemory = C?.normalizeContextMemory
+ ? C.normalizeContextMemory(raw)
+ : (raw && typeof raw === 'object' ? raw : null);
+ if (state.chatSession && typeof state.chatSession === 'object') {
+ state.chatSession.context_memory = state.contextMemory?.summary
+ ? state.contextMemory
+ : null;
+ }
+ if (persist && !state.restoringChat) {
+ persistHistory();
+ }
+ updateCtxChip();
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
+
+ function resetContextMemory({ persist = true } = {}) {
+ setContextMemory(ctxApi()?.emptyContextMemory?.() || {
+ summary: '', untilCount: 0, foldedTurns: 0, at: 0, uiCollapsed: false, promptEvalCount: null,
+ }, { persist });
+ }
+
+ function historyCharsForBudget(messages) {
+ return (messages || []).reduce((n, m) => n + String(m?.content || '').length, 0);
+ }
+
+ function currentBudgetEstimate(modelMessages) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const memChars = mem.summary ? mem.summary.length : 0;
+ const hist = modelMessages || assembleOutgoingMessages();
+ const numCtx = Number($('sa_num_ctx')?.value) || state.config?.assistant?.num_ctx || 16384;
+ const numPredict = Number(state.config?.assistant?.num_predict) || 3072;
+ if (!C?.estimateBudget) {
+ return {
+ used: 0, numCtx, level: 'ok', estimated: 0, fromEval: false,
+ systemChars: state.lastSystemChars || 0,
+ historyChars: historyCharsForBudget(hist),
+ memoryChars: memChars,
+ threshold: Math.floor((numCtx - numPredict) * COMPRESS_AT),
+ };
+ }
+ return C.estimateBudget({
+ systemChars: state.lastSystemChars || 0,
+ historyChars: historyCharsForBudget(hist),
+ memoryChars: memChars,
+ numCtx,
+ numPredict,
+ charsPerToken: CHARS_PER_TOKEN,
+ compressAt: COMPRESS_AT,
+ promptEvalCount: state.lastPromptEvalCount ?? mem.promptEvalCount,
+ });
+ }
+
+ function assembleOutgoingMessages({ includePendingUser } = {}) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ let msgs;
+ if (C?.assembleModelMessages) {
+ msgs = C.assembleModelMessages(state.history, mem, HISTORY_KEEP_TURNS).map((m) => {
+ let content = String(m.content || '');
+ if (m.role === 'assistant') {
+ content = stripJsonFencesForHistory(content);
+ }
+ return { role: m.role, content: content.slice(0, 4000) };
+ });
+ } else {
+ msgs = state.history.slice(-historyMessageLimit()).map((m) => {
+ let content = String(m.content || '');
+ if (m.role === 'assistant') {
+ content = stripJsonFencesForHistory(content);
+ }
+ return { role: m.role, content: content.slice(0, 4000) };
+ });
+ }
+ if (includePendingUser) {
+ msgs.push({ role: 'user', content: String(includePendingUser) });
+ }
+ return msgs;
+ }
+
+ function buildCompressUserPrompt() {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const fold = C?.messagesToFold
+ ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS)
+ : [];
+ const lines = [];
+ if (mem.summary) {
+ lines.push('## Previous conversation memory');
+ lines.push(mem.summary);
+ lines.push('');
+ }
+ lines.push('## Dialogue chunk to fold');
+ for (const m of fold) {
+ const role = m.role === 'assistant' ? 'Assistant' : 'User';
+ let content = String(m.content || '');
+ if (m.role === 'assistant') {
+ content = stripJsonFencesForHistory(content);
+ }
+ content = content.slice(0, 1500);
+ lines.push(`### ${role}`);
+ lines.push(content || '(empty)');
+ lines.push('');
+ }
+ lines.push('Compress the chunk into the required heading format. Merge with previous memory when present.');
+ return { prompt: lines.join('\n'), foldCount: fold.length, fold };
+ }
+
+ function applyCompressResult(summaryText, foldCount, { uiCollapsed = false } = {}) {
+ const C = ctxApi();
+ const prev = getContextMemory();
+ const merged = C?.mergeSummary
+ ? C.mergeSummary(prev.summary, summaryText)
+ : String(summaryText || '').trim() || prev.summary;
+ const nextUntil = prev.untilCount + Math.max(0, foldCount);
+ setContextMemory({
+ summary: merged,
+ untilCount: nextUntil,
+ foldedTurns: Math.floor(nextUntil / 2),
+ at: Date.now(),
+ uiCollapsed: uiCollapsed || prev.uiCollapsed,
+ promptEvalCount: state.lastPromptEvalCount,
+ });
+ }
+
+ function callOllamaOnce(payload) {
+ return new Promise((resolve, reject) => {
+ const fail = (err) => reject(new Error(String(err || 'Chat failed')));
+ const ok = (data) => {
+ if (data?.error) {
+ fail(data.error);
+ return;
+ }
+ resolve(data || {});
+ };
+ if (typeof makeWSRequest === 'function') {
+ let settled = false;
+ makeWSRequest(
+ 'AssistentChatWS',
+ payload,
+ (data) => {
+ if (settled) {
+ return;
+ }
+ if (data?.error) {
+ settled = true;
+ fail(data.error);
+ return;
+ }
+ if (data?.done || data?.reply != null) {
+ settled = true;
+ ok(data);
+ }
+ },
+ 0,
+ (err) => {
+ if (settled) {
+ return;
+ }
+ genericRequest('AssistentChat', payload, (data) => {
+ settled = true;
+ ok(data);
+ }, 0, (err2) => {
+ settled = true;
+ fail(err2 || err);
+ });
+ },
+ );
+ return;
+ }
+ genericRequest('AssistentChat', payload, ok, 0, fail);
+ });
+ }
+
+ /**
+ * Run compress_history via the same chat model. Does not nest sendChat.
+ * @returns {Promise} true if memory updated
+ */
+ async function runCompressTurn({ uiCollapsed = false, chatEpoch = state.chatEpoch } = {}) {
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const fold = C?.messagesToFold
+ ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS)
+ : [];
+ if (!fold.length) {
+ return false;
+ }
+ const model = $('sa_model')?.value;
+ if (!model) {
+ setStatus('Выбери модель Ollama в ⚙');
+ return false;
+ }
+ const { prompt, foldCount } = buildCompressUserPrompt();
+ if (!foldCount) {
+ return false;
+ }
+ state.compressing = true;
+ updateCtxChip();
+ setBusyPhase('compressing');
+ setStatus('Сжимаю контекст…');
+ const persona = $('sa_persona')?.value || 'neutral';
+ const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
+ const payload = {
+ baseUrl,
+ model,
+ pack: 'compress_history',
+ persona,
+ includeBase: false,
+ messages: [{ role: 'user', content: prompt }],
+ context_json: JSON.stringify({
+ compress: true,
+ previous_memory: mem.summary || null,
+ fold_count: foldCount,
+ }),
+ skills: [],
+ embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
+ };
+ try {
+ const data = await callOllamaOnce(payload);
+ if (chatEpoch !== state.chatEpoch) {
+ return false;
+ }
+ if (data.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(data.prompt_eval_count) || null;
+ } else if (data.raw?.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(data.raw.prompt_eval_count) || null;
+ }
+ const reply = String(data.reply || '').trim();
+ if (!reply) {
+ setStatus('Сжатие: пустой ответ модели');
+ return false;
+ }
+ applyCompressResult(reply, foldCount, { uiCollapsed });
+ if (uiCollapsed) {
+ renderHistoryIntoUi(state.history);
+ }
+ setStatus('Контекст сжат');
+ return true;
+ } catch (e) {
+ console.warn('Assistent compress failed', e);
+ setStatus(`Сжатие не удалось: ${e.message || e}`);
+ return false;
+ } finally {
+ state.compressing = false;
+ updateCtxChip();
+ }
+ }
+
+ function maybeAutoCompressBeforeSend(chatEpoch) {
+ if (!COMPRESS_AUTO) {
+ return Promise.resolve(false);
+ }
+ const C = ctxApi();
+ const mem = getContextMemory();
+ const msgs = assembleOutgoingMessages();
+ const budget = currentBudgetEstimate(msgs);
+ const should = C?.shouldCompress
+ ? C.shouldCompress(budget, mem, (state.history || []).filter((m) => m && !m.systemish).length, {
+ keepMessages: historyMessageLimit(),
+ })
+ : false;
+ if (!should) {
+ return Promise.resolve(false);
+ }
+ return runCompressTurn({ uiCollapsed: false, chatEpoch });
+ }
+
+ function formatCtxChipLabel(budget) {
+ const C = ctxApi();
+ const used = C?.formatTokenShort ? C.formatTokenShort(budget.used) : String(budget.used || 0);
+ const cap = C?.formatTokenShort ? C.formatTokenShort(budget.numCtx) : String(budget.numCtx || 0);
+ return `${used} / ${cap}`;
+ }
+
+ function updateCtxChip() {
+ const chip = $('sa_ctx_chip');
+ if (!chip) {
+ return;
+ }
+ const budget = currentBudgetEstimate();
+ const mem = getContextMemory();
+ const label = formatCtxChipLabel(budget);
+ const textEl = chip.querySelector('.sa-ctx-chip-text');
+ if (textEl) {
+ textEl.textContent = label;
+ } else {
+ chip.textContent = label;
+ }
+ chip.classList.remove('sa-ctx-ok', 'sa-ctx-warn', 'sa-ctx-hot', 'sa-ctx-compressing', 'sa-ctx-has-mem');
+ if (state.compressing) {
+ chip.classList.add('sa-ctx-compressing');
+ } else {
+ chip.classList.add(`sa-ctx-${budget.level || 'ok'}`);
+ }
+ if (mem.summary) {
+ chip.classList.add('sa-ctx-has-mem');
+ }
+ const src = budget.fromEval ? 'факт Ollama' : 'оценка';
+ chip.title = `Контекст модели · ${src}${mem.summary ? ' · есть саммари' : ''}`;
+ const dot = chip.querySelector('.sa-ctx-dot');
+ if (dot) {
+ dot.hidden = !mem.summary;
+ }
+ }
+
+ function toggleCtxPanel(force) {
+ const panel = $('sa_ctx_panel');
+ const chip = $('sa_ctx_chip');
+ if (!panel || !chip) {
+ return;
+ }
+ const open = force != null ? !!force : !state.ctxPanelOpen;
+ state.ctxPanelOpen = open;
+ panel.hidden = !open;
+ chip.setAttribute('aria-expanded', open ? 'true' : 'false');
+ if (open) {
+ renderCtxPanel();
+ }
+ }
+
+ function renderCtxPanel() {
+ const body = $('sa_ctx_panel_body');
+ const bar = $('sa_ctx_bar_fill');
+ const auto = $('sa_ctx_auto');
+ if (!body) {
+ return;
+ }
+ const budget = currentBudgetEstimate();
+ const mem = getContextMemory();
+ const layers = state.lastSystemLayers || {};
+ const layerRows = Object.entries(layers)
+ .filter(([k]) => k !== 'total')
+ .map(([k, v]) => `${escapeHtml(k)} ${Number(v) || 0}
`)
+ .join('');
+ const keep = HISTORY_KEEP_TURNS;
+ const uncovered = Math.max(0, (state.history || []).filter((m) => m && !m.systemish).length - (mem.untilCount || 0));
+ body.innerHTML = `
+ ${budget.fromEval ? 'Токены (prompt_eval)' : 'Оценка токенов'} · порог ${budget.threshold || '—'}
+ system ${budget.systemChars || 0}
+ history ${budget.historyChars || 0}
+ memory ${budget.memoryChars || 0}
+ ${layerRows ? `system_layers
${layerRows}` : ''}
+ Модель видит: ${mem.summary ? `саммари (${mem.foldedTurns || 0} ходов) +` : ''} последние ${keep} ходов · сырых в окне ≈ ${Math.min(uncovered, historyMessageLimit())}
+ ${mem.summary ? `${escapeHtml(mem.summary.slice(0, 800))}${mem.summary.length > 800 ? '…' : ''} ` : 'Саммари ещё нет — старые ходы просто отбрасываются.
'}
+ `;
+ if (bar) {
+ const pct = Math.max(0, Math.min(100, (budget.used / (budget.numCtx || 1)) * 100));
+ bar.style.width = `${pct}%`;
+ bar.dataset.level = budget.level || 'ok';
+ }
+ if (auto) {
+ auto.checked = !!COMPRESS_AUTO;
+ }
+ updateCtxChip();
+ }
+
+ async function compressNowFromUi() {
+ if (state.busy || state.generating || state.compressing) {
+ setStatus('Занято — дождись конца ответа');
+ return;
+ }
+ const model = $('sa_model')?.value;
+ if (!model) {
+ setStatus('Выбери модель Ollama в ⚙');
+ return;
+ }
+ state.busy = true;
+ setInterruptVisible(true);
+ startBusyUi('compressing');
+ const epoch = state.chatEpoch;
+ try {
+ const ok = await runCompressTurn({ uiCollapsed: true, chatEpoch: epoch });
+ if (!ok) {
+ setStatus('Нечего сжимать (хвост ≤ keep)');
+ }
+ } finally {
+ if (epoch === state.chatEpoch) {
+ state.busy = false;
+ setInterruptVisible(state.generating);
+ stopBusyUi(getContextMemory().summary ? 'Контекст сжат' : 'Готово');
+ }
+ updateCtxChip();
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
+ }
+
+ function resetCompressionFromUi() {
+ resetContextMemory();
+ renderHistoryIntoUi(state.history);
+ setStatus('Сжатие сброшено — модель снова видит только last-K');
+ if (state.ctxPanelOpen) {
+ renderCtxPanel();
+ }
+ }
+
function flashImagePane(slotId) {
const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`);
if (!el) {
@@ -1884,10 +2212,11 @@
const note = document.createElement('div');
note.className = 'sa-patch-actions sa-patch-silent sa-patch-current';
const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')
+ || !!patch.generate
|| !!state.pendingSilentGen;
note.textContent = willGen
- ? 'Применено автоматически · Generate…'
- : 'Применено автоматически';
+ ? 'В сессию · Generate…'
+ : 'В сессию';
host.appendChild(note);
return;
}
@@ -1906,46 +2235,66 @@
btn.addEventListener('click', () => applyPatch(patch, which));
actions.appendChild(btn);
}
+ const toSession = document.createElement('button');
+ toSession.type = 'button';
+ toSession.className = 'basic-button';
+ toSession.textContent = 'В сессию';
+ toSession.addEventListener('click', async () => {
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
+ }
+ await pushSessionToSwarm(state.chatSession);
+ rememberLastPatch(patch);
+ try {
+ const chat = findChat(state.activeChatId);
+ if (chat) {
+ chat.params = snapshotChatParams();
+ persistChatsStore();
+ }
+ } catch (e) { /* ignore */ }
+ setStatus('Патч в сессии');
+ });
+ actions.appendChild(toSession);
const genBtn = document.createElement('button');
genBtn.type = 'button';
genBtn.className = 'basic-button sa-btn-gen';
- genBtn.textContent = 'Применить + Generate';
+ genBtn.textContent = 'Сгенерировать';
genBtn.addEventListener('click', async () => {
if (isGenerateUnavailable()) {
return;
}
startBusyUi('silent_gen');
- await applyPatch(patch, 'all');
- await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true });
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
+ }
+ await pushSessionToSwarm(state.chatSession);
+ await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true, fromSession: true });
});
actions.appendChild(genBtn);
host.appendChild(actions);
syncPatchActionAvailability();
}
-
async function buildCurrentAndGenerate() {
if (state.busy || state.generating) {
setStatus('Занято — подожди или нажми Стоп');
return;
}
- if (isGenerateUnavailable()) {
+ if (typeof isGenerateUnavailable === 'function' && isGenerateUnavailable()) {
setStatus('Generate недоступен — дождись SwarmUI');
return;
}
- const patch = state.lastPatch;
- if (patch) {
- startBusyUi('silent_gen');
- setStatus('Собираю патч → Generate…');
- await applyPatch(patch, 'all');
- syncLiveParamsBar();
- await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true });
- return;
+ pullLiveIntoSession();
+ const S = window.SA && window.SA.session;
+ if (state.lastPatch && S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch);
}
- startBusyUi('generating');
- setStatus('Generate с текущим промптом…');
- await runGenerateFromPatch({ actions: ['generate'] }, { force: true });
+ if (typeof startBusyUi === 'function') startBusyUi(state.lastPatch ? 'silent_gen' : 'generating');
+ setStatus(state.lastPatch ? 'Сессия → Generate…' : 'Generate с текущей сессией…');
+ await pushSessionToSwarm(state.chatSession);
+ await runGenerateFromPatch({ actions: ['generate'] }, { force: true, fromSession: true });
}
-
function renderBoard() {
const board = $('sa_board');
if (!board) {
@@ -2442,7 +2791,8 @@
return t ? t.slice(0, 52) : 'Новый чат';
}
- function snapshotChatParams() {
+
+ function readLiveGenFields() {
let loras = [];
try {
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
@@ -2452,20 +2802,19 @@
}));
}
} catch (e) { /* ignore */ }
- let lastPatch = null;
+ let checkpoint = null;
try {
- lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null;
- } catch (e) {
- lastPatch = null;
- }
- let sessionExact = {};
- try {
- sessionExact = state.sessionExact && typeof state.sessionExact === 'object'
- ? JSON.parse(JSON.stringify(state.sessionExact))
- : {};
- } catch (e) {
- sessionExact = {};
- }
+ if (typeof resolveCurrentCheckpoint === 'function') {
+ const m = resolveCurrentCheckpoint();
+ if (m && (m.name || m.title)) {
+ checkpoint = {
+ name: m.name || m.title || null,
+ architecture: m.architecture || m.compat_class || m.class || null,
+ title: m.title || null,
+ };
+ }
+ }
+ } catch (e) { /* ignore */ }
return {
prompt: val('alt_prompt_textbox') || val('input_prompt') || '',
negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '',
@@ -2479,124 +2828,112 @@
scheduler: val('input_scheduler') || null,
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
loras,
- persona: $('sa_persona')?.value || 'neutral',
- pack: $('sa_pack')?.value || defaultPackId(),
- sessionExact,
- lastPatch,
+ checkpoint,
+ };
+ }
+
+ function boardSnapshotForSession() {
+ if (typeof ensureBoard === 'function') ensureBoard();
+ return {
+ slots: (state.slots || []).map((s) => ({
+ id: s.id, type: s.type, label: s.label,
+ src: s.src || null, attach: !!s.attach, note: s.note || null,
+ })),
+ selectedSlotId: state.selectedSlotId || 'ref1',
genResults: Array.isArray(state.genResults)
? state.genResults.map((r) => ({
- id: r.id,
- label: r.label,
- src: r.src || null,
- patch: r.patch || null,
+ id: r.id, label: r.label, src: r.src || null, patch: r.patch || null,
}))
: [],
selectedGenResultId: state.selectedGenResultId || null,
+ refSeq: state.refSeq || 1,
};
}
+ function pullLiveIntoSession() {
+ const S = window.SA && window.SA.session;
+ if (!S || typeof S.snapshotFromLive !== 'function') return state.chatSession;
+ state.chatSession = S.snapshotFromLive({
+ genFields: readLiveGenFields(),
+ board: boardSnapshotForSession(),
+ persona: $('sa_persona')?.value || 'neutral',
+ pack: $('sa_pack')?.value || 'ordinary',
+ context_memory: getContextMemory(),
+ });
+ return state.chatSession;
+ }
+
+ async function pushSessionToSwarm(session) {
+ const S = window.SA && window.SA.session;
+ const sess = session || state.chatSession || (S && S.emptySession && S.emptySession());
+ if (!sess || !sess.gen) return;
+ if (typeof applyPatch === 'function') await applyPatch({ ...sess.gen }, 'all');
+ try {
+ const name = sess.gen.checkpoint?.name
+ || (typeof sess.gen.checkpoint === 'string' ? sess.gen.checkpoint : null);
+ if (name && typeof currentModelHelper !== 'undefined' && currentModelHelper?.setModel) {
+ currentModelHelper.setModel(name);
+ }
+ } catch (e) { /* ignore */ }
+ if (sess.pack && typeof setPackValue === 'function') setPackValue(sess.pack, { flash: false });
+ if (sess.persona && typeof applyPersonaForChat === 'function') {
+ await applyPersonaForChat(sess.persona, { quiet: true });
+ }
+ state.chatSession = sess;
+ }
+
+ function snapshotChatParams() {
+ const S = window.SA && window.SA.session;
+ const session = pullLiveIntoSession();
+ if (S && typeof S.toPersistParams === 'function') return S.toPersistParams(session);
+ return session || {};
+ }
+
async function restoreChatParams(params) {
state.restoringChat = true;
try {
- // Always reset chat-scoped state so previous chat cannot leak.
state.sessionExact = {};
state.lastPatch = null;
state.lastUserParamIntent = false;
-
+ const S = window.SA && window.SA.session;
+ state.chatSession = S && typeof S.sessionFromLegacyParams === 'function'
+ ? S.sessionFromLegacyParams(params)
+ : (params && params.gen ? params : (S && S.emptySession ? S.emptySession() : { gen: {}, board: {} }));
if (!params || typeof params !== 'object') {
- clearGenResults();
- syncBuildGenButton();
- syncLiveParamsBar();
- syncModeBadge();
- renderBoard();
+ if (typeof clearGenResults === 'function') clearGenResults();
+ if (typeof renderBoard === 'function') renderBoard();
+ setContextMemory(null, { persist: false });
return { restored: false };
}
-
- const promptBox = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
- if (promptBox) {
- promptBox.value = params.prompt != null ? String(params.prompt) : '';
- promptBox.dispatchEvent(new Event('input', { bubbles: true }));
- promptBox.dispatchEvent(new Event('change', { bubbles: true }));
+ await pushSessionToSwarm(state.chatSession);
+ const board = state.chatSession.board || {};
+ if (Array.isArray(board.slots) && board.slots.length) {
+ state.slots = board.slots.map((s) => ({ ...s }));
+ if (board.selectedSlotId) state.selectedSlotId = board.selectedSlotId;
+ if (board.refSeq) state.refSeq = board.refSeq;
}
- setVal('input_negativeprompt', params.negative != null ? String(params.negative) : '');
- if (document.getElementById('alt_negativeprompt_textbox')) {
- setVal('alt_negativeprompt_textbox', params.negative != null ? String(params.negative) : '');
- }
- // Force-write numerics when present so prior chat values cannot stick.
- if (params.width != null) {
- setVal('input_width', String(params.width));
- }
- if (params.height != null) {
- setVal('input_height', String(params.height));
- }
- if (params.steps != null) {
- setVal('input_steps', String(params.steps));
- }
- if (params.cfg != null) {
- if (document.getElementById('input_cfgscale')) {
- setVal('input_cfgscale', String(params.cfg));
- } else {
- setVal('input_cfg', String(params.cfg));
- }
- }
- if (params.sigma_shift != null) {
- setVal('input_sigmashift', String(params.sigma_shift));
- }
- if (params.seed != null && params.seed !== '') {
- setVal('input_seed', String(params.seed));
- }
- if (params.sampler) {
- setVal('input_sampler', String(params.sampler));
- }
- if (params.scheduler) {
- setVal('input_scheduler', String(params.scheduler));
- }
- if (params.batch != null) {
- if (document.getElementById('input_images')) {
- setVal('input_images', String(params.batch));
- } else if (document.getElementById('input_batchsize')) {
- setVal('input_batchsize', String(params.batch));
- }
- }
-
- const loras = Array.isArray(params.loras) ? params.loras : [];
- await applyPatch({ loras }, 'loras');
-
- if (params.pack) {
- setPackValue(params.pack, { flash: false });
- }
- if (params.persona) {
- await applyPersonaForChat(params.persona, { quiet: true });
- }
- state.sessionExact = params.sessionExact && typeof params.sessionExact === 'object'
- ? { ...params.sessionExact }
- : {};
- state.lastPatch = params.lastPatch || null;
- if (Array.isArray(params.genResults) && params.genResults.length) {
- state.genResults = params.genResults.map((r, i) => ({
- id: r.id || `var${i + 1}`,
- label: r.label || `Вариант ${i + 1}`,
+ if (Array.isArray(board.genResults) && board.genResults.length) {
+ state.genResults = board.genResults.map((r, i) => ({
+ id: r.id || ('var' + (i + 1)),
+ label: r.label || ('Вариант ' + (i + 1)),
src: r.src || null,
patch: r.patch || null,
}));
- state.selectedGenResultId = params.selectedGenResultId
- || state.genResults.find((r) => r.src)?.id
- || state.genResults[0]?.id
- || null;
- const selected = state.genResults.find((r) => r.id === state.selectedGenResultId);
- const gen = generateSlot();
- if (gen && selected?.src) {
- gen.src = selected.src;
- }
- } else {
+ state.selectedGenResultId = board.selectedGenResultId
+ || state.genResults.find((x) => x.src)?.id
+ || state.genResults[0]?.id || null;
+ const selected = state.genResults.find((x) => x.id === state.selectedGenResultId);
+ const gen = typeof generateSlot === 'function' ? generateSlot() : null;
+ if (gen && selected?.src) gen.src = selected.src;
+ } else if (typeof clearGenResults === 'function') {
clearGenResults();
}
- syncBuildGenButton();
- syncLiveParamsBar();
- syncModeBadge();
- renderLoraChips();
- syncChipHighlight();
- renderBoard();
+ if (typeof syncBuildGenButton === 'function') syncBuildGenButton();
+ if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
+ if (typeof syncModeBadge === 'function') syncModeBadge();
+ if (typeof renderLoraChips === 'function') renderLoraChips();
+ if (typeof renderBoard === 'function') renderBoard();
+ setContextMemory(state.chatSession?.context_memory || null);
return { restored: true };
} finally {
state.restoringChat = false;
@@ -2797,9 +3134,36 @@
const list = slimHistoryMessages(messages);
if (!list.length) {
resetMessagesUi();
+ updateCtxChip();
return;
}
- for (const m of list) {
+ const mem = getContextMemory();
+ let start = 0;
+ if (mem.uiCollapsed && mem.summary && mem.untilCount > 0) {
+ const folded = list.slice(0, Math.min(mem.untilCount, list.length));
+ start = folded.length;
+ const details = document.createElement('details');
+ details.className = 'sa-msg sa-msg-compress';
+ const summary = document.createElement('summary');
+ summary.textContent = `Сжатый контекст · ${mem.foldedTurns || Math.floor(folded.length / 2)} ходов`;
+ details.appendChild(summary);
+ const body = document.createElement('div');
+ body.className = 'sa-msg-compress-body';
+ const pre = document.createElement('pre');
+ pre.className = 'sa-ctx-summary';
+ pre.textContent = mem.summary;
+ body.appendChild(pre);
+ for (const m of folded) {
+ const row = document.createElement('div');
+ row.className = `sa-msg-compress-row sa-msg-compress-${m.role}`;
+ row.textContent = `${m.role === 'assistant' ? 'Ассистент' : 'Вы'}: ${String(m.content || '').slice(0, 500)}`;
+ body.appendChild(row);
+ }
+ details.appendChild(body);
+ box.appendChild(details);
+ }
+ for (let i = start; i < list.length; i++) {
+ const m = list[i];
if (m.role === 'user') {
appendMessage('user', m.content, null, null, { historical: true });
} else {
@@ -2810,6 +3174,7 @@
});
}
}
+ updateCtxChip();
}
function updateSessionLabel() {
@@ -2842,8 +3207,25 @@
if (!ts) {
return '';
}
+ const t = Number(ts) || 0;
+ if (!t) {
+ return '';
+ }
+ const sec = Math.max(0, Math.floor((Date.now() - t) / 1000));
+ if (sec < 45) {
+ return 'сейчас';
+ }
+ if (sec < 3600) {
+ return `${Math.max(1, Math.floor(sec / 60))}м`;
+ }
+ if (sec < 86400) {
+ return `${Math.floor(sec / 3600)}ч`;
+ }
+ if (sec < 86400 * 14) {
+ return `${Math.floor(sec / 86400)}д`;
+ }
try {
- return new Date(ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+ return new Date(t).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
} catch (e) {
return '';
}
@@ -2866,6 +3248,24 @@
return false;
}
+ function chatListParamsBits(params) {
+ if (!params || typeof params !== 'object') {
+ return '';
+ }
+ const g = params.gen && typeof params.gen === 'object' ? params.gen : params;
+ const bits = [];
+ if (g.width && g.height) {
+ bits.push(`${g.width}×${g.height}`);
+ } else if (g.aspect) {
+ bits.push(String(g.aspect));
+ }
+ const loras = Array.isArray(g.loras) ? g.loras : [];
+ if (loras.length) {
+ bits.push(`LoRA ${loras.length}`);
+ }
+ return bits.join(' · ');
+ }
+
function renderChatsList() {
const root = $('sa_chats_list');
if (!root) {
@@ -2887,29 +3287,20 @@
if (!chats.length) {
root.innerHTML = q
? 'Ничего не нашлось.
'
- : 'Пока пусто. Напиши что-нибудь в чат — он появится здесь. Переключение вернёт и сообщения, и параметры Generate.
';
+ : 'Пока пусто — напиши в чат, и он появится здесь.
';
return;
}
+ const ico = ' ';
for (const c of chats) {
const row = document.createElement('div');
row.className = 'sa-chat-row' + (c.id === state.activeChatId ? ' sa-chat-row-active' : '');
row.dataset.id = c.id;
- const n = (c.messages || []).length || Number(c.messages_count) || 0;
- const bits = [];
- if (c.params?.width && c.params?.height) {
- bits.push(`${c.params.width}×${c.params.height}`);
- }
- if (c.params?.steps != null) {
- bits.push(`steps ${c.params.steps}`);
- }
- if (c.params?.cfg != null) {
- bits.push(`cfg ${c.params.cfg}`);
- }
- if (Array.isArray(c.params?.loras) && c.params.loras.length) {
- bits.push(`LoRA ${c.params.loras.length}`);
- }
- const noParams = !c.params ? ' · без снимка params' : '';
- row.innerHTML = `${escapeHtml(c.title || 'Чат')} ${escapeHtml([formatChatWhen(c.updatedAt), `${n} сообщ.`, bits.join(' · ')].filter(Boolean).join(' · ') + noParams)} × `;
+ row.setAttribute('role', 'listitem');
+ const when = formatChatWhen(c.updatedAt);
+ const title = escapeHtml(c.title || 'Новый чат');
+ const tipBits = [chatListParamsBits(c.params)].filter(Boolean);
+ const tip = tipBits.length ? ` title="${escapeHtml(tipBits.join(' · '))}"` : '';
+ row.innerHTML = `${ico}${title} ${escapeHtml(when)} × `;
root.appendChild(row);
}
}
@@ -2974,6 +3365,7 @@
try { state.streamEl.remove(); } catch (e) { /* ignore */ }
state.streamEl = null;
}
+ resetContextMemory({ persist: false });
syncBuildGenButton();
resetMessagesUi();
persistChatsStore();
@@ -2981,6 +3373,7 @@
renderBoard();
syncHistoryBadge();
renderChatsList();
+ updateCtxChip();
setStatus('Новый чат — параметры Generate как сейчас');
maybeWelcome();
}
@@ -3026,6 +3419,7 @@
try { state.streamEl.remove(); } catch (e) { /* ignore */ }
state.streamEl = null;
}
+ setContextMemory(chat.params?.context_memory || null, { persist: false });
renderHistoryIntoUi(state.history);
const result = await restoreChatParams(chat.params);
updateSessionLabel();
@@ -3124,11 +3518,13 @@
clearGenResults();
syncBuildGenButton();
clearPersistedHistory();
+ resetContextMemory({ persist: false });
resetMessagesUi('Чат очищен
Сообщения сброшены. Параметры Generate на месте. + — новый чат в Историю, История — прошлые диалоги.
');
setStatus('Чат очищен');
updateSessionLabel();
syncHistoryBadge();
renderBoard();
+ updateCtxChip();
}
function hideSlashMenu() {
@@ -3238,8 +3634,9 @@
renderMemoryList();
}
if (state.settingsTab === 'more') {
- fillKnobsFromConfig(data);
- }
+ fillKnobsFromConfig(data);
+ updateCtxChip();
+ }
}
});
}
@@ -3430,230 +3827,34 @@
}
function collectLiveContext() {
- const inv = state.inventory || {};
- const initCtx = readInitContext();
- const ctx = {
- architecture_ok: isKreaSelected(),
- checkpoint: null,
- prompt: slimPromptForContext(val('alt_prompt_textbox') || val('input_prompt') || ''),
- negative: slimPromptForContext(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || ''),
- width: parseInt(val('input_width') || '0', 10) || null,
- height: parseInt(val('input_height') || '0', 10) || null,
- steps: parseInt(val('input_steps') || '0', 10) || null,
- cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null,
- sigma_shift: parseFloat(val('input_sigmashift') || '') || null,
- seed: val('input_seed') || null,
- sampler: val('input_sampler') || val('input_samplerate') || null,
- scheduler: val('input_scheduler') || null,
- batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
- prompt_image_count: countPromptImages(),
- selected_loras: [],
- available_loras: [],
- available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 8),
- wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 20),
- inventory_at: inv.inventory_at || null,
- has_vision_image: visionReadySlots().length > 0,
- image_slots: slotCatalog(),
- attached_slot_ids: attachableSlots().map((s) => s.id),
- gen_results: (state.genResults || []).map((r) => ({
- id: r.id,
- label: r.label,
- has_image: !!r.src,
- selected: r.id === state.selectedGenResultId,
- })),
- selected_gen_result: state.selectedGenResultId || null,
- has_civitai_key: !!inv.has_civitai_key,
+ pullLiveIntoSession();
+ const S = window.SA && window.SA.session;
+ let initCtx = {};
+ try { if (typeof readInitContext === 'function') initCtx = readInitContext(); } catch (e) { /* ignore */ }
+ const extra = {
+ prompt_image_count: typeof countPromptImages === 'function' ? countPromptImages() : 0,
+ has_vision_image: typeof visionReadySlots === 'function' ? visionReadySlots().length > 0 : false,
+ image_slots: typeof slotCatalog === 'function' ? slotCatalog() : [],
+ attached_slot_ids: typeof attachableSlots === 'function' ? attachableSlots().map((s) => s.id) : [],
auto_apply: !!$('sa_auto_apply')?.checked,
- auto_generate: !!$('sa_auto_generate')?.checked,
- persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
- model_cards: [],
- user_prefs_count: 0,
+ auto_generate: !false /* auto_generate ignored 0.14 */,
...initCtx,
};
- {
- const slim = slimInventoryLoras(inv.loras || [], INVENTORY_PROMPT_NAMES);
- ctx.available_loras = slim;
- if ((inv.loras || []).length > slim.length) {
- ctx.available_loras_truncated = true;
- ctx.available_loras_total = (inv.loras || []).length;
+ if (S && typeof S.compactContext === 'function') {
+ const ctx = S.compactContext(state.chatSession, {
+ architecture_ok: typeof isKreaSelected === 'function' ? isKreaSelected() : true,
+ promptMax: typeof CONTEXT_PROMPT_MAX !== 'undefined' ? CONTEXT_PROMPT_MAX : 2000,
+ extra,
+ });
+ const block = ctxApi()?.conversationMemoryBlock?.(getContextMemory());
+ if (block) {
+ ctx.conversation_memory = block;
}
+ return ctx;
}
-
- try {
- const model = resolveCurrentCheckpoint();
- if (model.name || model.architecture) {
- ctx.checkpoint = {
- name: model.name || model.title || null,
- architecture: model.architecture || model.compat_class || model.class || null,
- title: model.title || null,
- };
- }
- } catch (e) { /* ignore */ }
-
- try {
- if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
- const byName = new Map();
- for (const l of inv.loras || []) {
- if (l?.name) {
- byName.set(String(l.name).toLowerCase(), l);
- }
- }
- ctx.selected_loras = loraHelper.selected.map((l) => {
- const name = l.name || l;
- const invRow = byName.get(String(name).toLowerCase()) || {};
- const out = {
- name,
- weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[name]) || invRow.default_weight || 1,
- };
- if (invRow.trigger_phrase) {
- out.trigger_phrase = invRow.trigger_phrase;
- }
- if (Array.isArray(invRow.triggers) && invRow.triggers.length) {
- out.triggers = invRow.triggers.slice(0, 8);
- }
- if (invRow.blurb) {
- out.blurb = invRow.blurb;
- }
- return out;
- });
- // selected_loras = enabled; do not also emit enabled_loras (duplicate).
- }
- } catch (e) { /* ignore */ }
-
- // Recommendation cards: checkpoint + selected LoRAs only when they add beyond inventory.
- const cardKeys = [];
- const seenCard = new Set();
- const addKey = (kind, name) => {
- if (!kind || !name) {
- return;
- }
- const key = `${kind}:${name}`;
- if (seenCard.has(key)) {
- return;
- }
- seenCard.add(key);
- cardKeys.push({ kind, name });
- };
- if (ctx.checkpoint?.name) {
- addKey('checkpoint', ctx.checkpoint.name);
- }
- for (const l of ctx.selected_loras || []) {
- if (l?.name) {
- addKey('lora', l.name);
- }
- }
- for (const k of cardKeys) {
- const cached = state.modelCards[`${k.kind}:${k.name}`];
- if (!cached) {
- continue;
- }
- const slim = slimCardForContext(cached);
- if (!slim) {
- continue;
- }
- if (k.kind === 'lora') {
- const sel = (ctx.selected_loras || []).find(
- (l) => String(l.name || '').toLowerCase() === String(k.name).toLowerCase(),
- );
- const inventoryRich = !!(sel && (sel.triggers?.length || sel.trigger_phrase || sel.blurb));
- const cardExtra = !!(slim.when || slim.avoid || slim.prompt_hint || slim.notes);
- if (inventoryRich && !cardExtra) {
- continue;
- }
- }
- ctx.model_cards.push(slim);
- }
-
- // Fallback if inventory empty
- if (!ctx.available_loras.length) {
- try {
- const models = (typeof allModels !== 'undefined' && allModels) || (typeof model_list !== 'undefined' && model_list) || [];
- const list = Array.isArray(models) ? models : Object.values(models || {});
- for (const m of list) {
- if (!m) {
- continue;
- }
- const folder = `${m.folder || m.path || ''}`;
- const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
- const inLoraFolder = /lora/i.test(folder);
- if (!(isLora || inLoraFolder)) {
- continue;
- }
- ctx.available_loras.push({
- name: m.name || m.title,
- title: m.title || m.name,
- trigger_phrase: m.trigger_phrase || m.trigger || (m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger)) || null,
- architecture: m.architecture || null,
- });
- }
- if (ctx.available_loras.length > INVENTORY_PROMPT_NAMES) {
- ctx.available_loras = ctx.available_loras.slice(0, INVENTORY_PROMPT_NAMES);
- }
- } catch (e) { /* ignore */ }
- }
-
- try {
- const model = ctx.checkpoint || {};
- const blob = `${model.name || ''} ${model.title || ''}`.toLowerCase();
- const hasRaw = /\braw\b/.test(blob);
- const hasTurbo = /\bturbo\b/.test(blob);
- const profile = hasRaw && !hasTurbo ? 'raw' : 'turbo';
- ctx.krea_profile = profile;
- const defaults = mergedGenerationDefaults(profile);
- // Only send recommended_params when live UI differs from Exact-backed defaults
- // (Exact itself is already in the system prompt).
- const rec = {
- steps: defaults.steps ?? 8,
- cfg: defaults.cfg ?? 1,
- sigma_shift: defaults.sigma_shift ?? 1.15,
- };
- if (defaults.aspect) {
- rec.aspect = defaults.aspect;
- }
- const liveDiffers = (ctx.steps != null && ctx.steps !== rec.steps)
- || (ctx.cfg != null && ctx.cfg !== rec.cfg)
- || (ctx.sigma_shift != null && ctx.sigma_shift !== rec.sigma_shift);
- if (liveDiffers) {
- ctx.recommended_params = rec;
- }
- } catch (e) {
- ctx.krea_profile = 'turbo';
- }
-
- ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length
- ? { ...state.sessionExact }
- : undefined;
- // Exact KV is already in the system prompt — do not duplicate the full blob into live context.
- if (!ctx.session_exact) {
- delete ctx.session_exact;
- }
-
- return ctx;
+ const g = (state.chatSession && state.chatSession.gen) || {};
+ return { session: true, prompt: g.prompt || '', negative: g.negative || '', ...extra };
}
-
- function slimCardForContext(card) {
- if (!card || typeof card !== 'object') {
- return null;
- }
- const out = {
- kind: card.kind || null,
- name: card.name || null,
- triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : undefined,
- weight: card.weight != null ? card.weight : undefined,
- when: card.when ? String(card.when).slice(0, 160) : undefined,
- avoid: card.avoid ? String(card.avoid).slice(0, 120) : undefined,
- prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : undefined,
- notes: card.notes ? String(card.notes).slice(0, 200) : undefined,
- };
- const clean = {};
- for (const [k, v] of Object.entries(out)) {
- if (v != null && v !== '') {
- clean[k] = v;
- }
- }
- return clean;
- }
-
function slimInventoryLoras(list, limit) {
const selected = new Set();
try {
@@ -3768,50 +3969,6 @@
return next.slice(0, max);
}
- function isCardObject(obj) {
- if (window.SA && typeof SA.isCardObject === 'function') {
- return SA.isCardObject(obj);
- }
- if (!obj || typeof obj !== 'object') {
- return false;
- }
- // Prefer card shape over gen patch when both could match.
- const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
- const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
- || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null
- || obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
- if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
- return true;
- }
- return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
- }
-
- function extractCardJson(text) {
- if (!text) {
- return null;
- }
- const re = /```(?:json)?\s*([\s\S]*?)```/gi;
- let match;
- let last = null;
- while ((match = re.exec(text)) !== null) {
- try {
- const obj = JSON.parse(match[1].trim());
- if (isCardObject(obj)) {
- last = obj;
- }
- } catch (e) { /* ignore */ }
- }
- if (last) {
- return last;
- }
- try {
- const obj = JSON.parse(text.trim());
- return isCardObject(obj) ? obj : null;
- } catch (e) {
- return null;
- }
- }
-
function extractPatch(text) {
if (window.SA && typeof SA.extractPatch === 'function') {
return SA.extractPatch(text);
@@ -4638,8 +4795,21 @@
}
async function runGenerateFromPatch(patch, opts = {}) {
+ // 0.14: session is source of truth for Generate
+ if (typeof pullLiveIntoSession === 'function') pullLiveIntoSession();
+ if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) {
+ const fromSess = { ...state.chatSession.gen, generate: true, actions: ['generate'] };
+ if (patch && typeof patch === 'object') {
+ for (const k of Object.keys(patch)) {
+ if (patch[k] != null) fromSess[k] = patch[k];
+ }
+ }
+ patch = fromSess;
+ if (typeof pushSessionToSwarm === 'function') await pushSessionToSwarm(state.chatSession);
+ }
+
const force = !!opts.force;
- if ((!force && !$('sa_auto_generate')?.checked) || !patchHasGenTrigger(patch)) {
+ if ((!force && false /* auto_generate ignored 0.14 */) || !patchHasGenTrigger(patch)) {
return null;
}
@@ -4965,10 +5135,7 @@
const silent = !!(meta && meta.silentPatch);
mountPatchBlock(div, finalPatch, { silent });
}
- if (civitaiResults && civitaiResults.length) {
- div.appendChild(buildCivitaiCards(civitaiResults));
- }
- if (role === 'assistant' && !(meta && meta.historical)) {
+if (role === 'assistant' && !(meta && meta.historical)) {
mountCurateButtons(div, meta);
}
box.appendChild(div);
@@ -5009,7 +5176,7 @@
const obj = JSON.parse(match[1].trim());
const terminal = (window.SA && typeof SA.isTerminalStreamPatch === 'function')
? SA.isTerminalStreamPatch(obj)
- : (isPatchObject(obj) || isCardObject(obj));
+ : (typeof isPatchObject === 'function' && isPatchObject(obj));
if (terminal) {
return true;
}
@@ -5028,7 +5195,7 @@
const obj = JSON.parse(match[1].trim());
const terminal = (window.SA && typeof SA.isTerminalStreamPatch === 'function')
? SA.isTerminalStreamPatch(obj)
- : (isPatchObject(obj) || isCardObject(obj));
+ : (typeof isPatchObject === 'function' && isPatchObject(obj));
if (terminal) {
lastEnd = match.index + match[0].length;
}
@@ -5078,11 +5245,10 @@
}
el.classList.remove('sa-streaming', 'sa-typing');
mountAssistantMeta(el, meta || undefined);
- const card = extractCardJson(fullReply);
- const { prose, patch } = extractPatch(fullReply);
+const { prose, patch } = extractPatch(fullReply);
setAssistantBody(el, prose || fullReply || '');
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
- if (patch && !isCardObject(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
+ if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
mountPatchBlock(el, patch, { silent });
} else if (card) {
@@ -5093,10 +5259,7 @@
wrap.appendChild(pre);
el.appendChild(wrap);
}
- if (civitaiResults && civitaiResults.length) {
- el.appendChild(buildCivitaiCards(civitaiResults));
- }
- if (!(meta && meta.historical)) {
+if (!(meta && meta.historical)) {
mountCurateButtons(el, meta);
}
scrollMessagesToBottom();
@@ -5168,138 +5331,6 @@
return !!state.trainingLock || document.getElementById('swarm_assistent_root')?.classList.contains('sa-root-training-lock');
}
- function buildCivitaiCards(results) {
- const list = document.createElement('div');
- list.className = 'sa-civitai-list';
- for (const r of results) {
- const card = document.createElement('div');
- card.className = 'sa-civitai-card' + (r.already_installed ? ' sa-installed' : '');
- const title = document.createElement('div');
- title.className = 'sa-civitai-title';
- title.textContent = r.name || r.file_name || 'LoRA';
- card.appendChild(title);
- const meta = document.createElement('div');
- meta.className = 'sa-civitai-meta';
- const bits = [
- r.base_model || '?',
- r.krea_likely ? 'Krea?' : null,
- r.already_installed ? 'already installed' : null,
- (r.triggers || []).slice(0, 3).join(', ') || null,
- ].filter(Boolean);
- meta.textContent = bits.join(' · ');
- card.appendChild(meta);
- const actions = document.createElement('div');
- actions.className = 'sa-civitai-actions';
- if (r.already_installed) {
- const note = document.createElement('span');
- note.textContent = 'Уже установлена';
- actions.appendChild(note);
- } else if (r.download_url) {
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'basic-button sa-primary';
- btn.textContent = 'Подтвердить скачивание';
- btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn));
- actions.appendChild(btn);
- } else {
- const note = document.createElement('span');
- note.textContent = 'Нет URL скачивания';
- actions.appendChild(note);
- }
- card.appendChild(actions);
- list.appendChild(card);
- }
- return list;
- }
-
- function downloadCivitaiLoRA(card, btn) {
- if (!card.download_url) {
- return;
- }
- if (btn) {
- btn.disabled = true;
- btn.textContent = 'Скачиваю…';
- }
- setStatus(`Скачиваю ${card.file_name || card.name}…`);
- setInterruptVisible(true);
- const payload = {
- url: card.download_url,
- type: 'LoRA',
- name: card.file_name || card.name || 'lora',
- };
- const onDone = (ok, msg) => {
- setInterruptVisible(state.busy || state.generating);
- if (ok) {
- setStatus(`Скачано ${payload.name}`);
- if (btn) {
- btn.textContent = 'Скачано';
- }
- refreshInventory(async () => {
- await maybeWriteCardAfterDownload({
- kind: 'lora',
- name: payload.name,
- civitai: card,
- });
- }, { rescan: true });
- } else {
- setStatus(msg || 'Ошибка скачивания');
- if (btn) {
- btn.disabled = false;
- btn.textContent = 'Подтвердить скачивание';
- }
- appendMessage('error', msg || 'Ошибка скачивания');
- }
- };
- if (typeof makeWSRequest === 'function') {
- makeWSRequest(
- 'DoModelDownloadWS',
- payload,
- (data) => {
- if (data.error) {
- onDone(false, String(data.error));
- return;
- }
- if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
- if (data.success || data.overall_percent >= 0.99) {
- // Swarm docs: download does not always refresh model list — force both.
- triggerSwarmModelRefresh(() => onDone(true));
- } else if (data.current_percent != null) {
- setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
- }
- }
- },
- 0,
- (err) => onDone(false, String(err || 'Download failed')),
- );
- } else {
- onDone(false, 'makeWSRequest unavailable');
- }
- }
-
- async function maybeWriteCardAfterDownload({ kind, name, civitai }) {
- const display = name || civitai?.file_name || civitai?.name || 'model';
- appendSystemNote(`Downloaded ${display}. Writing a recommendation card…`);
- setPackValue('catalog_card', { flash: true });
- const meta = {
- triggers: civitai?.triggers || [],
- base_model: civitai?.base_model,
- civitai_url: civitai?.url || civitai?.civitai_url,
- version_id: civitai?.version_id || civitai?.modelVersionId,
- name: display,
- };
- if ($('sa_input')) {
- $('sa_input').value = '';
- }
- await sendChat({
- forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`,
- skipSlash: true,
- skipAutoPack: true,
- fromDownload: true,
- fromCards: true,
- cardTarget: { kind: kind || 'lora', name: display, meta },
- });
- }
-
function wantsAutoVision() {
return !!$('sa_auto_vision')?.checked;
}
@@ -5499,7 +5530,7 @@
persona = 'aggressive';
localStorage.setItem(LS_PERSONA, persona);
}
- const view = localStorage.getItem(LS_VIEW);
+ let view = localStorage.getItem(LS_VIEW);
const auto = localStorage.getItem(LS_AUTO_VISION);
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
@@ -5545,7 +5576,8 @@
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
- if (view === 'cards' || view === 'chat' || view === 'settings' || view === 'train') {
+ if (view === 'cards') { view = 'chat'; }
+ if (view === 'chat' || view === 'settings' || view === 'train') {
state.view = view;
}
const drawer = localStorage.getItem(LS_CHATS_DRAWER);
@@ -5565,7 +5597,7 @@
persona: $('sa_persona')?.value || 'neutral',
auto_vision: !!$('sa_auto_vision')?.checked,
auto_apply: !!$('sa_auto_apply')?.checked,
- auto_generate: !!$('sa_auto_generate')?.checked,
+ auto_generate: !false /* auto_generate ignored 0.14 */,
auto_critique: !!$('sa_auto_critique')?.checked,
auto_download: !!$('sa_auto_download')?.checked,
park_llm: !!$('sa_park_llm')?.checked,
@@ -5611,7 +5643,8 @@
fill(LS_PACK, ui.pack, (v) => { if ($('sa_pack')) { $('sa_pack').value = v; } });
fill(LS_PERSONA, ui.persona, (v) => { if ($('sa_persona')) { $('sa_persona').value = v; } });
fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty('--sa-image-width', v));
- if (ui.view === 'cards' || ui.view === 'chat' || ui.view === 'settings' || ui.view === 'train') {
+ if (ui.view === 'cards') { ui.view = 'chat'; }
+ if (ui.view === 'chat' || ui.view === 'settings' || ui.view === 'train') {
fill(LS_VIEW, ui.view, (v) => { state.view = v; });
}
if (ui.board_tab === 'refs' || ui.board_tab === 'generate') {
@@ -5757,6 +5790,15 @@
if (asst.history_keep_turns != null) {
HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4);
}
+ if (asst.compress_at != null) {
+ COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(asst.compress_at) || 0.7));
+ }
+ if (asst.chars_per_token != null) {
+ CHARS_PER_TOKEN = Math.max(1.5, Number(asst.chars_per_token) || 3.2);
+ }
+ if (asst.compress_auto != null) {
+ COMPRESS_AUTO = !!asst.compress_auto;
+ }
if (asst.max_ref_slots != null) {
MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4);
}
@@ -5773,6 +5815,7 @@
INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24);
}
fillKnobsFromConfig(data);
+ updateCtxChip();
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
@@ -6390,9 +6433,9 @@
const n = state.inventory.loras.length;
const ck = state.inventory.checkpoints.length;
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
- prefetchActiveModelCards();
+
if (state.view === 'cards') {
- renderCardsList();
+
}
if (done) {
done(state.inventory);
@@ -6594,7 +6637,7 @@
});
if (state.settingsTab === 'craft') {
refreshMemoryList();
- refreshWantedQueue();
+
}
if (state.settingsTab === 'user') {
refreshUserPrefs();
@@ -6625,6 +6668,12 @@
};
setNum('sa_num_ctx', asst.num_ctx);
setNum('sa_history_keep', asst.history_keep_turns);
+ setNum('sa_compress_at', asst.compress_at != null ? asst.compress_at : COMPRESS_AT);
+ setNum('sa_chars_per_token', asst.chars_per_token != null ? asst.chars_per_token : CHARS_PER_TOKEN);
+ const autoEl = $('sa_compress_auto');
+ if (autoEl) {
+ autoEl.checked = asst.compress_auto != null ? !!asst.compress_auto : COMPRESS_AUTO;
+ }
setNum('sa_memory_top_k', asst.memory_top_k);
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
const weightEl = $('sa_user_prefs_weight');
@@ -6656,6 +6705,9 @@
const assistant = {
num_ctx: num('sa_num_ctx'),
history_keep_turns: num('sa_history_keep'),
+ compress_at: num('sa_compress_at'),
+ chars_per_token: num('sa_chars_per_token'),
+ compress_auto: $('sa_compress_auto') ? !!$('sa_compress_auto').checked : null,
memory_top_k: num('sa_memory_top_k'),
user_prefs_weight: num('sa_user_prefs_weight'),
};
@@ -6689,6 +6741,16 @@
exact: data.exact || state.config?.exact,
});
}
+ if (assistant.compress_at != null) {
+ COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(assistant.compress_at) || 0.7));
+ }
+ if (assistant.chars_per_token != null) {
+ CHARS_PER_TOKEN = Math.max(1.5, Number(assistant.chars_per_token) || 3.2);
+ }
+ if (assistant.compress_auto != null) {
+ COMPRESS_AUTO = !!assistant.compress_auto;
+ }
+ updateCtxChip();
setStatus('Knobs сохранены в overlay');
},
0,
@@ -7075,62 +7137,6 @@
.toLowerCase();
}
- function refreshWantedQueue() {
- if (typeof genericRequest !== 'function') {
- return;
- }
- genericRequest(
- 'AssistentListWanted',
- {},
- (data) => {
- const items = Array.isArray(data?.items) ? data.items : [];
- state.wanted = { count: data?.count ?? items.length, items };
- const keys = new Set();
- for (const item of items) {
- const leaf = modelKeyLeaf(item?.title);
- if (leaf) {
- keys.add(leaf);
- }
- if (item?.version_id) {
- keys.add(`v${item.version_id}`);
- }
- }
- state.wantedKeys = keys;
- const el = $('sa_mem_wanted');
- if (el) {
- el.textContent = state.wanted.count
- ? `Очередь wanted: ${state.wanted.count} (скачается на следующем up)`
- : 'Очередь wanted: пусто';
- el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join('\n');
- }
- if (state.view === 'cards') {
- renderCardsList();
- }
- },
- 0,
- () => {
- const el = $('sa_mem_wanted');
- if (el) {
- el.textContent = 'Очередь wanted: —';
- }
- },
- );
- }
-
- function isWantedModel(row) {
- const keys = state.wantedKeys;
- if (!keys || !keys.size) {
- return false;
- }
- for (const candidate of [row?.name, row?.title]) {
- const leaf = modelKeyLeaf(candidate);
- if (leaf && keys.has(leaf)) {
- return true;
- }
- }
- return false;
- }
-
function setOllamaHealth(level, text, title) {
state.ollamaHealth = level;
const el = $('sa_ollama_health');
@@ -7171,17 +7177,11 @@
);
}
- function setCardStatus(msg) {
- const el = $('sa_card_status');
- if (el) {
- el.textContent = msg || '';
- }
- }
-
function setView(view) {
if (view === 'cards') {
- state.view = 'cards';
- } else if (view === 'settings') {
+ view = 'chat';
+ }
+ if (view === 'settings') {
state.view = 'settings';
} else if (view === 'train') {
state.view = 'train';
@@ -7189,15 +7189,11 @@
state.view = 'chat';
}
const chat = $('sa_view_chat');
- const cards = $('sa_view_cards');
const settings = $('sa_view_settings');
const train = $('sa_view_train');
if (chat) {
chat.hidden = state.view !== 'chat';
}
- if (cards) {
- cards.hidden = state.view !== 'cards';
- }
if (settings) {
settings.hidden = state.view !== 'settings';
}
@@ -7210,13 +7206,10 @@
$(id)?.setAttribute('aria-selected', on ? 'true' : 'false');
};
tabActive('sa_tab_chat', state.view === 'chat');
- tabActive('sa_tab_cards', state.view === 'cards');
tabActive('sa_tab_settings', state.view === 'settings');
tabActive('sa_tab_train', state.view === 'train');
saveSettings();
- if (state.view === 'cards') {
- renderCardsList();
- } else if (state.view === 'settings') {
+ if (state.view === 'settings') {
setSettingsTab(state.settingsTab || 'behavior');
} else if (state.view === 'train') {
window.SA?.training?.render?.();
@@ -7237,422 +7230,6 @@
setView('chat');
}
- function prefetchCard(kind, name) {
- return new Promise((resolve) => {
- if (!kind || !name || typeof genericRequest !== 'function') {
- resolve(null);
- return;
- }
- const key = `${kind}:${name}`;
- genericRequest(
- 'AssistentGetCard',
- { kind, name },
- (data) => {
- if (data?.card) {
- state.modelCards[key] = data.card;
- }
- resolve(data?.card || null);
- },
- 0,
- () => resolve(null),
- );
- });
- }
-
- async function prefetchActiveModelCards() {
- const keys = [];
- const seen = new Set();
- const add = (kind, name) => {
- if (!kind || !name) {
- return;
- }
- const key = `${kind}:${name}`;
- if (seen.has(key)) {
- return;
- }
- seen.add(key);
- keys.push({ kind, name });
- };
- try {
- const ck = resolveCurrentCheckpoint();
- if (ck?.name) {
- add('checkpoint', ck.name);
- }
- } catch (e) { /* ignore */ }
- try {
- if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
- for (const l of loraHelper.selected) {
- add('lora', l?.name || l);
- }
- }
- } catch (e) { /* ignore */ }
- for (const l of state.inventory?.loras || []) {
- if (l?.has_card) {
- add('lora', l.name);
- }
- if (keys.length >= 14) {
- break;
- }
- }
- for (const c of state.inventory?.checkpoints || []) {
- if (c?.has_card) {
- add('checkpoint', c.name);
- }
- if (keys.length >= 16) {
- break;
- }
- }
- await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
- }
-
- function cardsCatalog() {
- const kind = $('sa_cards_kind')?.value || 'all';
- const inv = state.inventory || {};
- const rows = [];
- if (kind === 'all' || kind === 'checkpoint') {
- for (const c of inv.checkpoints || []) {
- rows.push({
- kind: 'checkpoint',
- name: c.name,
- title: c.title || c.name,
- has_card: !!c.has_card,
- hash: c.hash || '',
- preview_url: c.preview_url || null,
- has_sidecar: !!c.has_sidecar,
- });
- }
- }
- if (kind === 'all' || kind === 'lora') {
- for (const l of inv.loras || []) {
- rows.push({
- kind: 'lora',
- name: l.name,
- title: l.title || l.name,
- has_card: !!l.has_card,
- trigger: l.trigger_phrase,
- hash: l.hash || '',
- preview_url: l.preview_url || null,
- has_sidecar: !!l.has_sidecar,
- });
- }
- }
- return rows;
- }
-
- function renderCardsList() {
- const root = $('sa_cards_list');
- if (!root) {
- return;
- }
- root.innerHTML = '';
- const rows = cardsCatalog();
- if (!rows.length) {
- root.innerHTML = 'Inventory пуст — Обновить.
';
- return;
- }
- for (const row of rows) {
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'sa-card-row';
- if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
- btn.classList.add('sa-selected');
- }
- const thumb = row.preview_url
- ? ` `
- : '
';
- const metaBits = [];
- metaBits.push(row.has_card ? 'card ✓' : 'нет card');
- if (row.has_sidecar) {
- metaBits.push('sidecar');
- }
- if (isWantedModel(row)) {
- metaBits.push('⏳ wanted');
- btn.classList.add('sa-card-row-wanted');
- }
- if (row.trigger) {
- metaBits.push(String(row.trigger).slice(0, 40));
- }
- btn.innerHTML = `${thumb}${escapeHtml(row.kind)}
${escapeHtml(row.title || row.name)}
${escapeHtml(metaBits.join(' · '))}
↗ `;
- btn.addEventListener('click', (e) => {
- if (e.target?.closest?.('[data-chat]')) {
- e.preventDefault();
- e.stopPropagation();
- sendCardToChat(row);
- return;
- }
- selectCardModel(row);
- });
- root.appendChild(btn);
- }
- }
-
- function sendCardToChat(row) {
- if (!row?.name) {
- return;
- }
- selectCardModel(row);
- setView('chat');
- const kind = row.kind === 'checkpoint' ? 'checkpoint' : 'LoRA';
- const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : '';
- if ($('sa_input')) {
- $('sa_input').value = `Используй ${kind} «${row.name}».${triggers} Учти карточку/triggers и предложи патч.`;
- $('sa_input').focus();
- }
- setStatus(`В чат → ${row.name}`);
- }
-
- function wireCardForm() {
- const sync = () => {
- if ($('sa_card_show_json')?.checked) {
- syncCardJsonFromForm();
- }
- };
- ['sa_card_triggers', 'sa_card_weight', 'sa_card_when', 'sa_card_avoid', 'sa_card_hint', 'sa_card_notes', 'sa_card_url']
- .forEach((id) => $(id)?.addEventListener('change', sync));
- $('sa_card_show_json')?.addEventListener('change', () => {
- const on = !!$('sa_card_show_json')?.checked;
- const ta = $('sa_card_json');
- if (ta) {
- ta.hidden = !on;
- if (on) {
- syncCardJsonFromForm();
- }
- }
- });
- $('sa_card_json')?.addEventListener('change', () => {
- if ($('sa_card_show_json')?.checked) {
- applyCardToForm(readCardDraft() || {});
- }
- });
- }
-
- function applyCardToForm(card) {
- card = card || {};
- const triggers = Array.isArray(card.triggers) ? card.triggers.join(', ') : (card.triggers || '');
- if ($('sa_card_triggers')) {
- $('sa_card_triggers').value = triggers;
- }
- if ($('sa_card_weight')) {
- $('sa_card_weight').value = card.weight != null ? card.weight : (state.cardsSelection?.kind === 'lora' ? 0.8 : 1);
- }
- if ($('sa_card_when')) {
- $('sa_card_when').value = card.when || '';
- }
- if ($('sa_card_avoid')) {
- $('sa_card_avoid').value = card.avoid || '';
- }
- if ($('sa_card_hint')) {
- $('sa_card_hint').value = card.prompt_hint || '';
- }
- if ($('sa_card_notes')) {
- $('sa_card_notes').value = card.notes || '';
- }
- if ($('sa_card_url')) {
- $('sa_card_url').value = card.civitai_url || '';
- }
- if ($('sa_card_json')) {
- $('sa_card_json').value = JSON.stringify(card, null, 2);
- }
- }
-
- function syncCardJsonFromForm() {
- const sel = state.cardsSelection || {};
- let base = {};
- try {
- base = JSON.parse($('sa_card_json')?.value || '{}');
- } catch (e) {
- base = {};
- }
- const triggers = String($('sa_card_triggers')?.value || '')
- .split(/[,;]/)
- .map((s) => s.trim())
- .filter(Boolean);
- const card = {
- ...base,
- kind: sel.kind || base.kind || 'lora',
- name: sel.name || base.name || '',
- triggers,
- weight: parseFloat($('sa_card_weight')?.value || '0.8') || 0.8,
- when: $('sa_card_when')?.value || '',
- avoid: $('sa_card_avoid')?.value || '',
- prompt_hint: $('sa_card_hint')?.value || '',
- notes: $('sa_card_notes')?.value || '',
- civitai_url: $('sa_card_url')?.value || '',
- version_id: base.version_id != null ? base.version_id : null,
- };
- if ($('sa_card_json')) {
- $('sa_card_json').value = JSON.stringify(card, null, 2);
- }
- return card;
- }
-
- function renderCardPreviews(urls) {
- const root = $('sa_card_previews');
- if (!root) {
- return;
- }
- const list = (urls || []).filter(Boolean).slice(0, 6);
- root.innerHTML = '';
- if (!list.length) {
- root.hidden = true;
- return;
- }
- root.hidden = false;
- for (const url of list) {
- const img = document.createElement('img');
- img.className = 'sa-card-thumb';
- img.src = url;
- img.alt = 'preview';
- img.title = 'Клик — на вкладку Refs';
- img.addEventListener('click', () => {
- setBoardTab('refs');
- addRefFromUrl(url);
- setStatus('Превью → Refs');
- });
- root.appendChild(img);
- }
- }
-
- function mergeCivitaiIntoCard(card, data) {
- const out = { ...(card || {}) };
- const civ = data?.civitai;
- if (!out.triggers?.length && data?.trigger_phrase) {
- out.triggers = [data.trigger_phrase];
- }
- if (civ) {
- const trained = civ.trainedWords || civ.trained_words;
- if ((!out.triggers || !out.triggers.length) && Array.isArray(trained) && trained.length) {
- out.triggers = trained.slice(0, 12);
- }
- if (!out.civitai_url) {
- const mid = civ.modelId || civ.model?.id || civ.model?.modelId;
- const vid = civ.id || data.version_id;
- if (mid && vid) {
- out.civitai_url = `https://civitai.red/models/${mid}?modelVersionId=${vid}`;
- } else if (vid) {
- out.civitai_url = `https://civitai.red/models/0?modelVersionId=${vid}`;
- }
- }
- if (out.version_id == null && (civ.id || data.version_id)) {
- out.version_id = civ.id || data.version_id;
- }
- if (!out.notes && civ.description) {
- out.notes = String(civ.description).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 400);
- }
- }
- if (out.version_id == null && data?.version_id) {
- out.version_id = data.version_id;
- }
- return out;
- }
-
- function formatMetaStatus(data) {
- if (!data) {
- return 'Нет ответа';
- }
- if (data.error) {
- return String(data.error);
- }
- const parts = [];
- if (data.has_sidecar) {
- parts.push(`Сидикарь ✓ · version ${data.version_id || '?'}`);
- } else if (data.fetched) {
- parts.push(`Civitai ✓ · version ${data.version_id || '?'}`);
- } else {
- parts.push('Сидикаря нет');
- }
- const n = (data.example_urls || data.preview_urls || []).length;
- if (n) {
- parts.push(`${n} кадр${n === 1 ? '' : 'а'}`);
- }
- if (data.has_card) {
- parts.push('карточка Assistent ✓');
- } else {
- parts.push('карточки Assistent нет');
- }
- if (data.fetch_error) {
- parts.push(String(data.fetch_error));
- }
- return parts.join(' · ');
- }
-
- function applyCardMetaResponse(row, data, { preserveUser } = {}) {
- let card = data.card || {
- kind: row.kind,
- name: row.name,
- triggers: data.trigger_phrase ? [data.trigger_phrase] : [],
- weight: row.kind === 'lora' ? 0.8 : 1,
- when: '',
- avoid: '',
- prompt_hint: '',
- notes: '',
- civitai_url: '',
- version_id: data.version_id || null,
- };
- if (preserveUser) {
- const current = syncCardJsonFromForm();
- card = {
- ...mergeCivitaiIntoCard(card, data),
- when: current.when || card.when || '',
- avoid: current.avoid || card.avoid || '',
- prompt_hint: current.prompt_hint || card.prompt_hint || '',
- notes: current.notes || card.notes || '',
- };
- } else {
- card = mergeCivitaiIntoCard(card, data);
- }
- applyCardToForm(card);
- state.modelCards[`${row.kind}:${row.name}`] = card;
- const urls = [
- ...(data.preview_urls || []),
- ...(data.example_urls || []),
- ];
- renderCardPreviews(urls);
- const badge = $('sa_card_badge');
- if (badge) {
- badge.hidden = false;
- badge.textContent = data.has_card ? 'card ✓' : (data.has_sidecar || data.fetched ? 'meta ✓' : 'нет меты');
- }
- setCardStatus(formatMetaStatus(data));
- }
-
- function selectCardModel(row) {
- state.cardsSelection = row;
- renderCardsList();
- if ($('sa_card_title')) {
- $('sa_card_title').textContent = row.title || row.name;
- }
- const badge = $('sa_card_badge');
- if (badge) {
- badge.hidden = false;
- badge.textContent = '…';
- }
- setCardStatus('Читаю локальную мету…');
- genericRequest(
- 'AssistentGetCardMeta',
- { kind: row.kind, name: row.name, fetch: false },
- (data) => applyCardMetaResponse(row, data || {}),
- 0,
- (err) => setCardStatus(String(err || 'Ошибка загрузки')),
- );
- }
-
- function fetchCardMetaLive() {
- const row = state.cardsSelection;
- if (!row) {
- setCardStatus('Выбери модель');
- return;
- }
- setCardStatus('Сидикаря нет · ищу по SHA…');
- genericRequest(
- 'AssistentGetCardMeta',
- { kind: row.kind, name: row.name, fetch: true },
- (data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }),
- 0,
- (err) => setCardStatus(String(err || 'Civitai: ошибка запроса')),
- );
- }
-
async function addRefFromUrl(url) {
if (!url) {
return;
@@ -7660,80 +7237,6 @@
addRefSlot({ src: url, select: false });
}
- function readCardDraft() {
- const fromForm = syncCardJsonFromForm();
- if ($('sa_card_show_json')?.checked) {
- const raw = $('sa_card_json')?.value || '';
- try {
- return JSON.parse(raw);
- } catch (e) {
- setCardStatus('Невалидный JSON');
- return null;
- }
- }
- return fromForm;
- }
-
- function saveCurrentCard({ enqueue } = {}) {
- const sel = state.cardsSelection;
- if (!sel) {
- setCardStatus('Выбери модель');
- return;
- }
- const card = readCardDraft();
- if (!card) {
- return;
- }
- card.kind = card.kind || sel.kind;
- card.name = card.name || sel.name;
- setCardStatus('Сохраняю…');
- genericRequest(
- 'AssistentSaveCard',
- { kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue },
- (data) => {
- if (data.error) {
- setCardStatus(data.error);
- return;
- }
- state.modelCards[`${sel.kind}:${sel.name}`] = card;
- setCardStatus(data.installed
- ? `Карточка Assistent сохранена · ${data.path}`
- : `Черновик + wanted · ${data.path}`);
- refreshInventory(() => renderCardsList());
- if (enqueue || !data.installed) {
- refreshWantedQueue();
- }
- },
- 0,
- (err) => setCardStatus(String(err || 'Ошибка сохранения')),
- );
- }
-
- function enqueueWantedOnly() {
- const sel = state.cardsSelection;
- const card = readCardDraft() || {};
- if (!sel && !card.civitai_url) {
- setCardStatus('Нужна модель или civitai_url');
- return;
- }
- genericRequest(
- 'AssistentEnqueueWanted',
- {
- kind: (card.kind || sel?.kind || 'lora'),
- url: card.civitai_url || '',
- version_id: card.version_id || 0,
- title: card.name || sel?.name || '',
- card,
- },
- (data) => {
- setCardStatus(data.already ? 'Уже в wanted' : `Wanted → ${data.path}`);
- refreshWantedQueue();
- },
- 0,
- (err) => setCardStatus(String(err || 'Ошибка enqueue')),
- );
- }
-
function shortLoraName(name) {
const s = String(name || '');
const base = s.split(/[/\\]/).pop() || s;
@@ -7856,41 +7359,6 @@
filter.focus();
}
- async function generateCardWithAssistent() {
- const sel = state.cardsSelection;
- if (!sel) {
- setCardStatus('Выбери модель');
- return;
- }
- if (state.busy) {
- setCardStatus('Чат занят');
- return;
- }
- setPackValue('catalog_card', { flash: true });
- setView('chat');
- const meta = await new Promise((resolve) => {
- genericRequest(
- 'AssistentGetCardMeta',
- { kind: sel.kind, name: sel.name },
- (data) => resolve(data),
- 0,
- () => resolve(null),
- );
- });
- const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`;
- await sendChat({
- forcedUserText: forced,
- skipSlash: true,
- skipAutoPack: true,
- fromCards: true,
- cardTarget: {
- kind: sel.kind,
- name: sel.name,
- meta,
- },
- });
- }
-
function inventoryIsStale(maxAgeMs = 20000) {
if (!state.inventoryFetchedAt) {
return true;
@@ -7928,167 +7396,163 @@
}
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
- const { fromAutoCritique, fromVisionHop, fromCards, fromDebug } = opts;
- if (fromCards) {
- const card = extractCardJson(reply);
- if (card) {
- if ($('sa_card_json')) {
- $('sa_card_json').value = JSON.stringify(card, null, 2);
- }
- if (opts.fromDownload || opts.cardTarget) {
- const kind = card.kind || opts.cardTarget?.kind || 'lora';
- const name = card.name || opts.cardTarget?.name;
- if (name && typeof genericRequest === 'function') {
- genericRequest(
- 'AssistentSaveCard',
- { kind, name, card, enqueue_wanted: false },
- (data) => {
- if (data?.path) {
- state.modelCards[`${kind}:${name}`] = card;
- setCardStatus(data.installed ? `Card saved → ${data.path}` : `Card draft → ${data.path}`);
- setStatus(`Card saved for ${name}`);
- }
- },
- 0,
- () => setCardStatus('Card draft ready — Save manually'),
- );
- }
- } else {
- setView('cards');
- setCardStatus('Draft from Assistent — review & Save');
- }
- }
- return;
- }
- const { patch } = extractPatch(reply);
+ const { fromAutoCritique, fromVisionHop, fromDebug } = opts;
+ void civitaiResults;
if (fromDebug) {
- // Q&A only — a debug explanation never touches generation state.
state.pendingSilentGen = false;
return;
}
- const commanded = !!opts.userWantsGenerate
- || (!isMachineTurn(opts) && userImpliesGenerate(opts.userText || ''));
- let effective = patch;
- if (!effective && !fromVisionHop && !fromAutoCritique) {
- const aspect = parseAspectFromUserText(opts.userText || '');
- if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ''))) {
- effective = { aspect, actions: ['generate'] };
- if (state.lastPatch?.prompt) {
- effective.prompt = state.lastPatch.prompt;
- }
- appendSystemNote(`Патч пустой — применил aspect ${aspect} сам.`);
- }
- }
- if (!effective && !fromVisionHop && !fromAutoCritique) {
- const synthesized = synthesizePatchAfterEmptyFence(reply, opts.userText || '', opts);
- if (synthesized) {
- effective = synthesized;
- appendSystemNote(synthesized.actions
- ? 'Патч пустой — собрал prompt из ответа и запустил Generate.'
- : 'Патч пустой — собрал prompt из ответа.');
- }
- }
- if (!effective && !fromVisionHop && !fromAutoCritique && commanded
- && claimTurnHop('empty_patch')) {
- appendSystemNote('Нужен кадр — прошу JSON с prompt + generate.');
- await sendChat({
- skipSlash: true,
- skipAutoPack: true,
- fromEmptyPatchRetry: true,
- userWantsGenerate: true,
- forcedUserText:
- 'Пользователь уже просит кадр (это следует из сообщения, даже без слова «генерируй»). '
- + 'Ответь ТОЛЬКО одним fenced JSON: '
- + '{"prompt":"","negative":"","actions":["generate"]}. '
- + 'prompt — английский. Без прозы, без «скажи сгенерируй».',
- });
- return;
- }
- if (opts.fromPromptEnRetry && effective) {
+ const extracted = typeof extractPatch === 'function' ? extractPatch(reply) : { patch: null };
+ let effective = extracted && extracted.patch ? extracted.patch : null;
+ if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === 'function') {
effective = mergePromptEnRewrite(effective);
}
- if (effective) {
- rememberLastPatch(effective);
+ const S = window.SA && window.SA.session;
+ const act = getActivity();
+ if (effective && act && typeof act.noteModelCommands === 'function') {
+ act.noteModelCommands(effective);
+ }
+ if (effective && S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
+ activityDone('delta', {
+ kind: 'delta',
+ label: 'Обновил сессию',
+ detail: Object.keys(effective).filter((k) => effective[k] != null
+ && !['actions', 'notes'].includes(k)).slice(0, 10).join(', '),
+ });
+ try {
+ const chat = typeof findChat === 'function' ? findChat(state.activeChatId) : null;
+ if (chat && typeof snapshotChatParams === 'function') {
+ chat.params = snapshotChatParams();
+ if (typeof persistChatsStore === 'function') persistChatsStore();
+ }
+ } catch (e) { /* ignore */ }
+ if (typeof rememberLastPatch === 'function') rememberLastPatch(effective);
}
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
- doInterruptNow();
- }
- if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
- const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely)
- || civitaiResults.find((r) => !r.already_installed && r.download_url);
- if (pick) {
- downloadCivitaiLoRA(pick, null);
- }
+ if (typeof doInterruptNow === 'function') doInterruptNow();
}
const intent = resolveTurnIntent(effective, opts.userText || '', opts);
if (effective) {
if (intent.generate) {
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
- if (!acts.includes('generate')) {
- effective = { ...effective, actions: acts.concat('generate') };
- }
- } else {
+ effective = { ...effective, actions: acts.includes('generate') ? acts : acts.concat('generate'), generate: true };
+ } else if (typeof stripGenerateAction === 'function') {
effective = stripGenerateAction(effective);
+ if (effective && effective.generate) {
+ effective = { ...effective };
+ delete effective.generate;
+ }
}
- if (!intent.look) {
- effective = stripLookAt(effective);
- }
- rememberLastPatch(effective);
+ if (!intent.look && typeof stripLookAt === 'function') effective = stripLookAt(effective);
+ if (typeof rememberLastPatch === 'function') rememberLastPatch(effective);
}
if (intent.vetoed) {
state.pendingSilentGen = false;
+ const a = getActivity();
+ if (a) {
+ a.skip('generate', {
+ kind: 'generate',
+ label: 'Generate отменён',
+ detail: 'пользователь попросил не генерировать',
+ });
+ }
}
- if (effective && intent.look && !fromVisionHop && !fromAutoCritique) {
- const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
- if (hopped) {
+
+ const askList = Array.isArray(intent.ask) ? intent.ask : [];
+ if (askList.length && typeof claimTurnHop === 'function' && claimTurnHop('ask')) {
+ activityStep('ask', {
+ kind: 'ask',
+ label: `Запросил ${askList.join(', ')}`,
+ detail: 'подгружаю детали…',
+ status: 'running',
+ });
+ pullLiveIntoSession();
+ const bits = [];
+ if (askList.some((a) => /settings/i.test(String(a)))) {
+ const dump = S && typeof S.fullSettingsDump === 'function'
+ ? S.fullSettingsDump(state.chatSession, {
+ exact: state.exact, kreaProfiles: state.kreaProfiles, sessionExact: state.sessionExact,
+ })
+ : collectLiveContext();
+ bits.push('SETTINGS_JSON:\n' + JSON.stringify(dump));
+ }
+ if (askList.some((a) => /inventory/i.test(String(a)))) {
+ const inv = state.inventory || {};
+ bits.push('INVENTORY_JSON:\n' + JSON.stringify({
+ loras: (inv.loras || []).slice(0, 40).map((l) => ({ name: l.name || l, trigger_phrase: l.trigger_phrase || null })),
+ checkpoints: (inv.checkpoints || []).slice(0, 16).map((c) => c.name || c),
+ wildcards: (inv.wildcards || []).slice(0, 20).map((w) => w.name || w),
+ }));
+ }
+ if (bits.length) {
+ activityDone('ask', { detail: 'детали отправлены модели' });
+ await sendChat({ skipSlash: true, skipAutoPack: true, fromAskHop: true, forcedUserText: bits.join('\n\n') });
return;
}
}
- // Maximize chat-model prep: structure + EN for Krea before Swarm Generate runs.
- if (intent.generate && effective?.prompt && promptNeedsKreaPrep(effective.prompt)
- && !fromVisionHop && claimTurnHop('krea_prep')) {
+ if (effective && intent.look && !fromVisionHop && !fromAutoCritique) {
+ activityStep('look', {
+ kind: 'look',
+ label: 'Смотрит на кадр',
+ status: 'running',
+ });
+ if (typeof maybeVisionHop === 'function') {
+ const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
+ if (hopped) {
+ activityDone('look', { detail: 'vision hop' });
+ return;
+ }
+ activityDone('look', { detail: 'кадр недоступен' });
+ }
+ }
+ if (intent.generate && effective?.prompt
+ && typeof promptNeedsKreaPrep === 'function' && promptNeedsKreaPrep(effective.prompt)
+ && !fromVisionHop && typeof claimTurnHop === 'function' && claimTurnHop('krea_prep')) {
state.pendingPromptEnMerge = { ...effective };
- appendSystemNote('Готовлю промпт для Krea 2 чат-моделью (EN + структура)…');
- setBusyPhase('refining');
+ activityStep('prep', {
+ kind: 'prep',
+ label: 'Готовлю промпт для Krea',
+ detail: 'EN + структура',
+ status: 'running',
+ });
+ if (typeof appendSystemNote === 'function') appendSystemNote('Готовлю промпт для Krea…');
await sendChat({
- skipSlash: true,
- skipAutoPack: true,
- fromPromptEnRetry: true,
- userWantsGenerate: true,
- forcedUserText: buildKreaPromptPrepRequest(effective),
+ skipSlash: true, skipAutoPack: true, fromPromptEnRetry: true, userWantsGenerate: true,
+ forcedUserText: typeof buildKreaPromptPrepRequest === 'function'
+ ? buildKreaPromptPrepRequest(effective) : String(effective.prompt || ''),
});
return;
}
- const doApply = !!(effective && (intent.generate || $('sa_auto_apply')?.checked));
- if (doApply) {
- if (intent.generate) {
- startBusyUi('silent_gen');
- } else {
- setBusyPhase('applying');
+ if (intent.generate) {
+ activityStep('generate', {
+ kind: 'generate',
+ label: intent.vetoed ? 'Generate отменён (вето)' : 'Generate',
+ status: intent.vetoed ? 'skip' : 'running',
+ });
+ if (typeof startBusyUi === 'function') startBusyUi('silent_gen');
+ if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
+ await pushSessionToSwarm(state.chatSession);
+ if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
+ const srcOut = await runGenerateFromPatch(
+ { ...(effective || {}), actions: ['generate'], generate: true },
+ { force: true, fromSession: true },
+ );
+ activityDone('generate', { detail: srcOut ? 'кадр готов' : 'без кадра' });
+ if (srcOut) {
+ if (typeof maybeAutoCritique === 'function') await maybeAutoCritique(srcOut);
+ if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
}
- await applyPatch(effective, 'all');
- syncLiveParamsBar();
- if (intent.generate) {
- if (effective?.prompt && promptNeedsKreaPrep(effective.prompt)) {
- setStatus('Промпт всё ещё не EN/Krea-ready — Generate с тем что есть');
- }
- const src = await runGenerateFromPatch(
- { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ['generate'] },
- { force: true },
- );
- if (src) {
- await maybeAutoCritique(src);
- await maybeAutoVisionLook(src);
- }
- } else if (!state.generating) {
+ } else if (effective && $('sa_auto_apply')?.checked) {
+ if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
+ await pushSessionToSwarm(state.chatSession);
+ if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
+ if (!state.generating && typeof stopBusyUi === 'function') {
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : '');
}
- } else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) {
- setStatus('Ответ без JSON-патча — ничего не применено');
}
state.pendingSilentGen = false;
}
-
async function applyQuickPatch(patch, note) {
const withActions = { ...patch };
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
@@ -8096,11 +7560,15 @@
}
const prevIntent = state.lastUserParamIntent;
state.lastUserParamIntent = true;
- await applyPatch(withActions, 'all');
+ const S = window.SA && window.SA.session;
+ if (S) {
+ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
+ }
+ await pushSessionToSwarm(state.chatSession);
state.lastUserParamIntent = prevIntent;
setStatus(note || 'Applied');
- if ($('sa_auto_generate')?.checked) {
- await runGenerateFromPatch(withActions);
+ if (patchHasGenTrigger(withActions)) {
+ await runGenerateFromPatch(withActions, { force: true, fromSession: true });
}
syncChipHighlight();
}
@@ -8194,7 +7662,7 @@
`persona=${persona} · pack=${pack}`,
`chat=${chatModel} · embed=${embed}`,
`skills=${(state.enabledSkills || []).join(',') || '—'}`,
- `auto: apply=${!!$('sa_auto_apply')?.checked} gen=${!!$('sa_auto_generate')?.checked} vision=${!!$('sa_auto_vision')?.checked} critique=${!!$('sa_auto_critique')?.checked}`,
+ `auto: apply=${!!$('sa_auto_apply')?.checked} gen=${!false /* auto_generate ignored 0.14 */} vision=${!!$('sa_auto_vision')?.checked} critique=${!!$('sa_auto_critique')?.checked}`,
'',
'Live SwarmUI:',
` ckpt=${ctx.checkpoint?.name || '—'} · krea_profile=${ctx.krea_profile || profile}`,
@@ -8209,6 +7677,11 @@
state.lastSystemLayers
? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(' · ')}`
: ' system_layers: — (отправь сообщение, чтобы заполнить)',
+ (() => {
+ const b = currentBudgetEstimate();
+ const mem = getContextMemory();
+ return ` budget≈${b.used}/${b.numCtx} (${b.fromEval ? 'eval' : 'est'}) · level=${b.level} · memory_until=${mem.untilCount || 0} · auto_compress=${COMPRESS_AUTO}`;
+ })(),
'',
'Exact defaults (merged):',
` generation=${JSON.stringify(exactGen)}`,
@@ -8254,6 +7727,10 @@
setStatus('/history');
return true;
}
+ if (cmd === 'compress' || cmd === 'compact' || cmd === 'сжать') {
+ await compressNowFromUi();
+ return true;
+ }
if (cmd === 'debug' || cmd === 'dbg' || cmd === 'why') {
const dump = buildDebugSummary();
appendSystemNote(dump);
@@ -8398,7 +7875,7 @@
}
if (cmd === 'pack') {
if (!setPackValue(arg, { flash: true, user: true })) {
- setStatus('Pack: write|ordinary|critique|compose|params|inpaint|describe|card|persona');
+ setStatus('Pack: write|ordinary|critique|compose|params|inpaint|describe|persona');
} else {
setStatus(`Pack → ${$('sa_pack')?.value}`);
}
@@ -8412,21 +7889,6 @@
await startHornyGame();
return true;
}
- if (cmd === 'civitai') {
- if (!arg) {
- setStatus('/civitai ');
- return true;
- }
- if ($('sa_input')) {
- $('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`;
- }
- setPackValue(defaultPackId(), { flash: true });
- await sendChat({
- skipAutoPack: true,
- forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`,
- });
- return true;
- }
if (cmd === 'persona') {
const sub = (parts[1] || 'new').toLowerCase();
const rest = parts.slice(2).join(' ').trim();
@@ -8515,7 +7977,7 @@
if (!isMachineTurn(opts)) {
state.lastUserParamIntent = userTextMentionsParams(text);
state.lastUserControlIntent = userTextMentionsControls(text);
- state.pendingSilentGen = userImpliesGenerate(text);
+ state.pendingSilentGen = false; // 0.14
}
if (!isMachineTurn(opts) && !opts.skipSlash) {
@@ -8565,10 +8027,8 @@
setPackValue(guessed, { flash: true });
}
}
-
- // Cards mode must not be overridden by auto-pack; keep catalog_card.
- if (!opts.fromDebug && (opts.fromCards || state.view === 'cards')) {
- setPackValue('catalog_card', { flash: false });
+ if (!opts.fromDebug && (false)) {
+ setPackValue('ordinary', { flash: false });
}
const pack = opts.fromDebug ? 'debug_explain' : ($('sa_pack')?.value || defaultPackId());
@@ -8585,6 +8045,16 @@
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
state.llmParked = false;
setInterruptVisible(true);
+ if (!isContinuationTurn(opts) && !opts.fromAskHop) {
+ activityBegin(opts.fromDebug ? 'Debug' : 'Ход Assistent');
+ activityStep('think', { kind: 'think', label: 'Думаю…', status: 'running' });
+ } else if (opts.fromAskHop) {
+ activityStep('think', { kind: 'think', label: 'Отвечает с деталями…', status: 'running' });
+ } else if (opts.fromVisionHop) {
+ activityStep('look', { kind: 'look', label: 'Смотрит изображение…', status: 'running' });
+ } else if (opts.fromPromptEnRetry) {
+ activityStep('prep', { kind: 'prep', label: 'Дописываю EN-промпт…', status: 'running' });
+ }
if (state.expectColdLoad && !isContinuationTurn(opts)) {
startBusyUi('warming');
setStatus('Возвращаю LLM в GPU…');
@@ -8605,7 +8075,7 @@
setStatus('Обновляю inventory…');
try {
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
- await prefetchActiveModelCards();
+
} catch (e) {
console.warn('Assistent inventory refresh', e);
}
@@ -8670,6 +8140,20 @@
}
}
+ // Auto-compress after the new user turn is in history (budget includes it).
+ if (!opts.fromDebug && !opts.fromCompress && !opts.fromAskHop && !opts.fromVisionHop
+ && !opts.fromAutoCritique && !isContinuationTurn(opts)) {
+ try {
+ await maybeAutoCompressBeforeSend(chatEpoch);
+ } catch (e) {
+ console.warn('Assistent auto-compress', e);
+ }
+ if (chatEpoch !== state.chatEpoch) {
+ return;
+ }
+ startBusyUi(state.expectColdLoad ? 'loading' : 'thinking');
+ }
+
const context = collectLiveContext();
// has_vision_image = board has a real frame (even when JPEG is not in this request).
// images_in_request = JPEG bytes are attached to the last user message this turn.
@@ -8678,29 +8162,15 @@
context.attached_slot_ids = attachableSlots().map((s) => s.id);
context.vision_slot_ids = visionSlots.map((s) => s.id);
context.persona = persona;
- if (opts.cardTarget) {
- context.card_target = opts.cardTarget;
- }
- if (opts.fromCards || pack === 'catalog_card') {
- context.auto_apply = false;
- context.auto_generate = false;
- }
- await prefetchActiveModelCards();
if (chatEpoch !== state.chatEpoch) {
return;
}
- // Refresh cards into context after prefetch
- const refreshed = collectLiveContext();
- context.model_cards = refreshed.model_cards;
- const messages = state.history.slice(-historyMessageLimit()).map((m) => {
- let content = String(m.content || '');
- if (m.role === 'assistant') {
- content = stripJsonFencesForHistory(content);
- }
- return { role: m.role, content: content.slice(0, 4000) };
- });
- if (opts.skipAppendUser) {
- messages.push({ role: 'user', content: text });
+ let messages = assembleOutgoingMessages(
+ opts.skipAppendUser ? { includePendingUser: text } : undefined,
+ );
+ // Fallback if assemble returned empty but we have text to send.
+ if (!messages.length && text) {
+ messages = [{ role: 'user', content: text }];
}
if (images && messages.length) {
messages[messages.length - 1].images = images;
@@ -8732,11 +8202,19 @@
if (meta.system_layers && typeof meta.system_layers === 'object') {
state.lastSystemLayers = meta.system_layers;
}
+ if (meta.prompt_eval_count != null) {
+ state.lastPromptEvalCount = Number(meta.prompt_eval_count) || null;
+ const mem = getContextMemory();
+ if (mem.summary) {
+ setContextMemory({ ...mem, promptEvalCount: state.lastPromptEvalCount }, { persist: true });
+ }
+ }
try {
state.lastContextChars = (context && JSON.stringify(context).length) || 0;
} catch (e) {
state.lastContextChars = 0;
}
+ updateCtxChip();
const prose = extractPatch(reply).prose || reply;
state.history.push({ role: 'assistant', content: prose, persona, pack });
persistHistory();
@@ -8762,6 +8240,7 @@
state.busy = false;
setInterruptVisible(true);
}
+ updateCtxChip();
}
};
@@ -8824,7 +8303,11 @@
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai);
- finishOk(reply, civitai, { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, civitai, {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count,
+ });
}
},
0,
@@ -8852,7 +8335,11 @@
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
- finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, data.civitai_results || [], {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count,
+ });
},
0,
(err2) => finishErr(String(err2 || err || 'Chat failed')),
@@ -8875,7 +8362,11 @@
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
- finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers });
+ finishOk(reply, data.civitai_results || [], {
+ system_chars: data.system_chars,
+ system_layers: data.system_layers,
+ prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count,
+ });
},
0,
(err) => finishErr(String(err || 'Chat failed')),
@@ -9049,12 +8540,12 @@
loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => {
refreshModels();
refreshInventory(() => {
- renderCardsList();
+
renderLoraChips();
});
});
probeOllamaHealth();
- refreshWantedQueue();
+
}
function wire() {
@@ -9084,9 +8575,38 @@
wireSplitter();
registerSendButton();
wireSlashInput();
- wireCardForm();
+
$('sa_btn_new_chat')?.addEventListener('click', () => startNewChat({ saveCurrent: true }));
+ $('sa_ctx_chip')?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ toggleCtxPanel();
+ });
+ $('sa_ctx_close')?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ toggleCtxPanel(false);
+ });
+ $('sa_ctx_panel')?.addEventListener('click', (e) => e.stopPropagation());
+ $('sa_ctx_compress')?.addEventListener('click', () => compressNowFromUi());
+ $('sa_ctx_reset')?.addEventListener('click', () => resetCompressionFromUi());
+ $('sa_ctx_auto')?.addEventListener('change', () => {
+ COMPRESS_AUTO = !!$('sa_ctx_auto')?.checked;
+ const settingsAuto = $('sa_compress_auto');
+ if (settingsAuto) {
+ settingsAuto.checked = COMPRESS_AUTO;
+ }
+ if (state.config?.assistant) {
+ state.config.assistant.compress_auto = COMPRESS_AUTO;
+ }
+ setStatus(COMPRESS_AUTO ? 'Автосжатие включено' : 'Автосжатие выключено');
+ });
+ $('sa_compress_auto')?.addEventListener('change', () => {
+ COMPRESS_AUTO = !!$('sa_compress_auto')?.checked;
+ const panelAuto = $('sa_ctx_auto');
+ if (panelAuto) {
+ panelAuto.checked = COMPRESS_AUTO;
+ }
+ });
$('sa_btn_chats')?.addEventListener('click', (e) => {
e.stopPropagation();
setChatsPanelOpen(!state.chatsPanelOpen);
@@ -9144,25 +8664,20 @@
});
$('sa_tab_chat')?.addEventListener('click', () => setView('chat'));
- $('sa_tab_cards')?.addEventListener('click', () => setView('cards'));
+
$('sa_tab_train')?.addEventListener('click', () => setView('train'));
$('sa_tab_settings')?.addEventListener('click', () => openSettings(state.settingsTab || 'behavior'));
$('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate'));
$('sa_board_tab_refs')?.addEventListener('click', () => setBoardTab('refs'));
$('sa_persona')?.addEventListener('change', onPersonaChanged);
$('sa_persona_delete')?.addEventListener('click', () => deleteCurrentOverlayPersona());
- $('sa_cards_kind')?.addEventListener('change', renderCardsList);
- $('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
- $('sa_btn_card_meta')?.addEventListener('click', () => fetchCardMetaLive());
- $('sa_btn_card_generate')?.addEventListener('click', () => generateCardWithAssistent());
- $('sa_btn_card_save')?.addEventListener('click', () => saveCurrentCard());
- $('sa_btn_card_wanted')?.addEventListener('click', () => enqueueWantedOnly());
+
document.querySelectorAll('#sa_settings .sa-stab').forEach((btn) => {
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-stab')));
});
$('sa_btn_mem_refresh')?.addEventListener('click', () => {
refreshMemoryList();
- refreshWantedQueue();
+
});
$('sa_mem_kind')?.addEventListener('change', renderMemoryList);
$('sa_mem_scope')?.addEventListener('change', renderMemoryList);
@@ -9270,7 +8785,7 @@
probeOllamaHealth();
});
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory(() => {
- renderCardsList();
+
renderLoraChips();
}, { rescan: true }));
$('sa_btn_add_ref')?.addEventListener('click', () => {
@@ -9337,17 +8852,13 @@
closeAllMoreMenus();
clearPatchBlocksOnly();
});
- $('sa_btn_card_to_chat')?.addEventListener('click', () => {
- if (state.cardsSelection) {
- sendCardToChat(state.cardsSelection);
- } else {
- setCardStatus('Сначала выбери модель в списке');
- }
- });
document.addEventListener('click', () => {
if (state.chatsPanelOpen) {
setChatsPanelOpen(false);
}
+ if (state.ctxPanelOpen) {
+ toggleCtxPanel(false);
+ }
closeAllMoreMenus();
});
$('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
@@ -9426,7 +8937,7 @@
}, 45000);
setInterval(() => {
if (!state.busy && !state.generating) {
- refreshWantedQueue();
+
}
}, 120000);
window.addEventListener('beforeunload', () => {
diff --git a/src/context.js b/src/context.js
new file mode 100644
index 0000000..294e5df
--- /dev/null
+++ b/src/context.js
@@ -0,0 +1,194 @@
+/**
+ * Context budget + conversation memory helpers for chat compression.
+ */
+
+export function emptyContextMemory() {
+ return {
+ summary: '',
+ untilCount: 0,
+ foldedTurns: 0,
+ at: 0,
+ uiCollapsed: false,
+ promptEvalCount: null,
+ };
+}
+
+export function normalizeContextMemory(raw) {
+ if (!raw || typeof raw !== 'object') {
+ return emptyContextMemory();
+ }
+ const summary = String(raw.summary || '').trim();
+ return {
+ summary,
+ untilCount: Math.max(0, Number(raw.untilCount) || 0),
+ foldedTurns: Math.max(0, Number(raw.foldedTurns) || 0),
+ at: Number(raw.at) || 0,
+ uiCollapsed: !!raw.uiCollapsed && !!summary,
+ promptEvalCount: raw.promptEvalCount != null ? Number(raw.promptEvalCount) || null : null,
+ };
+}
+
+/** Estimate tokens from character counts. */
+export function charsToTokens(chars, charsPerToken = 3.2) {
+ const cpt = Math.max(1.5, Number(charsPerToken) || 3.2);
+ const n = Math.max(0, Number(chars) || 0);
+ return Math.ceil(n / cpt);
+}
+
+/**
+ * @param {{
+ * systemChars?: number,
+ * historyChars?: number,
+ * memoryChars?: number,
+ * numCtx?: number,
+ * numPredict?: number,
+ * charsPerToken?: number,
+ * compressAt?: number,
+ * promptEvalCount?: number|null,
+ * }} opts
+ */
+export function estimateBudget(opts = {}) {
+ const numCtx = Math.max(1024, Number(opts.numCtx) || 16384);
+ const numPredict = Math.max(256, Number(opts.numPredict) || 3072);
+ const charsPerToken = Math.max(1.5, Number(opts.charsPerToken) || 3.2);
+ const compressAt = Math.min(0.95, Math.max(0.4, Number(opts.compressAt) || 0.7));
+ const systemChars = Math.max(0, Number(opts.systemChars) || 0);
+ const historyChars = Math.max(0, Number(opts.historyChars) || 0);
+ const memoryChars = Math.max(0, Number(opts.memoryChars) || 0);
+ const inputChars = systemChars + historyChars + memoryChars;
+ const estimated = charsToTokens(inputChars, charsPerToken);
+ const used = opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0
+ ? Number(opts.promptEvalCount)
+ : estimated;
+ const headroom = Math.max(1024, numCtx - numPredict);
+ const threshold = Math.floor(headroom * compressAt);
+ const ratio = numCtx > 0 ? used / numCtx : 0;
+ let level = 'ok';
+ if (ratio >= 0.85 || used >= threshold) {
+ level = 'hot';
+ } else if (ratio >= 0.65 || used >= threshold * 0.85) {
+ level = 'warn';
+ }
+ return {
+ numCtx,
+ numPredict,
+ headroom,
+ threshold,
+ systemChars,
+ historyChars,
+ memoryChars,
+ inputChars,
+ estimated,
+ used,
+ fromEval: opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0,
+ ratio,
+ level,
+ charsPerToken,
+ compressAt,
+ };
+}
+
+/**
+ * Whether auto-compress should run before the next chat turn.
+ * @param {ReturnType} budget
+ * @param {{ untilCount?: number, summary?: string }} memory
+ * @param {number} historyLen — full transcript length
+ * @param {{ keepMessages?: number }} opts — messages kept raw (turns*2)
+ */
+export function shouldCompress(budget, memory, historyLen, opts = {}) {
+ const keep = Math.max(2, Number(opts.keepMessages) || 8);
+ const len = Math.max(0, Number(historyLen) || 0);
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const uncovered = Math.max(0, len - until);
+ if (uncovered <= keep) {
+ return false;
+ }
+ const used = budget?.used ?? 0;
+ const threshold = budget?.threshold ?? Infinity;
+ return used >= threshold;
+}
+
+/**
+ * Messages the model should see: optional covered-by-summary skip + last keep raw.
+ * @param {Array<{role:string,content?:string,systemish?:boolean}>} history
+ * @param {{ untilCount?: number }} memory
+ * @param {number} keepTurns
+ */
+export function assembleModelMessages(history, memory, keepTurns) {
+ const keep = Math.max(1, Number(keepTurns) || 4) * 2;
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const list = (history || []).filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish);
+ const afterSummary = until > 0 ? list.slice(until) : list;
+ const window = afterSummary.length > keep ? afterSummary.slice(-keep) : afterSummary;
+ return window.map((m) => ({
+ role: m.role,
+ content: String(m.content || '').slice(0, 4000),
+ }));
+}
+
+/** How many leading messages can be folded into a new summary (leave keep raw). */
+export function messagesToFold(history, memory, keepTurns) {
+ const keep = Math.max(1, Number(keepTurns) || 4) * 2;
+ const list = (history || []).filter((m) => m && (m.role === 'user' || m.role === 'assistant') && !m.systemish);
+ const until = Math.max(0, Number(memory?.untilCount) || 0);
+ const foldEnd = Math.max(until, list.length - keep);
+ if (foldEnd <= until) {
+ return [];
+ }
+ return list.slice(until, foldEnd);
+}
+
+export function mergeSummary(oldSummary, incoming) {
+ const next = String(incoming || '').trim();
+ if (!next) {
+ return String(oldSummary || '').trim();
+ }
+ const prev = String(oldSummary || '').trim();
+ if (!prev) {
+ return next;
+ }
+ // Prefer the model output when it already incorporates prior memory.
+ return next;
+}
+
+export function formatTokenShort(n) {
+ const v = Math.max(0, Number(n) || 0);
+ if (v >= 10000) {
+ return `${(v / 1000).toFixed(1)}k`;
+ }
+ if (v >= 1000) {
+ return `${(v / 1000).toFixed(1)}k`;
+ }
+ return String(Math.round(v));
+}
+
+export function conversationMemoryBlock(memory, maxChars = 2400) {
+ const m = normalizeContextMemory(memory);
+ if (!m.summary) {
+ return null;
+ }
+ let text = m.summary;
+ if (text.length > maxChars) {
+ text = `${text.slice(0, maxChars)}…`;
+ }
+ return {
+ summary: text,
+ until_count: m.untilCount,
+ folded_turns: m.foldedTurns,
+ };
+}
+
+export function attachContext(SA) {
+ SA.context = {
+ emptyContextMemory,
+ normalizeContextMemory,
+ charsToTokens,
+ estimateBudget,
+ shouldCompress,
+ assembleModelMessages,
+ messagesToFold,
+ mergeSummary,
+ formatTokenShort,
+ conversationMemoryBlock,
+ };
+}
diff --git a/src/intent.js b/src/intent.js
index 6b280db..90710aa 100644
--- a/src/intent.js
+++ b/src/intent.js
@@ -1,4 +1,4 @@
-/** Turn intent heuristics — pure functions testable with node --test. */
+/** Turn intent — veto only; generate comes from model `generate: true` (or legacy actions). */
export function cyrTokenRe(alts) {
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
@@ -6,55 +6,9 @@ export function cyrTokenRe(alts) {
return new RegExp(`${boundary}(?:${alts})${end}`, 'i');
}
-export function userAsksGenerate(text) {
- const t = String(text || '').trim();
- if (!t) {
- return false;
- }
- if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
- return true;
- }
- if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
- return true;
- }
- const letter = '[0-9A-Za-z_А-Яа-яЁё]';
- const stem = `${letter}*`;
- return cyrTokenRe(
- 'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|'
- + `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
- + `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
- + 'run\\s+generat|/gen',
- ).test(t);
-}
-
-export function userAsksContinue(text) {
- const t = String(text || '').trim();
- if (!t) {
- return false;
- }
- if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) {
- return true;
- }
- return cyrTokenRe(
- 'давай\\s+дальше|следующ(ий|ая|ее|ую)\\s+кадр|ещё\\s+кадр|еще\\s+кадр|'
- + 'кадр\\s*№?\\s*\\d+|сделай\\s+следующ',
- ).test(t);
-}
-
-export function isSameButAspectRequest(text) {
- const t = String(text || '').trim();
- if (!t) {
- return false;
- }
- return cyrTokenRe(
- 'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|'
- + 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)',
- ).test(t);
-}
-
export function userAsksNoGenerate(text) {
const t = String(text || '').trim();
- if (!t || userAsksGenerate(t)) {
+ if (!t) {
return false;
}
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
@@ -63,19 +17,12 @@ export function userAsksNoGenerate(text) {
return cyrTokenRe(
'запомн|запомни|запомним|сохрани|сохраним|шаблон|'
+ 'базов(ый|ого|ому|ым|ая|ую|ое)?\\s+промпт|'
- + 'не\\s+генерир|без\\s+генерац|не\\s+надо\\s+генер|только\\s+запомн|пока\\s+запомн|'
- + 'не\\s+рисуй|не\\s+запускай\\s+генер',
+ + 'не\\s+генерир[а-яё]*|без\\s+генерац[а-яё]*|не\\s+надо\\s+генер[а-яё]*|только\\s+запомн[а-яё]*|пока\\s+запомн[а-яё]*|'
+ + 'не\\s+рисуй|не\\s+запускай\\s+генер[а-яё]*|'
+ + 'только\\s+(ответь|скажи|объясни)',
).test(t);
}
-export function userCommandsGenerate(text) {
- const t = String(text || '').trim();
- if (!t || userAsksNoGenerate(t)) {
- return false;
- }
- return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t);
-}
-
export function userAsksLook(text) {
const t = String(text || '').trim();
if (!t) {
@@ -93,68 +40,15 @@ export function userAsksLook(text) {
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
}
-export function userIsChatNotFrame(text) {
+export function isSameButAspectRequest(text) {
const t = String(text || '').trim();
if (!t) {
return false;
}
- if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) {
- return true;
- }
- if (cyrTokenRe(
- 'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|'
- + 'список\\s+лор|где\\s+настрой|что\\s+значит|'
- + 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|'
- + 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр',
- ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) {
- return true;
- }
- return false;
-}
-
-export function userImpliesGenerate(text) {
- const t = String(text || '').trim();
- if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) {
- return false;
- }
- if (userCommandsGenerate(t)) {
- return true;
- }
- if (t.length < 8) {
- return false;
- }
- const wantsLook = userAsksLook(t);
- const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t)
- || /\b(fix|redo|redraw|improve)\b/i.test(t);
- if (wantsLook && !wantsRedraw) {
- return false;
- }
- if (cyrTokenRe(
- 'нарису|сгенер|перерису|'
- + 'сделай\\s+(картинк|изображен|фото|кадр)|'
- + 'хочу\\s+(картинк|изображен|фото|увидеть|видеть)|'
- + 'покажи\\s+как\\s+(она|он|это)|'
- + 'сделай\\s+(её|ее|его|мне)\\s|'
- + 'пусть\\s+будет|'
- + 'другой\\s+(ракурс|свет|наряд|поза)|'
- + 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|'
- + 'ещё\\s+одн|еще\\s+одн',
- ).test(t)) {
- return true;
- }
- if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) {
- return true;
- }
- const isQuestion = /[??]\s*$/.test(t);
- if (isQuestion) {
- return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t);
- }
- return false;
-}
-
-export function packBlocksAutoGenerate(pack) {
- const p = String(pack || '');
- return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain';
+ return cyrTokenRe(
+ 'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|'
+ + 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)',
+ ).test(t);
}
export function packWantsVision(pack) {
@@ -162,30 +56,17 @@ export function packWantsVision(pack) {
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
}
-export function resolveTurnIntent(patch, userText, opts = {}, packId = '') {
- const machine = !!opts.machineTurn;
- const vetoed = !machine && userAsksNoGenerate(userText);
- const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText));
- const implied = !machine && userImpliesGenerate(userText);
- const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate');
-
- let generate;
- if (vetoed || opts.fromAutoCritique) {
- generate = false;
- } else if (commanded) {
- generate = true;
- } else if (packBlocksAutoGenerate(packId)) {
- generate = false;
- } else {
- generate = modelAsked || implied;
- }
-
+/** Model generate + explicit veto. No RU imply/command heuristics. */
+export function resolveTurnIntent(patch, userText, opts = {}) {
+ const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
+ const modelAsked = patch?.generate === true
+ || (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'));
+ const generate = !vetoed && !opts.fromAutoCritique && !!modelAsked;
const hasLook = !!patch
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
- const honorLook = opts.fromAutoCritique || opts.fromVisionHop
- || (!machine && userAsksLook(userText))
- || packWantsVision(packId);
- const look = !!(hasLook && !vetoed && !generate && honorLook);
-
- return { generate, look, vetoed };
+ const look = !!(hasLook && !vetoed && !generate);
+ const ask = Array.isArray(patch?.ask)
+ ? patch.ask.map(String)
+ : (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
+ return { generate, look, vetoed, ask };
}
diff --git a/src/main.js b/src/main.js
index f09910c..2f64400 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,11 +1,17 @@
import { attachApi } from './api.js';
import { attachPatch, setPatchKeys } from './patch.js';
import { attachPersist } from './persist.js';
+import { attachSession } from './session.js';
+import { attachContext } from './context.js';
+import { attachActivity } from './activity.js';
window.SA = window.SA || {};
attachApi(window.SA);
attachPatch(window.SA);
attachPersist(window.SA);
+attachSession(window.SA);
+attachContext(window.SA);
+attachActivity(window.SA);
/** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */
window.SA.applyConfigPatchKeys = function (config) {
diff --git a/src/patch.js b/src/patch.js
index 82da9ab..b5afbe0 100644
--- a/src/patch.js
+++ b/src/patch.js
@@ -1,17 +1,15 @@
-/** Patch detection / extraction — mirrors AssistentPatch.cs. */
+/** Patch detection / extraction — mirrors AssistentPatch.cs (0.14 sparse session deltas). */
const DEFAULT_PATCH_KEYS = [
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler',
- 'actions', 'search_query', 'civitai_query',
+ 'actions', 'generate', 'ask',
'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',
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
'creativity', 'intensity', 'complexity', 'movement',
- 'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'memories', 'memory',
- 'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
- 'inventory_query', 'skills', 'persona_shelves', 'persona_clone', 'persona', 'controls',
- 'variants',
+ 'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'persona', 'controls',
+ 'inventory_query', 'variants',
];
let PATCH_KEYS = DEFAULT_PATCH_KEYS.slice();
@@ -32,27 +30,10 @@ function has(obj, key) {
return obj[key] !== undefined && obj[key] !== null;
}
-export function isCardObject(obj) {
- if (!obj || typeof obj !== 'object') {
- return false;
- }
- const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
- const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions
- || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null
- || obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
- if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
- return true;
- }
- return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
-}
-
export function isPatchObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
- if (isCardObject(obj)) {
- return false;
- }
return PATCH_KEYS.some((k) => has(obj, k));
}
@@ -60,8 +41,12 @@ export function normalizePatch(patch) {
if (!patch || typeof patch !== 'object') {
return patch;
}
- if (!has(patch, 'search_query') && has(patch, 'civitai_query')) {
- patch.search_query = patch.civitai_query;
+ const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
+ if (patch.generate === true || acts.includes('generate')) {
+ patch.generate = true;
+ }
+ if (typeof patch.ask === 'string') {
+ patch.ask = [patch.ask];
}
if (!has(patch, 'init_creativity') && has(patch, 'denoise')) {
patch.init_creativity = patch.denoise;
@@ -100,7 +85,13 @@ export function isTerminalStreamPatch(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
- if (isCardObject(obj)) {
+ if (obj.generate === true) {
+ return true;
+ }
+ if (Array.isArray(obj.ask) && obj.ask.length) {
+ return true;
+ }
+ if (typeof obj.ask === 'string' && obj.ask) {
return true;
}
if (Array.isArray(obj.variants) && obj.variants.length) {
@@ -109,17 +100,8 @@ export function isTerminalStreamPatch(obj) {
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
return true;
}
- if (obj.search_query != null || obj.civitai_query != null
- || obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
- return true;
- }
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
- const hopOrGen = [
- 'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags',
- 'list_inventory', 'search_civitai', 'interrupt', 'generate',
- 'memory_upsert', 'user_pref_upsert',
- ];
- if (acts.some((a) => hopOrGen.includes(a))) {
+ if (acts.includes('generate')) {
return true;
}
if (String(obj.prompt || '').trim().length >= 48) {
@@ -127,8 +109,7 @@ export function isTerminalStreamPatch(obj) {
}
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.memories != null || obj.user_prefs != null) {
+ || obj.seed != null || obj.controls != null) {
return true;
}
return false;
@@ -137,7 +118,6 @@ export function isTerminalStreamPatch(obj) {
export function attachPatch(SA) {
SA.PATCH_KEYS = PATCH_KEYS;
SA.setPatchKeys = setPatchKeys;
- SA.isCardObject = isCardObject;
SA.isPatchObject = isPatchObject;
SA.isTerminalStreamPatch = isTerminalStreamPatch;
SA.normalizePatch = normalizePatch;
diff --git a/src/session.js b/src/session.js
new file mode 100644
index 0000000..218b1e8
--- /dev/null
+++ b/src/session.js
@@ -0,0 +1,368 @@
+/**
+ * Per-chat generation session — source of truth for Generate params, board, LoRAs.
+ * Sparse model deltas merge into the active session; buttons always generate from it.
+ */
+
+const MAX_DATA_URL_CHARS = 350_000;
+
+const GEN_KEYS = [
+ 'prompt', 'negative', 'width', 'height', 'aspect', 'steps', 'cfg', 'sigma_shift',
+ 'seed', 'sampler', 'scheduler', 'batch', 'checkpoint', 'loras', 'controls',
+ 'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
+ 'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
+];
+
+export function emptySession() {
+ return {
+ gen: {
+ prompt: '',
+ negative: '',
+ width: null,
+ height: null,
+ aspect: null,
+ steps: null,
+ cfg: null,
+ sigma_shift: null,
+ seed: null,
+ sampler: null,
+ scheduler: null,
+ batch: null,
+ checkpoint: null,
+ loras: [],
+ controls: {},
+ use_init_image: false,
+ clear_init_image: false,
+ init_creativity: null,
+ denoise: null,
+ use_mask_image: false,
+ clear_mask_image: false,
+ mask_blur: null,
+ mask_grow: null,
+ },
+ board: {
+ slots: [],
+ selectedSlotId: 'ref1',
+ genResults: [],
+ selectedGenResultId: null,
+ refSeq: 1,
+ },
+ persona: 'neutral',
+ pack: 'ordinary',
+ context_memory: null,
+ };
+}
+
+/** Normalize boolean generate + legacy actions:["generate"]. */
+export function normalizeDelta(raw) {
+ if (!raw || typeof raw !== 'object') {
+ return null;
+ }
+ const delta = { ...raw };
+ const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
+ if (delta.generate === true || acts.includes('generate')) {
+ delta.generate = true;
+ }
+ if (typeof delta.ask === 'string') {
+ delta.ask = [delta.ask];
+ }
+ if (!Array.isArray(delta.ask)) {
+ delete delta.ask;
+ } else {
+ delta.ask = delta.ask.map(String).filter(Boolean);
+ }
+ return delta;
+}
+
+export function patchWantsGenerate(patch) {
+ if (!patch || typeof patch !== 'object') {
+ return false;
+ }
+ if (patch.generate === true) {
+ return true;
+ }
+ const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
+ return acts.includes('generate');
+}
+
+export function patchAskList(patch) {
+ const n = normalizeDelta(patch);
+ return Array.isArray(n?.ask) ? n.ask : [];
+}
+
+/**
+ * Merge sparse model delta into session.gen (and pack/persona if present).
+ * Does not run Generate — caller decides via patchWantsGenerate + veto.
+ */
+export function mergeDelta(session, rawDelta) {
+ const base = session && typeof session === 'object' ? structuredCloneSession(session) : emptySession();
+ const delta = normalizeDelta(rawDelta);
+ if (!delta) {
+ return base;
+ }
+ if (!base.gen) {
+ base.gen = emptySession().gen;
+ }
+ for (const key of GEN_KEYS) {
+ if (delta[key] === undefined || delta[key] === null) {
+ continue;
+ }
+ if (key === 'loras' && Array.isArray(delta.loras)) {
+ base.gen.loras = delta.loras.map((l) => ({
+ name: l?.name || l,
+ weight: l?.weight != null ? Number(l.weight) : 1,
+ triggers: Array.isArray(l?.triggers) ? l.triggers : undefined,
+ trigger_phrase: l?.trigger_phrase || undefined,
+ })).filter((l) => l.name);
+ continue;
+ }
+ if (key === 'controls' && typeof delta.controls === 'object') {
+ base.gen.controls = { ...(base.gen.controls || {}), ...delta.controls };
+ continue;
+ }
+ if (key === 'checkpoint') {
+ base.gen.checkpoint = typeof delta.checkpoint === 'object'
+ ? { ...delta.checkpoint }
+ : { name: String(delta.checkpoint) };
+ continue;
+ }
+ base.gen[key] = delta[key];
+ }
+ if (delta.images != null && delta.batch == null) {
+ base.gen.batch = delta.images;
+ }
+ if (delta.pack) {
+ base.pack = String(delta.pack);
+ }
+ if (delta.persona) {
+ base.persona = String(delta.persona);
+ }
+ return base;
+}
+
+function structuredCloneSession(session) {
+ try {
+ return JSON.parse(JSON.stringify(session));
+ } catch {
+ return emptySession();
+ }
+}
+
+function slimSrc(src) {
+ if (!src || typeof src !== 'string') {
+ return null;
+ }
+ const s = src.trim();
+ if (!s || s.startsWith('#')) {
+ return null;
+ }
+ if (s.startsWith('data:') && s.length > MAX_DATA_URL_CHARS) {
+ return null;
+ }
+ return s;
+}
+
+/** Snapshot live UI + board into a persistable session object. */
+export function snapshotFromLive({
+ genFields,
+ board,
+ persona,
+ pack,
+ context_memory,
+}) {
+ const session = emptySession();
+ if (genFields && typeof genFields === 'object') {
+ for (const key of GEN_KEYS) {
+ if (genFields[key] !== undefined) {
+ session.gen[key] = genFields[key];
+ }
+ }
+ }
+ session.persona = persona || 'neutral';
+ session.pack = pack || 'ordinary';
+ if (context_memory && typeof context_memory === 'object') {
+ session.context_memory = context_memory;
+ }
+ if (board && typeof board === 'object') {
+ session.board = {
+ slots: (board.slots || []).map((s) => ({
+ id: s.id,
+ type: s.type,
+ label: s.label,
+ src: slimSrc(s.src),
+ attach: !!s.attach,
+ note: s.note || null,
+ })),
+ selectedSlotId: board.selectedSlotId || 'ref1',
+ genResults: (board.genResults || []).map((r) => ({
+ id: r.id,
+ label: r.label,
+ src: slimSrc(r.src),
+ patch: r.patch || null,
+ })),
+ selectedGenResultId: board.selectedGenResultId || null,
+ refSeq: board.refSeq || 1,
+ };
+ }
+ return session;
+}
+
+/** Convert legacy flat chat.params into session shape. */
+export function sessionFromLegacyParams(params) {
+ if (!params || typeof params !== 'object') {
+ return emptySession();
+ }
+ if (params.gen && typeof params.gen === 'object') {
+ const s = emptySession();
+ s.gen = { ...s.gen, ...params.gen };
+ if (params.board && typeof params.board === 'object') {
+ s.board = { ...s.board, ...params.board };
+ }
+ s.persona = params.persona || s.persona;
+ s.pack = params.pack || s.pack;
+ if (params.context_memory && typeof params.context_memory === 'object') {
+ s.context_memory = params.context_memory;
+ }
+ return s;
+ }
+ const s = emptySession();
+ for (const key of GEN_KEYS) {
+ if (params[key] !== undefined && params[key] !== null) {
+ s.gen[key] = params[key];
+ }
+ }
+ if (Array.isArray(params.loras)) {
+ s.gen.loras = params.loras;
+ }
+ if (params.checkpoint) {
+ s.gen.checkpoint = typeof params.checkpoint === 'object'
+ ? params.checkpoint
+ : { name: String(params.checkpoint) };
+ }
+ s.persona = params.persona || 'neutral';
+ s.pack = params.pack || 'ordinary';
+ if (params.context_memory && typeof params.context_memory === 'object') {
+ s.context_memory = params.context_memory;
+ }
+ s.board.genResults = Array.isArray(params.genResults) ? params.genResults : [];
+ s.board.selectedGenResultId = params.selectedGenResultId || null;
+ if (Array.isArray(params.slots)) {
+ s.board.slots = params.slots;
+ }
+ if (params.selectedSlotId) {
+ s.board.selectedSlotId = params.selectedSlotId;
+ }
+ if (params.refSeq) {
+ s.board.refSeq = params.refSeq;
+ }
+ return s;
+}
+
+/** Persist blob for AssistentSaveChat.params */
+export function toPersistParams(session) {
+ const s = session && typeof session === 'object' ? session : emptySession();
+ const out = {
+ gen: s.gen || emptySession().gen,
+ board: s.board || emptySession().board,
+ persona: s.persona || 'neutral',
+ pack: s.pack || 'ordinary',
+ };
+ if (s.context_memory && typeof s.context_memory === 'object') {
+ out.context_memory = s.context_memory;
+ }
+ return out;
+}
+
+function slimText(t, max) {
+ const s = String(t || '');
+ if (s.length <= max) {
+ return s;
+ }
+ return `${s.slice(0, max)}…`;
+}
+
+/** Compact context for every LLM turn. */
+export function compactContext(session, extras = {}) {
+ const s = session && typeof session === 'object' ? session : emptySession();
+ const g = s.gen || {};
+ const board = s.board || {};
+ const slots = board.slots || [];
+ const genSlot = slots.find((x) => x.type === 'generate' || x.id === 'generate');
+ const refs = slots.filter((x) => x.type === 'ref' || String(x.id || '').startsWith('ref'));
+ return {
+ session: true,
+ prompt: slimText(g.prompt, extras.promptMax || 2000),
+ negative: slimText(g.negative, 500),
+ aspect: g.aspect || null,
+ width: g.width ?? null,
+ height: g.height ?? null,
+ steps: g.steps ?? null,
+ cfg: g.cfg ?? null,
+ seed: g.seed ?? null,
+ sigma_shift: g.sigma_shift ?? null,
+ sampler: g.sampler || null,
+ scheduler: g.scheduler || null,
+ batch: g.batch ?? null,
+ checkpoint: g.checkpoint?.name || g.checkpoint || null,
+ selected_loras: (g.loras || []).map((l) => ({
+ name: l.name || l,
+ weight: l.weight != null ? l.weight : 1,
+ })),
+ persona: s.persona || 'neutral',
+ pack: s.pack || 'ordinary',
+ board: {
+ has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)),
+ refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })),
+ gen_results: (board.genResults || []).map((r) => ({
+ id: r.id,
+ label: r.label,
+ has_image: !!r.src,
+ selected: r.id === board.selectedGenResultId,
+ })),
+ selected_slot: board.selectedSlotId || null,
+ },
+ architecture_ok: extras.architecture_ok !== false,
+ ...extras.extra,
+ };
+}
+
+/** Full dump for ask:settings hop. */
+export function fullSettingsDump(session, extras = {}) {
+ const compact = compactContext(session, extras);
+ const s = session && typeof session === 'object' ? session : emptySession();
+ return {
+ ...compact,
+ detail: 'settings',
+ gen: { ...(s.gen || {}) },
+ controls: s.gen?.controls || {},
+ exact: extras.exact || null,
+ krea_profiles: extras.kreaProfiles || null,
+ session_exact: extras.sessionExact || null,
+ };
+}
+
+export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
+ const delta = normalizeDelta(patch) || {};
+ const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
+ const generate = !vetoed && patchWantsGenerate(delta);
+ const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
+ const look = !!(hasLook && !generate && !vetoed);
+ const ask = patchAskList(delta);
+ return { generate, look, vetoed, ask };
+}
+
+export function attachSession(SA) {
+ SA.session = {
+ emptySession,
+ normalizeDelta,
+ mergeDelta,
+ patchWantsGenerate,
+ patchAskList,
+ snapshotFromLive,
+ sessionFromLegacyParams,
+ toPersistParams,
+ compactContext,
+ fullSettingsDump,
+ resolveTurnIntent,
+ GEN_KEYS,
+ };
+}
diff --git a/test/context.test.js b/test/context.test.js
new file mode 100644
index 0000000..aa28b0a
--- /dev/null
+++ b/test/context.test.js
@@ -0,0 +1,99 @@
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ emptyContextMemory,
+ normalizeContextMemory,
+ charsToTokens,
+ estimateBudget,
+ shouldCompress,
+ assembleModelMessages,
+ messagesToFold,
+ mergeSummary,
+ conversationMemoryBlock,
+} from '../src/context.js';
+
+describe('context.js', () => {
+ it('charsToTokens ceil divide', () => {
+ assert.equal(charsToTokens(32, 3.2), 10);
+ assert.equal(charsToTokens(0, 3.2), 0);
+ });
+
+ it('estimateBudget prefers prompt_eval_count', () => {
+ const est = estimateBudget({
+ systemChars: 3200,
+ historyChars: 3200,
+ memoryChars: 0,
+ numCtx: 16384,
+ numPredict: 3072,
+ charsPerToken: 3.2,
+ compressAt: 0.7,
+ });
+ assert.equal(est.fromEval, false);
+ assert.ok(est.estimated > 0);
+ assert.equal(est.used, est.estimated);
+
+ const fact = estimateBudget({
+ ...est,
+ systemChars: 3200,
+ historyChars: 3200,
+ promptEvalCount: 14000,
+ numCtx: 16384,
+ numPredict: 3072,
+ compressAt: 0.7,
+ });
+ assert.equal(fact.fromEval, true);
+ assert.equal(fact.used, 14000);
+ assert.equal(fact.level, 'hot');
+ });
+
+ it('shouldCompress only when over threshold and uncovered > keep', () => {
+ const budget = estimateBudget({
+ systemChars: 20000,
+ historyChars: 20000,
+ numCtx: 16384,
+ numPredict: 3072,
+ compressAt: 0.7,
+ charsPerToken: 3.2,
+ });
+ assert.equal(shouldCompress(budget, emptyContextMemory(), 20, { keepMessages: 8 }), true);
+ assert.equal(shouldCompress(budget, emptyContextMemory(), 6, { keepMessages: 8 }), false);
+ assert.equal(
+ shouldCompress(budget, { untilCount: 12, summary: 'x' }, 20, { keepMessages: 8 }),
+ false,
+ );
+ });
+
+ it('assembleModelMessages skips covered prefix then keeps last window', () => {
+ const history = [];
+ for (let i = 0; i < 12; i++) {
+ history.push({ role: i % 2 === 0 ? 'user' : 'assistant', content: `m${i}` });
+ }
+ const msgs = assembleModelMessages(history, { untilCount: 4 }, 2);
+ assert.equal(msgs.length, 4);
+ assert.equal(msgs[0].content, 'm8');
+ assert.equal(msgs[3].content, 'm11');
+ });
+
+ it('messagesToFold leaves keepTurns raw', () => {
+ const history = Array.from({ length: 10 }, (_, i) => ({
+ role: i % 2 === 0 ? 'user' : 'assistant',
+ content: `m${i}`,
+ }));
+ const fold = messagesToFold(history, emptyContextMemory(), 2);
+ assert.equal(fold.length, 6);
+ assert.equal(fold[0].content, 'm0');
+ assert.equal(fold[5].content, 'm5');
+ });
+
+ it('normalize + merge + conversation block', () => {
+ const m = normalizeContextMemory({ summary: ' hello ', untilCount: 4, uiCollapsed: true });
+ assert.equal(m.summary, 'hello');
+ assert.equal(m.uiCollapsed, true);
+ assert.equal(mergeSummary('old', 'new'), 'new');
+ assert.equal(mergeSummary('old', ''), 'old');
+ const block = conversationMemoryBlock(m, 100);
+ assert.equal(block.summary, 'hello');
+ assert.equal(block.until_count, 4);
+ assert.equal(conversationMemoryBlock(emptyContextMemory()), null);
+ });
+});
diff --git a/test/intent.test.js b/test/intent.test.js
index 353f3aa..ad938c2 100644
--- a/test/intent.test.js
+++ b/test/intent.test.js
@@ -2,7 +2,6 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
userAsksLook,
- userCommandsGenerate,
userAsksNoGenerate,
resolveTurnIntent,
packWantsVision,
@@ -14,17 +13,26 @@ describe('intent.js', () => {
assert.equal(userAsksLook('нарисуй лису'), false);
});
- it('userCommandsGenerate respects veto', () => {
- assert.equal(userCommandsGenerate('сгенерируй кадр'), true);
- assert.equal(userCommandsGenerate('только запомни промпт'), false);
+ it('userAsksNoGenerate vetoes remember / no-gen', () => {
assert.equal(userAsksNoGenerate('только запомни промпт'), true);
+ assert.equal(userAsksNoGenerate('не генерируй'), true);
+ assert.equal(userAsksNoGenerate('нарисуй лису'), false);
});
- it('resolveTurnIntent honors look without generate', () => {
- const patch = { look_at: ['generate'] };
- const intent = resolveTurnIntent(patch, 'что не так с кадром?', {}, 'ordinary');
- assert.equal(intent.generate, false);
- assert.equal(intent.look, true);
+ it('resolveTurnIntent uses model generate only + veto', () => {
+ const lookOnly = resolveTurnIntent({ look_at: ['generate'] }, 'что не так с кадром?', {});
+ assert.equal(lookOnly.generate, false);
+ assert.equal(lookOnly.look, true);
+
+ const gen = resolveTurnIntent({ prompt: 'fox', generate: true }, 'нарисуй лису', {});
+ assert.equal(gen.generate, true);
+
+ const vetoed = resolveTurnIntent({ prompt: 'fox', generate: true }, 'только запомни промпт', {});
+ assert.equal(vetoed.generate, false);
+ assert.equal(vetoed.vetoed, true);
+
+ const noHeuristic = resolveTurnIntent({ prompt: 'fox' }, 'нарисуй красивую лису в снегу', {});
+ assert.equal(noHeuristic.generate, false);
});
it('packWantsVision for critique pack', () => {
diff --git a/test/patch.test.js b/test/patch.test.js
index 857923c..e1dd880 100644
--- a/test/patch.test.js
+++ b/test/patch.test.js
@@ -3,35 +3,75 @@ import assert from 'node:assert/strict';
import {
extractPatch,
isPatchObject,
- isCardObject,
normalizePatch,
setPatchKeys,
} from '../src/patch.js';
+import {
+ mergeDelta,
+ emptySession,
+ patchWantsGenerate,
+ sessionFromLegacyParams,
+ toPersistParams,
+ resolveTurnIntent,
+} from '../src/session.js';
describe('patch.js', () => {
it('extractPatch finds generation patch in fence', () => {
- const text = 'Here you go\n```json\n{"prompt":"A red fox in snow","actions":["generate"]}\n```';
+ const text = 'Here you go\n```json\n{"prompt":"A red fox in snow","generate":true}\n```';
const { prose, patch } = extractPatch(text);
assert.ok(patch);
assert.equal(patch.prompt, 'A red fox in snow');
+ assert.equal(patch.generate, true);
assert.ok(!prose.includes('```'));
});
- it('isCardObject vs isPatchObject', () => {
- const card = { kind: 'lora', name: 'Foo', triggers: ['bar'] };
- const gen = { prompt: 'test', actions: ['generate'] };
- assert.equal(isCardObject(card), true);
- assert.equal(isPatchObject(card), false);
- assert.equal(isPatchObject(gen), true);
- });
-
- it('normalizePatch aliases civitai_query', () => {
- const p = normalizePatch({ civitai_query: 'anime style' });
- assert.equal(p.search_query, 'anime style');
+ it('normalizePatch maps legacy actions generate', () => {
+ const p = normalizePatch({ prompt: 'x', actions: ['generate'] });
+ assert.equal(p.generate, true);
});
it('scheduler-only patch is detected with full key list', () => {
- setPatchKeys(['prompt', 'scheduler']);
+ setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
});
});
+
+describe('session.js', () => {
+ it('mergeDelta is sparse and keeps other fields', () => {
+ let s = emptySession();
+ s.gen.prompt = 'old';
+ s.gen.steps = 8;
+ s = mergeDelta(s, { aspect: '16:9', generate: true });
+ assert.equal(s.gen.prompt, 'old');
+ assert.equal(s.gen.steps, 8);
+ assert.equal(s.gen.aspect, '16:9');
+ assert.equal(patchWantsGenerate({ generate: true }), true);
+ assert.equal(patchWantsGenerate({ actions: ['generate'] }), true);
+ });
+
+ it('legacy flat params upgrade to session shape', () => {
+ const s = sessionFromLegacyParams({
+ prompt: 'fox',
+ steps: 28,
+ loras: [{ name: 'a', weight: 0.8 }],
+ genResults: [{ id: 'var1', src: '/View/x.png' }],
+ persona: 'leonid',
+ });
+ assert.equal(s.gen.prompt, 'fox');
+ assert.equal(s.gen.steps, 28);
+ assert.equal(s.board.genResults.length, 1);
+ const blob = toPersistParams(s);
+ assert.ok(blob.gen);
+ assert.ok(blob.board);
+ });
+
+ it('resolveTurnIntent vetoes generate', () => {
+ const intent = resolveTurnIntent(
+ { generate: true, prompt: 'x' },
+ 'не генерируй',
+ { vetoFn: (t) => /не\s+генерир/i.test(t) },
+ );
+ assert.equal(intent.generate, false);
+ assert.equal(intent.vetoed, true);
+ });
+});